Skip to main content

fret_ui_kit/declarative/
action_hooks.rs

1use fret_runtime::{ActionId, CommandId, Effect, Model, WeakModel};
2use fret_ui::ElementContext;
3use fret_ui::UiHost;
4use fret_ui::action::UiActionHostExt;
5
6use crate::command::ElementCommandGatingExt as _;
7use crate::primitives::roving_focus_group;
8
9use std::any::Any;
10use std::cell::RefCell;
11use std::rc::Rc;
12use std::sync::Arc;
13
14/// Component-layer helpers for registering runtime action hooks (ADR 0074).
15///
16/// These helpers keep common interaction policies out of `crates/fret-ui` while remaining easy to
17/// use in declarative authoring code.
18///
19/// Note: command-dispatch helpers are intentionally gated (`*_if_enabled`) so "disabled" UI stays
20/// consistent across surfaces (menus, command palette, shortcuts, OS menus).
21pub trait ActionHooksExt {
22    fn pressable_dispatch_command_if_enabled(&mut self, command: CommandId);
23
24    fn pressable_dispatch_command_if_enabled_opt(&mut self, command: Option<CommandId>);
25
26    fn pressable_dispatch_command_with_payload_if_enabled<T>(
27        &mut self,
28        command: CommandId,
29        payload: T,
30    ) where
31        T: Any + Send + Sync + Clone + 'static;
32
33    fn pressable_dispatch_command_with_payload_if_enabled_opt<T>(
34        &mut self,
35        command: Option<CommandId>,
36        payload: T,
37    ) where
38        T: Any + Send + Sync + Clone + 'static;
39
40    fn pressable_dispatch_command_with_payload_factory_if_enabled(
41        &mut self,
42        command: CommandId,
43        payload: Arc<dyn Fn() -> Box<dyn Any + Send + Sync> + 'static>,
44    );
45
46    fn pressable_dispatch_command_with_payload_factory_if_enabled_opt(
47        &mut self,
48        command: Option<CommandId>,
49        payload: Arc<dyn Fn() -> Box<dyn Any + Send + Sync> + 'static>,
50    );
51
52    fn pressable_dispatch_action_if_enabled(&mut self, action: ActionId);
53
54    fn pressable_dispatch_action_if_enabled_opt(&mut self, action: Option<ActionId>);
55
56    fn pressable_dispatch_action_with_payload_if_enabled<T>(
57        &mut self,
58        action: ActionId,
59        payload: T,
60    ) where
61        T: Any + Send + Sync + Clone + 'static;
62
63    fn pressable_dispatch_action_with_payload_if_enabled_opt<T>(
64        &mut self,
65        action: Option<ActionId>,
66        payload: T,
67    ) where
68        T: Any + Send + Sync + Clone + 'static;
69
70    fn pressable_update_model<T, F>(&mut self, model: &Model<T>, update: F)
71    where
72        T: 'static,
73        F: Fn(&mut T) + 'static;
74
75    fn pressable_update_weak_model<T, F>(&mut self, model: &WeakModel<T>, update: F)
76    where
77        T: 'static,
78        F: Fn(&mut T) + 'static;
79
80    fn pressable_set_model<T>(&mut self, model: &Model<T>, value: T)
81    where
82        T: Clone + 'static;
83
84    fn pressable_set_weak_model<T>(&mut self, model: &WeakModel<T>, value: T)
85    where
86        T: Clone + 'static;
87
88    fn pressable_toggle_bool(&mut self, model: &Model<bool>);
89
90    fn pressable_toggle_bool_weak(&mut self, model: &WeakModel<bool>);
91
92    fn pressable_set_bool(&mut self, model: &Model<bool>, value: bool);
93
94    fn pressable_set_bool_weak(&mut self, model: &WeakModel<bool>, value: bool);
95
96    fn pressable_set_arc_str(&mut self, model: &Model<Arc<str>>, value: Arc<str>);
97
98    fn pressable_set_arc_str_weak(&mut self, model: &WeakModel<Arc<str>>, value: Arc<str>);
99
100    fn pressable_set_option_arc_str(&mut self, model: &Model<Option<Arc<str>>>, value: Arc<str>);
101
102    fn pressable_set_option_arc_str_weak(
103        &mut self,
104        model: &WeakModel<Option<Arc<str>>>,
105        value: Arc<str>,
106    );
107
108    fn pressable_toggle_vec_arc_str(&mut self, model: &Model<Vec<Arc<str>>>, value: Arc<str>);
109
110    fn pressable_toggle_vec_arc_str_weak(
111        &mut self,
112        model: &WeakModel<Vec<Arc<str>>>,
113        value: Arc<str>,
114    );
115
116    fn dismissible_close_bool(&mut self, open: &Model<bool>);
117
118    fn dismissible_close_bool_weak(&mut self, open: &WeakModel<bool>);
119
120    #[track_caller]
121    fn roving_select_option_arc_str(
122        &mut self,
123        model: &Model<Option<Arc<str>>>,
124        values: Arc<[Arc<str>]>,
125    );
126
127    #[track_caller]
128    fn roving_select_option_arc_str_weak(
129        &mut self,
130        model: &WeakModel<Option<Arc<str>>>,
131        values: Arc<[Arc<str>]>,
132    );
133
134    #[track_caller]
135    fn roving_typeahead_first_char_arc_str(&mut self, labels: Arc<[Arc<str>]>);
136
137    #[track_caller]
138    fn roving_typeahead_prefix_arc_str(&mut self, labels: Arc<[Arc<str>]>, timeout_ticks: u64);
139
140    /// Install an APG-aligned default keyboard navigation policy for `RovingFlex`.
141    ///
142    /// This keeps navigation policy out of `crates/fret-ui` while keeping declarative call sites
143    /// small and consistent.
144    fn roving_nav_apg(&mut self);
145}
146
147impl<H: UiHost> ActionHooksExt for ElementContext<'_, H> {
148    fn pressable_dispatch_command_if_enabled(&mut self, command: CommandId) {
149        if !self.command_is_enabled(&command) {
150            return;
151        }
152        self.pressable_add_on_activate(Arc::new(move |host, acx, reason| {
153            host.record_pending_command_dispatch_source(acx, &command, reason);
154            host.dispatch_command(Some(acx.window), command.clone());
155        }));
156    }
157
158    fn pressable_dispatch_command_if_enabled_opt(&mut self, command: Option<CommandId>) {
159        let Some(command) = command else {
160            return;
161        };
162        self.pressable_dispatch_command_if_enabled(command);
163    }
164
165    fn pressable_dispatch_command_with_payload_if_enabled<T>(
166        &mut self,
167        command: CommandId,
168        payload: T,
169    ) where
170        T: Any + Send + Sync + Clone + 'static,
171    {
172        if !self.command_is_enabled(&command) {
173            return;
174        }
175        self.pressable_add_on_activate(Arc::new(move |host, acx, reason| {
176            host.record_pending_command_dispatch_source(acx, &command, reason);
177            host.record_pending_action_payload(acx, &command, Box::new(payload.clone()));
178            host.dispatch_command(Some(acx.window), command.clone());
179        }));
180    }
181
182    fn pressable_dispatch_command_with_payload_if_enabled_opt<T>(
183        &mut self,
184        command: Option<CommandId>,
185        payload: T,
186    ) where
187        T: Any + Send + Sync + Clone + 'static,
188    {
189        let Some(command) = command else {
190            return;
191        };
192        self.pressable_dispatch_command_with_payload_if_enabled(command, payload);
193    }
194
195    fn pressable_dispatch_command_with_payload_factory_if_enabled(
196        &mut self,
197        command: CommandId,
198        payload: Arc<dyn Fn() -> Box<dyn Any + Send + Sync> + 'static>,
199    ) {
200        if !self.command_is_enabled(&command) {
201            return;
202        }
203        self.pressable_add_on_activate(Arc::new(move |host, acx, reason| {
204            host.record_pending_command_dispatch_source(acx, &command, reason);
205            host.record_pending_action_payload(acx, &command, payload());
206            host.dispatch_command(Some(acx.window), command.clone());
207        }));
208    }
209
210    fn pressable_dispatch_command_with_payload_factory_if_enabled_opt(
211        &mut self,
212        command: Option<CommandId>,
213        payload: Arc<dyn Fn() -> Box<dyn Any + Send + Sync> + 'static>,
214    ) {
215        let Some(command) = command else {
216            return;
217        };
218        self.pressable_dispatch_command_with_payload_factory_if_enabled(command, payload);
219    }
220
221    fn pressable_dispatch_action_if_enabled(&mut self, action: ActionId) {
222        self.pressable_dispatch_command_if_enabled(action);
223    }
224
225    fn pressable_dispatch_action_if_enabled_opt(&mut self, action: Option<ActionId>) {
226        self.pressable_dispatch_command_if_enabled_opt(action);
227    }
228
229    fn pressable_dispatch_action_with_payload_if_enabled<T>(&mut self, action: ActionId, payload: T)
230    where
231        T: Any + Send + Sync + Clone + 'static,
232    {
233        self.pressable_dispatch_command_with_payload_if_enabled(action, payload);
234    }
235
236    fn pressable_dispatch_action_with_payload_if_enabled_opt<T>(
237        &mut self,
238        action: Option<ActionId>,
239        payload: T,
240    ) where
241        T: Any + Send + Sync + Clone + 'static,
242    {
243        self.pressable_dispatch_command_with_payload_if_enabled_opt(action, payload);
244    }
245
246    fn pressable_update_model<T, F>(&mut self, model: &Model<T>, update: F)
247    where
248        T: 'static,
249        F: Fn(&mut T) + 'static,
250    {
251        let model = model.clone();
252        self.pressable_add_on_activate(Arc::new(move |host, acx, _reason| {
253            let _ = host.models_mut().update(&model, |v| update(v));
254            host.request_redraw(acx.window);
255            host.push_effect(Effect::RequestAnimationFrame(acx.window));
256        }));
257    }
258
259    fn pressable_update_weak_model<T, F>(&mut self, model: &WeakModel<T>, update: F)
260    where
261        T: 'static,
262        F: Fn(&mut T) + 'static,
263    {
264        let model = model.clone();
265        self.pressable_add_on_activate(Arc::new(move |host, acx, _reason| {
266            let _ = host.update_weak_model(&model, |v| update(v));
267            host.request_redraw(acx.window);
268            host.push_effect(Effect::RequestAnimationFrame(acx.window));
269        }));
270    }
271
272    fn pressable_set_model<T>(&mut self, model: &Model<T>, value: T)
273    where
274        T: Clone + 'static,
275    {
276        self.pressable_update_model(model, move |v| *v = value.clone());
277    }
278
279    fn pressable_set_weak_model<T>(&mut self, model: &WeakModel<T>, value: T)
280    where
281        T: Clone + 'static,
282    {
283        self.pressable_update_weak_model(model, move |v| *v = value.clone());
284    }
285
286    fn pressable_toggle_bool(&mut self, model: &Model<bool>) {
287        self.pressable_update_model(model, |v| *v = !*v);
288    }
289
290    fn pressable_toggle_bool_weak(&mut self, model: &WeakModel<bool>) {
291        self.pressable_update_weak_model(model, |v| *v = !*v);
292    }
293
294    fn pressable_set_bool(&mut self, model: &Model<bool>, value: bool) {
295        self.pressable_set_model(model, value);
296    }
297
298    fn pressable_set_bool_weak(&mut self, model: &WeakModel<bool>, value: bool) {
299        self.pressable_set_weak_model(model, value);
300    }
301
302    fn pressable_set_arc_str(&mut self, model: &Model<Arc<str>>, value: Arc<str>) {
303        self.pressable_set_model(model, value);
304    }
305
306    fn pressable_set_arc_str_weak(&mut self, model: &WeakModel<Arc<str>>, value: Arc<str>) {
307        self.pressable_set_weak_model(model, value);
308    }
309
310    fn pressable_set_option_arc_str(&mut self, model: &Model<Option<Arc<str>>>, value: Arc<str>) {
311        self.pressable_set_model(model, Some(value));
312    }
313
314    fn pressable_set_option_arc_str_weak(
315        &mut self,
316        model: &WeakModel<Option<Arc<str>>>,
317        value: Arc<str>,
318    ) {
319        self.pressable_set_weak_model(model, Some(value));
320    }
321
322    fn pressable_toggle_vec_arc_str(&mut self, model: &Model<Vec<Arc<str>>>, value: Arc<str>) {
323        let model = model.clone();
324        self.pressable_add_on_activate(Arc::new(move |host, _cx, _reason| {
325            let value = value.clone();
326            let _ = host.models_mut().update(&model, |v| {
327                if let Some(pos) = v.iter().position(|it| it.as_ref() == value.as_ref()) {
328                    v.remove(pos);
329                } else {
330                    v.push(value.clone());
331                }
332            });
333        }));
334    }
335
336    fn pressable_toggle_vec_arc_str_weak(
337        &mut self,
338        model: &WeakModel<Vec<Arc<str>>>,
339        value: Arc<str>,
340    ) {
341        let model = model.clone();
342        self.pressable_add_on_activate(Arc::new(move |host, _cx, _reason| {
343            let value = value.clone();
344            let _ = host.update_weak_model(&model, |v| {
345                if let Some(pos) = v.iter().position(|it| it.as_ref() == value.as_ref()) {
346                    v.remove(pos);
347                } else {
348                    v.push(value.clone());
349                }
350            });
351        }));
352    }
353
354    fn dismissible_close_bool(&mut self, open: &Model<bool>) {
355        let open = open.clone();
356        self.dismissible_add_on_dismiss_request(Arc::new(move |host, _cx, _req| {
357            let _ = host.models_mut().update(&open, |v| *v = false);
358        }));
359    }
360
361    fn dismissible_close_bool_weak(&mut self, open: &WeakModel<bool>) {
362        let open = open.clone();
363        self.dismissible_add_on_dismiss_request(Arc::new(move |host, _cx, _req| {
364            let _ = host.update_weak_model(&open, |v| *v = false);
365        }));
366    }
367
368    #[track_caller]
369    fn roving_select_option_arc_str(
370        &mut self,
371        model: &Model<Option<Arc<str>>>,
372        values: Arc<[Arc<str>]>,
373    ) {
374        let model = model.clone();
375        struct RovingSelectOptionArcStrState {
376            values: Rc<RefCell<Arc<[Arc<str>]>>>,
377            handler: fret_ui::action::OnRovingActiveChange,
378        }
379
380        let handler = self.slot_state(
381            || {
382                let values_cell: Rc<RefCell<Arc<[Arc<str>]>>> =
383                    Rc::new(RefCell::new(values.clone()));
384                let values_read = values_cell.clone();
385                let handler: fret_ui::action::OnRovingActiveChange = Arc::new(
386                    move |host: &mut dyn fret_ui::action::UiActionHost, _cx, idx| {
387                        let values = values_read.borrow();
388                        let Some(value) = values.get(idx).cloned() else {
389                            return;
390                        };
391                        let next = Some(value);
392                        let _ = host.models_mut().update(&model, |v| *v = next);
393                    },
394                );
395
396                RovingSelectOptionArcStrState {
397                    values: values_cell,
398                    handler,
399                }
400            },
401            |state| {
402                *state.values.borrow_mut() = values.clone();
403                state.handler.clone()
404            },
405        );
406
407        self.roving_add_on_active_change(handler);
408    }
409
410    #[track_caller]
411    fn roving_select_option_arc_str_weak(
412        &mut self,
413        model: &WeakModel<Option<Arc<str>>>,
414        values: Arc<[Arc<str>]>,
415    ) {
416        let model = model.clone();
417        struct RovingSelectOptionArcStrState {
418            values: Rc<RefCell<Arc<[Arc<str>]>>>,
419            handler: fret_ui::action::OnRovingActiveChange,
420        }
421
422        let handler = self.slot_state(
423            || {
424                let values_cell: Rc<RefCell<Arc<[Arc<str>]>>> =
425                    Rc::new(RefCell::new(values.clone()));
426                let values_read = values_cell.clone();
427                let handler: fret_ui::action::OnRovingActiveChange = Arc::new(
428                    move |host: &mut dyn fret_ui::action::UiActionHost, _cx, idx| {
429                        let values = values_read.borrow();
430                        let Some(value) = values.get(idx).cloned() else {
431                            return;
432                        };
433                        let next = Some(value);
434                        let _ = host.update_weak_model(&model, |v| *v = next);
435                    },
436                );
437
438                RovingSelectOptionArcStrState {
439                    values: values_cell,
440                    handler,
441                }
442            },
443            |state| {
444                *state.values.borrow_mut() = values.clone();
445                state.handler.clone()
446            },
447        );
448
449        self.roving_add_on_active_change(handler);
450    }
451
452    #[track_caller]
453    fn roving_typeahead_first_char_arc_str(&mut self, labels: Arc<[Arc<str>]>) {
454        roving_focus_group::typeahead_first_char_arc_str(self, labels);
455    }
456
457    #[track_caller]
458    fn roving_typeahead_prefix_arc_str(&mut self, labels: Arc<[Arc<str>]>, timeout_ticks: u64) {
459        roving_focus_group::typeahead_prefix_arc_str(self, labels, timeout_ticks);
460    }
461
462    fn roving_nav_apg(&mut self) {
463        roving_focus_group::nav_apg(self);
464    }
465}