use super::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ContainerCursor {
Silent,
BeforeRouting(CursorIcon),
AfterRouting(CursorIcon),
}
struct CursorLeaf {
hover_icon: Option<CursorIcon>,
drag_icon: Option<CursorIcon>,
captures: bool,
captured: bool,
}
impl crate::widget::Widget for CursorLeaf {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(Size::new(100.0, 30.0))
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
let InputEvent::Pointer(p) = event else {
return EventResult::Ignored;
};
match p.phase {
PointerPhase::Move => {
if self.captured {
if let Some(icon) = self.drag_icon {
ctx.set_cursor(icon);
}
return EventResult::Handled;
}
let size = ctx.size();
let inside = p.position.x >= 0.0
&& p.position.y >= 0.0
&& p.position.x < size.width
&& p.position.y < size.height;
if inside && let Some(icon) = self.hover_icon {
ctx.set_cursor(icon);
}
EventResult::Ignored
}
PointerPhase::Down => {
if self.captures {
ctx.capture_pointer();
self.captured = true;
}
EventResult::Handled
}
PointerPhase::Up | PointerPhase::Cancel => {
self.captured = false;
EventResult::Ignored
}
}
}
}
struct CursorPair {
top: crate::widget::ChildPod,
bottom: crate::widget::ChildPod,
own: ContainerCursor,
}
impl crate::widget::Widget for CursorPair {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
self.top.layout_child(ctx, bc);
self.top.set_origin(Point::ZERO);
self.bottom.layout_child(ctx, bc);
self.bottom.set_origin(Point::new(0.0, 30.0));
bc.max()
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
if let ContainerCursor::BeforeRouting(icon) = self.own {
ctx.set_cursor(icon);
}
let result = self.route(ctx, event);
if let ContainerCursor::AfterRouting(icon) = self.own {
ctx.set_cursor(icon);
}
result
}
}
impl CursorPair {
fn route(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
let releases = matches!(
event,
InputEvent::Pointer(p)
if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
);
for pod in [&mut self.top, &mut self.bottom] {
if pod.is_active() {
let r = pod.event_child(ctx, event);
if releases {
pod.set_active(false);
}
return r;
}
}
let pos = event.position();
for pod in [&mut self.top, &mut self.bottom] {
if pod.contains(pos) {
return pod.event_child(ctx, event);
}
}
EventResult::Ignored
}
}
#[derive(Clone, Copy)]
struct CursorPairView {
own: ContainerCursor,
captures: bool,
}
impl View<()> for CursorPairView {
type Element = CursorPair;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> CursorPair {
CursorPair {
top: crate::widget::ChildPod::new(Box::new(CursorLeaf {
hover_icon: Some(CursorIcon::Pointer),
drag_icon: Some(CursorIcon::Grabbing),
captures: self.captures,
captured: false,
})),
bottom: crate::widget::ChildPod::new(Box::new(CursorLeaf {
hover_icon: Some(CursorIcon::Text),
drag_icon: None,
captures: false,
captured: false,
})),
own: self.own,
}
}
fn rebuild(&self, _p: &Self, _e: &mut CursorPair, _c: &mut BuildCtx<'_>) -> ChangeFlags {
ChangeFlags::NONE
}
}
struct CursorHarness {
root: RenderRoot<(), CursorPairView>,
state: (),
}
impl CursorHarness {
fn new(own: ContainerCursor, captures: bool) -> Self {
let mut root: RenderRoot<(), CursorPairView> = RenderRoot::new();
let mut state = ();
root.rebuild(
&mut move |_: &mut ()| CursorPairView { own, captures },
&mut state,
);
root.layout(Size::new(100.0, 60.0));
CursorHarness { root, state }
}
fn dispatch(&mut self, phase: PointerPhase, x: f64, y: f64) -> EventOutcome {
let event = InputEvent::Pointer(PointerEvent {
phase,
position: Point::new(x, y),
button: PointerButton::Primary,
});
self.root.event(&mut self.state, &event)
}
fn move_over(&mut self, leaf: Leaf) {
match leaf {
Leaf::Top => self.dispatch(PointerPhase::Move, 50.0, 15.0),
Leaf::Bottom => self.dispatch(PointerPhase::Move, 50.0, 45.0),
};
}
fn cursor(&self) -> CursorIcon {
self.root.cursor()
}
}
#[test]
fn a_move_resolves_the_requested_cursor_and_absence_resolves_default() {
let mut h = CursorHarness::new(ContainerCursor::Silent, false);
assert_eq!(
h.cursor(),
CursorIcon::Default,
"nothing has asked for anything yet"
);
h.move_over(Leaf::Top);
assert_eq!(
h.cursor(),
CursorIcon::Pointer,
"the request reached the root"
);
h.move_over(Leaf::Bottom);
assert_eq!(h.cursor(), CursorIcon::Text);
h.dispatch(PointerPhase::Move, 50.0, 200.0);
assert_eq!(
h.cursor(),
CursorIcon::Default,
"a widget that stops asking falls back with nothing to clear"
);
}
#[test]
fn a_cursor_change_does_not_ask_for_a_repaint() {
let mut h = CursorHarness::new(ContainerCursor::Silent, false);
let outcome = h.dispatch(PointerPhase::Move, 50.0, 15.0);
assert_eq!(h.cursor(), CursorIcon::Pointer);
assert!(
!outcome.needs_redraw,
"a cursor request alone never schedules a frame"
);
}
#[test]
fn the_last_writer_on_the_routed_path_wins() {
let mut h = CursorHarness::new(ContainerCursor::BeforeRouting(CursorIcon::ColResize), false);
h.move_over(Leaf::Top);
assert_eq!(
h.cursor(),
CursorIcon::Pointer,
"the innermost widget the route reached spoke last"
);
h.dispatch(PointerPhase::Move, 50.0, 200.0);
assert_eq!(h.cursor(), CursorIcon::ColResize);
let mut h = CursorHarness::new(ContainerCursor::AfterRouting(CursorIcon::NotAllowed), false);
h.move_over(Leaf::Top);
assert_eq!(
h.cursor(),
CursorIcon::NotAllowed,
"a container overriding its children asks after routing"
);
}
#[test]
fn only_a_pointer_move_re_resolves_the_cursor() {
let mut h = CursorHarness::new(ContainerCursor::Silent, false);
h.move_over(Leaf::Top);
assert_eq!(h.cursor(), CursorIcon::Pointer);
h.dispatch(PointerPhase::Down, 50.0, 15.0);
assert_eq!(h.cursor(), CursorIcon::Pointer, "a Down leaves it standing");
h.dispatch(PointerPhase::Up, 50.0, 15.0);
assert_eq!(h.cursor(), CursorIcon::Pointer, "an Up leaves it standing");
h.root.event(
&mut h.state,
&InputEvent::Scroll {
position: Point::new(50.0, 15.0),
delta: crate::event::ScrollDelta::Lines(0.0, 1.0),
},
);
assert_eq!(h.cursor(), CursorIcon::Pointer);
h.root.event(&mut h.state, &InputEvent::Housekeeping);
assert_eq!(h.cursor(), CursorIcon::Pointer);
}
#[test]
fn a_captured_drag_keeps_the_capturing_widgets_cursor() {
let mut h = CursorHarness::new(ContainerCursor::Silent, true);
h.move_over(Leaf::Top);
assert_eq!(h.cursor(), CursorIcon::Pointer);
h.dispatch(PointerPhase::Down, 50.0, 15.0);
assert!(h.root.is_pointer_captured());
assert_eq!(h.cursor(), CursorIcon::Pointer);
h.dispatch(PointerPhase::Move, 50.0, 16.0);
assert_eq!(h.cursor(), CursorIcon::Grabbing);
h.dispatch(PointerPhase::Move, 50.0, 45.0);
assert_eq!(
h.cursor(),
CursorIcon::Grabbing,
"a captured drag keeps its own cursor outside its bounds"
);
h.dispatch(PointerPhase::Move, 50.0, 500.0);
assert_eq!(h.cursor(), CursorIcon::Grabbing);
h.dispatch(PointerPhase::Up, 50.0, 45.0);
h.move_over(Leaf::Bottom);
assert_eq!(h.cursor(), CursorIcon::Text);
}