zng_app/
access.rs

1//! Accessibility/automation events.
2
3use zng_txt::Txt;
4use zng_view_api::access::AccessCmd;
5
6pub use zng_view_api::access::ScrollCmd;
7
8use crate::{
9    event::{event, event_args},
10    widget::{WidgetId, info::WidgetPath},
11    window::{WINDOWS_APP, WindowId},
12};
13
14pub(super) fn on_access_init(window_id: WindowId) {
15    let args = AccessInitedArgs::now(window_id);
16    ACCESS_INITED_EVENT.notify(args)
17}
18
19pub(super) fn on_access_deinit(window_id: WindowId) {
20    let args = AccessDeinitedArgs::now(window_id);
21    ACCESS_DEINITED_EVENT.notify(args)
22}
23
24fn find_wgt(window_id: WindowId, widget_id: WidgetId) -> Option<WidgetPath> {
25    WINDOWS_APP.widget_tree(window_id)?.get(widget_id).map(|w| w.path())
26}
27
28pub(super) fn on_access_command(window_id: WindowId, widget_id: WidgetId, command: AccessCmd) {
29    let widget = match find_wgt(window_id, widget_id) {
30        Some(w) => w,
31        None => return,
32    };
33    match command {
34        AccessCmd::Click(primary) => {
35            let args = AccessClickArgs::now(widget, primary);
36            ACCESS_CLICK_EVENT.notify(args)
37        }
38        AccessCmd::Focus(focus) => {
39            let args = AccessFocusArgs::now(widget, focus);
40            ACCESS_FOCUS_EVENT.notify(args)
41        }
42        AccessCmd::FocusNavOrigin => {
43            let args = AccessFocusNavOriginArgs::now(widget);
44            ACCESS_FOCUS_NAV_ORIGIN_EVENT.notify(args)
45        }
46        AccessCmd::SetExpanded(expanded) => {
47            let args = AccessExpanderArgs::now(widget, expanded);
48            ACCESS_EXPANDER_EVENT.notify(args)
49        }
50        AccessCmd::Increment(inc) => {
51            let args = AccessIncrementArgs::now(widget, inc);
52            ACCESS_INCREMENT_EVENT.notify(args)
53        }
54        AccessCmd::SetToolTipVis(vis) => {
55            let args = AccessToolTipArgs::now(widget, vis);
56            ACCESS_TOOLTIP_EVENT.notify(args)
57        }
58        AccessCmd::ReplaceSelectedText(s) => {
59            let args = AccessTextArgs::now(widget, s, true);
60            ACCESS_TEXT_EVENT.notify(args)
61        }
62        AccessCmd::Scroll(s) => {
63            let args = AccessScrollArgs::now(widget, s);
64            ACCESS_SCROLL_EVENT.notify(args)
65        }
66        AccessCmd::SelectText {
67            start: (start_wgt, start_idx),
68            caret: (caret_wgt, caret_idx),
69        } => {
70            let start_wgt = match find_wgt(window_id, WidgetId::from_raw(start_wgt.0)) {
71                Some(w) => w,
72                None => return,
73            };
74            let caret_wgt = match find_wgt(window_id, WidgetId::from_raw(caret_wgt.0)) {
75                Some(w) => w,
76                None => return,
77            };
78            let args = AccessSelectionArgs::now((start_wgt, start_idx), (caret_wgt, caret_idx));
79            ACCESS_SELECTION_EVENT.notify(args)
80        }
81        AccessCmd::SetString(s) => {
82            let args = AccessTextArgs::now(widget, s, false);
83            ACCESS_TEXT_EVENT.notify(args)
84        }
85        AccessCmd::SetNumber(n) => {
86            let args = AccessNumberArgs::now(widget, n);
87            ACCESS_NUMBER_EVENT.notify(args)
88        }
89        a => {
90            tracing::warn!("access command `{a:?}` not implemented");
91        }
92    }
93}
94
95event_args! {
96    /// Arguments for the [`ACCESS_INITED_EVENT`].
97    pub struct AccessInitedArgs {
98        /// Target window.
99        pub window_id: WindowId,
100
101        ..
102
103        /// Broadcast to all.
104        fn is_in_target(&self, _id: WidgetId) -> bool {
105            true
106        }
107    }
108
109    /// Arguments for the [`ACCESS_DEINITED_EVENT`].
110    pub struct AccessDeinitedArgs {
111        /// Target window.
112        pub window_id: WindowId,
113
114        ..
115
116        /// Broadcast to all.
117        fn is_in_target(&self, _id: WidgetId) -> bool {
118            true
119        }
120    }
121
122    /// Arguments for the [`ACCESS_CLICK_EVENT`].
123    pub struct AccessClickArgs {
124        /// Target.
125        pub target: WidgetPath,
126
127        /// Is primary click (default action).
128        ///
129        /// If `false` is context click.
130        pub is_primary: bool,
131
132        ..
133
134        /// If is in `target`.
135        fn is_in_target(&self, id: WidgetId) -> bool {
136            self.target.contains(id)
137        }
138    }
139
140    /// Arguments for the [`ACCESS_FOCUS_EVENT`].
141    pub struct AccessFocusArgs {
142        /// Target.
143        pub target: WidgetPath,
144
145        /// If the widget must be focused.
146        ///
147        /// If `true` the widget is focused, if `false` and the widget is focused, does ESC.
148        pub focus: bool,
149
150        ..
151
152        /// If is in `target`.
153        fn is_in_target(&self, id: WidgetId) -> bool {
154            self.target.contains(id)
155        }
156    }
157
158    /// Arguments for the [`ACCESS_FOCUS_NAV_ORIGIN_EVENT`].
159    pub struct AccessFocusNavOriginArgs {
160        /// Target.
161        pub target: WidgetPath,
162
163        ..
164
165        /// If is in `target`.
166        fn is_in_target(&self, id: WidgetId) -> bool {
167            self.target.contains(id)
168        }
169    }
170
171    /// Arguments for the [`ACCESS_EXPANDER_EVENT`].
172    pub struct AccessExpanderArgs {
173        /// Target.
174        pub target: WidgetPath,
175
176        /// New expanded value.
177        pub expanded: bool,
178
179        ..
180
181        /// If is in `target`.
182        fn is_in_target(&self, id: WidgetId) -> bool {
183            self.target.contains(id)
184        }
185    }
186
187    /// Arguments for the [`ACCESS_INCREMENT_EVENT`].
188    pub struct AccessIncrementArgs {
189        /// Target.
190        pub target: WidgetPath,
191
192        /// Increment steps.
193        ///
194        /// Usually is -1 or 1.
195        pub delta: i32,
196
197        ..
198
199        /// If is in `target`.
200        fn is_in_target(&self, id: WidgetId) -> bool {
201            self.target.contains(id)
202        }
203    }
204
205    /// Arguments for the [`ACCESS_TOOLTIP_EVENT`].
206    pub struct AccessToolTipArgs {
207        /// Target.
208        pub target: WidgetPath,
209
210        /// New tooltip visibility.
211        pub visible: bool,
212
213        ..
214
215        /// If is in `target`.
216        fn is_in_target(&self, id: WidgetId) -> bool {
217            self.target.contains(id)
218        }
219    }
220
221    /// Arguments for the [`ACCESS_SCROLL_EVENT`].
222    pub struct AccessScrollArgs {
223        /// Target.
224        pub target: WidgetPath,
225
226        /// Scroll command.
227        pub command: ScrollCmd,
228
229        ..
230
231        /// If is in `target`.
232        fn is_in_target(&self, id: WidgetId) -> bool {
233            self.target.contains(id)
234        }
235    }
236
237    /// Arguments for the [`ACCESS_TEXT_EVENT`].
238    pub struct AccessTextArgs {
239        /// Target.
240        pub target: WidgetPath,
241
242        /// Replacement text.
243        pub txt: Txt,
244
245        /// If only the selected text is replaced.
246        ///
247        /// Note that if the selection is empty the text is just inserted at the caret position, or is appended if there
248        /// is no caret.
249        pub selection_only: bool,
250
251        ..
252
253        /// If is in `target`.
254        fn is_in_target(&self, id: WidgetId) -> bool {
255            self.target.contains(id)
256        }
257    }
258
259    /// Arguments for the [`ACCESS_NUMBER_EVENT`].
260    pub struct AccessNumberArgs {
261        /// Target.
262        pub target: WidgetPath,
263
264        /// Replacement number.
265        pub num: f64,
266
267        ..
268
269        /// If is in `target`.
270        fn is_in_target(&self, id: WidgetId) -> bool {
271            self.target.contains(id)
272        }
273    }
274
275    /// Arguments for the [`ACCESS_SELECTION_EVENT`].
276    pub struct AccessSelectionArgs {
277        /// Selection start.
278        ///
279        /// Text widget and character index where the selection *starts*.
280        pub start: (WidgetPath, usize),
281        /// Selection end.
282        ///
283        /// This is where the caret is placed, it does not need to be greater than the start.
284        pub caret: (WidgetPath, usize),
285
286        ..
287
288        /// If is in `start` or `end` paths.
289        fn is_in_target(&self, id: WidgetId) -> bool {
290            self.start.0.contains(id) || self.caret.0.contains(id)
291        }
292    }
293}
294impl AccessClickArgs {
295    /// Is context click.
296    pub fn is_context(&self) -> bool {
297        !self.is_primary
298    }
299}
300
301event! {
302    /// Accessibility info is now required for the window.
303    pub static ACCESS_INITED_EVENT: AccessInitedArgs;
304
305    /// Accessibility info is no longer required for the window.
306    pub static ACCESS_DEINITED_EVENT: AccessDeinitedArgs;
307
308    /// Run the primary or context click action.
309    pub static ACCESS_CLICK_EVENT: AccessClickArgs;
310
311    /// Focus or escape focus on a widget.
312    pub static ACCESS_FOCUS_EVENT: AccessFocusArgs;
313
314    /// Sets the focus navigation origin.
315    pub static ACCESS_FOCUS_NAV_ORIGIN_EVENT: AccessFocusNavOriginArgs;
316
317    /// Expand or collapse the widget content.
318    pub static ACCESS_EXPANDER_EVENT: AccessExpanderArgs;
319
320    /// Increment or decrement the widget value by steps.
321    pub static ACCESS_INCREMENT_EVENT: AccessIncrementArgs;
322
323    /// Show or hide the widget's tooltip.
324    pub static ACCESS_TOOLTIP_EVENT: AccessToolTipArgs;
325
326    /// Run a scroll command.
327    pub static ACCESS_SCROLL_EVENT: AccessScrollArgs;
328
329    /// Replace the text content.
330    pub static ACCESS_TEXT_EVENT: AccessTextArgs;
331
332    /// Replace the number value.
333    pub static ACCESS_NUMBER_EVENT: AccessNumberArgs;
334
335    /// Select text.
336    pub static ACCESS_SELECTION_EVENT: AccessSelectionArgs;
337}
338
339/// Accessibility service.
340pub struct ACCESS;
341
342impl ACCESS {
343    /// Click the widget in the window.
344    ///
345    /// If `is_primary` is `true` a primary click is generated, if it is `false` a context click is generated.
346    pub fn click(&self, widget: WidgetPath, is_primary: bool) {
347        ACCESS_CLICK_EVENT.notify(AccessClickArgs::now(widget, is_primary));
348    }
349
350    /// Show tooltip for widget in the window, if it has any tooltip.
351    ///
352    /// The tooltip can auto-hide following the same rules as tooltips shown by hover.
353    pub fn show_tooltip(&self, widget: WidgetPath) {
354        ACCESS_TOOLTIP_EVENT.notify(AccessToolTipArgs::now(widget, true));
355    }
356
357    /// Hide tooltip for the widget in the window, if it has any tooltip showing.
358    pub fn hide_tooltip(&self, widget: WidgetPath) {
359        ACCESS_TOOLTIP_EVENT.notify(AccessToolTipArgs::now(widget, false));
360    }
361}