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 }
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}