Skip to main content

fret_ui/tree/
commands.rs

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        // `focus` / `barrier_root` are only proxies for the shortcut-routing context. Root
47        // replacement and other retained-tree repairs can change the authoritative key-context
48        // stack without changing either proxy (for example, when no node is focused). Re-check
49        // the current key-context stack before continuing a multi-stroke sequence.
50        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    /// Publishes authoritative window-level runtime snapshots for the tree's current retained
197    /// state.
198    ///
199    /// Raw `UiTree` mutation APIs (`set_root`, `set_focus`, overlay/layer mutation, subtree
200    /// removal, and similar helpers) only update retained tree state. Cross-surface consumers that
201    /// read `WindowInputContextService`, `WindowKeyContextStackService`,
202    /// `PendingShortcutOverlayState`, or
203    /// `WindowCommandActionAvailabilityService` become authoritative only after this publish step
204    /// or another full snapshot commit boundary such as declarative rebuild or non-pointer input
205    /// dispatch. Paint-only boundaries refresh `WindowInputContextService`, but they do not
206    /// republish the full key-context / command-availability snapshot set.
207    ///
208    /// Layout-time raw focus/layer mutations are the one exception: they automatically schedule a
209    /// post-layout refine so final layout boundaries can republish authoritative snapshots without
210    /// forcing policy code to publish from inside `layout()`.
211    ///
212    /// Call this after imperative tree mutations when later same-frame consumers must observe the
213    /// new authoritative window state immediately.
214    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            // Declarative overlay/content roots can attach before final layout makes them part of
243            // the authoritative dispatch snapshot. Preserve the element identity as a deferred
244            // target so the final-layout snapshot refine can recover focus instead of dropping it
245            // for the rest of the frame.
246            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    /// Finalize a declarative rebuild that mounted a detached root and only later attached it to
277    /// the retained tree.
278    ///
279    /// `render_dismissible_root_with_hooks(...)` can rebuild an overlay/portal root before the
280    /// caller attaches that root to a layer or parent. In that case the helper defers the window
281    /// snapshot commit until the root is actually attached. Call this after `push_overlay_root`,
282    /// `set_children`, or another attach operation that makes the returned root authoritative for
283    /// same-frame window-level consumers.
284    ///
285    /// This is intentionally narrower than `publish_window_runtime_snapshots(...)`: raw imperative
286    /// tree mutation still requires an explicit commit, while declarative detached-root authoring
287    /// can finish its pending commit once attachment is complete.
288    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    /// GPUI naming parity: "is this action available along the dispatch path?"
332    ///
333    /// Note: Fret models "actions" as `CommandId` today (especially for widget-scoped commands).
334    #[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    /// GPUI naming parity for availability queries.
340    #[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        // When focus lives in a non-default layer (e.g. a non-modal overlay), we still want
418        // widget-scoped command availability to fall back to the default root so global shortcuts
419        // and menus remain usable.
420        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    /// Publish a per-window action availability snapshot for widget-scoped commands.
645    ///
646    /// This is a data-only integration seam for runner/platform and UI-kit layers (menus, command
647    /// palette, shortcut help). Most apps should prefer publishing a filtered snapshot (e.g. only
648    /// menu/palette command sets) at the app-driver layer.
649    ///
650    /// Notes:
651    /// - This retained-runtime helper publishes a conservative baseline: for each widget-scoped
652    ///   command in the registry, `NotHandled` is treated as "unavailable" (`false`) so
653    ///   cross-surface gating behaves consistently.
654    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                    // For widget-scoped commands, “not handled anywhere on the dispatch path”
749                    // means “not available” (disabled) for cross-surface gating (menus, palettes,
750                    // shortcuts).
751                    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        // Publish a post-dispatch snapshot so runner-level integration surfaces (e.g. OS menubars)
1031        // see the latest focus/modal state without waiting for the next paint pass.
1032        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    /// Focus traversal mechanism used by both the runtime default and component-owned focus scopes.
1147    ///
1148    /// Notes:
1149    /// - `roots` are treated as the authoritative traversal roots for this dispatch path.
1150    /// - `scope_root` gates authoritative geometry clipping when layout is current.
1151    /// - This is intentionally conservative until we formalize a scroll-into-view contract (ADR 0068).
1152    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        // Only scroll *ancestors* of the target into view.
1208        //
1209        // If the target itself is scrollable, attempting to scroll it “into view” via itself can
1210        // incorrectly mutate its offset (e.g. resetting a virtual list to top when it receives
1211        // focus).
1212        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                // Once an ancestor handles the request, outer ancestors should align that
1260                // ancestor's effective viewport rather than the original deep target bounds.
1261                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}