Skip to main content

fret_runtime/
window_input_context.rs

1use std::collections::HashMap;
2
3use fret_core::AppWindowId;
4
5use crate::{GlobalsHost, InputContext, WindowCommandAvailabilityService};
6
7/// Window-scoped `InputContext` snapshots published by the UI runtime.
8///
9/// This is a data-only integration seam that allows runner/platform layers (e.g. OS menu bars) to
10/// access focus/modal state without depending on `fret-ui` internals.
11#[derive(Debug, Default)]
12pub struct WindowInputContextService {
13    by_window: HashMap<AppWindowId, InputContext>,
14}
15
16impl WindowInputContextService {
17    pub fn window_count(&self) -> usize {
18        self.by_window.len()
19    }
20
21    pub fn snapshot(&self, window: AppWindowId) -> Option<&InputContext> {
22        self.by_window.get(&window)
23    }
24
25    pub fn set_snapshot(&mut self, window: AppWindowId, input_ctx: InputContext) {
26        self.by_window.insert(window, input_ctx);
27    }
28
29    pub fn remove_window(&mut self, window: AppWindowId) {
30        self.by_window.remove(&window);
31    }
32}
33
34fn apply_window_command_availability(
35    app: &impl GlobalsHost,
36    window: AppWindowId,
37    mut input_ctx: InputContext,
38) -> InputContext {
39    if let Some(availability) = app
40        .global::<WindowCommandAvailabilityService>()
41        .and_then(|svc| svc.snapshot(window))
42        .copied()
43    {
44        input_ctx.edit_can_undo = availability.edit_can_undo;
45        input_ctx.edit_can_redo = availability.edit_can_redo;
46        input_ctx.router_can_back = availability.router_can_back;
47        input_ctx.router_can_forward = availability.router_can_forward;
48    }
49    input_ctx
50}
51
52/// Best-effort: returns the published window input context snapshot, correcting command
53/// availability booleans from the authoritative `WindowCommandAvailabilityService` when present.
54pub fn best_effort_input_context_for_window(
55    app: &impl GlobalsHost,
56    window: AppWindowId,
57) -> Option<InputContext> {
58    app.global::<WindowInputContextService>()
59        .and_then(|svc| svc.snapshot(window))
60        .cloned()
61        .map(|input_ctx| apply_window_command_availability(app, window, input_ctx))
62}
63
64/// Best-effort: returns the published window input context snapshot if present, otherwise falls
65/// back to `fallback_input_ctx`, correcting command-availability booleans from the authoritative
66/// `WindowCommandAvailabilityService` when present.
67pub fn best_effort_input_context_for_window_with_fallback(
68    app: &impl GlobalsHost,
69    window: AppWindowId,
70    fallback_input_ctx: InputContext,
71) -> InputContext {
72    let input_ctx = app
73        .global::<WindowInputContextService>()
74        .and_then(|svc| svc.snapshot(window))
75        .cloned()
76        .unwrap_or(fallback_input_ctx);
77    apply_window_command_availability(app, window, input_ctx)
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    use std::any::{Any, TypeId};
85    use std::collections::HashMap;
86
87    #[derive(Default)]
88    struct TestGlobalsHost {
89        globals: HashMap<TypeId, Box<dyn Any>>,
90    }
91
92    impl GlobalsHost for TestGlobalsHost {
93        fn set_global<T: Any>(&mut self, value: T) {
94            self.globals.insert(TypeId::of::<T>(), Box::new(value));
95        }
96
97        fn global<T: Any>(&self) -> Option<&T> {
98            self.globals
99                .get(&TypeId::of::<T>())
100                .and_then(|value| value.downcast_ref::<T>())
101        }
102
103        fn with_global_mut<T: Any, R>(
104            &mut self,
105            init: impl FnOnce() -> T,
106            f: impl FnOnce(&mut T, &mut Self) -> R,
107        ) -> R {
108            let type_id = TypeId::of::<T>();
109            let mut value = match self.globals.remove(&type_id) {
110                Some(value) => *value
111                    .downcast::<T>()
112                    .expect("TestGlobalsHost stored wrong type"),
113                None => init(),
114            };
115
116            let out = f(&mut value, self);
117            self.globals.insert(type_id, Box::new(value));
118            out
119        }
120    }
121
122    #[test]
123    fn best_effort_input_context_overlays_authoritative_command_availability() {
124        let mut host = TestGlobalsHost::default();
125        let window = AppWindowId::default();
126
127        host.with_global_mut(WindowInputContextService::default, |svc, _host| {
128            svc.set_snapshot(
129                window,
130                InputContext {
131                    edit_can_undo: false,
132                    edit_can_redo: false,
133                    router_can_back: false,
134                    router_can_forward: false,
135                    ..Default::default()
136                },
137            );
138        });
139        host.with_global_mut(WindowCommandAvailabilityService::default, |svc, _host| {
140            svc.set_router_availability(window, true, false);
141            svc.set_edit_availability(window, true, false);
142        });
143
144        let input_ctx =
145            best_effort_input_context_for_window(&host, window).expect("published input context");
146        assert!(input_ctx.edit_can_undo);
147        assert!(!input_ctx.edit_can_redo);
148        assert!(input_ctx.router_can_back);
149        assert!(!input_ctx.router_can_forward);
150    }
151
152    #[test]
153    fn best_effort_input_context_fallback_inherits_command_availability() {
154        let mut host = TestGlobalsHost::default();
155        let window = AppWindowId::default();
156
157        host.with_global_mut(WindowCommandAvailabilityService::default, |svc, _host| {
158            svc.set_router_availability(window, true, true);
159            svc.set_edit_availability(window, false, true);
160        });
161
162        let input_ctx = best_effort_input_context_for_window_with_fallback(
163            &host,
164            window,
165            InputContext::default(),
166        );
167        assert!(!input_ctx.edit_can_undo);
168        assert!(input_ctx.edit_can_redo);
169        assert!(input_ctx.router_can_back);
170        assert!(input_ctx.router_can_forward);
171    }
172}