1use super::*;
2use crate::widget::{CommandAvailability, CommandAvailabilityCx};
3use fret_runtime::CommandScope;
4use std::sync::Arc;
5
6impl<H: UiHost> UiTree<H> {
7 pub(crate) fn defer_declarative_window_snapshot_commit(&mut self, root: NodeId) {
8 self.pending_declarative_window_snapshot_roots
9 .retain(|pending| self.nodes.contains_key(*pending));
10 self.pending_declarative_window_snapshot_roots.insert(root);
11 }
12
13 pub(crate) fn clear_declarative_window_snapshot_commit(&mut self, root: NodeId) {
14 self.pending_declarative_window_snapshot_roots.remove(&root);
15 }
16
17 pub(in crate::tree) fn revalidate_focus_for_dispatch_snapshot(
18 &mut self,
19 frame_id: fret_runtime::FrameId,
20 active_focus_layers: &[NodeId],
21 barrier_root: Option<NodeId>,
22 reason: &'static str,
23 ) {
24 let dispatch_snapshot = self.build_dispatch_snapshot_for_layer_roots(
25 frame_id,
26 active_focus_layers,
27 barrier_root,
28 );
29 if self
30 .focus
31 .is_some_and(|node| dispatch_snapshot.pre.get(node).is_none())
32 {
33 self.set_focus_unchecked(None, reason);
34 }
35 }
36
37 pub(in crate::tree) fn revalidate_pending_shortcut_for_current_routing_context(
38 &mut self,
39 app: &mut H,
40 barrier_root: Option<NodeId>,
41 ) {
42 if self.replaying_pending_shortcut || self.pending_shortcut.keystrokes.is_empty() {
43 return;
44 }
45
46 let current_key_contexts = self.shortcut_key_context_stack(app, barrier_root);
51 if (self.pending_shortcut.focus.is_some() && self.pending_shortcut.focus != self.focus)
52 || self.pending_shortcut.barrier_root != barrier_root
53 || self.pending_shortcut.key_contexts.as_slice() != current_key_contexts.as_slice()
54 {
55 self.clear_pending_shortcut(app);
56 }
57 }
58
59 pub(in crate::tree) fn current_window_input_context(
60 &self,
61 app: &mut H,
62 ui_has_modal: bool,
63 focus_is_text_input: bool,
64 ) -> InputContext {
65 let caps = app
66 .global::<PlatformCapabilities>()
67 .cloned()
68 .unwrap_or_default();
69 let mut input_ctx = InputContext {
70 platform: Platform::current(),
71 caps,
72 ui_has_modal,
73 window_arbitration: self
74 .window
75 .map(|_| self.window_input_arbitration_snapshot()),
76 focus_is_text_input,
77 text_boundary_mode: fret_runtime::TextBoundaryMode::UnicodeWord,
78 edit_can_undo: true,
79 edit_can_redo: true,
80 router_can_back: false,
81 router_can_forward: false,
82 dispatch_phase: InputDispatchPhase::Bubble,
83 };
84 if let Some(window) = self.window {
85 if let Some(mode) = app
86 .global::<fret_runtime::WindowTextBoundaryModeService>()
87 .and_then(|svc| svc.mode(window))
88 {
89 input_ctx.text_boundary_mode = mode;
90 }
91 if let Some(availability) = app
92 .global::<fret_runtime::WindowCommandAvailabilityService>()
93 .and_then(|svc| svc.snapshot(window))
94 .copied()
95 {
96 input_ctx.edit_can_undo = availability.edit_can_undo;
97 input_ctx.edit_can_redo = availability.edit_can_redo;
98 input_ctx.router_can_back = availability.router_can_back;
99 input_ctx.router_can_forward = availability.router_can_forward;
100 }
101 }
102 if let Some(mode) = self.focus_text_boundary_mode_override() {
103 input_ctx.text_boundary_mode = mode;
104 }
105 input_ctx
106 }
107
108 pub(in crate::tree) fn publish_window_input_context_snapshot(
109 &self,
110 app: &mut H,
111 input_ctx: &InputContext,
112 ) {
113 let Some(window) = self.window else {
114 return;
115 };
116 let needs_update = app
117 .global::<fret_runtime::WindowInputContextService>()
118 .and_then(|svc| svc.snapshot(window))
119 .is_none_or(|prev| prev != input_ctx);
120 if needs_update {
121 app.with_global_mut(
122 fret_runtime::WindowInputContextService::default,
123 |svc, _app| {
124 svc.set_snapshot(window, input_ctx.clone());
125 },
126 );
127 }
128 }
129
130 pub(in crate::tree) fn publish_window_input_context_snapshot_untracked(
131 &self,
132 app: &mut H,
133 input_ctx: &InputContext,
134 only_if_changed: bool,
135 ) {
136 let Some(window) = self.window else {
137 return;
138 };
139 if only_if_changed {
140 let needs_update = app
141 .global::<fret_runtime::WindowInputContextService>()
142 .and_then(|svc| svc.snapshot(window))
143 .is_none_or(|prev| prev != input_ctx);
144 if !needs_update {
145 return;
146 }
147 }
148 app.with_global_mut_untracked(
149 fret_runtime::WindowInputContextService::default,
150 |svc, _app| {
151 svc.set_snapshot(window, input_ctx.clone());
152 },
153 );
154 }
155
156 pub(in crate::tree) fn publish_window_key_context_stack_snapshot(
157 &self,
158 app: &mut H,
159 key_contexts: Vec<Arc<str>>,
160 ) {
161 let Some(window) = self.window else {
162 return;
163 };
164 let needs_update = app
165 .global::<fret_runtime::WindowKeyContextStackService>()
166 .and_then(|svc| svc.snapshot(window))
167 .is_none_or(|prev| prev != key_contexts.as_slice());
168 if needs_update {
169 app.with_global_mut(
170 fret_runtime::WindowKeyContextStackService::default,
171 |svc, _app| {
172 svc.set_snapshot(window, key_contexts);
173 },
174 );
175 }
176 }
177
178 pub(in crate::tree) fn publish_post_dispatch_runtime_snapshots_for_event(
179 &mut self,
180 app: &mut H,
181 event: &Event,
182 ) {
183 let focus_is_text_input = self.focus_is_text_input(app);
184 self.set_ime_allowed(app, focus_is_text_input);
185
186 let (_active_layers, barrier_root) = self.active_input_layers();
187 if matches!(event, Event::Pointer(fret_core::PointerEvent::Move { .. })) {
188 let input_ctx =
189 self.current_window_input_context(app, barrier_root.is_some(), focus_is_text_input);
190 self.publish_window_input_context_snapshot_untracked(app, &input_ctx, false);
191 } else {
192 self.publish_window_runtime_snapshots(app);
193 }
194 }
195
196 pub fn publish_window_runtime_snapshots(&mut self, app: &mut H) {
215 self.pending_declarative_window_snapshot_roots
216 .retain(|pending| self.nodes.contains_key(*pending));
217 self.resolve_pending_focus_target_if_needed(app);
218 let focused_element_before_revalidate = self.window.and_then(|window| {
219 self.focus.and_then(|focused| {
220 crate::elements::with_window_state(app, window, |state| {
221 state.element_for_node(focused)
222 })
223 })
224 });
225 let (_active_input_layers, input_barrier_root) = self.active_input_layers();
226 let (active_focus_layers, focus_barrier_root) = self.active_focus_layers();
227 let barrier_root = focus_barrier_root.or(input_barrier_root);
228
229 let focus_before_revalidate = self.focus;
230 self.revalidate_focus_for_dispatch_snapshot(
231 app.frame_id(),
232 active_focus_layers.as_slice(),
233 barrier_root,
234 "commands: focus missing from dispatch snapshot",
235 );
236 if focus_before_revalidate.is_some()
237 && self.focus.is_none()
238 && let Some(window) = self.window
239 && let Some(element) = focused_element_before_revalidate
240 && crate::elements::element_identity_is_live_in_current_frame(app, window, element)
241 {
242 self.pending_focus_target = Some(element);
247 self.request_post_layout_window_runtime_snapshot_refine();
248 }
249
250 self.revalidate_pending_shortcut_for_current_routing_context(app, barrier_root);
251
252 let focus_is_text_input = self.focus_is_text_input(app);
253 let input_ctx = self.current_window_input_context(
254 app,
255 input_barrier_root.is_some(),
256 focus_is_text_input,
257 );
258
259 self.publish_window_input_context_snapshot(app, &input_ctx);
260 self.publish_window_command_action_availability_snapshot(app, &input_ctx);
261 self.refresh_pending_shortcut_overlay_state_if_needed(app, &input_ctx);
262 }
263
264 pub(in crate::tree) fn request_post_layout_window_runtime_snapshot_refine(&mut self) {
265 self.pending_post_layout_window_runtime_snapshot_refine = true;
266 }
267
268 pub(in crate::tree) fn request_post_layout_window_runtime_snapshot_refine_if_layout_active(
269 &mut self,
270 ) {
271 if self.layout_call_depth > 0 {
272 self.request_post_layout_window_runtime_snapshot_refine();
273 }
274 }
275
276 pub fn commit_pending_declarative_window_runtime_snapshots(
289 &mut self,
290 app: &mut H,
291 root: NodeId,
292 ) -> bool {
293 self.pending_declarative_window_snapshot_roots
294 .retain(|pending| self.nodes.contains_key(*pending));
295 if !self
296 .pending_declarative_window_snapshot_roots
297 .contains(&root)
298 {
299 return false;
300 }
301
302 let attached = self.node_layer(root).is_some() || self.node_parent(root).is_some();
303 if !attached {
304 return false;
305 }
306
307 self.pending_declarative_window_snapshot_roots.remove(&root);
308 self.publish_window_runtime_snapshots(app);
309 true
310 }
311
312 fn focus_menu_bar_command_availability(&self, app: &mut H) -> CommandAvailability {
313 let Some(window) = self.window else {
314 return CommandAvailability::NotHandled;
315 };
316 let present = app
317 .global::<fret_runtime::WindowMenuBarFocusService>()
318 .is_some_and(|svc| svc.present(window));
319 if present {
320 CommandAvailability::Available
321 } else {
322 CommandAvailability::NotHandled
323 }
324 }
325
326 #[stacksafe::stacksafe]
327 pub fn is_command_available(&mut self, app: &mut H, command: &CommandId) -> bool {
328 self.command_availability(app, command) == CommandAvailability::Available
329 }
330
331 #[stacksafe::stacksafe]
335 pub fn is_action_available(&mut self, app: &mut H, command: &CommandId) -> bool {
336 self.is_command_available(app, command)
337 }
338
339 #[stacksafe::stacksafe]
341 pub fn action_availability(&mut self, app: &mut H, command: &CommandId) -> CommandAvailability {
342 self.command_availability(app, command)
343 }
344
345 #[stacksafe::stacksafe]
346 pub fn command_availability(
347 &mut self,
348 app: &mut H,
349 command: &CommandId,
350 ) -> CommandAvailability {
351 if command.as_str() == "focus.menu_bar" {
352 return self.focus_menu_bar_command_availability(app);
353 }
354
355 let Some(base_root) = self
356 .base_layer
357 .and_then(|id| self.layers.get(id).map(|l| l.root))
358 else {
359 return CommandAvailability::NotHandled;
360 };
361
362 let (_active_input_layers, input_barrier_root) = self.active_input_layers();
363 let (active_focus_layers, focus_barrier_root) = self.active_focus_layers();
364 let barrier_root = focus_barrier_root.or(input_barrier_root);
365 let dispatch_snapshot = self.build_dispatch_snapshot_for_layer_roots(
366 app.frame_id(),
367 active_focus_layers.as_slice(),
368 barrier_root,
369 );
370 let caps = app
371 .global::<PlatformCapabilities>()
372 .cloned()
373 .unwrap_or_default();
374 let mut input_ctx: InputContext = InputContext {
375 platform: Platform::current(),
376 caps,
377 ui_has_modal: input_barrier_root.is_some(),
378 window_arbitration: None,
379 focus_is_text_input: self.focus_is_text_input(app),
380 text_boundary_mode: fret_runtime::TextBoundaryMode::UnicodeWord,
381 edit_can_undo: true,
382 edit_can_redo: true,
383 router_can_back: false,
384 router_can_forward: false,
385 dispatch_phase: InputDispatchPhase::Bubble,
386 };
387 if let Some(window) = self.window {
388 if let Some(mode) = app
389 .global::<fret_runtime::WindowTextBoundaryModeService>()
390 .and_then(|svc| svc.mode(window))
391 {
392 input_ctx.text_boundary_mode = mode;
393 }
394 if let Some(availability) = app
395 .global::<fret_runtime::WindowCommandAvailabilityService>()
396 .and_then(|svc| svc.snapshot(window))
397 .copied()
398 {
399 input_ctx.edit_can_undo = availability.edit_can_undo;
400 input_ctx.edit_can_redo = availability.edit_can_redo;
401 input_ctx.router_can_back = availability.router_can_back;
402 input_ctx.router_can_forward = availability.router_can_forward;
403 }
404 input_ctx.window_arbitration = Some(self.window_input_arbitration_snapshot());
405 }
406
407 if self
408 .focus
409 .is_some_and(|n| dispatch_snapshot.pre.get(n).is_none())
410 {
411 self.set_focus_unchecked(None, "commands: focus missing from dispatch snapshot");
412 }
413
414 let default_root = barrier_root.unwrap_or(base_root);
415 let start = self.focus.unwrap_or(default_root);
416 let mut availability = self.command_availability_from_node(app, &input_ctx, start, command);
417 if availability == CommandAvailability::NotHandled && start != default_root {
421 availability =
422 self.command_availability_from_node(app, &input_ctx, default_root, command);
423 }
424
425 if availability == CommandAvailability::NotHandled
426 && matches!(command.as_str(), "focus.next" | "focus.previous")
427 {
428 return self.focus_traversal_command_availability(
429 app,
430 app.frame_id(),
431 &dispatch_snapshot,
432 barrier_root,
433 );
434 }
435
436 availability
437 }
438
439 fn focus_traversal_command_availability(
440 &mut self,
441 app: &mut H,
442 frame_id: fret_runtime::FrameId,
443 dispatch_snapshot: &UiDispatchSnapshot,
444 barrier_root: Option<NodeId>,
445 ) -> CommandAvailability {
446 self.focus_traversal_command_availability_for_snapshot(
447 app,
448 frame_id,
449 dispatch_snapshot,
450 barrier_root,
451 )
452 .0
453 }
454
455 fn focus_traversal_command_availability_for_snapshot(
456 &mut self,
457 app: &mut H,
458 frame_id: fret_runtime::FrameId,
459 dispatch_snapshot: &UiDispatchSnapshot,
460 scope_root: Option<NodeId>,
461 ) -> (CommandAvailability, bool) {
462 let (focusables, needs_layout_refine) = self.focus_traversal_candidates_for_snapshot(
463 app,
464 frame_id,
465 dispatch_snapshot,
466 scope_root,
467 );
468
469 (
470 if focusables.is_empty() {
471 CommandAvailability::NotHandled
472 } else {
473 CommandAvailability::Available
474 },
475 needs_layout_refine && !focusables.is_empty(),
476 )
477 }
478
479 fn focus_traversal_candidates_for_snapshot(
480 &mut self,
481 app: &mut H,
482 frame_id: fret_runtime::FrameId,
483 dispatch_snapshot: &UiDispatchSnapshot,
484 scope_root: Option<NodeId>,
485 ) -> (Vec<NodeId>, bool) {
486 let scope_root = scope_root.or(dispatch_snapshot.barrier_root).or_else(|| {
487 self.base_layer
488 .and_then(|id| self.layers.get(id).map(|l| l.root))
489 });
490 let Some(scope_root) = scope_root else {
491 return (Vec::new(), false);
492 };
493
494 let mut focusables: Vec<NodeId> = Vec::new();
495 let needs_layout_refine = self.last_layout_frame_id != Some(frame_id)
496 || self.node_subtree_layout_dirty(scope_root);
497 if needs_layout_refine {
498 for &root in &dispatch_snapshot.active_layer_roots {
499 self.collect_focusables_structural(app, root, dispatch_snapshot, &mut focusables);
500 }
501 } else {
502 let scope_bounds = self
503 .nodes
504 .get(scope_root)
505 .map(|n| n.bounds)
506 .unwrap_or_default();
507 for &root in &dispatch_snapshot.active_layer_roots {
508 self.collect_focusables(root, dispatch_snapshot, scope_bounds, &mut focusables);
509 }
510 }
511
512 (focusables, needs_layout_refine)
513 }
514
515 fn collect_focusables_structural(
516 &self,
517 app: &mut H,
518 node: NodeId,
519 dispatch_snapshot: &UiDispatchSnapshot,
520 out: &mut Vec<NodeId>,
521 ) {
522 if dispatch_snapshot.pre.get(node).is_none() {
523 return;
524 }
525
526 let Some(n) = self.nodes.get(node) else {
527 return;
528 };
529
530 let (is_focusable, traverse_children) =
531 self.structural_focus_traversal_state_for_node(app, node);
532 if is_focusable {
533 out.push(node);
534 }
535
536 if traverse_children {
537 for &child in &n.children {
538 self.collect_focusables_structural(app, child, dispatch_snapshot, out);
539 }
540 }
541 }
542
543 fn structural_focus_traversal_state_for_node(&self, app: &mut H, node: NodeId) -> (bool, bool) {
544 if let Some(window) = self.window
545 && let Some((is_focusable, traverse_children)) =
546 crate::declarative::frame::with_element_record_for_node(
547 app,
548 window,
549 node,
550 |record| match &record.instance {
551 crate::declarative::frame::ElementInstance::TextInput(_)
552 | crate::declarative::frame::ElementInstance::TextArea(_)
553 | crate::declarative::frame::ElementInstance::TextInputRegion(_) => {
554 (true, true)
555 }
556 crate::declarative::frame::ElementInstance::SelectableText(_) => {
557 (true, true)
558 }
559 crate::declarative::frame::ElementInstance::Pressable(props) => {
560 (props.enabled && props.focusable, props.enabled)
561 }
562 crate::declarative::frame::ElementInstance::Semantics(props) => {
563 (props.focusable && !props.disabled && !props.hidden, true)
564 }
565 crate::declarative::frame::ElementInstance::InteractivityGate(props) => {
566 (false, props.present && props.interactive)
567 }
568 crate::declarative::frame::ElementInstance::FocusTraversalGate(props) => {
569 (false, props.traverse)
570 }
571 crate::declarative::frame::ElementInstance::Spinner(_) => (false, false),
572 _ => (false, true),
573 },
574 )
575 {
576 return (is_focusable, traverse_children);
577 }
578
579 let Some(n) = self.nodes.get(node) else {
580 return (false, true);
581 };
582 let prepaint =
583 (!self.inspection_active && !n.invalidation.hit_test && !n.invalidation.layout)
584 .then_some(n.prepaint_hit_test)
585 .flatten();
586 (
587 prepaint
588 .as_ref()
589 .map(|p| p.is_focusable)
590 .unwrap_or_else(|| n.widget.as_ref().is_some_and(|w| w.is_focusable())),
591 prepaint
592 .as_ref()
593 .map(|p| p.focus_traversal_children)
594 .unwrap_or_else(|| {
595 n.widget
596 .as_ref()
597 .map(|w| w.focus_traversal_children())
598 .unwrap_or(true)
599 }),
600 )
601 }
602
603 #[stacksafe::stacksafe]
604 fn command_availability_from_node(
605 &mut self,
606 app: &mut H,
607 input_ctx: &InputContext,
608 start: NodeId,
609 command: &CommandId,
610 ) -> CommandAvailability {
611 let mut node_id = start;
612 loop {
613 let (availability, parent) = self.with_widget_mut(node_id, |widget, tree| {
614 let parent = tree.nodes.get(node_id).and_then(|n| n.parent);
615 let window = tree.window;
616 let focus = tree.focus;
617 let mut cx = CommandAvailabilityCx {
618 app,
619 tree: &*tree,
620 node: node_id,
621 window,
622 input_ctx: input_ctx.clone(),
623 focus,
624 };
625 (widget.command_availability(&mut cx, command), parent)
626 });
627
628 match availability {
629 CommandAvailability::Available | CommandAvailability::Blocked => {
630 return availability;
631 }
632 CommandAvailability::NotHandled => {}
633 }
634
635 node_id = match parent {
636 Some(parent) => parent,
637 None => break,
638 };
639 }
640
641 CommandAvailability::NotHandled
642 }
643
644 pub fn publish_window_command_action_availability_snapshot(
655 &mut self,
656 app: &mut H,
657 input_ctx: &InputContext,
658 ) {
659 let Some(window) = self.window else {
660 return;
661 };
662
663 let Some(base_root) = self
664 .base_layer
665 .and_then(|id| self.layers.get(id).map(|l| l.root))
666 else {
667 return;
668 };
669 let (_active_input_layers, input_barrier_root) = self.active_input_layers();
670 let (active_focus_layers, focus_barrier_root) = self.active_focus_layers();
671 let barrier_root = focus_barrier_root.or(input_barrier_root);
672 let dispatch_snapshot = self.build_dispatch_snapshot_for_layer_roots(
673 app.frame_id(),
674 active_focus_layers.as_slice(),
675 barrier_root,
676 );
677 self.revalidate_focus_for_dispatch_snapshot(
678 app.frame_id(),
679 active_focus_layers.as_slice(),
680 barrier_root,
681 "commands: focus missing from dispatch snapshot",
682 );
683
684 let default_root = barrier_root.unwrap_or(base_root);
685 let focus = self.focus;
686 let focus_in_default_root = focus.is_some_and(|n| self.is_descendant(default_root, n));
687 let start = focus.unwrap_or(default_root);
688 let next_key_contexts = self.shortcut_key_context_stack(app, barrier_root);
689 let mut focus_traversal_snapshot: Option<(CommandAvailability, bool)> = None;
690
691 let mut snapshot: HashMap<CommandId, bool> = HashMap::new();
692 let widget_commands: Vec<CommandId> = app
693 .commands()
694 .iter()
695 .filter_map(|(id, meta)| (meta.scope == CommandScope::Widget).then_some(id.clone()))
696 .collect();
697
698 for id in widget_commands {
699 if id.as_str() == "focus.menu_bar" {
700 let present = app
701 .global::<fret_runtime::WindowMenuBarFocusService>()
702 .is_some_and(|svc| svc.present(window));
703 snapshot.insert(id, present);
704 continue;
705 }
706
707 let mut availability = self.command_availability_from_node(app, input_ctx, start, &id);
708 if availability == CommandAvailability::NotHandled
709 && focus.is_some()
710 && !focus_in_default_root
711 && start != default_root
712 {
713 availability =
714 self.command_availability_from_node(app, input_ctx, default_root, &id);
715 }
716 if availability == CommandAvailability::NotHandled
717 && matches!(id.as_str(), "focus.next" | "focus.previous")
718 {
719 let (focus_traversal_availability, needs_layout_refine) = *focus_traversal_snapshot
720 .get_or_insert_with(|| {
721 self.focus_traversal_command_availability_for_snapshot(
722 app,
723 app.frame_id(),
724 &dispatch_snapshot,
725 barrier_root,
726 )
727 });
728 availability = focus_traversal_availability;
729 if needs_layout_refine {
730 self.pending_post_layout_window_runtime_snapshot_refine = true;
731 }
732 }
733 if availability == CommandAvailability::NotHandled && id.as_str() == "focus.menu_bar" {
734 let present = app
735 .global::<fret_runtime::WindowMenuBarFocusService>()
736 .is_some_and(|svc| svc.present(window));
737 snapshot.insert(id, present);
738 continue;
739 }
740 match availability {
741 CommandAvailability::Available => {
742 snapshot.insert(id, true);
743 }
744 CommandAvailability::Blocked => {
745 snapshot.insert(id, false);
746 }
747 CommandAvailability::NotHandled => {
748 snapshot.insert(id, false);
752 }
753 }
754 }
755
756 self.publish_window_key_context_stack_snapshot(app, next_key_contexts);
757
758 let needs_update = app
759 .global::<fret_runtime::WindowCommandActionAvailabilityService>()
760 .and_then(|svc| svc.snapshot(window))
761 .is_none_or(|prev| prev != &snapshot);
762 if needs_update {
763 app.with_global_mut(
764 fret_runtime::WindowCommandActionAvailabilityService::default,
765 |svc, _app| {
766 svc.set_snapshot(window, snapshot);
767 },
768 );
769 }
770 }
771
772 pub(in crate::tree) fn refine_pending_window_runtime_snapshots_after_layout(
773 &mut self,
774 app: &mut H,
775 ) {
776 if !std::mem::take(&mut self.pending_post_layout_window_runtime_snapshot_refine) {
777 return;
778 }
779 self.publish_window_runtime_snapshots(app);
780 }
781
782 #[stacksafe::stacksafe]
783 pub fn dispatch_command(
784 &mut self,
785 app: &mut H,
786 services: &mut dyn UiServices,
787 command: &CommandId,
788 ) -> bool {
789 let Some(base_root) = self
790 .base_layer
791 .and_then(|id| self.layers.get(id).map(|l| l.root))
792 else {
793 return false;
794 };
795
796 let (_active_input_layers, input_barrier_root) = self.active_input_layers();
797 let (active_focus_layers, focus_barrier_root) = self.active_focus_layers();
798 let barrier_root = focus_barrier_root.or(input_barrier_root);
799 let dispatch_snapshot = self.build_dispatch_snapshot_for_layer_roots(
800 app.frame_id(),
801 active_focus_layers.as_slice(),
802 barrier_root,
803 );
804 let caps = app
805 .global::<PlatformCapabilities>()
806 .cloned()
807 .unwrap_or_default();
808 let mut input_ctx = InputContext {
809 platform: Platform::current(),
810 caps,
811 ui_has_modal: input_barrier_root.is_some(),
812 window_arbitration: None,
813 focus_is_text_input: self.focus_is_text_input(app),
814 text_boundary_mode: fret_runtime::TextBoundaryMode::UnicodeWord,
815 edit_can_undo: true,
816 edit_can_redo: true,
817 router_can_back: false,
818 router_can_forward: false,
819 dispatch_phase: InputDispatchPhase::Bubble,
820 };
821 if let Some(window) = self.window {
822 if let Some(mode) = app
823 .global::<fret_runtime::WindowTextBoundaryModeService>()
824 .and_then(|svc| svc.mode(window))
825 {
826 input_ctx.text_boundary_mode = mode;
827 }
828 if let Some(availability) = app
829 .global::<fret_runtime::WindowCommandAvailabilityService>()
830 .and_then(|svc| svc.snapshot(window))
831 .copied()
832 {
833 input_ctx.edit_can_undo = availability.edit_can_undo;
834 input_ctx.edit_can_redo = availability.edit_can_redo;
835 input_ctx.router_can_back = availability.router_can_back;
836 input_ctx.router_can_forward = availability.router_can_forward;
837 }
838
839 let window_arbitration = self.window_input_arbitration_snapshot();
840 input_ctx.window_arbitration = Some(window_arbitration);
841
842 let needs_update = app
843 .global::<fret_runtime::WindowInputContextService>()
844 .and_then(|svc| svc.snapshot(window))
845 .is_none_or(|prev| prev != &input_ctx);
846 if needs_update {
847 app.with_global_mut(
848 fret_runtime::WindowInputContextService::default,
849 |svc, _app| {
850 svc.set_snapshot(window, input_ctx.clone());
851 },
852 );
853 }
854 }
855 let is_focus_traversal_command =
856 matches!(command.as_str(), "focus.next" | "focus.previous");
857
858 if self
859 .focus
860 .is_some_and(|n| dispatch_snapshot.pre.get(n).is_none())
861 {
862 self.set_focus_unchecked(None, "commands: focus missing from dispatch snapshot");
863 }
864 self.revalidate_pending_shortcut_for_current_routing_context(app, barrier_root);
865
866 let default_root = barrier_root.unwrap_or(base_root);
867 let focus = self.focus;
868
869 let source = if let Some(window) = self.window {
870 app.with_global_mut(
871 fret_runtime::WindowPendingCommandDispatchSourceService::default,
872 |svc, app| {
873 svc.consume(window, app.tick_id(), command)
874 .unwrap_or_else(fret_runtime::CommandDispatchSourceV1::programmatic)
875 },
876 )
877 } else {
878 fret_runtime::CommandDispatchSourceV1::programmatic()
879 };
880
881 let source_node = source.element.and_then(|element| {
882 self.resolve_live_attached_node_for_element(
883 app,
884 self.window,
885 crate::GlobalElementId(element),
886 )
887 });
888
889 let start = source_node.or(focus).unwrap_or(default_root);
890 let start_in_default_root =
891 start == default_root || self.is_descendant(default_root, start);
892
893 let mut bubble_from = |start: NodeId| -> (bool, bool, bool, Option<NodeId>) {
894 let mut node_id = start;
895 let mut handled = false;
896 let mut needs_redraw = false;
897 let mut stopped = false;
898 let mut handled_by_node: Option<NodeId> = None;
899
900 loop {
901 let (
902 did_handle,
903 invalidations,
904 requested_focus,
905 notify_requested,
906 notify_requested_location,
907 stop_bubbling,
908 parent,
909 ) = self.with_widget_mut(node_id, |widget, tree| {
910 let parent = tree.nodes.get(node_id).and_then(|n| n.parent);
911 let window = tree.window;
912 let focus = tree.focus;
913 let mut cx = CommandCx {
914 app,
915 services: &mut *services,
916 tree,
917 node: node_id,
918 window,
919 input_ctx: input_ctx.clone(),
920 focus,
921 invalidations: Vec::new(),
922 requested_focus: None,
923 notify_requested: false,
924 notify_requested_location: None,
925 stop_propagation: false,
926 };
927 let did_handle = widget.command(&mut cx, command);
928 (
929 did_handle,
930 cx.invalidations,
931 cx.requested_focus,
932 cx.notify_requested,
933 cx.notify_requested_location,
934 cx.stop_propagation,
935 parent,
936 )
937 });
938
939 if did_handle {
940 handled = true;
941 handled_by_node = handled_by_node.or(Some(node_id));
942 }
943
944 if !invalidations.is_empty() || requested_focus.is_some() || notify_requested {
945 needs_redraw = true;
946 }
947
948 for (id, inv) in invalidations {
949 self.mark_invalidation(id, inv);
950 }
951
952 if notify_requested {
953 self.debug_record_notify_request(
954 app.frame_id(),
955 node_id,
956 notify_requested_location,
957 );
958 self.mark_invalidation_with_source(
959 node_id,
960 Invalidation::Paint,
961 UiDebugInvalidationSource::Notify,
962 );
963 needs_redraw = true;
964 }
965
966 if let Some(focus) = requested_focus {
967 let (active_roots, barrier_root) = self.active_input_layers();
968 let snapshot = self.build_dispatch_snapshot_for_layer_roots(
969 app.frame_id(),
970 active_roots.as_slice(),
971 barrier_root,
972 );
973 if self.focus_request_is_allowed(
974 app,
975 self.window,
976 &active_roots,
977 focus,
978 Some(&snapshot),
979 ) {
980 if let Some(prev) = self.focus {
981 self.mark_invalidation(prev, Invalidation::Paint);
982 }
983 self.focus = Some(focus);
984 self.mark_invalidation(focus, Invalidation::Paint);
985 }
986 }
987
988 if did_handle {
989 break;
990 }
991 if stop_bubbling {
992 stopped = true;
993 break;
994 }
995
996 node_id = match parent {
997 Some(parent) => parent,
998 None => break,
999 };
1000 }
1001
1002 (handled, needs_redraw, stopped, handled_by_node)
1003 };
1004
1005 let (mut handled, mut needs_redraw, mut stopped, mut handled_by_node) = bubble_from(start);
1006 let mut used_default_root_fallback = false;
1007 if !handled && !stopped && start != default_root && !start_in_default_root {
1008 used_default_root_fallback = true;
1009 let (handled2, needs_redraw2, stopped2, handled_by_node2) = bubble_from(default_root);
1010 handled = handled || handled2;
1011 needs_redraw = needs_redraw || needs_redraw2;
1012 stopped = stopped || stopped2;
1013 handled_by_node = handled_by_node.or(handled_by_node2);
1014 }
1015
1016 if !handled && !stopped && is_focus_traversal_command {
1017 handled = self.dispatch_focus_traversal(
1018 app,
1019 command,
1020 active_focus_layers.as_slice(),
1021 barrier_root,
1022 );
1023 needs_redraw = true;
1024 }
1025
1026 if needs_redraw {
1027 self.request_redraw_coalesced(app);
1028 }
1029
1030 if let Some(window) = self.window {
1033 let (_active_layers, input_barrier_root) = self.active_input_layers();
1034 let (_active_focus_layers, focus_barrier_root) = self.active_focus_layers();
1035 let barrier_root = focus_barrier_root.or(input_barrier_root);
1036 self.revalidate_pending_shortcut_for_current_routing_context(app, barrier_root);
1037 let caps = app
1038 .global::<PlatformCapabilities>()
1039 .cloned()
1040 .unwrap_or_default();
1041 let mut input_ctx = InputContext {
1042 platform: Platform::current(),
1043 caps,
1044 ui_has_modal: input_barrier_root.is_some(),
1045 window_arbitration: None,
1046 focus_is_text_input: self.focus_is_text_input(app),
1047 text_boundary_mode: fret_runtime::TextBoundaryMode::UnicodeWord,
1048 edit_can_undo: true,
1049 edit_can_redo: true,
1050 router_can_back: false,
1051 router_can_forward: false,
1052 dispatch_phase: InputDispatchPhase::Bubble,
1053 };
1054 if let Some(mode) = app
1055 .global::<fret_runtime::WindowTextBoundaryModeService>()
1056 .and_then(|svc| svc.mode(window))
1057 {
1058 input_ctx.text_boundary_mode = mode;
1059 }
1060 if let Some(availability) = app
1061 .global::<fret_runtime::WindowCommandAvailabilityService>()
1062 .and_then(|svc| svc.snapshot(window))
1063 .copied()
1064 {
1065 input_ctx.edit_can_undo = availability.edit_can_undo;
1066 input_ctx.edit_can_redo = availability.edit_can_redo;
1067 input_ctx.router_can_back = availability.router_can_back;
1068 input_ctx.router_can_forward = availability.router_can_forward;
1069 }
1070
1071 let window_arbitration = self.window_input_arbitration_snapshot();
1072 input_ctx.window_arbitration = Some(window_arbitration);
1073
1074 let needs_update = app
1075 .global::<fret_runtime::WindowInputContextService>()
1076 .and_then(|svc| svc.snapshot(window))
1077 .is_none_or(|prev| prev != &input_ctx);
1078 if needs_update {
1079 app.with_global_mut(
1080 fret_runtime::WindowInputContextService::default,
1081 |svc, _app| {
1082 svc.set_snapshot(window, input_ctx.clone());
1083 },
1084 );
1085 }
1086
1087 self.publish_window_command_action_availability_snapshot(app, &input_ctx);
1088 self.refresh_pending_shortcut_overlay_state_if_needed(app, &input_ctx);
1089 }
1090
1091 if let Some(window) = self.window {
1092 let handled_by_element = handled_by_node
1093 .and_then(|node| self.node_element(node))
1094 .map(|id| id.0);
1095 let started_from_focus = focus.is_some();
1096
1097 app.with_global_mut(
1098 fret_runtime::WindowCommandDispatchDiagnosticsStore::default,
1099 |store, app| {
1100 let handled_by_scope = if handled {
1101 Some(fret_runtime::CommandScope::Widget)
1102 } else {
1103 None
1104 };
1105 store.record(fret_runtime::CommandDispatchDecisionV1 {
1106 seq: 0,
1107 frame_id: app.frame_id(),
1108 tick_id: app.tick_id(),
1109 window,
1110 command: command.clone(),
1111 source,
1112 handled,
1113 handled_by_element,
1114 handled_by_scope,
1115 handled_by_driver: false,
1116 stopped,
1117 started_from_focus,
1118 used_default_root_fallback,
1119 });
1120 },
1121 );
1122 }
1123
1124 handled
1125 }
1126
1127 fn dispatch_focus_traversal(
1128 &mut self,
1129 app: &mut H,
1130 command: &CommandId,
1131 active_focus_layers: &[NodeId],
1132 scope_root: Option<NodeId>,
1133 ) -> bool {
1134 let direction = match command.as_str() {
1135 "focus.next" => Some(true),
1136 "focus.previous" => Some(false),
1137 _ => None,
1138 };
1139 let Some(forward) = direction else {
1140 return false;
1141 };
1142
1143 self.focus_traverse_in_roots(app, active_focus_layers, forward, scope_root)
1144 }
1145
1146 pub fn focus_traverse_in_roots(
1153 &mut self,
1154 app: &mut H,
1155 roots: &[NodeId],
1156 forward: bool,
1157 scope_root: Option<NodeId>,
1158 ) -> bool {
1159 let dispatch_snapshot =
1160 self.build_dispatch_snapshot_for_layer_roots(app.frame_id(), roots, scope_root);
1161 let (focusables, _) = self.focus_traversal_candidates_for_snapshot(
1162 app,
1163 app.frame_id(),
1164 &dispatch_snapshot,
1165 scope_root,
1166 );
1167 if focusables.is_empty() {
1168 return true;
1169 }
1170
1171 let next = match self
1172 .focus
1173 .and_then(|f| focusables.iter().position(|n| *n == f))
1174 {
1175 Some(idx) => {
1176 if forward {
1177 focusables[(idx + 1) % focusables.len()]
1178 } else {
1179 focusables[(idx + focusables.len() - 1) % focusables.len()]
1180 }
1181 }
1182 None => {
1183 if forward {
1184 focusables[0]
1185 } else {
1186 focusables[focusables.len() - 1]
1187 }
1188 }
1189 };
1190
1191 if self.focus != Some(next) {
1192 if let Some(prev) = self.focus {
1193 self.mark_invalidation(prev, Invalidation::Paint);
1194 }
1195 self.focus = Some(next);
1196 self.mark_invalidation(next, Invalidation::Paint);
1197 self.scroll_node_into_view(app, next);
1198 }
1199 self.request_redraw_coalesced(app);
1200 true
1201 }
1202 pub fn scroll_node_into_view(&mut self, app: &mut H, target: NodeId) -> bool {
1203 let Some(target_bounds) = self.nodes.get(target).map(|n| n.bounds) else {
1204 return false;
1205 };
1206
1207 let mut node = self.nodes.get(target).and_then(|n| n.parent);
1213 let mut any_scrolled = false;
1214 let mut descendant_bounds = target_bounds;
1215 while let Some(id) = node {
1216 let parent = self.nodes.get(id).and_then(|n| n.parent);
1217 node = parent;
1218
1219 let Some(bounds) = self.nodes.get(id).map(|n| n.bounds) else {
1220 continue;
1221 };
1222
1223 let Some(widget) = self.nodes.get(id).and_then(|n| n.widget.as_ref()) else {
1224 continue;
1225 };
1226 if !widget.can_scroll_descendant_into_view() {
1227 continue;
1228 }
1229
1230 let result = self.with_widget_mut(id, |widget, tree| {
1231 let mut cx = crate::widget::ScrollIntoViewCx {
1232 app,
1233 node: id,
1234 window: tree.window,
1235 bounds,
1236 };
1237 widget.scroll_descendant_into_view(&mut cx, descendant_bounds)
1238 });
1239
1240 if let crate::widget::ScrollIntoViewResult::Handled {
1241 did_scroll,
1242 propagated_bounds,
1243 } = result
1244 {
1245 if did_scroll {
1246 any_scrolled = true;
1247 self.mark_invalidation(id, Invalidation::HitTest);
1248 if self.focus == Some(target)
1249 && self
1250 .nodes
1251 .get(target)
1252 .and_then(|n| n.widget.as_ref())
1253 .is_some_and(|w| w.is_text_input())
1254 {
1255 self.mark_invalidation(target, Invalidation::Paint);
1256 }
1257 self.request_redraw_coalesced(app);
1258 }
1259 descendant_bounds = propagated_bounds.unwrap_or(bounds);
1262 continue;
1263 }
1264 }
1265
1266 any_scrolled
1267 }
1268
1269 pub fn scroll_by(&mut self, app: &mut H, target: NodeId, delta: Point) -> bool {
1270 let Some(bounds) = self.nodes.get(target).map(|n| n.bounds) else {
1271 return false;
1272 };
1273
1274 let result = self.with_widget_mut(target, |widget, tree| {
1275 let mut cx = crate::widget::ScrollByCx {
1276 app,
1277 node: target,
1278 window: tree.window,
1279 bounds,
1280 };
1281 widget.scroll_by(&mut cx, delta)
1282 });
1283
1284 match result {
1285 crate::widget::ScrollByResult::NotHandled => false,
1286 crate::widget::ScrollByResult::Handled { did_scroll } => {
1287 if did_scroll {
1288 self.mark_invalidation(target, Invalidation::HitTestOnly);
1289 self.request_redraw_coalesced(app);
1290 }
1291 did_scroll
1292 }
1293 }
1294 }
1295}