///|
/// Hover observer modifier: emits a message when the pointer enters or leaves
/// the wrapped region.
///
/// Deliberately transparent like `OnSecondaryTapModifier` and
/// `OnTapWithFrameModifier`: hover feedback is an observer, not a control. It
/// contributes no semantics role, no semantics action, and no focusability, so
/// a hoverable container does not announce itself as a Button or join focus
/// traversal. It never captures the pointer and never touches press state.
///
/// ## Why this records the transition in `ViewStateContext::hovered`
///
/// Host backends never emit a leave event, and a `Move` landing outside the
/// element is not delivered to it by hit-testing. A leave reaches the element
/// only as the runtime's synthetic `PointerPhase::Exit`, and the runtime
/// synthesizes that `Exit` exclusively for a subtree that still reports
/// `hovered`/`pressed`. An observer that tracked its previous hover state in a
/// private slot would therefore never receive the leave event at all.
///
/// (The runtime-side predicate that decides this is intentionally not named
/// here: `moui/core` is checked for runtime-internal identifiers as plain
/// substrings, comments included, so spelling it out would trip
/// `validate-api-surface`.)
///
/// Recording the transition in `ctx.state.hovered` is what keeps this
/// modifier's own leave path reachable. It is also the established idiom:
/// `OnTapModifier` maintains the same field through
/// `recognize_press_tap`/`pressable_state_to_view_state`.
priv struct OnHoverModifier[Msg] {
handler : (Bool) -> Msg
}
///|
/// `true` when this pointer event means the pointer is over the region.
///
/// Mirrors `recognize_press_tap`'s phase mapping: the runtime's synthetic
/// `Exit` and a `Cancel` both mean "no longer hovering"; every other phase is
/// decided by geometry against the element frame.
fn hover_event_inside(event : PointerEvent, frame : Rect) -> Bool {
match event.phase {
Exit | Cancel => false
_ => frame.contains(event.position)
}
}
///|
fn[Msg] OnHoverModifier::to_any_modifier(
self : OnHoverModifier[Msg],
) -> AnyModifier[Msg] {
let handler = self.handler
{
..noop_modifier(),
identity_kind: "OnHover",
event: fn(ctx) {
match ctx.event {
Pointer(event) => {
let hovered = hover_event_inside(event, ctx.frame)
// Transitions only. The runtime delivers a `Move` for every pointer
// sample inside the element, and hit-testing keeps sending them while
// the pointer stays put, so re-emitting on each one would flood the
// app's update loop with redundant hover messages.
if hovered == ctx.state.hovered {
ViewEventResult::ignored()
} else {
{
changed: true,
activated: false,
focused: false,
// An observer owns no gesture. Capturing here would route later
// presses to this element and mis-attribute clicks and drags
// inside the hoverable region.
captured: false,
state: Some({ ..ctx.state, hovered, }),
text_control: None,
// Repaint only on a real transition; a continuous pointer stream
// inside the element must not keep the frame dirty.
dirty: ViewPaintDirty,
messages: [handler(hovered)],
}
}
}
_ => ViewEventResult::ignored()
}
},
}
}