ailloli_ui_runtime/input/
input_router.rs

1//! Stateful pointer, keyboard, focus, and activation routing for a retained tree.
2
3use std::collections::BTreeMap;
4
5use ailloli_ui_core::event::pointer::{
6    ActivationKind, PointerButton, PointerEvent, PointerId, PointerSource,
7};
8use ailloli_ui_core::event::{FocusEvent, Key as KeyboardKey, KeyState, Modifiers, NamedKey};
9use ailloli_ui_core::{ElementId, Event, LogicalWindowId, Point, Rect};
10
11use super::{
12    absolute_paint_bounds, dispatch_event_bubbling, dispatch_event_envelope_bubbling,
13    dispatch_event_to_target, hit_test_overlay_target, hit_test_target, ActivationPolicy,
14    EventEnvelope, EventMeta, FocusManager, FocusPolicy, HitTestEngine, HoverCursorRole, InputRole,
15};
16use crate::app::{PresentationGeneration, RuntimeHandle};
17use crate::element::{ElementKind, ElementTree, Key};
18use crate::input::ChromeAction;
19use crate::popup::{PopupIntent, PopupMountPolicy, HEADLESS_POPUP_WINDOW_ID};
20
21/// Whether `element_id` should show pointer interaction when hit target is `target`.
22///
23/// Avoids lighting all sibling buttons when the hit is an empty flex `Row`/`Column` gap.
24fn widget_paint_pointer_match<A: 'static>(
25    tree: &ElementTree<A>,
26    element_id: ElementId,
27    target: ElementId,
28) -> bool {
29    if element_id == target {
30        return true;
31    }
32    if tree.is_ancestor_of(element_id, target) {
33        return true;
34    }
35    if tree.parent_of(element_id) == Some(target) {
36        return !flex_row_or_column_widget(tree, target);
37    }
38    false
39}
40
41/// Detects built-in flex containers by their stable diagnostic name.
42fn flex_row_or_column_widget<A: 'static>(tree: &ElementTree<A>, id: ElementId) -> bool {
43    tree.get(id).is_some_and(|el| {
44        matches!(
45            &el.kind,
46            ElementKind::Widget(w) if matches!(w.debug_name(), "Row" | "Column")
47        )
48    })
49}
50
51/// Legacy/provider-facing input action summary.
52///
53/// Runtime event routing primarily dispatches widget events directly; this enum
54/// remains a compact description for pointer target/button and window-chrome
55/// actions. Positions are logical pixels.
56///
57/// # Examples
58///
59/// ```
60/// use ailloli_ui_core::{ElementId, Point};
61/// use ailloli_ui_runtime::input::Action;
62/// let action = Action::PointerButton { target: Some(ElementId(1)), pos: Point::new(2.0, 3.0), pressed: true };
63/// assert!(matches!(action, Action::PointerButton { pressed: true, .. }));
64/// ```
65#[derive(Debug, Clone, PartialEq)]
66pub enum Action {
67    /// Mouse/pointer hover target changed.
68    PointerTargetChanged {
69        /// Previous target, or `None` outside all elements.
70        old: Option<ElementId>,
71        /// New target, or `None` outside all elements.
72        new: Option<ElementId>,
73    },
74    /// Pointer button transition at a logical position.
75    PointerButton {
76        /// Hit/captured target, or `None` when no element owns it.
77        target: Option<ElementId>,
78        /// Window-space logical-pixel position.
79        pos: Point,
80        /// `true` for press and `false` for release.
81        pressed: bool,
82    },
83    /// Provider-neutral window chrome request.
84    Chrome(ChromeAction),
85}
86
87/// Paint-time interaction flags resolved for one widget.
88///
89/// `focused` is strict ownership; `focus_within` additionally includes
90/// ancestors. Hover and press can be inherited through the hit-test chain for
91/// widget painting.
92///
93/// # Examples
94///
95/// ```
96/// use ailloli_ui_runtime::input::InputInteraction;
97/// let interaction = InputInteraction { focused: true, focus_within: true, hovered: false, pressed: false };
98/// assert!(interaction.focused && interaction.focus_within);
99/// ```
100#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
101pub struct InputInteraction {
102    /// Whether this exact element owns keyboard focus.
103    pub focused: bool,
104    /// `true` when this element or one of its descendants owns keyboard focus.
105    ///
106    /// This is paint-only metadata. It does not change the actual focus target
107    /// and leaves `focused` strict, so existing control focus rings keep their
108    /// current behavior.
109    pub focus_within: bool,
110    /// Whether pointer hit state paints this widget as hovered.
111    pub hovered: bool,
112    /// Whether active pointer capture/press paints this widget as pressed.
113    pub pressed: bool,
114}
115
116/// Copyable strict focus/hover/press targets for one pointer view.
117///
118/// `hovered` and `pressed` belong to the selected pointer ID; `focused` is
119/// global to the router/window and is identical across pointer snapshots.
120///
121/// # Examples
122///
123/// ```
124/// use ailloli_ui_core::ElementId;
125/// use ailloli_ui_runtime::input::InputSnapshot;
126/// let snapshot = InputSnapshot { focused: Some(ElementId(1)), hovered: None, pressed: None };
127/// assert_eq!(snapshot.interaction_for(ElementId(1)).focused, true);
128/// ```
129#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
130pub struct InputSnapshot {
131    /// Strict keyboard-focus owner.
132    pub focused: Option<ElementId>,
133    /// Strict pointer hover target.
134    pub hovered: Option<ElementId>,
135    /// Strict pointer press target.
136    pub pressed: Option<ElementId>,
137}
138
139/// Resolves snapshot targets into per-element paint interaction.
140impl InputSnapshot {
141    /// Resolves strict interaction flags for `id` without consulting a tree.
142    ///
143    /// `focus_within` equals strict `focused` here. Use
144    /// [`Self::interaction_for_widget_paint`] to include ancestor/child paint
145    /// propagation.
146    ///
147    /// # Examples
148    ///
149    /// ```
150    /// use ailloli_ui_core::ElementId;
151    /// use ailloli_ui_runtime::input::InputSnapshot;
152    /// let snapshot = InputSnapshot { focused: Some(ElementId(1)), hovered: Some(ElementId(2)), pressed: None };
153    /// let interaction = snapshot.interaction_for(ElementId(2));
154    /// assert!(!interaction.focused && interaction.hovered && !interaction.pressed);
155    /// ```
156    pub fn interaction_for(self, id: ElementId) -> InputInteraction {
157        InputInteraction {
158            focused: self.focused == Some(id),
159            focus_within: self.focused == Some(id),
160            hovered: self.hovered == Some(id),
161            pressed: self.pressed == Some(id),
162        }
163    }
164
165    /// Interaction state for paint: walks the hit-test chain without false sibling hovers.
166    ///
167    /// - Parent (e.g. `Button`) hovers when the pointer hits a child (`Icon`).
168    /// - Child hovers when the hit is on a non-flex parent (button padding).
169    /// - Hit on empty `Row`/`Column` gaps does not hover all sibling buttons.
170    ///
171    /// Focus remains strict on the focused element only.
172    /// `focus_within` follows stored parent links. Malformed parent cycles can
173    /// therefore inherit the nontermination behavior of `ElementTree::is_ancestor_of`.
174    ///
175    /// # Examples
176    ///
177    /// ```
178    /// use ailloli_ui_core::ElementId;
179    /// use ailloli_ui_runtime::element::{ElementKind, ElementTree};
180    /// use ailloli_ui_runtime::input::InputSnapshot;
181    /// let mut tree = ElementTree::<()>::new();
182    /// let root = tree.create_element(ElementKind::Empty, None, None);
183    /// let child = tree.create_element(ElementKind::Empty, None, Some(root));
184    /// let interaction = InputSnapshot { focused: Some(child), hovered: None, pressed: None }
185    ///     .interaction_for_widget_paint(&tree, root);
186    /// assert!(!interaction.focused && interaction.focus_within);
187    /// ```
188    pub fn interaction_for_widget_paint<A: 'static>(
189        self,
190        tree: &crate::element::ElementTree<A>,
191        id: ElementId,
192    ) -> InputInteraction {
193        let mut out = self.interaction_for(id);
194        if let Some(focused) = self.focused {
195            out.focus_within = tree.is_ancestor_of(id, focused);
196        }
197        if let Some(h) = self.hovered {
198            out.hovered = widget_paint_pointer_match(tree, id, h);
199        }
200        if let Some(p) = self.pressed {
201            out.pressed = widget_paint_pointer_match(tree, id, p);
202        }
203        out
204    }
205}
206
207/// Observable result of routing one provider-neutral event.
208///
209/// Dispatch and visual interaction changes are independent: a keyboard event
210/// can dispatch without requiring redraw, while stale hover cleanup can redraw
211/// without dispatching the incoming event.
212///
213/// # Examples
214///
215/// ```
216/// use ailloli_ui_runtime::input::RouteOutcome;
217/// let outcome = RouteOutcome { interaction_changed: true, event_dispatched: false };
218/// assert!(outcome.needs_redraw());
219/// ```
220#[derive(Debug, Clone, Default, PartialEq, Eq)]
221pub struct RouteOutcome {
222    /// Whether focus/hover/press/role or popup visual state changed.
223    pub interaction_changed: bool,
224    /// Whether the event was delivered or consumed by an authority.
225    pub event_dispatched: bool,
226}
227
228/// Host redraw interpretation for routed outcomes.
229impl RouteOutcome {
230    /// Returns exactly `interaction_changed`.
231    ///
232    /// Widget-triggered runtime invalidation is tracked separately and may
233    /// require redraw even when this returns `false`.
234    ///
235    /// # Examples
236    ///
237    /// ```
238    /// use ailloli_ui_runtime::input::RouteOutcome;
239    /// assert!(!RouteOutcome { interaction_changed: false, event_dispatched: true }.needs_redraw());
240    /// ```
241    pub fn needs_redraw(&self) -> bool {
242        self.interaction_changed
243    }
244}
245
246/// Signature used to detect identity changes behind a reused numeric ID.
247#[derive(Debug, Clone, PartialEq, Eq)]
248struct TargetSignature {
249    /// Optional retained key distinguishing reused element identities.
250    key: Option<Key>,
251    /// Target family and policies relevant to routing identity.
252    kind: TargetKind,
253}
254
255/// Identity-relevant target kind and widget policies.
256#[derive(Debug, Clone, PartialEq, Eq)]
257enum TargetKind {
258    /// Structural element with no component or widget behavior.
259    Empty,
260    /// Retained component boundary without direct input policies.
261    Component,
262    /// Interactive widget and its identity-relevant routing policies.
263    Widget {
264        /// Stable diagnostic widget type name.
265        debug_name: &'static str,
266        /// Keyboard focus behavior for pointer activation.
267        focus_policy: FocusPolicy,
268        /// Press/release activation behavior.
269        activation_policy: ActivationPolicy,
270    },
271}
272
273/// Per-pointer hover, capture, press, activation, and semantic state.
274#[derive(Debug, Default, Clone)]
275struct PointerRouteState {
276    /// Current hit-test target for this pointer.
277    hovered: Option<ElementId>,
278    /// Target on which the active button gesture began.
279    pressed: Option<ElementId>,
280    /// Explicit pointer-capture owner overriding hit testing.
281    capture: Option<ElementId>,
282    /// Identity signature paired with [`Self::hovered`].
283    hovered_signature: Option<TargetSignature>,
284    /// Identity signature paired with [`Self::pressed`].
285    pressed_signature: Option<TargetSignature>,
286    /// Identity signature paired with [`Self::capture`].
287    capture_signature: Option<TargetSignature>,
288    /// Semantic input role paired with [`Self::hovered`].
289    hovered_input_role: InputRole,
290    /// Semantic input role paired with [`Self::pressed`].
291    pressed_input_role: InputRole,
292    /// Semantic input role paired with [`Self::capture`].
293    capture_input_role: InputRole,
294    /// Activation semantics from the latest sample for this pointer.
295    activation: ActivationKind,
296    /// An outside-dismiss press is consumed through its matching release so
297    /// a control behind the popup cannot activate on release alone.
298    popup_consumed_gesture: bool,
299    /// Last finite logical position received for cancellation synthesis.
300    last_position: Point,
301    /// Last modifier snapshot received for cancellation synthesis.
302    last_modifiers: Modifiers,
303    /// Last host envelope metadata, preserving pointer identity when present.
304    last_event_meta: Option<EventMeta>,
305}
306
307/// Compact arguments for a pointer button transition.
308#[derive(Debug, Clone, Copy)]
309struct PointerButtonInput {
310    /// Pointer whose button changed state.
311    pointer_id: PointerId,
312    /// Event location in logical window coordinates.
313    pos: Point,
314    /// Button that transitioned.
315    button: PointerButton,
316    /// `true` for a press and `false` for a release.
317    pressed: bool,
318}
319
320/// Snapshot and liveness helpers for one pointer state record.
321impl PointerRouteState {
322    /// Copies strict targets and combines them with global focus.
323    fn snapshot(&self, focused: Option<ElementId>) -> InputSnapshot {
324        InputSnapshot {
325            focused,
326            hovered: self.hovered,
327            pressed: self.pressed,
328        }
329    }
330
331    /// Returns whether this pointer has no retained routing responsibility.
332    fn is_empty(&self) -> bool {
333        self.hovered.is_none()
334            && self.pressed.is_none()
335            && self.capture.is_none()
336            && !self.popup_consumed_gesture
337    }
338}
339
340/// Per-window input state: hit-test, hover, press, focus, and event dispatch.
341///
342/// Pointer states are isolated by [`PointerId`] in deterministic key order.
343/// Mouse convenience methods use [`PointerId::MOUSE`]. The router owns no
344/// platform handles and remains provider-neutral; callbacks execute
345/// synchronously on the UI thread.
346///
347/// # Examples
348///
349/// ```
350/// use ailloli_ui_runtime::input::InputRouter;
351/// let router = InputRouter::default();
352/// assert!(router.hovered().is_none() && router.focused().is_none());
353/// ```
354#[derive(Debug, Default, Clone)]
355pub struct InputRouter {
356    /// Stateless rectangle hit-test helper.
357    pub hit_test: HitTestEngine,
358    /// Low-level strict keyboard-focus store.
359    pub focus: FocusManager,
360    /// Per-pointer routing records ordered by stable pointer identity.
361    pointers: BTreeMap<PointerId, PointerRouteState>,
362    /// Identity signature paired with the current keyboard focus owner.
363    focused_signature: Option<TargetSignature>,
364    /// Semantic input role paired with the current focus owner.
365    focused_input_role: InputRole,
366}
367
368/// Focus, semantic-role, pointer, popup, and event-routing operations.
369impl InputRouter {
370    /// Returns the mouse hover target, or `None`.
371    ///
372    /// # Examples
373    ///
374    /// ```
375    /// use ailloli_ui_runtime::input::InputRouter;
376    /// assert_eq!(InputRouter::default().hovered(), None);
377    /// ```
378    pub fn hovered(&self) -> Option<ElementId> {
379        self.hovered_for(PointerId::MOUSE)
380    }
381
382    /// Returns one pointer's hover target, or `None` for unknown/idle IDs.
383    ///
384    /// # Examples
385    ///
386    /// ```
387    /// use ailloli_ui_core::event::PointerId;
388    /// use ailloli_ui_runtime::input::InputRouter;
389    /// assert_eq!(InputRouter::default().hovered_for(PointerId::MOUSE), None);
390    /// ```
391    pub fn hovered_for(&self, pointer_id: PointerId) -> Option<ElementId> {
392        self.pointers
393            .get(&pointer_id)
394            .and_then(|state| state.hovered)
395    }
396
397    /// Returns the strict keyboard-focus owner.
398    ///
399    /// # Examples
400    ///
401    /// ```
402    /// use ailloli_ui_runtime::input::InputRouter;
403    /// assert_eq!(InputRouter::default().focused(), None);
404    /// ```
405    pub fn focused(&self) -> Option<ElementId> {
406        self.focus.focused()
407    }
408
409    /// Returns focus plus mouse hover/press strict targets.
410    ///
411    /// # Examples
412    ///
413    /// ```
414    /// use ailloli_ui_runtime::input::{InputRouter, InputSnapshot};
415    /// assert_eq!(InputRouter::default().snapshot(), InputSnapshot::default());
416    /// ```
417    pub fn snapshot(&self) -> InputSnapshot {
418        self.snapshot_for(PointerId::MOUSE)
419    }
420
421    /// Returns focus plus strict targets for one pointer ID.
422    ///
423    /// Unknown pointer IDs still include global focus and use `None` for hover
424    /// and press.
425    ///
426    /// # Examples
427    ///
428    /// ```
429    /// use ailloli_ui_core::event::PointerId;
430    /// use ailloli_ui_runtime::input::{InputRouter, InputSnapshot};
431    /// assert_eq!(InputRouter::default().snapshot_for(PointerId::MOUSE), InputSnapshot::default());
432    /// ```
433    pub fn snapshot_for(&self, pointer_id: PointerId) -> InputSnapshot {
434        self.pointers
435            .get(&pointer_id)
436            .map(|state| state.snapshot(self.focused()))
437            .unwrap_or(InputSnapshot {
438                focused: self.focused(),
439                ..InputSnapshot::default()
440            })
441    }
442
443    /// Iterates pointer IDs with retained nonempty state in sorted order.
444    ///
445    /// The iterator borrows the router and allocates nothing.
446    ///
447    /// # Examples
448    ///
449    /// ```
450    /// use ailloli_ui_runtime::input::InputRouter;
451    /// assert_eq!(InputRouter::default().active_pointer_ids().count(), 0);
452    /// ```
453    pub fn active_pointer_ids(&self) -> impl Iterator<Item = PointerId> + '_ {
454        self.pointers.keys().copied()
455    }
456
457    /// Clears every pointer's hover/press/capture/consumed-gesture state.
458    ///
459    /// Returns whether any retained pointer state was nonempty. Focus is not
460    /// changed and no widget events are dispatched. Use
461    /// [`Self::cancel_pointer_state`] when capture owners must end gestures.
462    ///
463    /// # Examples
464    ///
465    /// ```
466    /// use ailloli_ui_runtime::input::InputRouter;
467    /// assert!(!InputRouter::default().clear_pointer_state());
468    /// ```
469    pub fn clear_pointer_state(&mut self) -> bool {
470        let changed = self.pointers.values().any(|state| !state.is_empty());
471        self.pointers.clear();
472        changed
473    }
474
475    /// Clears focus and cached focus metadata without dispatching blur.
476    ///
477    /// Returns whether a focus ID was present. Hosts preserving widget event
478    /// contracts should use [`Self::blur_tree`].
479    ///
480    /// # Examples
481    ///
482    /// ```
483    /// use ailloli_ui_core::ElementId;
484    /// use ailloli_ui_runtime::input::InputRouter;
485    /// let mut router = InputRouter::default();
486    /// router.focus.set_focused(Some(ElementId(3)));
487    /// assert!(router.clear_focus());
488    /// assert_eq!(router.focused(), None);
489    /// ```
490    pub fn clear_focus(&mut self) -> bool {
491        let changed = self.focused().is_some();
492        self.focus.set_focused(None);
493        self.focused_signature = None;
494        self.focused_input_role = InputRole::None;
495        changed
496    }
497
498    /// Clears focus from a host-owned tree and dispatches its matching blur.
499    ///
500    /// Unlike [`Self::clear_focus`], this method preserves the widget event
501    /// contract. Hosts use it when focus ownership moves between retained
502    /// trees, for example between the main tree and a popup overlay.
503    /// Returns `true` only when focus changed. A stale focus ID is cleared but
504    /// receives no blur because it is absent from `tree`.
505    ///
506    /// # Examples
507    ///
508    /// ```
509    /// use ailloli_ui_core::ElementId;
510    /// use ailloli_ui_runtime::app::RuntimeHandle;
511    /// use ailloli_ui_runtime::element::ElementTree;
512    /// use ailloli_ui_runtime::input::InputRouter;
513    /// let mut router = InputRouter::default();
514    /// router.focus.set_focused(Some(ElementId(9)));
515    /// assert!(router.blur_tree(&ElementTree::<()>::new(), RuntimeHandle::new()));
516    /// ```
517    pub fn blur_tree<A: 'static>(
518        &mut self,
519        tree: &ElementTree<A>,
520        runtime: RuntimeHandle<A>,
521    ) -> bool {
522        self.set_focus(tree, runtime, None)
523    }
524
525    /// Moves focus through one retained tree in deterministic depth-first order.
526    ///
527    /// `reverse` selects the Shift+Tab direction. At either boundary, `wrap`
528    /// selects the opposite end; otherwise focus remains unchanged. The root
529    /// participates when it is focusable, which keeps leaf-only popup trees
530    /// keyboard reachable.
531    /// Missing/malformed child IDs are skipped by focus-policy lookup, but a
532    /// cyclic child graph can recurse indefinitely.
533    ///
534    /// # Examples
535    ///
536    /// ```
537    /// use ailloli_ui_runtime::app::RuntimeHandle;
538    /// use ailloli_ui_runtime::element::ElementTree;
539    /// use ailloli_ui_runtime::input::InputRouter;
540    /// let mut router = InputRouter::default();
541    /// assert!(!router.cycle_focus_descendant(&ElementTree::<()>::new(), RuntimeHandle::new(), false, true));
542    /// ```
543    pub fn cycle_focus_descendant<A: 'static>(
544        &mut self,
545        tree: &ElementTree<A>,
546        runtime: RuntimeHandle<A>,
547        reverse: bool,
548        wrap: bool,
549    ) -> bool {
550        let Some(root) = tree.root() else {
551            return false;
552        };
553        let mut focusable = Vec::new();
554        collect_focusable_depth_first(tree, root, &mut focusable);
555        if focusable.is_empty() {
556            return false;
557        }
558
559        let current = self
560            .focused()
561            .and_then(|focused| focusable.iter().position(|candidate| *candidate == focused));
562        let target = match (current, reverse) {
563            (None, false) => focusable.first().copied(),
564            (None, true) => focusable.last().copied(),
565            (Some(index), false) if index + 1 < focusable.len() => Some(focusable[index + 1]),
566            (Some(index), true) if index > 0 => Some(focusable[index - 1]),
567            (Some(_), false) if wrap => focusable.first().copied(),
568            (Some(_), true) if wrap => focusable.last().copied(),
569            (Some(_), _) => None,
570        };
571
572        target.is_some_and(|target| self.set_focus(tree, runtime, Some(target)))
573    }
574
575    /// Moves focus to the first focusable node in a host-owned subtree.
576    ///
577    /// # Examples
578    ///
579    /// ```
580    /// use ailloli_ui_runtime::app::RuntimeHandle;
581    /// use ailloli_ui_runtime::element::ElementTree;
582    /// use ailloli_ui_runtime::input::InputRouter;
583    /// let mut router = InputRouter::default();
584    /// assert!(!router.clear_focus());
585    /// # let _ = (ElementTree::<()>::new(), RuntimeHandle::<()>::new());
586    /// ```
587    pub(crate) fn focus_first_descendant<A: 'static>(
588        &mut self,
589        tree: &ElementTree<A>,
590        runtime: RuntimeHandle<A>,
591    ) -> bool {
592        let target = tree
593            .root()
594            .and_then(|root| focus_target_for_key(tree, root));
595        self.set_focus(tree, runtime, target)
596    }
597
598    /// Clears focus in a host-owned subtree and emits the matching blur event.
599    ///
600    /// # Examples
601    ///
602    /// ```
603    /// use ailloli_ui_runtime::input::InputRouter;
604    /// assert!(InputRouter::default().focused().is_none());
605    /// ```
606    pub(crate) fn blur_subtree<A: 'static>(
607        &mut self,
608        tree: &ElementTree<A>,
609        runtime: RuntimeHandle<A>,
610    ) -> bool {
611        self.blur_tree(tree, runtime)
612    }
613
614    /// Returns the current focused widget's semantic input role.
615    ///
616    /// Missing, empty, component, or unfocused targets return `InputRole::None`.
617    ///
618    /// # Examples
619    ///
620    /// ```
621    /// use ailloli_ui_runtime::element::ElementTree;
622    /// use ailloli_ui_runtime::input::{InputRole, InputRouter};
623    /// assert_eq!(InputRouter::default().focused_input_role(&ElementTree::<()>::new()), InputRole::None);
624    /// ```
625    pub fn focused_input_role<A: 'static>(&self, tree: &ElementTree<A>) -> InputRole {
626        self.focused()
627            .and_then(|id| input_role(tree, id))
628            .unwrap_or(InputRole::None)
629    }
630
631    /// Returns the mouse-hovered widget's semantic input role.
632    ///
633    /// # Examples
634    ///
635    /// ```
636    /// use ailloli_ui_runtime::element::ElementTree;
637    /// use ailloli_ui_runtime::input::{InputRole, InputRouter};
638    /// assert_eq!(InputRouter::default().hovered_input_role(&ElementTree::<()>::new()), InputRole::None);
639    /// ```
640    pub fn hovered_input_role<A: 'static>(&self, tree: &ElementTree<A>) -> InputRole {
641        self.hovered()
642            .and_then(|id| input_role(tree, id))
643            .unwrap_or(InputRole::None)
644    }
645
646    /// Resolves the mouse-hover cursor role without positional specialization.
647    ///
648    /// `Inherit` walks parents; no hovered target or reaching the root resolves
649    /// to `Default`. A malformed parent cycle can loop indefinitely.
650    ///
651    /// # Examples
652    ///
653    /// ```
654    /// use ailloli_ui_runtime::element::ElementTree;
655    /// use ailloli_ui_runtime::input::{HoverCursorRole, InputRouter};
656    /// assert_eq!(InputRouter::default().hovered_cursor_role(&ElementTree::<()>::new()), HoverCursorRole::Default);
657    /// ```
658    pub fn hovered_cursor_role<A: 'static>(&self, tree: &ElementTree<A>) -> HoverCursorRole {
659        self.hovered_cursor_role_impl(tree, None)
660    }
661
662    /// Resolves the mouse-hover cursor role at a window-space logical point.
663    ///
664    /// Widgets can choose contextual resize/text roles from absolute bounds and
665    /// cached layout. Missing layout on a hovered widget behaves as `Inherit`.
666    ///
667    /// # Examples
668    ///
669    /// ```
670    /// use ailloli_ui_core::Point;
671    /// use ailloli_ui_runtime::element::ElementTree;
672    /// use ailloli_ui_runtime::input::{HoverCursorRole, InputRouter};
673    /// assert_eq!(InputRouter::default().hovered_cursor_role_at(&ElementTree::<()>::new(), Point::new(1.0, 2.0)), HoverCursorRole::Default);
674    /// ```
675    pub fn hovered_cursor_role_at<A: 'static>(
676        &self,
677        tree: &ElementTree<A>,
678        pos: Point,
679    ) -> HoverCursorRole {
680        self.hovered_cursor_role_impl(tree, Some(pos))
681    }
682
683    /// Shared ancestor-resolution implementation with optional position.
684    fn hovered_cursor_role_impl<A: 'static>(
685        &self,
686        tree: &ElementTree<A>,
687        pos: Option<Point>,
688    ) -> HoverCursorRole {
689        let Some(mut id) = self.hovered() else {
690            return HoverCursorRole::Default;
691        };
692        loop {
693            match hover_cursor_role(tree, id, pos).unwrap_or(HoverCursorRole::Inherit) {
694                HoverCursorRole::Inherit => {
695                    let Some(parent) = tree.parent_of(id) else {
696                        return HoverCursorRole::Default;
697                    };
698                    id = parent;
699                }
700                role @ (HoverCursorRole::Default
701                | HoverCursorRole::Pointer
702                | HoverCursorRole::Text
703                | HoverCursorRole::ResizeX
704                | HoverCursorRole::ResizeY) => return role,
705            }
706        }
707    }
708
709    /// Returns the focused widget's absolute logical IME cursor rectangle.
710    ///
711    /// Returns `None` without focus, for stale/non-widget targets, without a
712    /// cached layout, or when the widget declines to expose a rectangle.
713    ///
714    /// # Examples
715    ///
716    /// ```
717    /// use ailloli_ui_runtime::element::ElementTree;
718    /// use ailloli_ui_runtime::input::InputRouter;
719    /// assert_eq!(InputRouter::default().focused_ime_cursor_rect(&ElementTree::<()>::new()), None);
720    /// ```
721    pub fn focused_ime_cursor_rect<A: 'static>(&self, tree: &ElementTree<A>) -> Option<Rect> {
722        let id = self.focused()?;
723        let el = tree.get(id)?;
724        let layout = el.layout.as_ref()?;
725        let bounds = super::absolute_paint_bounds(tree, id).unwrap_or(layout.paint_bounds);
726        match &el.kind {
727            ElementKind::Widget(widget) => widget.ime_cursor_rect(bounds, layout),
728            _ => None,
729        }
730    }
731
732    /// Routes a metadata-free legacy event through hit test/focus/popup authority.
733    ///
734    /// Pointer events use the mouse ID, popup work uses the headless presentation
735    /// sentinel, and widget [`super::EventCtx::event_meta`] returns `None`.
736    /// Routing also prunes stale targets and popup owners, applies focus-key and
737    /// popup intents, and may synchronously invoke arbitrary widget code. Window
738    /// focus loss dispatches pointer cancellation to every capture owner before
739    /// clearing pointer state.
740    ///
741    /// # Examples
742    ///
743    /// ```
744    /// use ailloli_ui_core::event::{Event, FocusEvent};
745    /// use ailloli_ui_runtime::app::RuntimeHandle;
746    /// use ailloli_ui_runtime::element::ElementTree;
747    /// use ailloli_ui_runtime::input::{InputRouter, RouteOutcome};
748    /// let outcome = InputRouter::default().route_event(
749    ///     &ElementTree::<()>::new(), RuntimeHandle::new(), &Event::Focus(FocusEvent::new(true)),
750    /// );
751    /// assert_eq!(outcome, RouteOutcome::default());
752    /// ```
753    pub fn route_event<A: 'static>(
754        &mut self,
755        tree: &ElementTree<A>,
756        runtime: RuntimeHandle<A>,
757        event: &Event,
758    ) -> RouteOutcome {
759        self.route_event_impl(tree, runtime, event, None, true)
760    }
761
762    /// Routes a provider-neutral event envelope while preserving its metadata
763    /// in [`super::EventCtx`] and isolating pointer state by [`PointerId`].
764    ///
765    /// Presentation metadata scopes popup pruning/dismissal and pointer metadata
766    /// selects the independent pointer state. Metadata is not validated against
767    /// the enclosed event variant. Window focus loss cancels each captured
768    /// pointer with its last envelope metadata before clearing pointer state.
769    ///
770    /// # Examples
771    ///
772    /// ```
773    /// use std::time::Duration;
774    /// use ailloli_ui_core::event::{Event, FocusEvent};
775    /// use ailloli_ui_runtime::app::{PresentationGeneration, RuntimeHandle};
776    /// use ailloli_ui_runtime::element::ElementTree;
777    /// use ailloli_ui_runtime::input::{EventEnvelope, EventId, EventMeta, EventTimestamp, InputRouter};
778    /// let meta = EventMeta::new(EventId::new(1), EventTimestamp::new(Duration::ZERO), "main", PresentationGeneration::INITIAL);
779    /// let envelope = EventEnvelope::new(meta, Event::Focus(FocusEvent::new(true)));
780    /// assert!(!InputRouter::default().route_envelope(&ElementTree::<()>::new(), RuntimeHandle::new(), &envelope).event_dispatched);
781    /// ```
782    pub fn route_envelope<A: 'static>(
783        &mut self,
784        tree: &ElementTree<A>,
785        runtime: RuntimeHandle<A>,
786        envelope: &EventEnvelope,
787    ) -> RouteOutcome {
788        self.route_event_impl(tree, runtime, envelope.event(), Some(envelope.meta()), true)
789    }
790
791    /// Routes within a host-owned subtree after the host has already applied
792    /// presentation-wide popup dismissal and focus intents.
793    ///
794    /// This keeps pointer metadata intact without running the global popup
795    /// authority a second time with subtree-local coordinates.
796    ///
797    /// # Examples
798    ///
799    /// ```
800    /// use ailloli_ui_runtime::input::InputRouter;
801    /// assert!(InputRouter::default().active_pointer_ids().next().is_none());
802    /// ```
803    pub(crate) fn route_subtree_envelope<A: 'static>(
804        &mut self,
805        tree: &ElementTree<A>,
806        runtime: RuntimeHandle<A>,
807        envelope: &EventEnvelope,
808    ) -> RouteOutcome {
809        self.route_event_impl(
810            tree,
811            runtime,
812            envelope.event(),
813            Some(envelope.meta()),
814            false,
815        )
816    }
817
818    /// Shared routing pipeline with optional metadata and popup-authority pass.
819    fn route_event_impl<A: 'static>(
820        &mut self,
821        tree: &ElementTree<A>,
822        runtime: RuntimeHandle<A>,
823        event: &Event,
824        event_meta: Option<&EventMeta>,
825        route_popup_authority: bool,
826    ) -> RouteOutcome {
827        let stale_state_changed = self.retain_existing_targets(tree);
828        let input_role_changed = self.refresh_target_input_roles(tree);
829        let focus_request_changed = self.apply_pending_focus_request(tree, runtime.clone());
830        runtime.prune_stale_popup_owners(|element_id| tree.get(element_id).is_some());
831        if route_popup_authority {
832            if let Some(meta) = event_meta {
833                runtime.close_stale_popup_presentations(
834                    meta.logical_window_id(),
835                    meta.presentation_generation(),
836                );
837            }
838        }
839
840        let pointer_id = event_meta
841            .and_then(EventMeta::pointer)
842            .map(|pointer| pointer.id())
843            .unwrap_or(PointerId::MOUSE);
844        if let Event::Pointer(pointer_event) = event {
845            let state = self.pointers.entry(pointer_id).or_default();
846            state.last_position = pointer_event.position();
847            state.last_modifiers = pointer_event.modifiers();
848            state.last_event_meta = event_meta.cloned();
849        }
850        let mut popup_target_override = None;
851        let mut popup_consumed_without_target = false;
852
853        if route_popup_authority {
854            if let Event::Pointer(PointerEvent::Button {
855                pos, pressed: true, ..
856            }) = event
857            {
858                let (window, generation) = popup_presentation(event_meta);
859                let backend_target = hit_test_overlay_target(tree, *pos);
860                let portal = runtime.popup_portal();
861                let portal = portal.borrow();
862                let backend_popup_hit = backend_target.and_then(|target| {
863                    portal.open_ids().rev().find(|popup_id| {
864                        portal.request(*popup_id).is_some_and(|request| {
865                            let owner = request.owner();
866                            portal.bounds(*popup_id).is_none()
867                                && owner.logical_window_id() == &window
868                                && owner.presentation_generation() == generation
869                                && owner.element_tree_id() == runtime.element_tree_id()
870                                && (owner.element_id() == target
871                                    || tree.is_ancestor_of(owner.element_id(), target))
872                        })
873                    })
874                });
875                let popup_hit =
876                    portal.resolve_pointer_hit(&window, generation, *pos, backend_popup_hit);
877                let backend_dispatch_target = popup_hit
878                    .filter(|popup_id| Some(*popup_id) == backend_popup_hit)
879                    .and(backend_target);
880                drop(portal);
881                let popup_outcome = runtime.route_popup_pointer_press_with_backend_hit(
882                    &window,
883                    generation,
884                    *pos,
885                    backend_popup_hit,
886                );
887                if popup_outcome.handled() {
888                    popup_target_override = backend_dispatch_target.or_else(|| {
889                        popup_hit.and_then(|popup_id| {
890                            runtime
891                                .popup_portal()
892                                .borrow()
893                                .request(popup_id)
894                                .filter(|request| {
895                                    request.owner().element_tree_id() == runtime.element_tree_id()
896                                        && request.mount_policy()
897                                            == PopupMountPolicy::ProceduralFallback
898                                })
899                                .map(|request| request.owner().element_id())
900                        })
901                    });
902                    popup_consumed_without_target = popup_target_override.is_none();
903                }
904            }
905        }
906
907        let escape_consumed = route_popup_authority
908            && matches!(
909                event,
910                Event::Keyboard(key)
911                    if key.state == KeyState::Pressed
912                        && key.key == KeyboardKey::Named(NamedKey::Escape)
913            )
914            && {
915                let (window, generation) = popup_presentation(event_meta);
916                runtime.route_popup_escape(&window, generation).handled()
917            };
918
919        let mut outcome = if popup_consumed_without_target {
920            let state = self.pointers.entry(pointer_id).or_default();
921            state.pressed = None;
922            state.capture = None;
923            state.pressed_signature = None;
924            state.capture_signature = None;
925            state.pressed_input_role = InputRole::None;
926            state.capture_input_role = InputRole::None;
927            state.popup_consumed_gesture = true;
928            RouteOutcome {
929                interaction_changed: true,
930                event_dispatched: true,
931            }
932        } else if escape_consumed {
933            RouteOutcome {
934                interaction_changed: true,
935                event_dispatched: true,
936            }
937        } else {
938            match event {
939                Event::Pointer(PointerEvent::Moved { pos, .. }) => self.route_pointer_move(
940                    tree,
941                    runtime.clone(),
942                    event,
943                    event_meta,
944                    pointer_id,
945                    *pos,
946                ),
947                Event::Pointer(PointerEvent::Button {
948                    pos,
949                    button,
950                    pressed,
951                    ..
952                }) => self.route_pointer_button(
953                    tree,
954                    runtime.clone(),
955                    event,
956                    event_meta,
957                    PointerButtonInput {
958                        pointer_id,
959                        pos: *pos,
960                        button: *button,
961                        pressed: *pressed,
962                    },
963                    popup_target_override,
964                ),
965                Event::Pointer(PointerEvent::Cancelled { .. }) => {
966                    self.route_pointer_cancel(tree, runtime.clone(), event, event_meta, pointer_id)
967                }
968                Event::Pointer(PointerEvent::Wheel { pos, .. }) => {
969                    let target = self
970                        .pointer_capture_for(pointer_id)
971                        .or_else(|| self.hit(tree, *pos));
972                    self.dispatch_to_optional_target(
973                        tree,
974                        runtime.clone(),
975                        event,
976                        event_meta,
977                        target,
978                        false,
979                    )
980                }
981                Event::Pointer(_) => RouteOutcome::default(),
982                Event::Keyboard(_) | Event::Ime(_) => {
983                    let target = self.focused();
984                    self.dispatch_to_optional_target(
985                        tree,
986                        runtime.clone(),
987                        event,
988                        event_meta,
989                        target,
990                        false,
991                    )
992                }
993                Event::File(file) => match file.pos() {
994                    Some(pos) => {
995                        let target = self
996                            .pointer_capture_for(PointerId::MOUSE)
997                            .or_else(|| self.hit(tree, pos));
998                        self.dispatch_to_optional_target(
999                            tree,
1000                            runtime.clone(),
1001                            event,
1002                            event_meta,
1003                            target,
1004                            false,
1005                        )
1006                    }
1007                    None => {
1008                        let target = self.hovered().or_else(|| self.focused());
1009                        self.dispatch_to_optional_target(
1010                            tree,
1011                            runtime.clone(),
1012                            event,
1013                            event_meta,
1014                            target,
1015                            false,
1016                        )
1017                    }
1018                },
1019                Event::Window(ailloli_ui_core::event::WindowEvent::Focused { focused: false }) => {
1020                    let cancelled = self.cancel_pointer_state(tree, runtime.clone());
1021                    let focus_changed = self.set_focus(tree, runtime.clone(), None);
1022                    let interaction_changed = cancelled.interaction_changed | focus_changed;
1023                    RouteOutcome {
1024                        interaction_changed,
1025                        event_dispatched: cancelled.event_dispatched | focus_changed,
1026                    }
1027                }
1028                Event::Focus(_) => RouteOutcome::default(),
1029                Event::Window(_) => RouteOutcome::default(),
1030                _ => RouteOutcome::default(),
1031            }
1032        };
1033        outcome.interaction_changed |= stale_state_changed;
1034        outcome.interaction_changed |= input_role_changed;
1035        outcome.interaction_changed |= focus_request_changed;
1036        outcome.interaction_changed |= self.apply_pending_focus_request(tree, runtime.clone());
1037        if route_popup_authority {
1038            outcome.interaction_changed |=
1039                self.apply_pending_popup_intents(tree, runtime.clone(), event_meta);
1040        }
1041        outcome.interaction_changed |= self.refresh_target_input_roles(tree);
1042        outcome
1043    }
1044
1045    /// Applies a programmatic focus-key request after the tree has been laid out.
1046    ///
1047    /// The request is consumed even when the key is missing/duplicate or resolves
1048    /// to no focusable target. Returns whether strict focus changed.
1049    ///
1050    /// # Examples
1051    ///
1052    /// ```
1053    /// use ailloli_ui_runtime::app::RuntimeHandle;
1054    /// use ailloli_ui_runtime::element::ElementTree;
1055    /// use ailloli_ui_runtime::input::InputRouter;
1056    /// let runtime = RuntimeHandle::<()>::new();
1057    /// runtime.request_focus_key("missing");
1058    /// assert!(!InputRouter::default().apply_pending_focus_request(&ElementTree::new(), runtime.clone()));
1059    /// assert!(runtime.take_focus_key_request().is_none());
1060    /// ```
1061    pub fn apply_pending_focus_request<A: 'static>(
1062        &mut self,
1063        tree: &ElementTree<A>,
1064        runtime: RuntimeHandle<A>,
1065    ) -> bool {
1066        let Some(key) = runtime.take_focus_key_request() else {
1067            return false;
1068        };
1069        let Ok(id) = tree.resolve_element_by_view_key(&key) else {
1070            return false;
1071        };
1072        let focus = focus_target_for_key(tree, id);
1073        self.set_focus(tree, runtime, focus)
1074    }
1075
1076    /// Applies the provider-neutral overlay backend effects emitted by the
1077    /// popup authority. Presentation and dismissal invalidate the frame;
1078    /// focus intents resolve against the complete owner namespace.
1079    ///
1080    /// Metadata-free calls use the headless window and initial generation.
1081    fn apply_pending_popup_intents<A: 'static>(
1082        &mut self,
1083        tree: &ElementTree<A>,
1084        runtime: RuntimeHandle<A>,
1085        event_meta: Option<&EventMeta>,
1086    ) -> bool {
1087        let (logical_window_id, presentation_generation) = popup_presentation(event_meta);
1088        self.apply_pending_popup_intents_for_presentation(
1089            tree,
1090            runtime,
1091            &logical_window_id,
1092            presentation_generation,
1093        )
1094    }
1095
1096    /// Applies queued popup focus/presentation effects belonging to one exact
1097    /// retained tree and presentation.
1098    ///
1099    /// Hosts call this after a retained popup subtree consumes an event, and
1100    /// once per redraw for programmatic popup changes emitted outside input
1101    /// dispatch. Intents owned by sibling windows or popup trees remain queued.
1102    ///
1103    /// Returns `true` for present/dismiss intents, retained-overlay focus
1104    /// delegation, or an actual owner-tree focus change. Missing/stale popup
1105    /// requests are consumed and skipped.
1106    ///
1107    /// # Examples
1108    ///
1109    /// ```
1110    /// use ailloli_ui_core::LogicalWindowId;
1111    /// use ailloli_ui_runtime::app::{PresentationGeneration, RuntimeHandle};
1112    /// use ailloli_ui_runtime::element::ElementTree;
1113    /// use ailloli_ui_runtime::input::InputRouter;
1114    /// let mut router = InputRouter::default();
1115    /// assert!(!router.apply_pending_popup_intents_for_presentation(
1116    ///     &ElementTree::<()>::new(), RuntimeHandle::new(), &LogicalWindowId::new("main"), PresentationGeneration::INITIAL,
1117    /// ));
1118    /// ```
1119    pub fn apply_pending_popup_intents_for_presentation<A: 'static>(
1120        &mut self,
1121        tree: &ElementTree<A>,
1122        runtime: RuntimeHandle<A>,
1123        logical_window_id: &LogicalWindowId,
1124        presentation_generation: PresentationGeneration,
1125    ) -> bool {
1126        let mut changed = false;
1127        for intent in runtime.take_pending_popup_intents_for(
1128            runtime.element_tree_id(),
1129            logical_window_id,
1130            presentation_generation,
1131        ) {
1132            match intent {
1133                PopupIntent::Present { .. } | PopupIntent::Dismiss { .. } => {
1134                    changed = true;
1135                }
1136                PopupIntent::MoveFocusInto { popup_id, .. } => {
1137                    let request = runtime
1138                        .popup_portal()
1139                        .borrow()
1140                        .request(popup_id)
1141                        .map(|request| (request.owner().clone(), request.mount_policy()));
1142                    let Some((owner, mount_policy)) = request else {
1143                        continue;
1144                    };
1145                    if mount_policy == PopupMountPolicy::RetainedOverlay {
1146                        // PopupOverlayMounts owns focus within a dedicated tree.
1147                        changed = true;
1148                    } else if owner.element_tree_id() == runtime.element_tree_id()
1149                        && owner.logical_window_id() == logical_window_id
1150                        && owner.presentation_generation() == presentation_generation
1151                    {
1152                        if let Some(target) = nearest_focusable(tree, owner.element_id()) {
1153                            changed |= self.set_focus(tree, runtime.clone(), Some(target));
1154                        }
1155                    }
1156                }
1157                PopupIntent::RestoreFocus { owner } => {
1158                    if owner.element_tree_id() == runtime.element_tree_id()
1159                        && owner.logical_window_id() == logical_window_id
1160                        && owner.presentation_generation() == presentation_generation
1161                    {
1162                        if let Some(target) = focus_target_for_key(tree, owner.element_id()) {
1163                            changed |= self.set_focus(tree, runtime.clone(), Some(target));
1164                        }
1165                    }
1166                }
1167            }
1168        }
1169        changed
1170    }
1171
1172    /// Updates one pointer's hover target and dispatches move to capture/hit.
1173    fn route_pointer_move<A: 'static>(
1174        &mut self,
1175        tree: &ElementTree<A>,
1176        runtime: RuntimeHandle<A>,
1177        event: &Event,
1178        event_meta: Option<&EventMeta>,
1179        pointer_id: PointerId,
1180        pos: Point,
1181    ) -> RouteOutcome {
1182        let hit = self.hit(tree, pos);
1183        let (old_hover, new_hover, target) = {
1184            let state = self.pointers.entry(pointer_id).or_default();
1185            let old_hover = state.hovered;
1186            state.hovered = hit;
1187            state.hovered_signature = hit.and_then(|id| target_signature(tree, id));
1188            state.hovered_input_role = target_input_role(tree, hit);
1189            (old_hover, state.hovered, state.capture.or(hit))
1190        };
1191        let mut outcome = self.dispatch_to_optional_target(
1192            tree,
1193            runtime,
1194            event,
1195            event_meta,
1196            target,
1197            old_hover != new_hover,
1198        );
1199        outcome.interaction_changed |= old_hover != new_hover;
1200        outcome
1201    }
1202
1203    /// Applies focus/capture/activation and dispatches one button transition.
1204    fn route_pointer_button<A: 'static>(
1205        &mut self,
1206        tree: &ElementTree<A>,
1207        runtime: RuntimeHandle<A>,
1208        event: &Event,
1209        event_meta: Option<&EventMeta>,
1210        input: PointerButtonInput,
1211        target_override: Option<ElementId>,
1212    ) -> RouteOutcome {
1213        let PointerButtonInput {
1214            pointer_id,
1215            pos,
1216            button,
1217            pressed,
1218        } = input;
1219        if !pressed
1220            && self
1221                .pointers
1222                .get(&pointer_id)
1223                .is_some_and(|state| state.popup_consumed_gesture)
1224        {
1225            if let Some(state) = self.pointers.get_mut(&pointer_id) {
1226                state.popup_consumed_gesture = false;
1227                state.activation = ActivationKind::Normal;
1228            }
1229            if self
1230                .pointers
1231                .get(&pointer_id)
1232                .is_some_and(PointerRouteState::is_empty)
1233            {
1234                self.pointers.remove(&pointer_id);
1235            }
1236            return RouteOutcome {
1237                interaction_changed: true,
1238                event_dispatched: true,
1239            };
1240        }
1241
1242        let hit = target_override.or_else(|| self.hit(tree, pos));
1243        let event_activation = event_meta
1244            .and_then(EventMeta::pointer)
1245            .map(|pointer| pointer.activation())
1246            .unwrap_or(ActivationKind::Normal);
1247        let ends_touch = !pressed
1248            && event_meta
1249                .and_then(EventMeta::pointer)
1250                .is_some_and(|pointer| pointer.source() == PointerSource::Touch);
1251        let mut interaction_changed = false;
1252        let (target, gesture_activation) = if pressed {
1253            if button == PointerButton::Left {
1254                let new_focus = hit.and_then(|id| nearest_focusable(tree, id));
1255                interaction_changed |= self.set_focus(tree, runtime.clone(), new_focus);
1256            }
1257            let state = self.pointers.entry(pointer_id).or_default();
1258            state.pressed = hit;
1259            state.capture = hit;
1260            state.pressed_signature = hit.and_then(|id| target_signature(tree, id));
1261            state.capture_signature = state.pressed_signature.clone();
1262            state.pressed_input_role = target_input_role(tree, hit);
1263            state.capture_input_role = state.pressed_input_role;
1264            state.activation = event_activation;
1265            state.popup_consumed_gesture = false;
1266            interaction_changed |= hit.is_some();
1267            (hit, event_activation)
1268        } else {
1269            let state = self.pointers.entry(pointer_id).or_default();
1270            let target = state.capture.or(hit);
1271            let gesture_activation = if event_activation == ActivationKind::FocusOnly {
1272                ActivationKind::FocusOnly
1273            } else {
1274                state.activation
1275            };
1276            interaction_changed |= state.pressed.is_some()
1277                || state.capture.is_some()
1278                || (ends_touch && state.hovered.is_some());
1279            state.pressed = None;
1280            state.capture = None;
1281            state.pressed_signature = None;
1282            state.capture_signature = None;
1283            state.pressed_input_role = InputRole::None;
1284            state.capture_input_role = InputRole::None;
1285            state.activation = ActivationKind::Normal;
1286            state.popup_consumed_gesture = false;
1287            (target, gesture_activation)
1288        };
1289
1290        let outcome = if activation_is_allowed(tree, target, gesture_activation) {
1291            self.dispatch_to_optional_target(
1292                tree,
1293                runtime,
1294                event,
1295                event_meta,
1296                target,
1297                interaction_changed,
1298            )
1299        } else {
1300            RouteOutcome {
1301                interaction_changed,
1302                event_dispatched: false,
1303            }
1304        };
1305        if ends_touch
1306            || self
1307                .pointers
1308                .get(&pointer_id)
1309                .is_some_and(PointerRouteState::is_empty)
1310        {
1311            self.pointers.remove(&pointer_id);
1312        }
1313        outcome
1314    }
1315
1316    /// Clears one pointer's press/capture and dispatches cancellation if owned.
1317    fn route_pointer_cancel<A: 'static>(
1318        &mut self,
1319        tree: &ElementTree<A>,
1320        runtime: RuntimeHandle<A>,
1321        event: &Event,
1322        event_meta: Option<&EventMeta>,
1323        pointer_id: PointerId,
1324    ) -> RouteOutcome {
1325        let Some(state) = self.pointers.get_mut(&pointer_id) else {
1326            return RouteOutcome::default();
1327        };
1328        let clears_touch = event_meta
1329            .and_then(EventMeta::pointer)
1330            .is_some_and(|pointer| pointer.source() == PointerSource::Touch);
1331        let popup_consumed_gesture = state.popup_consumed_gesture;
1332        let (target, interaction_changed) = {
1333            let target = state.capture.or(state.pressed);
1334            let interaction_changed = state.pressed.is_some()
1335                || state.capture.is_some()
1336                || (clears_touch && state.hovered.is_some());
1337            state.pressed = None;
1338            state.capture = None;
1339            state.pressed_signature = None;
1340            state.capture_signature = None;
1341            state.pressed_input_role = InputRole::None;
1342            state.capture_input_role = InputRole::None;
1343            state.activation = ActivationKind::Normal;
1344            state.popup_consumed_gesture = false;
1345            (target, interaction_changed)
1346        };
1347
1348        let mut outcome = self.dispatch_to_optional_target(
1349            tree,
1350            runtime,
1351            event,
1352            event_meta,
1353            target,
1354            interaction_changed,
1355        );
1356        if popup_consumed_gesture {
1357            outcome.interaction_changed = true;
1358            outcome.event_dispatched = true;
1359        }
1360        if clears_touch
1361            || self
1362                .pointers
1363                .get(&pointer_id)
1364                .is_some_and(PointerRouteState::is_empty)
1365        {
1366            self.pointers.remove(&pointer_id);
1367        }
1368        outcome
1369    }
1370
1371    /// Dispatches with/without metadata when a target exists and builds outcome.
1372    fn dispatch_to_optional_target<A: 'static>(
1373        &self,
1374        tree: &ElementTree<A>,
1375        runtime: RuntimeHandle<A>,
1376        event: &Event,
1377        event_meta: Option<&EventMeta>,
1378        target: Option<ElementId>,
1379        interaction_changed: bool,
1380    ) -> RouteOutcome {
1381        if let Some(target) = target {
1382            if let Some(event_meta) = event_meta {
1383                let envelope = EventEnvelope::new(event_meta.clone(), event.clone());
1384                dispatch_event_envelope_bubbling(tree, runtime, target, &envelope);
1385            } else {
1386                dispatch_event_bubbling(tree, runtime, target, event);
1387            }
1388            RouteOutcome {
1389                interaction_changed,
1390                event_dispatched: true,
1391            }
1392        } else {
1393            RouteOutcome {
1394                interaction_changed,
1395                event_dispatched: false,
1396            }
1397        }
1398    }
1399
1400    /// Hit-tests overlay-first retained geometry at a logical point.
1401    fn hit<A>(&self, tree: &ElementTree<A>, pos: Point) -> Option<ElementId> {
1402        hit_test_target(tree, &self.hit_test, pos, None)
1403    }
1404
1405    /// Returns one pointer's current capture target.
1406    fn pointer_capture_for(&self, pointer_id: PointerId) -> Option<ElementId> {
1407        self.pointers
1408            .get(&pointer_id)
1409            .and_then(|state| state.capture)
1410    }
1411
1412    /// Cancels every pressed/captured pointer before clearing pointer state.
1413    ///
1414    /// Capture owners synchronously receive [`PointerEvent::Cancelled`] with
1415    /// the last known position, modifiers, and pointer metadata when available.
1416    /// Hosts should use this before detaching or suspending a presentation; the
1417    /// lower-level [`Self::clear_pointer_state`] intentionally dispatches no
1418    /// widget events.
1419    ///
1420    /// # Examples
1421    ///
1422    /// ```
1423    /// use ailloli_ui_runtime::{
1424    ///     app::RuntimeHandle,
1425    ///     element::ElementTree,
1426    ///     input::{InputRouter, RouteOutcome},
1427    /// };
1428    /// let mut router = InputRouter::default();
1429    /// let outcome = router.cancel_pointer_state(
1430    ///     &ElementTree::<()>::new(),
1431    ///     RuntimeHandle::new(),
1432    /// );
1433    /// assert_eq!(outcome, RouteOutcome::default());
1434    /// ```
1435    pub fn cancel_pointer_state<A: 'static>(
1436        &mut self,
1437        tree: &ElementTree<A>,
1438        runtime: RuntimeHandle<A>,
1439    ) -> RouteOutcome {
1440        let pointer_ids = self.pointers.keys().copied().collect::<Vec<_>>();
1441        let mut combined = RouteOutcome::default();
1442        for pointer_id in pointer_ids {
1443            let Some(state) = self.pointers.get(&pointer_id) else {
1444                continue;
1445            };
1446            if state.capture.is_none() && state.pressed.is_none() {
1447                continue;
1448            }
1449            let event = Event::Pointer(PointerEvent::cancelled(
1450                state.last_position,
1451                state.last_modifiers,
1452            ));
1453            let meta = state.last_event_meta.clone();
1454            let outcome =
1455                self.route_pointer_cancel(tree, runtime.clone(), &event, meta.as_ref(), pointer_id);
1456            combined.interaction_changed |= outcome.interaction_changed;
1457            combined.event_dispatched |= outcome.event_dispatched;
1458        }
1459        combined.interaction_changed |= self.clear_pointer_state();
1460        combined
1461    }
1462
1463    /// Transitions strict focus, dispatching blur before focus synchronously.
1464    fn set_focus<A: 'static>(
1465        &mut self,
1466        tree: &ElementTree<A>,
1467        runtime: RuntimeHandle<A>,
1468        new_focus: Option<ElementId>,
1469    ) -> bool {
1470        let old_focus = self.focused();
1471        if old_focus == new_focus {
1472            return false;
1473        }
1474
1475        if let Some(old) = old_focus.filter(|id| tree.get(*id).is_some()) {
1476            dispatch_event_to_target(
1477                tree,
1478                runtime.clone(),
1479                old,
1480                &Event::Focus(FocusEvent::new(false)),
1481            );
1482        }
1483
1484        self.focus.set_focused(new_focus);
1485        self.focused_signature = new_focus.and_then(|id| target_signature(tree, id));
1486        self.focused_input_role = target_input_role(tree, new_focus);
1487
1488        if let Some(new) = new_focus.filter(|id| tree.get(*id).is_some()) {
1489            dispatch_event_to_target(tree, runtime, new, &Event::Focus(FocusEvent::new(true)));
1490        }
1491
1492        true
1493    }
1494
1495    /// Drops removed or identity-changed targets and returns whether state changed.
1496    fn retain_existing_targets<A: 'static>(&mut self, tree: &ElementTree<A>) -> bool {
1497        let mut changed = false;
1498        if target_stale(tree, self.focused(), &self.focused_signature) {
1499            self.focus.set_focused(None);
1500            self.focused_signature = None;
1501            self.focused_input_role = InputRole::None;
1502            changed = true;
1503        }
1504        self.pointers.retain(|_, state| {
1505            if target_stale(tree, state.hovered, &state.hovered_signature) {
1506                state.hovered = None;
1507                state.hovered_signature = None;
1508                state.hovered_input_role = InputRole::None;
1509                changed = true;
1510            }
1511            if target_stale(tree, state.pressed, &state.pressed_signature) {
1512                state.pressed = None;
1513                state.pressed_signature = None;
1514                state.pressed_input_role = InputRole::None;
1515                changed = true;
1516            }
1517            if target_stale(tree, state.capture, &state.capture_signature) {
1518                state.capture = None;
1519                state.capture_signature = None;
1520                state.capture_input_role = InputRole::None;
1521                changed = true;
1522            }
1523            if state.pressed.is_none() && state.capture.is_none() {
1524                state.activation = ActivationKind::Normal;
1525            }
1526            !state.is_empty()
1527        });
1528        changed
1529    }
1530
1531    /// Refreshes cached semantic roles for focus and every active pointer target.
1532    fn refresh_target_input_roles<A: 'static>(&mut self, tree: &ElementTree<A>) -> bool {
1533        let focused_changed =
1534            refresh_target_input_role(tree, self.focused(), &mut self.focused_input_role);
1535        let mut pointer_changed = false;
1536        for state in self.pointers.values_mut() {
1537            pointer_changed |=
1538                refresh_target_input_role(tree, state.hovered, &mut state.hovered_input_role);
1539            pointer_changed |=
1540                refresh_target_input_role(tree, state.pressed, &mut state.pressed_input_role);
1541            pointer_changed |=
1542                refresh_target_input_role(tree, state.capture, &mut state.capture_input_role);
1543        }
1544        focused_changed || pointer_changed
1545    }
1546}
1547
1548/// Extracts the popup presentation identity, using the stable headless identity
1549/// when an event has no platform metadata.
1550fn popup_presentation(event_meta: Option<&EventMeta>) -> (LogicalWindowId, PresentationGeneration) {
1551    event_meta.map_or_else(
1552        || {
1553            (
1554                LogicalWindowId::new(HEADLESS_POPUP_WINDOW_ID),
1555                PresentationGeneration::INITIAL,
1556            )
1557        },
1558        |meta| {
1559            (
1560                meta.logical_window_id().clone(),
1561                meta.presentation_generation(),
1562            )
1563        },
1564    )
1565}
1566
1567/// Reports whether a retained target disappeared or changed semantic identity.
1568///
1569/// A missing target is not stale. A present target is stale when its element no
1570/// longer exists, or when both an old and current signature exist and differ.
1571fn target_stale<A: 'static>(
1572    tree: &ElementTree<A>,
1573    target: Option<ElementId>,
1574    signature: &Option<TargetSignature>,
1575) -> bool {
1576    let Some(id) = target else {
1577        return false;
1578    };
1579    let Some(current) = target_signature(tree, id) else {
1580        return true;
1581    };
1582    signature.as_ref().is_some_and(|old| old != &current)
1583}
1584
1585/// Captures the key and input-relevant kind of an existing retained element.
1586fn target_signature<A: 'static>(tree: &ElementTree<A>, id: ElementId) -> Option<TargetSignature> {
1587    let el = tree.get(id)?;
1588    let kind = match &el.kind {
1589        ElementKind::Empty => TargetKind::Empty,
1590        ElementKind::Component(_) => TargetKind::Component,
1591        ElementKind::Widget(widget) => TargetKind::Widget {
1592            debug_name: widget.debug_name(),
1593            focus_policy: widget.focus_policy(),
1594            activation_policy: widget.activation_policy(),
1595        },
1596    };
1597    Some(TargetSignature {
1598        key: el.key.clone(),
1599        kind,
1600    })
1601}
1602
1603/// Recomputes one cached semantic input role and reports whether it changed.
1604fn refresh_target_input_role<A: 'static>(
1605    tree: &ElementTree<A>,
1606    target: Option<ElementId>,
1607    stored: &mut InputRole,
1608) -> bool {
1609    let next = target_input_role(tree, target);
1610    if *stored == next {
1611        return false;
1612    }
1613    *stored = next;
1614    true
1615}
1616
1617/// Resolves an optional target's semantic input role, defaulting to `None`.
1618fn target_input_role<A: 'static>(tree: &ElementTree<A>, target: Option<ElementId>) -> InputRole {
1619    target
1620        .and_then(|id| input_role(tree, id))
1621        .unwrap_or(InputRole::None)
1622}
1623
1624/// Reads focusability for an existing element.
1625///
1626/// Empty and component elements are explicitly not focusable; a missing ID
1627/// returns `None`.
1628fn focus_policy<A: 'static>(tree: &ElementTree<A>, id: ElementId) -> Option<FocusPolicy> {
1629    let el = tree.get(id)?;
1630    match &el.kind {
1631        ElementKind::Widget(widget) => Some(widget.focus_policy()),
1632        _ => Some(FocusPolicy::NotFocusable),
1633    }
1634}
1635
1636/// Reads an element's activation policy, using `Inherit` for non-widgets and
1637/// missing IDs.
1638fn activation_policy<A: 'static>(tree: &ElementTree<A>, id: ElementId) -> ActivationPolicy {
1639    let Some(el) = tree.get(id) else {
1640        return ActivationPolicy::Inherit;
1641    };
1642    match &el.kind {
1643        ElementKind::Widget(widget) => widget.activation_policy(),
1644        ElementKind::Empty | ElementKind::Component(_) => ActivationPolicy::Inherit,
1645    }
1646}
1647
1648/// Resolves inherited activation policy through ancestors.
1649///
1650/// Reaching a root without an explicit policy defaults to suppressing
1651/// activation during a focus-only gesture.
1652fn resolved_activation_policy<A: 'static>(
1653    tree: &ElementTree<A>,
1654    mut target: ElementId,
1655) -> ActivationPolicy {
1656    loop {
1657        match activation_policy(tree, target) {
1658            ActivationPolicy::Inherit => {
1659                let Some(parent) = tree.parent_of(target) else {
1660                    return ActivationPolicy::SuppressOnFocusOnly;
1661                };
1662                target = parent;
1663            }
1664            policy => return policy,
1665        }
1666    }
1667}
1668
1669/// Tests whether the target may receive an activation for this gesture kind.
1670///
1671/// Normal gestures are always allowed. Focus-only gestures require a present
1672/// target whose resolved policy is `AllowOnFocusOnly`.
1673fn activation_is_allowed<A: 'static>(
1674    tree: &ElementTree<A>,
1675    target: Option<ElementId>,
1676    activation: ActivationKind,
1677) -> bool {
1678    if activation == ActivationKind::Normal {
1679        return true;
1680    }
1681    target.is_some_and(|target| {
1682        resolved_activation_policy(tree, target) == ActivationPolicy::AllowOnFocusOnly
1683    })
1684}
1685
1686/// Reads an existing element's input role; non-widgets yield `InputRole::None`.
1687fn input_role<A: 'static>(tree: &ElementTree<A>, id: ElementId) -> Option<InputRole> {
1688    let el = tree.get(id)?;
1689    match &el.kind {
1690        ElementKind::Widget(widget) => Some(widget.input_role()),
1691        _ => Some(InputRole::None),
1692    }
1693}
1694
1695/// Resolves an existing element's cursor role at an optional logical point.
1696///
1697/// Point-sensitive widget resolution requires committed layout. Without a
1698/// point, the widget's general role is returned; non-widgets inherit.
1699fn hover_cursor_role<A: 'static>(
1700    tree: &ElementTree<A>,
1701    id: ElementId,
1702    pos: Option<Point>,
1703) -> Option<HoverCursorRole> {
1704    let el = tree.get(id)?;
1705    match &el.kind {
1706        ElementKind::Widget(widget) => {
1707            let Some(pos) = pos else {
1708                return Some(widget.hover_cursor_role());
1709            };
1710            let layout = el.layout.as_ref()?;
1711            let bounds = absolute_paint_bounds(tree, id).unwrap_or(layout.paint_bounds);
1712            Some(widget.hover_cursor_role_at(bounds, layout, pos))
1713        }
1714        ElementKind::Empty | ElementKind::Component(_) => Some(HoverCursorRole::Inherit),
1715    }
1716}
1717
1718/// Finds the first focusable element on the inclusive ancestor chain.
1719fn nearest_focusable<A: 'static>(tree: &ElementTree<A>, mut id: ElementId) -> Option<ElementId> {
1720    loop {
1721        if focus_policy(tree, id) == Some(FocusPolicy::Focusable) {
1722            return Some(id);
1723        }
1724        id = tree.parent_of(id)?;
1725    }
1726}
1727
1728/// Resolves a keyed popup owner to itself, its first focusable descendant, or
1729/// its nearest focusable ancestor, in that priority order.
1730fn focus_target_for_key<A: 'static>(tree: &ElementTree<A>, id: ElementId) -> Option<ElementId> {
1731    if focus_policy(tree, id) == Some(FocusPolicy::Focusable) {
1732        return Some(id);
1733    }
1734    focusable_descendant(tree, id).or_else(|| nearest_focusable(tree, id))
1735}
1736
1737/// Returns the first focusable descendant in child-order depth-first traversal.
1738fn focusable_descendant<A: 'static>(tree: &ElementTree<A>, id: ElementId) -> Option<ElementId> {
1739    for child in tree.children_of(id) {
1740        if focus_policy(tree, *child) == Some(FocusPolicy::Focusable) {
1741            return Some(*child);
1742        }
1743        if let Some(found) = focusable_descendant(tree, *child) {
1744            return Some(found);
1745        }
1746    }
1747    None
1748}
1749
1750/// Appends focusable nodes in inclusive child-order depth-first traversal.
1751fn collect_focusable_depth_first<A: 'static>(
1752    tree: &ElementTree<A>,
1753    id: ElementId,
1754    focusable: &mut Vec<ElementId>,
1755) {
1756    if focus_policy(tree, id) == Some(FocusPolicy::Focusable) {
1757        focusable.push(id);
1758    }
1759    for child in tree.children_of(id) {
1760        collect_focusable_depth_first(tree, *child, focusable);
1761    }
1762}