Fix scrollbar regression
This commit is contained in:
@@ -1824,11 +1824,12 @@ fn focused_element_for_pointer(
|
||||
interaction_tree: &InteractionTree,
|
||||
event: &PointerEvent,
|
||||
) -> Option<ElementId> {
|
||||
interaction_tree
|
||||
.hit_path(event.position)
|
||||
let hit_path = interaction_tree.hit_path(event.position);
|
||||
hit_path
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|target| target.focusable.then_some(target.element_id).flatten())
|
||||
.or_else(|| hit_path.iter().rev().find_map(|target| target.element_id))
|
||||
}
|
||||
|
||||
fn element_contains_element(
|
||||
@@ -1869,7 +1870,6 @@ fn key_handler_for_focus<'a>(
|
||||
let focused_element = focused_element?;
|
||||
focused_ancestor_chain(&interaction_tree.root, focused_element)?
|
||||
.into_iter()
|
||||
.rev()
|
||||
.find_map(|element_id| handlers.get(&element_id))
|
||||
}
|
||||
|
||||
@@ -1933,6 +1933,7 @@ struct SignalInner<T> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ruin_ui::{KeyboardModifiers, Point, UiRuntime, WindowSpec};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct NamedValue(&'static str);
|
||||
@@ -1989,4 +1990,470 @@ mod tests {
|
||||
|
||||
assert_eq!(*seen_value.borrow(), Some(NamedValue("inner")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_dispatch_prefers_the_nearest_focused_ancestor_handler() {
|
||||
let outer_id = ElementId::new(41);
|
||||
let inner_id = ElementId::new(42);
|
||||
let root = Element::column().pointer_events(false).child(
|
||||
Element::column()
|
||||
.id(outer_id)
|
||||
.width(160.0)
|
||||
.height(120.0)
|
||||
.focusable(true)
|
||||
.child(
|
||||
Element::column()
|
||||
.id(inner_id)
|
||||
.width(120.0)
|
||||
.height(80.0)
|
||||
.focusable(true),
|
||||
),
|
||||
);
|
||||
let snapshot = ruin_ui::layout_snapshot(1, UiSize::new(200.0, 120.0), &root);
|
||||
let outer_hits = Rc::new(StdCell::new(0usize));
|
||||
let inner_hits = Rc::new(StdCell::new(0usize));
|
||||
|
||||
let mut handlers = HashMap::<ElementId, KeyHandler>::new();
|
||||
handlers.insert(
|
||||
outer_id,
|
||||
Rc::new({
|
||||
let outer_hits = Rc::clone(&outer_hits);
|
||||
move |_, _| {
|
||||
outer_hits.set(outer_hits.get() + 1);
|
||||
true
|
||||
}
|
||||
}),
|
||||
);
|
||||
handlers.insert(
|
||||
inner_id,
|
||||
Rc::new({
|
||||
let inner_hits = Rc::clone(&inner_hits);
|
||||
move |_, _| {
|
||||
inner_hits.set(inner_hits.get() + 1);
|
||||
true
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let handler = key_handler_for_focus(&handlers, Some(inner_id), &snapshot.interaction_tree)
|
||||
.expect("focused element should resolve a key handler");
|
||||
let _ = handler(
|
||||
&KeyboardEvent::new(
|
||||
0,
|
||||
KeyboardEventKind::Pressed,
|
||||
KeyboardKey::ArrowDown,
|
||||
KeyboardModifiers::default(),
|
||||
None,
|
||||
),
|
||||
&snapshot.interaction_tree,
|
||||
);
|
||||
|
||||
assert_eq!(inner_hits.get(), 1);
|
||||
assert_eq!(outer_hits.get(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scroll_box_arrow_keys_work_after_clicking_text_content() {
|
||||
let offset_slot = Rc::new(RefCell::new(None::<Signal<f32>>));
|
||||
let render = render_with_context(Rc::new(RenderState::default()), {
|
||||
let offset_slot = Rc::clone(&offset_slot);
|
||||
move || {
|
||||
let offset = use_signal(|| 0.0_f32);
|
||||
*offset_slot.borrow_mut() = Some(offset.clone());
|
||||
scroll_box()
|
||||
.height(120.0)
|
||||
.offset_y(offset)
|
||||
.children(text().children(
|
||||
"line 01\nline 02\nline 03\nline 04\nline 05\nline 06\nline 07\nline 08\nline 09\nline 10\nline 11\nline 12",
|
||||
))
|
||||
}
|
||||
});
|
||||
let offset = offset_slot
|
||||
.borrow()
|
||||
.clone()
|
||||
.expect("scroll signal should have been captured");
|
||||
let scrollbox_id = render
|
||||
.view
|
||||
.element
|
||||
.id
|
||||
.expect("scroll box should receive an element id");
|
||||
let snapshot =
|
||||
ruin_ui::layout_snapshot(1, UiSize::new(260.0, 160.0), render.view.element());
|
||||
let focused = focused_element_for_pointer(
|
||||
&snapshot.interaction_tree,
|
||||
&PointerEvent::new(
|
||||
1,
|
||||
Point::new(12.0, 12.0),
|
||||
PointerEventKind::Down {
|
||||
button: PointerButton::Primary,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
assert_eq!(focused, Some(scrollbox_id));
|
||||
|
||||
render.view.bindings.dispatch_key(
|
||||
focused,
|
||||
&KeyboardEvent::new(
|
||||
0,
|
||||
KeyboardEventKind::Pressed,
|
||||
KeyboardKey::ArrowDown,
|
||||
KeyboardModifiers::default(),
|
||||
None,
|
||||
),
|
||||
&snapshot.interaction_tree,
|
||||
);
|
||||
|
||||
assert!(offset.get() > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scroll_box_thumb_drag_updates_offset_signal() {
|
||||
let offset_slot = Rc::new(RefCell::new(None::<Signal<f32>>));
|
||||
let render = render_with_context(Rc::new(RenderState::default()), {
|
||||
let offset_slot = Rc::clone(&offset_slot);
|
||||
move || {
|
||||
let offset = use_signal(|| 0.0_f32);
|
||||
*offset_slot.borrow_mut() = Some(offset.clone());
|
||||
scroll_box()
|
||||
.height(120.0)
|
||||
.offset_y(offset)
|
||||
.children(text().children(
|
||||
"line 01\nline 02\nline 03\nline 04\nline 05\nline 06\nline 07\nline 08\nline 09\nline 10\nline 11\nline 12",
|
||||
))
|
||||
}
|
||||
});
|
||||
let offset = offset_slot
|
||||
.borrow()
|
||||
.clone()
|
||||
.expect("scroll signal should have been captured");
|
||||
let scrollbox_id = render
|
||||
.view
|
||||
.element
|
||||
.id
|
||||
.expect("scroll box should receive an element id");
|
||||
let snapshot =
|
||||
ruin_ui::layout_snapshot(1, UiSize::new(260.0, 160.0), render.view.element());
|
||||
let metrics = snapshot
|
||||
.interaction_tree
|
||||
.scroll_metrics_for_element(scrollbox_id)
|
||||
.expect("scroll metrics should exist for the scroll box");
|
||||
let thumb = metrics
|
||||
.scrollbar_thumb
|
||||
.expect("overflowing scroll box should expose a scrollbar thumb");
|
||||
let thumb_center = Point::new(
|
||||
thumb.origin.x + (thumb.size.width * 0.5),
|
||||
thumb.origin.y + (thumb.size.height * 0.5),
|
||||
);
|
||||
let hovered_targets = snapshot.interaction_tree.hit_path(thumb_center);
|
||||
let handler = scroll_handler_for_event(
|
||||
&render.view.bindings.on_scroll,
|
||||
Some(scrollbox_id),
|
||||
&hovered_targets,
|
||||
)
|
||||
.expect("scroll box should resolve its scroll handler")
|
||||
.clone();
|
||||
|
||||
handler(
|
||||
&RoutedPointerEvent {
|
||||
kind: RoutedPointerEventKind::Down {
|
||||
button: PointerButton::Primary,
|
||||
},
|
||||
target: hovered_targets
|
||||
.last()
|
||||
.cloned()
|
||||
.expect("thumb center should hit the scroll box"),
|
||||
pointer_id: 1,
|
||||
position: thumb_center,
|
||||
},
|
||||
&snapshot.interaction_tree,
|
||||
);
|
||||
handler(
|
||||
&RoutedPointerEvent {
|
||||
kind: RoutedPointerEventKind::Move,
|
||||
target: hovered_targets
|
||||
.last()
|
||||
.cloned()
|
||||
.expect("thumb center should hit the scroll box"),
|
||||
pointer_id: 1,
|
||||
position: Point::new(thumb_center.x, thumb_center.y + 24.0),
|
||||
},
|
||||
&snapshot.interaction_tree,
|
||||
);
|
||||
|
||||
assert!(offset.get() > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn live_input_path_scrolls_a_scroll_box_rendered_inside_a_branch() {
|
||||
let offset_slot = Rc::new(RefCell::new(None::<Signal<f32>>));
|
||||
let render = render_with_context(Rc::new(RenderState::default()), {
|
||||
let offset_slot = Rc::clone(&offset_slot);
|
||||
move || match true {
|
||||
true => {
|
||||
let offset = use_signal(|| 0.0_f32);
|
||||
*offset_slot.borrow_mut() = Some(offset.clone());
|
||||
column()
|
||||
.background(surfaces::raised())
|
||||
.gap(10.0)
|
||||
.children((
|
||||
text().children(("bytes = ", 4096)),
|
||||
scroll_box()
|
||||
.height(420.0)
|
||||
.offset_y(offset.clone())
|
||||
.padding(12.0)
|
||||
.background(surfaces::canvas())
|
||||
.border_radius(10.0)
|
||||
.border((2.0, colors::muted()))
|
||||
.children(
|
||||
text()
|
||||
.color(colors::muted())
|
||||
.font_family(TextFontFamily::Monospace)
|
||||
.children(
|
||||
"line 01\nline 02\nline 03\nline 04\nline 05\nline 06\nline 07\nline 08\nline 09\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\nline 21\nline 22\nline 23\nline 24",
|
||||
),
|
||||
),
|
||||
))
|
||||
}
|
||||
false => View::from_element(Element::column()),
|
||||
}
|
||||
});
|
||||
let offset = offset_slot
|
||||
.borrow()
|
||||
.clone()
|
||||
.expect("scroll signal should have been captured");
|
||||
let snapshot =
|
||||
ruin_ui::layout_snapshot(1, UiSize::new(1080.0, 760.0), render.view.element());
|
||||
let interaction_tree = RefCell::new(Some(snapshot.interaction_tree.clone()));
|
||||
let bindings = RefCell::new(render.view.bindings.clone());
|
||||
let mut pointer_router = PointerRouter::new();
|
||||
let mut input_state = InputState::new();
|
||||
let window = UiRuntime::headless()
|
||||
.create_window(WindowSpec::new("scrollbox-test"))
|
||||
.expect("headless window should be created");
|
||||
|
||||
MountedApp::<View>::handle_pointer_event(
|
||||
&window,
|
||||
&interaction_tree,
|
||||
&bindings,
|
||||
&mut pointer_router,
|
||||
&mut input_state,
|
||||
PointerEvent::new(
|
||||
1,
|
||||
Point::new(24.0, 64.0),
|
||||
PointerEventKind::Down {
|
||||
button: PointerButton::Primary,
|
||||
},
|
||||
),
|
||||
)
|
||||
.expect("pointer down should succeed");
|
||||
MountedApp::<View>::handle_keyboard_event(
|
||||
&interaction_tree,
|
||||
&bindings,
|
||||
&RefCell::new(Vec::new()),
|
||||
&input_state,
|
||||
KeyboardEvent::new(
|
||||
0,
|
||||
KeyboardEventKind::Pressed,
|
||||
KeyboardKey::ArrowDown,
|
||||
KeyboardModifiers::default(),
|
||||
None,
|
||||
),
|
||||
)
|
||||
.expect("keyboard event should succeed");
|
||||
|
||||
assert!(offset.get() > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scroll_box_stays_interactive_when_it_appears_on_a_later_render() {
|
||||
let state = Rc::new(RenderState::default());
|
||||
let ready_slot = Rc::new(RefCell::new(None::<Signal<bool>>));
|
||||
let offset_slot = Rc::new(RefCell::new(None::<Signal<f32>>));
|
||||
|
||||
let render_once =
|
||||
|state: Rc<RenderState>,
|
||||
ready_slot: Rc<RefCell<Option<Signal<bool>>>>,
|
||||
offset_slot: Rc<RefCell<Option<Signal<f32>>>>| {
|
||||
render_with_context(state, move || {
|
||||
let ready = use_signal(|| false);
|
||||
let offset = use_signal(|| 0.0_f32);
|
||||
*ready_slot.borrow_mut() = Some(ready.clone());
|
||||
*offset_slot.borrow_mut() = Some(offset.clone());
|
||||
|
||||
if ready.get() {
|
||||
column()
|
||||
.background(surfaces::raised())
|
||||
.gap(10.0)
|
||||
.children((
|
||||
text().children(("bytes = ", 4096)),
|
||||
scroll_box()
|
||||
.height(420.0)
|
||||
.offset_y(offset.clone())
|
||||
.padding(12.0)
|
||||
.background(surfaces::canvas())
|
||||
.border_radius(10.0)
|
||||
.border((2.0, colors::muted()))
|
||||
.children(
|
||||
text()
|
||||
.color(colors::muted())
|
||||
.font_family(TextFontFamily::Monospace)
|
||||
.children(
|
||||
"line 01\nline 02\nline 03\nline 04\nline 05\nline 06\nline 07\nline 08\nline 09\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\nline 21\nline 22\nline 23\nline 24",
|
||||
),
|
||||
),
|
||||
))
|
||||
} else {
|
||||
text().children("Loading file contents...")
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let _initial = render_once(
|
||||
Rc::clone(&state),
|
||||
Rc::clone(&ready_slot),
|
||||
Rc::clone(&offset_slot),
|
||||
);
|
||||
let ready = ready_slot
|
||||
.borrow()
|
||||
.clone()
|
||||
.expect("ready signal should have been captured");
|
||||
let offset = offset_slot
|
||||
.borrow()
|
||||
.clone()
|
||||
.expect("offset signal should have been captured");
|
||||
let _ = ready.set(true);
|
||||
|
||||
let render = render_once(state, ready_slot, Rc::clone(&offset_slot));
|
||||
let snapshot =
|
||||
ruin_ui::layout_snapshot(1, UiSize::new(1080.0, 760.0), render.view.element());
|
||||
let interaction_tree = RefCell::new(Some(snapshot.interaction_tree.clone()));
|
||||
let bindings = RefCell::new(render.view.bindings.clone());
|
||||
let mut pointer_router = PointerRouter::new();
|
||||
let mut input_state = InputState::new();
|
||||
let window = UiRuntime::headless()
|
||||
.create_window(WindowSpec::new("scrollbox-transition-test"))
|
||||
.expect("headless window should be created");
|
||||
|
||||
MountedApp::<View>::handle_pointer_event(
|
||||
&window,
|
||||
&interaction_tree,
|
||||
&bindings,
|
||||
&mut pointer_router,
|
||||
&mut input_state,
|
||||
PointerEvent::new(
|
||||
1,
|
||||
Point::new(24.0, 64.0),
|
||||
PointerEventKind::Down {
|
||||
button: PointerButton::Primary,
|
||||
},
|
||||
),
|
||||
)
|
||||
.expect("pointer down should succeed after branch switch");
|
||||
MountedApp::<View>::handle_keyboard_event(
|
||||
&interaction_tree,
|
||||
&bindings,
|
||||
&RefCell::new(Vec::new()),
|
||||
&input_state,
|
||||
KeyboardEvent::new(
|
||||
0,
|
||||
KeyboardEventKind::Pressed,
|
||||
KeyboardKey::ArrowDown,
|
||||
KeyboardModifiers::default(),
|
||||
None,
|
||||
),
|
||||
)
|
||||
.expect("keyboard event should succeed after branch switch");
|
||||
|
||||
assert!(offset.get() > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn live_input_path_scrolls_with_real_cargo_lock_contents() {
|
||||
let offset_slot = Rc::new(RefCell::new(None::<Signal<f32>>));
|
||||
let render = render_with_context(Rc::new(RenderState::default()), {
|
||||
let offset_slot = Rc::clone(&offset_slot);
|
||||
move || {
|
||||
let offset = use_signal(|| 0.0_f32);
|
||||
*offset_slot.borrow_mut() = Some(offset.clone());
|
||||
column().background(surfaces::raised()).gap(10.0).children((
|
||||
text().children(("bytes = ", include_str!("../../../Cargo.lock").len())),
|
||||
scroll_box()
|
||||
.height(420.0)
|
||||
.offset_y(offset.clone())
|
||||
.padding(12.0)
|
||||
.background(surfaces::canvas())
|
||||
.border_radius(10.0)
|
||||
.border((2.0, colors::muted()))
|
||||
.children(
|
||||
text()
|
||||
.color(colors::muted())
|
||||
.font_family(TextFontFamily::Monospace)
|
||||
.children(include_str!("../../../Cargo.lock")),
|
||||
),
|
||||
))
|
||||
}
|
||||
});
|
||||
let offset = offset_slot
|
||||
.borrow()
|
||||
.clone()
|
||||
.expect("scroll signal should have been captured");
|
||||
let snapshot =
|
||||
ruin_ui::layout_snapshot(1, UiSize::new(1080.0, 760.0), render.view.element());
|
||||
let interaction_tree = RefCell::new(Some(snapshot.interaction_tree.clone()));
|
||||
let bindings = RefCell::new(render.view.bindings.clone());
|
||||
let mut pointer_router = PointerRouter::new();
|
||||
let mut input_state = InputState::new();
|
||||
let window = UiRuntime::headless()
|
||||
.create_window(WindowSpec::new("scrollbox-cargo-lock-test"))
|
||||
.expect("headless window should be created");
|
||||
|
||||
MountedApp::<View>::handle_pointer_event(
|
||||
&window,
|
||||
&interaction_tree,
|
||||
&bindings,
|
||||
&mut pointer_router,
|
||||
&mut input_state,
|
||||
PointerEvent::new(
|
||||
1,
|
||||
Point::new(24.0, 64.0),
|
||||
PointerEventKind::Scroll {
|
||||
delta: Point::new(0.0, 48.0),
|
||||
},
|
||||
),
|
||||
)
|
||||
.expect("wheel event should succeed");
|
||||
|
||||
assert!(offset.get() > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rerendered_scroll_box_element_carries_the_updated_offset() {
|
||||
let state = Rc::new(RenderState::default());
|
||||
let offset_slot = Rc::new(RefCell::new(None::<Signal<f32>>));
|
||||
let render_once =
|
||||
|state: Rc<RenderState>, offset_slot: Rc<RefCell<Option<Signal<f32>>>>| {
|
||||
render_with_context(state, move || {
|
||||
let offset = use_signal(|| 0.0_f32);
|
||||
*offset_slot.borrow_mut() = Some(offset.clone());
|
||||
scroll_box()
|
||||
.height(120.0)
|
||||
.offset_y(offset.clone())
|
||||
.children(
|
||||
text().children("line 01\nline 02\nline 03\nline 04\nline 05\nline 06"),
|
||||
)
|
||||
})
|
||||
};
|
||||
|
||||
let _initial = render_once(Rc::clone(&state), Rc::clone(&offset_slot));
|
||||
let offset = offset_slot
|
||||
.borrow()
|
||||
.clone()
|
||||
.expect("offset signal should have been captured");
|
||||
let _ = offset.set(96.0);
|
||||
let render = render_once(state, offset_slot);
|
||||
let debug = format!("{:?}", render.view.element());
|
||||
|
||||
assert!(debug.contains("offset_y: 96.0"), "{debug}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user