Skip to main content

fret_ui_kit/primitives/
control_registry.rs

1use std::any::Any;
2use std::collections::HashMap;
3use std::fmt;
4use std::sync::Arc;
5
6use fret_core::AppWindowId;
7use fret_runtime::{CommandId, FrameId, Model};
8use fret_ui::action::{ActionCx, ActivateReason, UiActionHost};
9use fret_ui::{ElementContext, GlobalElementId, UiHost};
10
11use crate::headless::checked_state::CheckedState;
12
13#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14pub struct ControlId(Arc<str>);
15
16impl ControlId {
17    pub fn new(id: impl Into<Arc<str>>) -> Self {
18        Self(id.into())
19    }
20
21    pub fn as_str(&self) -> &str {
22        self.0.as_ref()
23    }
24}
25
26impl From<Arc<str>> for ControlId {
27    fn from(value: Arc<str>) -> Self {
28        Self(value)
29    }
30}
31
32impl From<String> for ControlId {
33    fn from(value: String) -> Self {
34        Self(Arc::from(value))
35    }
36}
37
38impl From<&str> for ControlId {
39    fn from(value: &str) -> Self {
40        Self(Arc::from(value))
41    }
42}
43
44pub type ControlPayloadFactory = Arc<dyn Fn() -> Box<dyn Any + Send + Sync> + 'static>;
45
46#[derive(Clone)]
47pub enum ControlAction {
48    ToggleBool(Model<bool>),
49    ToggleOptionalBool(Model<Option<bool>>),
50    ToggleCheckedState(Model<CheckedState>),
51    SetOptionalArcStr(Model<Option<Arc<str>>>, Arc<str>),
52    DispatchCommand {
53        command: CommandId,
54        payload: Option<ControlPayloadFactory>,
55    },
56    Sequence(Arc<[ControlAction]>),
57    Noop,
58    FocusOnly,
59}
60
61impl fmt::Debug for ControlAction {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        match self {
64            ControlAction::ToggleBool(model) => f.debug_tuple("ToggleBool").field(model).finish(),
65            ControlAction::ToggleOptionalBool(model) => {
66                f.debug_tuple("ToggleOptionalBool").field(model).finish()
67            }
68            ControlAction::ToggleCheckedState(model) => {
69                f.debug_tuple("ToggleCheckedState").field(model).finish()
70            }
71            ControlAction::SetOptionalArcStr(model, value) => f
72                .debug_tuple("SetOptionalArcStr")
73                .field(model)
74                .field(value)
75                .finish(),
76            ControlAction::DispatchCommand { command, payload } => f
77                .debug_struct("DispatchCommand")
78                .field("command", command)
79                .field("has_payload", &payload.is_some())
80                .finish(),
81            ControlAction::Sequence(actions) => f.debug_tuple("Sequence").field(actions).finish(),
82            ControlAction::Noop => f.write_str("Noop"),
83            ControlAction::FocusOnly => f.write_str("FocusOnly"),
84        }
85    }
86}
87
88impl ControlAction {
89    pub fn invoke(&self, host: &mut dyn UiActionHost, cx: ActionCx) {
90        match self {
91            ControlAction::ToggleBool(model) => {
92                let _ = host.models_mut().update(model, |v: &mut bool| *v = !*v);
93            }
94            ControlAction::ToggleOptionalBool(model) => {
95                let _ = host.models_mut().update(model, |v: &mut Option<bool>| {
96                    *v = match *v {
97                        None => Some(true),
98                        Some(true) => Some(false),
99                        Some(false) => Some(true),
100                    };
101                });
102            }
103            ControlAction::ToggleCheckedState(model) => {
104                let _ = host
105                    .models_mut()
106                    .update(model, |v: &mut CheckedState| *v = v.toggle());
107            }
108            ControlAction::SetOptionalArcStr(model, value) => {
109                let value = value.clone();
110                let _ = host
111                    .models_mut()
112                    .update(model, |v: &mut Option<Arc<str>>| *v = Some(value.clone()));
113            }
114            ControlAction::DispatchCommand { command, payload } => {
115                host.record_pending_command_dispatch_source(cx, command, ActivateReason::Pointer);
116                if let Some(payload) = payload {
117                    host.record_pending_action_payload(cx, command, payload());
118                }
119                host.dispatch_command(Some(cx.window), command.clone());
120            }
121            ControlAction::Sequence(actions) => {
122                for action in actions.iter() {
123                    action.invoke(host, cx);
124                }
125            }
126            ControlAction::Noop => {}
127            ControlAction::FocusOnly => {}
128        }
129    }
130}
131
132#[derive(Debug, Clone)]
133pub struct ControlEntry {
134    pub element: GlobalElementId,
135    pub enabled: bool,
136    pub action: ControlAction,
137}
138
139#[derive(Debug, Clone)]
140pub struct LabelEntry {
141    pub element: GlobalElementId,
142}
143
144#[derive(Debug, Clone)]
145pub struct DescriptionEntry {
146    pub element: GlobalElementId,
147}
148
149#[derive(Debug, Clone)]
150pub struct ErrorEntry {
151    pub element: GlobalElementId,
152}
153
154#[derive(Debug, Default, Clone)]
155pub struct ControlRegistry {
156    windows: HashMap<AppWindowId, WindowControlRegistry>,
157}
158
159#[derive(Debug, Default, Clone)]
160struct WindowControlRegistry {
161    frame_id: Option<FrameId>,
162    controls: HashMap<ControlId, ControlEntry>,
163    labels: HashMap<ControlId, LabelEntry>,
164    descriptions: HashMap<ControlId, DescriptionEntry>,
165    errors: HashMap<ControlId, ErrorEntry>,
166}
167
168impl ControlRegistry {
169    fn begin_frame(
170        &mut self,
171        window: AppWindowId,
172        frame_id: FrameId,
173    ) -> &mut WindowControlRegistry {
174        let entry = self.windows.entry(window).or_default();
175        if entry.frame_id != Some(frame_id) {
176            entry.frame_id = Some(frame_id);
177            // Do not clear `controls`/`labels` on a new frame.
178            //
179            // Some app shells use view caching (GPUI-style reuse) where a subtree may be reused
180            // without re-running the declarative builder for every child. Clearing the whole
181            // registry would require *all* controls/labels to re-register on every frame, which
182            // breaks label -> control forwarding for cached subtrees.
183            //
184            // Policy note: callers should treat `ControlId` as a stable, unique identifier within
185            // a window. Reusing the same id for unrelated controls can lead to stale forwarding.
186        }
187        entry
188    }
189
190    pub fn register_control(
191        &mut self,
192        window: AppWindowId,
193        frame_id: FrameId,
194        id: ControlId,
195        control: ControlEntry,
196    ) {
197        let st = self.begin_frame(window, frame_id);
198        st.controls.insert(id, control);
199    }
200
201    pub fn register_label(
202        &mut self,
203        window: AppWindowId,
204        frame_id: FrameId,
205        id: ControlId,
206        label: LabelEntry,
207    ) {
208        let st = self.begin_frame(window, frame_id);
209        st.labels.insert(id, label);
210    }
211
212    pub fn register_description(
213        &mut self,
214        window: AppWindowId,
215        frame_id: FrameId,
216        id: ControlId,
217        description: DescriptionEntry,
218    ) {
219        let st = self.begin_frame(window, frame_id);
220        st.descriptions.insert(id, description);
221    }
222
223    pub fn register_error(
224        &mut self,
225        window: AppWindowId,
226        frame_id: FrameId,
227        id: ControlId,
228        error: ErrorEntry,
229    ) {
230        let st = self.begin_frame(window, frame_id);
231        st.errors.insert(id, error);
232    }
233
234    pub fn control_for(&self, window: AppWindowId, id: &ControlId) -> Option<&ControlEntry> {
235        self.windows.get(&window)?.controls.get(id)
236    }
237
238    pub fn label_for(&self, window: AppWindowId, id: &ControlId) -> Option<&LabelEntry> {
239        self.windows.get(&window)?.labels.get(id)
240    }
241
242    pub fn description_for(
243        &self,
244        window: AppWindowId,
245        id: &ControlId,
246    ) -> Option<&DescriptionEntry> {
247        self.windows.get(&window)?.descriptions.get(id)
248    }
249
250    pub fn error_for(&self, window: AppWindowId, id: &ControlId) -> Option<&ErrorEntry> {
251        self.windows.get(&window)?.errors.get(id)
252    }
253
254    pub fn described_by_for(&self, window: AppWindowId, id: &ControlId) -> Option<GlobalElementId> {
255        let st = self.windows.get(&window)?;
256        st.errors
257            .get(id)
258            .map(|e| e.element)
259            .or_else(|| st.descriptions.get(id).map(|d| d.element))
260    }
261}
262
263#[derive(Default)]
264struct ControlRegistryService {
265    model: Option<Model<ControlRegistry>>,
266}
267
268pub fn control_registry_model<H: UiHost>(cx: &mut ElementContext<'_, H>) -> Model<ControlRegistry> {
269    cx.app
270        .with_global_mut(ControlRegistryService::default, |svc, app| {
271            svc.model
272                .get_or_insert_with(|| app.models_mut().insert(ControlRegistry::default()))
273                .clone()
274        })
275}