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}