use teksilo_canvas::{Point, SizeProposal};
use teksilo_core::event::{Modifiers, PointerButton, WidgetEvent};
use teksilo_core::widget_id::WidgetId;
use teksilo_core::widget_tree::WidgetTree;
pub(crate) fn assert_body_survives_thumb_drag(
tree: &mut WidgetTree,
view: WidgetId,
width: f32,
height: f32,
thumb_top: f32,
label: &str,
rows_in_viewport: impl Fn(&WidgetTree) -> usize,
) {
let pump = |t: &mut WidgetTree| {
t.layout(SizeProposal::exact(width, height));
t.render();
t.layout(SizeProposal::exact(width, height));
};
pump(tree);
let before = rows_in_viewport(tree);
assert!(
before > 0,
"{label}: precondition — the body must have rows before the drag"
);
let realized_before = descendant_ids(tree, view);
let x = width - 5.0;
let start = Point::new(x, thumb_top + 4.0);
tree.pointer_move(start);
tree.dispatch_event(WidgetEvent::PointerDown {
position: start,
button: PointerButton::Primary,
modifiers: Modifiers::NONE,
});
let travel = height - thumb_top - 8.0;
for step in 1..=10 {
let y = thumb_top + 4.0 + travel * (step as f32 / 10.0);
tree.dispatch_event(WidgetEvent::PointerMove {
position: Point::new(x, y),
});
pump(tree);
}
let during = rows_in_viewport(tree);
assert_ne!(
realized_before,
descendant_ids(tree, view),
"{label}: the thumb drag realized no new rows — the drag never took \
effect, so this test is not exercising the deferral at all"
);
tree.dispatch_event(WidgetEvent::PointerUp {
position: Point::new(x, height - 4.0),
button: PointerButton::Primary,
modifiers: Modifiers::NONE,
});
pump(tree);
let after = rows_in_viewport(tree);
assert!(
during > 0,
"{label}: the body went blank while the thumb was held \
(rows before={before}, during={during}, after release={after}). \
Row realization has moved back onto an ancestor of the scrollbar — \
see `common::thumb_drag_test`'s module docs."
);
assert!(
after > 0,
"{label}: the body is empty after releasing the thumb \
(before={before}, during={during}, after={after})"
);
}
pub(crate) fn assert_fling_survives_pane_rebuild(
tree: &mut WidgetTree,
width: f32,
height: f32,
scroll: &teksilo_core::signal::Signal<f32>,
label: &str,
trigger_pane_rebuild: impl FnOnce(),
) {
use std::time::Duration;
use teksilo_tokens::Easing;
tree.layout(SizeProposal::exact(width, height));
tree.render();
scroll.animate_to(4000.0, Duration::from_millis(400), Easing::EaseOut);
assert_eq!(
scroll.animation_target(),
Some(4000.0),
"{label}: precondition — the fling must be registered"
);
trigger_pane_rebuild();
tree.layout(SizeProposal::exact(width, height));
tree.render();
tree.layout(SizeProposal::exact(width, height));
assert_eq!(
scroll.animation_target(),
Some(4000.0),
"{label}: a body-pane rebuild cancelled the in-flight smooth scroll. \
`scroll_y` must be registered on the ROOT only, and the root must not \
rebuild on scroll-buffer exit — see this module's docs."
);
}
fn descendant_ids(tree: &WidgetTree, view: WidgetId) -> Vec<WidgetId> {
let mut out = Vec::new();
let mut walker = vec![view];
while let Some(id) = walker.pop() {
out.push(id);
for c in tree.children(id) {
walker.push(c);
}
}
out
}
pub(crate) fn placed_rows_in_band(
tree: &WidgetTree,
view: WidgetId,
width: f32,
height: f32,
) -> usize {
let mut n = 0;
let mut walker = vec![view];
while let Some(id) = walker.pop() {
if id != view {
let b = tree.bounds(id);
if b.height > 1.0 && b.width > width * 0.5 && b.y > -b.height && b.y < height {
n += 1;
}
}
for c in tree.children(id) {
walker.push(c);
}
}
n
}