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 != ¤t)
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}