Skip to main content

fret_ui_kit/declarative/
semantics.rs

1use std::sync::Arc;
2
3use fret_core::SemanticsRole;
4use fret_ui::element::{AnyElement, SemanticsDecoration};
5
6use crate::IntoUiElement;
7
8pub trait AnyElementSemanticsExt {
9    fn a11y(self, decoration: SemanticsDecoration) -> AnyElement;
10    fn a11y_role(self, role: SemanticsRole) -> AnyElement;
11    fn role(self, role: SemanticsRole) -> AnyElement;
12    fn a11y_label(self, label: impl Into<Arc<str>>) -> AnyElement;
13    fn test_id(self, id: impl Into<Arc<str>>) -> AnyElement;
14    fn a11y_value(self, value: impl Into<Arc<str>>) -> AnyElement;
15    fn a11y_disabled(self, disabled: bool) -> AnyElement;
16    fn a11y_selected(self, selected: bool) -> AnyElement;
17    fn a11y_expanded(self, expanded: bool) -> AnyElement;
18    fn a11y_checked(self, checked: Option<bool>) -> AnyElement;
19}
20
21impl AnyElementSemanticsExt for AnyElement {
22    fn a11y(self, decoration: SemanticsDecoration) -> AnyElement {
23        self.a11y(decoration)
24    }
25
26    fn a11y_role(self, role: SemanticsRole) -> AnyElement {
27        self.a11y_role(role)
28    }
29
30    fn role(self, role: SemanticsRole) -> AnyElement {
31        self.a11y_role(role)
32    }
33
34    fn a11y_label(self, label: impl Into<Arc<str>>) -> AnyElement {
35        self.a11y_label(label)
36    }
37
38    fn test_id(self, id: impl Into<Arc<str>>) -> AnyElement {
39        self.test_id(id)
40    }
41
42    fn a11y_value(self, value: impl Into<Arc<str>>) -> AnyElement {
43        self.a11y_value(value)
44    }
45
46    fn a11y_disabled(self, disabled: bool) -> AnyElement {
47        self.a11y_disabled(disabled)
48    }
49
50    fn a11y_selected(self, selected: bool) -> AnyElement {
51        self.a11y_selected(selected)
52    }
53
54    fn a11y_expanded(self, expanded: bool) -> AnyElement {
55        self.a11y_expanded(expanded)
56    }
57
58    fn a11y_checked(self, checked: Option<bool>) -> AnyElement {
59        self.a11y_checked(checked)
60    }
61}
62
63#[derive(Debug, Clone)]
64pub struct UiElementWithTestId<T> {
65    inner: T,
66    test_id: Arc<str>,
67}
68
69impl<T> UiElementWithTestId<T> {
70    pub fn new(inner: T, test_id: impl Into<Arc<str>>) -> Self {
71        Self {
72            inner,
73            test_id: test_id.into(),
74        }
75    }
76}
77
78impl<H: fret_ui::UiHost, T> IntoUiElement<H> for UiElementWithTestId<T>
79where
80    T: IntoUiElement<H>,
81{
82    #[track_caller]
83    fn into_element(self, cx: &mut fret_ui::ElementContext<'_, H>) -> fret_ui::element::AnyElement {
84        self.inner.into_element(cx).test_id(self.test_id)
85    }
86}
87
88pub trait UiElementTestIdExt: Sized {
89    fn test_id(self, id: impl Into<Arc<str>>) -> UiElementWithTestId<Self> {
90        UiElementWithTestId::new(self, id)
91    }
92}
93
94impl<T> UiElementTestIdExt for T {}
95
96#[derive(Debug, Clone)]
97pub struct UiElementWithA11y<T> {
98    inner: T,
99    decoration: SemanticsDecoration,
100}
101
102impl<T> UiElementWithA11y<T> {
103    pub fn new(inner: T, decoration: SemanticsDecoration) -> Self {
104        Self { inner, decoration }
105    }
106}
107
108impl<H: fret_ui::UiHost, T> IntoUiElement<H> for UiElementWithA11y<T>
109where
110    T: IntoUiElement<H>,
111{
112    #[track_caller]
113    fn into_element(self, cx: &mut fret_ui::ElementContext<'_, H>) -> fret_ui::element::AnyElement {
114        self.inner.into_element(cx).a11y(self.decoration)
115    }
116}
117
118pub trait UiElementA11yExt: Sized {
119    fn a11y(self, decoration: SemanticsDecoration) -> UiElementWithA11y<Self> {
120        UiElementWithA11y::new(self, decoration)
121    }
122
123    fn a11y_role(self, role: SemanticsRole) -> UiElementWithA11y<Self> {
124        self.a11y(SemanticsDecoration::default().role(role))
125    }
126
127    fn a11y_label(self, label: impl Into<Arc<str>>) -> UiElementWithA11y<Self> {
128        self.a11y(SemanticsDecoration::default().label(label))
129    }
130
131    fn a11y_value(self, value: impl Into<Arc<str>>) -> UiElementWithA11y<Self> {
132        self.a11y(SemanticsDecoration::default().value(value))
133    }
134
135    fn a11y_disabled(self, disabled: bool) -> UiElementWithA11y<Self> {
136        self.a11y(SemanticsDecoration::default().disabled(disabled))
137    }
138
139    fn a11y_selected(self, selected: bool) -> UiElementWithA11y<Self> {
140        self.a11y(SemanticsDecoration::default().selected(selected))
141    }
142
143    fn a11y_expanded(self, expanded: bool) -> UiElementWithA11y<Self> {
144        self.a11y(SemanticsDecoration::default().expanded(expanded))
145    }
146
147    fn a11y_checked(self, checked: Option<bool>) -> UiElementWithA11y<Self> {
148        self.a11y(SemanticsDecoration::default().checked(checked))
149    }
150}
151
152impl<T> UiElementA11yExt for T {}
153
154#[derive(Debug, Clone)]
155pub struct UiElementWithKeyContext<T> {
156    inner: T,
157    key_context: Arc<str>,
158}
159
160impl<T> UiElementWithKeyContext<T> {
161    pub fn new(inner: T, key_context: impl Into<Arc<str>>) -> Self {
162        Self {
163            inner,
164            key_context: key_context.into(),
165        }
166    }
167}
168
169impl<H: fret_ui::UiHost, T> IntoUiElement<H> for UiElementWithKeyContext<T>
170where
171    T: IntoUiElement<H>,
172{
173    #[track_caller]
174    fn into_element(self, cx: &mut fret_ui::ElementContext<'_, H>) -> fret_ui::element::AnyElement {
175        self.inner.into_element(cx).key_context(self.key_context)
176    }
177}
178
179pub trait UiElementKeyContextExt: Sized {
180    fn key_context(self, key_context: impl Into<Arc<str>>) -> UiElementWithKeyContext<Self> {
181        UiElementWithKeyContext::new(self, key_context)
182    }
183}
184
185impl<T> UiElementKeyContextExt for T {}
186
187#[cfg(test)]
188mod tests {
189    use std::any::{Any, TypeId};
190    use std::collections::HashMap;
191
192    use fret_core::{AppWindowId, Point, PointerId, Px, Rect, Size};
193    use fret_runtime::{
194        ClipboardToken, CommandRegistry, CommandsHost, DragHost, DragKindId, DragSession, Effect,
195        EffectSink, FrameId, GlobalsHost, ImageUploadToken, ModelHost, ModelId, ModelStore,
196        ModelsHost, ShareSheetToken, TickId, TimeHost, TimerToken,
197    };
198
199    use super::*;
200
201    #[derive(Default)]
202    struct TestUiHost {
203        globals: HashMap<TypeId, Box<dyn Any>>,
204        models: ModelStore,
205        commands: CommandRegistry,
206        tick_id: TickId,
207        frame_id: FrameId,
208        next_timer_token: u64,
209        next_clipboard_token: u64,
210        next_share_sheet_token: u64,
211        next_image_upload_token: u64,
212    }
213
214    impl GlobalsHost for TestUiHost {
215        fn set_global<T: Any>(&mut self, value: T) {
216            self.globals.insert(TypeId::of::<T>(), Box::new(value));
217        }
218
219        fn global<T: Any>(&self) -> Option<&T> {
220            self.globals
221                .get(&TypeId::of::<T>())
222                .and_then(|v| v.downcast_ref::<T>())
223        }
224
225        fn with_global_mut<T: Any, R>(
226            &mut self,
227            init: impl FnOnce() -> T,
228            f: impl FnOnce(&mut T, &mut Self) -> R,
229        ) -> R {
230            let type_id = TypeId::of::<T>();
231            let existing = self.globals.remove(&type_id);
232            let mut value = existing
233                .and_then(|v| v.downcast::<T>().ok().map(|v| *v))
234                .unwrap_or_else(init);
235            let out = f(&mut value, self);
236            self.globals.insert(type_id, Box::new(value));
237            out
238        }
239    }
240
241    impl ModelHost for TestUiHost {
242        fn models(&self) -> &ModelStore {
243            &self.models
244        }
245
246        fn models_mut(&mut self) -> &mut ModelStore {
247            &mut self.models
248        }
249    }
250
251    impl ModelsHost for TestUiHost {
252        fn take_changed_models(&mut self) -> Vec<ModelId> {
253            Vec::new()
254        }
255    }
256
257    impl CommandsHost for TestUiHost {
258        fn commands(&self) -> &CommandRegistry {
259            &self.commands
260        }
261    }
262
263    impl EffectSink for TestUiHost {
264        fn request_redraw(&mut self, _window: AppWindowId) {}
265
266        fn push_effect(&mut self, _effect: Effect) {}
267    }
268
269    impl TimeHost for TestUiHost {
270        fn tick_id(&self) -> TickId {
271            self.tick_id
272        }
273
274        fn frame_id(&self) -> FrameId {
275            self.frame_id
276        }
277
278        fn next_timer_token(&mut self) -> TimerToken {
279            let out = TimerToken(self.next_timer_token);
280            self.next_timer_token = self.next_timer_token.saturating_add(1);
281            out
282        }
283
284        fn next_clipboard_token(&mut self) -> ClipboardToken {
285            let out = ClipboardToken(self.next_clipboard_token);
286            self.next_clipboard_token = self.next_clipboard_token.saturating_add(1);
287            out
288        }
289
290        fn next_share_sheet_token(&mut self) -> ShareSheetToken {
291            let out = ShareSheetToken(self.next_share_sheet_token);
292            self.next_share_sheet_token = self.next_share_sheet_token.saturating_add(1);
293            out
294        }
295
296        fn next_image_upload_token(&mut self) -> ImageUploadToken {
297            let out = ImageUploadToken(self.next_image_upload_token);
298            self.next_image_upload_token = self.next_image_upload_token.saturating_add(1);
299            out
300        }
301    }
302
303    impl DragHost for TestUiHost {
304        fn drag(&self, _pointer_id: PointerId) -> Option<&DragSession> {
305            None
306        }
307
308        fn drag_mut(&mut self, _pointer_id: PointerId) -> Option<&mut DragSession> {
309            None
310        }
311
312        fn cancel_drag(&mut self, _pointer_id: PointerId) {}
313
314        fn any_drag_session(&self, _predicate: impl FnMut(&DragSession) -> bool) -> bool {
315            false
316        }
317
318        fn find_drag_pointer_id(
319            &self,
320            _predicate: impl FnMut(&DragSession) -> bool,
321        ) -> Option<PointerId> {
322            None
323        }
324
325        fn cancel_drag_sessions(
326            &mut self,
327            _predicate: impl FnMut(&DragSession) -> bool,
328        ) -> Vec<PointerId> {
329            Vec::new()
330        }
331
332        fn begin_drag_with_kind<T: Any>(
333            &mut self,
334            _pointer_id: PointerId,
335            _kind: DragKindId,
336            _source_window: AppWindowId,
337            _start: Point,
338            _payload: T,
339        ) {
340        }
341
342        fn begin_cross_window_drag_with_kind<T: Any>(
343            &mut self,
344            _pointer_id: PointerId,
345            _kind: DragKindId,
346            _source_window: AppWindowId,
347            _start: Point,
348            _payload: T,
349        ) {
350        }
351    }
352
353    #[test]
354    fn semantics_exts_allow_semantics_and_key_context_without_early_into_element() {
355        struct Dummy;
356        impl<H: fret_ui::UiHost> IntoUiElement<H> for Dummy {
357            #[track_caller]
358            fn into_element(
359                self,
360                cx: &mut fret_ui::ElementContext<'_, H>,
361            ) -> fret_ui::element::AnyElement {
362                cx.text("dummy")
363            }
364        }
365
366        let mut host = TestUiHost::default();
367        let mut runtime = fret_ui::ElementRuntime::new();
368        let window = AppWindowId::default();
369        let bounds = Rect::new(Point::new(Px(0.0), Px(0.0)), Size::new(Px(10.0), Px(10.0)));
370        let mut cx = fret_ui::ElementContext::new_for_root_name(
371            &mut host,
372            &mut runtime,
373            window,
374            bounds,
375            "root",
376        );
377
378        let el = Dummy
379            .a11y_role(SemanticsRole::Button)
380            .test_id("dummy.btn")
381            .key_context("dummy.ctx")
382            .into_element(&mut cx);
383
384        assert_eq!(el.key_context.as_deref(), Some("dummy.ctx"));
385        let deco = el
386            .semantics_decoration
387            .expect("expected semantics decoration");
388        assert_eq!(deco.role, Some(SemanticsRole::Button));
389        assert_eq!(deco.test_id.as_deref(), Some("dummy.btn"));
390    }
391}