192 lines
6.2 KiB
Rust
192 lines
6.2 KiB
Rust
use ai_app::ui::fixture::{PHONE_FRAME_MS, PHONE_SCALE, phone_size};
|
|
use iris::harness::{Harness, TouchAction};
|
|
use iris::prelude::*;
|
|
|
|
fn fence_scroll_in(h: &Harness, top: f32, bottom: f32) -> Option<(WidgetId, PixelRegion)> {
|
|
h.render
|
|
.active
|
|
.keys()
|
|
.copied()
|
|
.filter(|&id| {
|
|
h.rsc
|
|
.ui
|
|
.widgets
|
|
.get_dyn(id)
|
|
.and_then(|w| w.as_any().downcast_ref::<ScrollArea>())
|
|
.is_some_and(|s| s.axis() == Axis::X)
|
|
})
|
|
.find_map(|id| {
|
|
let r = h.render.window_region(&id, &h.rsc)?;
|
|
(r.top_left.y >= top && r.bot_right.y <= bottom).then_some((id, r))
|
|
})
|
|
}
|
|
|
|
fn is_scrolling(h: &Harness, id: WidgetId) -> bool {
|
|
h.rsc
|
|
.ui
|
|
.widgets
|
|
.get_dyn(id)
|
|
.and_then(|w| w.as_any().downcast_ref::<ScrollArea>())
|
|
.expect("the fence's scroll area is still drawn")
|
|
.is_scrolling()
|
|
}
|
|
|
|
fn amt(h: &Harness, id: WidgetId) -> f32 {
|
|
h.rsc
|
|
.ui
|
|
.widgets
|
|
.get_dyn(id)
|
|
.and_then(|w| w.as_any().downcast_ref::<ScrollArea>())
|
|
.expect("the fence's scroll area is still drawn")
|
|
.amt()
|
|
}
|
|
|
|
#[test]
|
|
fn a_flick_across_a_code_fence_keeps_moving_after_the_finger_leaves() {
|
|
use ai_app::client::transcript_fold::{TranscriptItem, TranscriptRow};
|
|
|
|
let mut h = Harness::new(phone_size(), PHONE_SCALE);
|
|
let opened = ai_app::ui::fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds");
|
|
let screen = opened.screen;
|
|
h.frame(0);
|
|
h.frame(PHONE_FRAME_MS);
|
|
|
|
let fence = TranscriptRow::Single(TranscriptItem::AssistantMsg {
|
|
seq: 9_000_000,
|
|
text: "```\none two three four five six seven eight nine ten eleven twelve \
|
|
thirteen fourteen fifteen sixteen seventeen eighteen twenty twentyone\n```"
|
|
.to_string(),
|
|
settled: true,
|
|
});
|
|
screen.push_row(&mut h.rsc, &fence);
|
|
(screen.list)(&mut h.rsc).jump_to_end();
|
|
h.frame(100);
|
|
h.frame(108);
|
|
|
|
let key = ai_app::ui::row::row_key(&fence.key());
|
|
let (top, bottom) = (screen.list)(&mut h.rsc)
|
|
.extent(key)
|
|
.expect("the fence row is on screen");
|
|
let (fence_scroll, box_) = fence_scroll_in(&h, top, bottom)
|
|
.expect("the pushed fence draws a horizontal scroll area of its own");
|
|
assert_eq!(amt(&h, fence_scroll), 0.0, "a fence opens at its start");
|
|
|
|
let y = (box_.top_left.y + box_.bot_right.y) / 2.0;
|
|
|
|
h.touch(TouchAction::Down, Vec2::new(900.0, y), 200);
|
|
for (i, x) in [860.0, 800.0, 720.0, 620.0].into_iter().enumerate() {
|
|
h.touch(TouchAction::Move, Vec2::new(x, y), 208 + 8 * i as u64);
|
|
}
|
|
h.touch(TouchAction::Up, Vec2::new(620.0, y), 240);
|
|
|
|
let at_release = amt(&h, fence_scroll);
|
|
assert!(
|
|
at_release > 0.0,
|
|
"the flick itself must have panned the fence, got {at_release}"
|
|
);
|
|
|
|
let mut t = 240;
|
|
while t <= 740 {
|
|
h.frame(t);
|
|
t += PHONE_FRAME_MS;
|
|
}
|
|
let coasted = amt(&h, fence_scroll);
|
|
assert!(
|
|
coasted > at_release + 1.0,
|
|
"the fence stopped dead at the release: {at_release} -> {coasted}"
|
|
);
|
|
|
|
let settled = coasted;
|
|
while t <= 4_000 {
|
|
h.frame(t);
|
|
t += PHONE_FRAME_MS;
|
|
}
|
|
let after = amt(&h, fence_scroll);
|
|
assert!(
|
|
after >= settled,
|
|
"a fling must not run backwards: {settled} -> {after}"
|
|
);
|
|
let last = after;
|
|
h.frame(t);
|
|
assert_eq!(last, amt(&h, fence_scroll), "the fling never settled");
|
|
}
|
|
|
|
#[test]
|
|
fn a_drag_away_from_a_coasting_fence_scrolls_the_list_and_leaves_it_coasting() {
|
|
use ai_app::client::transcript_fold::{TranscriptItem, TranscriptRow};
|
|
|
|
let mut h = Harness::new(phone_size(), PHONE_SCALE);
|
|
let opened = ai_app::ui::fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds");
|
|
let screen = opened.screen;
|
|
h.frame(0);
|
|
h.frame(PHONE_FRAME_MS);
|
|
|
|
let fence = TranscriptRow::Single(TranscriptItem::AssistantMsg {
|
|
seq: 9_000_000,
|
|
text: format!(
|
|
"```\n{}\n```",
|
|
(1..=200)
|
|
.map(|i| format!("word{i}"))
|
|
.collect::<Vec<_>>()
|
|
.join(" ")
|
|
),
|
|
settled: true,
|
|
});
|
|
screen.push_row(&mut h.rsc, &fence);
|
|
(screen.list)(&mut h.rsc).jump_to_end();
|
|
h.frame(100);
|
|
h.frame(108);
|
|
|
|
let key = ai_app::ui::row::row_key(&fence.key());
|
|
let (top, bottom) = (screen.list)(&mut h.rsc)
|
|
.extent(key)
|
|
.expect("the fence row is on screen");
|
|
let (fence_scroll, box_) = fence_scroll_in(&h, top, bottom)
|
|
.expect("the pushed fence draws a horizontal scroll area of its own");
|
|
let y = (box_.top_left.y + box_.bot_right.y) / 2.0;
|
|
|
|
h.touch(TouchAction::Down, Vec2::new(900.0, y), 200);
|
|
for (i, x) in [860.0, 800.0, 720.0, 620.0].into_iter().enumerate() {
|
|
h.touch(TouchAction::Move, Vec2::new(x, y), 208 + 8 * i as u64);
|
|
}
|
|
h.touch(TouchAction::Up, Vec2::new(620.0, y), 240);
|
|
h.frame(248);
|
|
assert!(
|
|
is_scrolling(&h, fence_scroll),
|
|
"the fence has to still be coasting for this to be the reported case",
|
|
);
|
|
|
|
let probe = box_.top_left.y - 500.0;
|
|
let row = (screen.list)(&mut h.rsc)
|
|
.key_at(probe)
|
|
.expect("a row that far up the screen");
|
|
let (row_top, row_bottom) = (screen.list)(&mut h.rsc).extent(row).expect("its extent");
|
|
let from = (row_top + row_bottom) / 2.0;
|
|
|
|
let list_before = (screen.list)(&mut h.rsc).anchor_position_display();
|
|
let fence_before = amt(&h, fence_scroll);
|
|
h.touch(TouchAction::Down, Vec2::new(540.0, from), 256);
|
|
let mut t = 264;
|
|
for i in 1..=8 {
|
|
h.touch(
|
|
TouchAction::Move,
|
|
Vec2::new(540.0, from + 20.0 * i as f32),
|
|
t,
|
|
);
|
|
t += 8;
|
|
}
|
|
h.touch(TouchAction::Up, Vec2::new(540.0, from + 160.0), t);
|
|
|
|
assert_ne!(
|
|
list_before,
|
|
(screen.list)(&mut h.rsc).anchor_position_display(),
|
|
"the drag was nowhere near the fence, so it belongs to the list",
|
|
);
|
|
assert!(
|
|
amt(&h, fence_scroll) > fence_before,
|
|
"the fence's fling must carry on through a gesture that was never \
|
|
its own: {fence_before} -> {}",
|
|
amt(&h, fence_scroll),
|
|
);
|
|
}
|