use super::*;
#[derive(Default)]
struct HoverProbe {
painted_hovered: Cell<bool>,
event_hovered: Cell<bool>,
}
struct HoverLeaf {
captures: bool,
latches: bool,
hovered: bool,
probe: Rc<HoverProbe>,
}
impl crate::widget::Widget for HoverLeaf {
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) {
self.probe.painted_hovered.set(ctx.is_hovered());
if self.latches {
self.hovered = ctx.is_hovered();
}
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
self.probe.event_hovered.set(ctx.is_hovered());
let InputEvent::Pointer(p) = event else {
return EventResult::Ignored;
};
match p.phase {
PointerPhase::Move => {
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 {
ctx.claim_hover();
}
if self.latches && self.hovered != inside {
self.hovered = inside;
ctx.request_redraw();
}
EventResult::Ignored
}
PointerPhase::Down => {
if self.captures {
ctx.capture_pointer();
}
EventResult::Handled
}
_ => EventResult::Ignored,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum GroupClaim {
Never,
AfterRouting,
BeforeRouting,
}
struct HoverGroup {
probe: Rc<HoverProbe>,
claims: GroupClaim,
child: Option<crate::widget::ChildPod>,
}
impl HoverGroup {
fn claims_on(&self, ctx: &EventCtx, event: &InputEvent) -> bool {
let InputEvent::Pointer(p) = event else {
return false;
};
let size = ctx.size();
matches!(p.phase, PointerPhase::Move)
&& p.position.x >= 0.0
&& p.position.y >= 0.0
&& p.position.x < size.width
&& p.position.y < size.height
}
}
impl crate::widget::Widget for HoverGroup {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
match &mut self.child {
Some(child) => {
let size = child.layout_child(ctx, bc);
child.set_origin(Point::ZERO);
size
}
None => bc.constrain(Size::new(100.0, 30.0)),
}
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
self.probe.painted_hovered.set(ctx.is_hovered());
if let Some(child) = &mut self.child {
child.paint_child(ctx, scene);
}
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
self.probe.event_hovered.set(ctx.is_hovered());
let claims = self.claims != GroupClaim::Never && self.claims_on(ctx, event);
if claims && self.claims == GroupClaim::BeforeRouting {
ctx.claim_hover();
}
let result = match &mut self.child {
Some(child) => child.event_child(ctx, event),
None => EventResult::Ignored,
};
if claims && self.claims == GroupClaim::AfterRouting {
ctx.claim_hover();
}
result
}
}
struct HoverPair {
top: crate::widget::ChildPod,
bottom: crate::widget::ChildPod,
}
impl crate::widget::Widget for HoverPair {
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) {
self.top.paint_child(ctx, scene);
self.bottom.paint_child(ctx, scene);
}
fn event(&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 HoverFixture {
captures: bool,
latches: bool,
nested: bool,
group_claims: GroupClaim,
drop_claimant: bool,
silent_reconciler: bool,
}
struct HoverPairView {
top: Rc<HoverProbe>,
bottom: Rc<HoverProbe>,
group: Rc<HoverProbe>,
fixture: HoverFixture,
}
impl HoverPairView {
fn leaf(&self, probe: &Rc<HoverProbe>) -> Box<dyn crate::widget::Widget> {
Box::new(HoverLeaf {
captures: self.fixture.captures,
latches: self.fixture.latches,
hovered: false,
probe: probe.clone(),
})
}
}
impl View<()> for HoverPairView {
type Element = HoverPair;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> HoverPair {
let top: Box<dyn crate::widget::Widget> = if self.fixture.nested {
Box::new(HoverGroup {
probe: self.group.clone(),
claims: self.fixture.group_claims,
child: Some(crate::widget::ChildPod::new(self.leaf(&self.top))),
})
} else {
self.leaf(&self.top)
};
HoverPair {
top: crate::widget::ChildPod::new(top),
bottom: crate::widget::ChildPod::new(self.leaf(&self.bottom)),
}
}
fn rebuild(&self, p: &Self, e: &mut HoverPair, _c: &mut BuildCtx<'_>) -> ChangeFlags {
let removes_child = self.fixture.drop_claimant && !p.fixture.drop_claimant;
if !removes_child {
return ChangeFlags::NONE;
}
let group = e
.top
.widget_mut()
.downcast_mut::<HoverGroup>()
.expect("the drop-claimant fixture is the nested one");
group.child = None;
if self.fixture.silent_reconciler {
ChangeFlags::NONE
} else {
ChangeFlags::LAYOUT | ChangeFlags::PAINT
}
}
}
struct HoverHarness {
root: RenderRoot<(), HoverPairView>,
top: Rc<HoverProbe>,
bottom: Rc<HoverProbe>,
group: Rc<HoverProbe>,
state: (),
fixture: HoverFixture,
}
impl HoverHarness {
fn new(captures: bool) -> Self {
Self::build(HoverFixture {
captures,
latches: true,
nested: false,
group_claims: GroupClaim::Never,
drop_claimant: false,
silent_reconciler: false,
})
}
fn without_consumer_flag() -> Self {
Self::build(HoverFixture {
captures: false,
latches: false,
nested: false,
group_claims: GroupClaim::Never,
drop_claimant: false,
silent_reconciler: false,
})
}
fn nested() -> Self {
Self::build(HoverFixture {
captures: false,
latches: true,
nested: true,
group_claims: GroupClaim::Never,
drop_claimant: false,
silent_reconciler: false,
})
}
fn nested_group_claiming(claims: GroupClaim) -> Self {
Self::build(HoverFixture {
captures: false,
latches: true,
nested: true,
group_claims: claims,
drop_claimant: false,
silent_reconciler: false,
})
}
fn nested_group_with_silent_reconciler() -> Self {
Self::build(HoverFixture {
captures: false,
latches: true,
nested: true,
group_claims: GroupClaim::AfterRouting,
drop_claimant: false,
silent_reconciler: true,
})
}
fn build(fixture: HoverFixture) -> Self {
let top = Rc::new(HoverProbe::default());
let bottom = Rc::new(HoverProbe::default());
let group = Rc::new(HoverProbe::default());
let mut root: RenderRoot<(), HoverPairView> = RenderRoot::new();
let mut state = ();
let (t, b, g) = (top.clone(), bottom.clone(), group.clone());
root.rebuild(
&mut move |_: &mut ()| HoverPairView {
top: t.clone(),
bottom: b.clone(),
group: g.clone(),
fixture,
},
&mut state,
);
root.layout(Size::new(100.0, 60.0));
HoverHarness {
root,
top,
bottom,
group,
state,
fixture,
}
}
fn rebuild_without_claimant(&mut self) -> ChangeFlags {
self.fixture.drop_claimant = true;
self.rebuild_current()
}
fn rebuild_unchanged(&mut self) -> ChangeFlags {
self.rebuild_current()
}
fn rebuild_current(&mut self) -> ChangeFlags {
let fixture = self.fixture;
let (t, b, g) = (self.top.clone(), self.bottom.clone(), self.group.clone());
let flags = self.root.rebuild(
&mut move |_: &mut ()| HoverPairView {
top: t.clone(),
bottom: b.clone(),
group: g.clone(),
fixture,
},
&mut self.state,
);
self.root.layout(Size::new(100.0, 60.0));
flags
}
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) -> EventOutcome {
match leaf {
Leaf::Top => self.dispatch(PointerPhase::Move, 50.0, 15.0),
Leaf::Bottom => self.dispatch(PointerPhase::Move, 50.0, 45.0),
}
}
fn paint(&mut self) {
let mut scene = RecordingScene::default();
self.root.paint(&mut scene, FrameTime::ZERO);
}
fn painted(&mut self) -> (bool, bool) {
self.paint();
(
self.top.painted_hovered.get(),
self.bottom.painted_hovered.get(),
)
}
}
#[test]
fn an_uncaptured_move_claims_hover_and_paint_reports_it() {
let mut h = HoverHarness::new(false);
assert!(!h.root.is_hover_active(), "nothing is hovered at rest");
assert_eq!(h.painted(), (false, false));
let outcome = h.move_over(Leaf::Top);
assert!(h.root.is_hover_active(), "the claim reached the root");
assert!(
!outcome.handled,
"a hovering widget need not consume the move"
);
assert_eq!(
h.painted(),
(true, false),
"the claimant reads as hovered, its sibling does not"
);
h.dispatch(PointerPhase::Move, 60.0, 20.0);
assert_eq!(h.painted(), (true, false));
}
#[test]
fn a_second_widgets_claim_clears_the_first_and_asks_for_a_repaint() {
let mut h = HoverHarness::new(false);
h.move_over(Leaf::Top);
assert_eq!(h.painted(), (true, false));
h.move_over(Leaf::Bottom);
assert!(h.root.is_hover_active());
assert_eq!(
h.painted(),
(false, true),
"the previous claimant lost its link when the new one recorded"
);
let outcome = h.dispatch(PointerPhase::Move, 50.0, 200.0);
assert!(
!h.root.is_hover_active(),
"a move claiming nothing ends the hover"
);
assert!(
outcome.needs_redraw,
"the widget that lost hover cannot ask for the repaint itself"
);
assert_eq!(h.painted(), (false, false));
let settled = h.dispatch(PointerPhase::Move, 50.0, 200.0);
assert!(
!settled.needs_redraw,
"an already-hoverless move requests nothing"
);
}
#[test]
fn a_captured_move_cannot_claim_hover() {
let mut h = HoverHarness::new(true);
h.dispatch(PointerPhase::Down, 50.0, 15.0);
assert!(h.root.is_pointer_captured());
assert!(!h.root.is_hover_active());
h.dispatch(PointerPhase::Move, 50.0, 16.0);
assert!(
!h.root.is_hover_active(),
"a captured pointer never creates hover"
);
assert_eq!(h.painted(), (false, false));
h.dispatch(PointerPhase::Move, 50.0, 45.0);
assert!(!h.root.is_hover_active());
assert_eq!(h.painted(), (false, false));
h.dispatch(PointerPhase::Up, 50.0, 15.0);
h.move_over(Leaf::Top);
assert!(h.root.is_hover_active());
assert_eq!(h.painted(), (true, false));
}
#[test]
fn a_down_up_or_cancel_ends_the_hover() {
for ending in [PointerPhase::Down, PointerPhase::Up, PointerPhase::Cancel] {
let mut h = HoverHarness::new(false);
h.move_over(Leaf::Top);
assert!(h.root.is_hover_active());
let outcome = h.dispatch(ending, 50.0, 15.0);
assert!(
!h.root.is_hover_active(),
"{ending:?} ends the hover link outright"
);
assert!(
outcome.needs_redraw,
"{ending:?} that dropped a hover asks for the repaint"
);
assert_eq!(h.painted(), (false, false));
}
}
#[test]
fn a_non_pointer_pass_leaves_a_live_hover_standing() {
let mut h = HoverHarness::new(false);
h.move_over(Leaf::Top);
assert_eq!(h.painted(), (true, false));
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!(h.root.is_hover_active());
h.root.event(&mut h.state, &InputEvent::Housekeeping);
assert!(h.root.is_hover_active());
assert_eq!(h.painted(), (true, false));
}
#[test]
fn a_container_on_the_claim_path_reads_hovered_and_a_sibling_does_not() {
let mut h = HoverHarness::nested();
h.move_over(Leaf::Top);
h.paint();
assert!(h.top.painted_hovered.get(), "the claimant itself");
assert!(
h.group.painted_hovered.get(),
"the container enclosing the claimant is on the path too"
);
assert!(
!h.bottom.painted_hovered.get(),
"a sibling leaf is off the path"
);
h.move_over(Leaf::Bottom);
h.paint();
assert!(!h.group.painted_hovered.get());
assert!(!h.top.painted_hovered.get());
assert!(h.bottom.painted_hovered.get());
h.move_over(Leaf::Top);
assert!(
!h.group.event_hovered.get(),
"the event read reports the previous pass, like the leaf's"
);
h.move_over(Leaf::Top);
assert!(
h.group.event_hovered.get(),
"the container observes the link its child holds"
);
}
#[test]
fn an_ancestor_claiming_after_routing_loses_to_its_child_and_still_reads_hovered() {
let mut h = HoverHarness::nested_group_claiming(GroupClaim::AfterRouting);
let gain = h.move_over(Leaf::Top);
h.paint();
assert!(
h.top.painted_hovered.get(),
"the child under the pointer holds the link"
);
assert!(
h.group.painted_hovered.get(),
"the container is on that path, so it reads hovered too"
);
assert!(
!h.bottom.painted_hovered.get(),
"a sibling leaf is off the path"
);
assert!(gain.needs_redraw, "hover gain repaints");
let settled = h.dispatch(PointerPhase::Move, 60.0, 20.0);
assert!(!settled.needs_redraw, "an unchanged flag asks for nothing");
h.paint();
assert!(h.top.painted_hovered.get());
assert!(h.group.painted_hovered.get());
}
#[test]
fn an_ancestor_claiming_before_routing_starves_its_subtree() {
let mut h = HoverHarness::nested_group_claiming(GroupClaim::BeforeRouting);
h.move_over(Leaf::Top);
h.paint();
assert!(
!h.top.painted_hovered.get(),
"the ancestor's earlier claim made its child ineligible"
);
assert!(
h.group.painted_hovered.get(),
"the outermost claimant is the one holding the link here"
);
assert!(!h.bottom.painted_hovered.get());
let again = h.dispatch(PointerPhase::Move, 60.0, 20.0);
assert!(
again.needs_redraw,
"the starved child re-flips its flag on every move"
);
h.paint();
assert!(!h.top.painted_hovered.get(), "and still paints no chrome");
}
#[test]
fn hover_gain_is_repainted_by_the_consumers_own_flag() {
let mut h = HoverHarness::new(false);
let gain = h.move_over(Leaf::Top);
assert!(gain.needs_redraw, "hover gain repaints");
assert_eq!(h.painted(), (true, false));
let settled = h.dispatch(PointerPhase::Move, 60.0, 20.0);
assert!(!settled.needs_redraw, "an unchanged flag asks for nothing");
let handoff = h.move_over(Leaf::Bottom);
assert!(handoff.needs_redraw, "a claimant handoff repaints");
assert_eq!(
h.painted(),
(false, true),
"one frame settles both the loss and the gain"
);
}
#[test]
fn a_claimant_without_its_own_flag_gets_only_the_loss_frame() {
let mut h = HoverHarness::without_consumer_flag();
let gain = h.move_over(Leaf::Top);
assert!(h.root.is_hover_active());
assert!(!gain.needs_redraw, "no widget asked, and the root cannot");
let handoff = h.move_over(Leaf::Bottom);
assert!(h.root.is_hover_active());
assert!(!handoff.needs_redraw, "a handoff is `true` → `true` here");
let loss = h.dispatch(PointerPhase::Move, 50.0, 200.0);
assert!(!h.root.is_hover_active());
assert!(loss.needs_redraw, "the root manufactures the loss frame");
}
#[test]
fn a_rebuild_that_removes_the_claimant_ends_the_hover() {
let mut h = HoverHarness::nested_group_claiming(GroupClaim::AfterRouting);
h.move_over(Leaf::Top);
h.paint();
assert!(h.root.is_hover_active());
assert!(h.top.painted_hovered.get(), "the claimant holds the link");
assert!(
h.group.painted_hovered.get(),
"its container is on the path"
);
let flags = h.rebuild_without_claimant();
assert!(
!h.root.is_hover_active(),
"the mirror cannot outlive the widget it described"
);
assert!(
flags.contains(ChangeFlags::PAINT),
"the correction states its own need for a frame, whatever the \
reconciler that dropped the claimant reported"
);
h.paint();
assert!(
!h.group.painted_hovered.get(),
"the surviving ancestor lost the link with its child"
);
assert!(
!h.bottom.painted_hovered.get(),
"and the sibling never had it"
);
h.move_over(Leaf::Top);
assert!(h.root.is_hover_active(), "the next move re-claims");
h.paint();
assert!(
h.group.painted_hovered.get(),
"the container is the claimant now"
);
}
#[test]
fn a_silent_reconcilers_removal_still_carries_its_own_repaint() {
let mut h = HoverHarness::nested_group_with_silent_reconciler();
h.move_over(Leaf::Top);
assert!(h.root.is_hover_active());
let flags = h.rebuild_without_claimant();
assert!(!h.root.is_hover_active(), "the link ends either way");
assert_eq!(
flags,
ChangeFlags::PAINT,
"and the frame it needs comes from the release, not from the diff"
);
}
#[test]
fn a_rebuild_that_keeps_the_claimant_leaves_the_hover_standing() {
let mut h = HoverHarness::nested_group_claiming(GroupClaim::AfterRouting);
h.move_over(Leaf::Top);
h.move_over(Leaf::Bottom);
assert!(h.root.is_hover_active());
h.rebuild_without_claimant();
assert!(
h.root.is_hover_active(),
"dropping a stale stamp is not a severance"
);
h.paint();
assert!(
h.bottom.painted_hovered.get(),
"the widget actually under the pointer keeps its chrome"
);
}
#[test]
fn another_roots_dying_claimant_cannot_end_this_roots_hover() {
let mut first = HoverHarness::nested_group_claiming(GroupClaim::AfterRouting);
let mut second = HoverHarness::nested_group_claiming(GroupClaim::AfterRouting);
first.move_over(Leaf::Top);
second.move_over(Leaf::Top);
assert_eq!(
first.root.hover_epoch, second.root.hover_epoch,
"the two roots' epochs collide, which is the whole premise"
);
assert!(first.root.is_hover_active());
assert!(second.root.is_hover_active());
drop(first);
second.rebuild_unchanged();
assert!(
second.root.is_hover_active(),
"a mark another root raised is none of this root's business"
);
second.paint();
assert!(
second.top.painted_hovered.get(),
"the widget under the pointer keeps its chrome"
);
}
#[test]
fn event_ctx_hover_reports_the_previous_pass_not_this_ones_claim() {
let mut h = HoverHarness::new(false);
h.move_over(Leaf::Top);
assert!(
!h.top.event_hovered.get(),
"a fresh claim does not retroactively flip `is_hovered`"
);
h.move_over(Leaf::Top);
assert!(
h.top.event_hovered.get(),
"the link recorded last pass is visible to this pass's handler"
);
}