kas_core/core/
role.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! Widget roles
7
8use crate::Id;
9use crate::dir::Direction;
10use crate::event::Key;
11#[allow(unused)] use crate::event::{Event, EventState};
12use crate::geom::Offset;
13use crate::layout::GridCellInfo;
14#[allow(unused)]
15use crate::messages::{DecrementStep, IncrementStep, SetValueF64};
16#[allow(unused)] use crate::{Layout, Tile};
17
18/// Describes a widget's purpose and capabilities
19///
20/// This `enum` does not describe children; use [`Tile::child_indices`] for
21/// that. This `enum` does not describe associated properties such as a label
22/// or labelled-by relationship.
23///
24/// ### Messages
25///
26/// Some roles of widget are expected to accept specific messages, as outlined
27/// below. See also [`EventState::send`] and related functions.
28#[non_exhaustive]
29pub enum Role<'a> {
30    /// The widget does not present any semantics under introspection
31    ///
32    /// This is equivalent to the [ARIA presentation role]: the widget will be
33    /// ignored by accessibility tools, while child widgets remain visible.
34    ///
35    /// [ARIA presentation role]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/presentation_role
36    None,
37    /// Role is unspecified or no listed role is applicable
38    ///
39    /// Unlike [`Role::None`], the widget and its attached properties (e.g.
40    /// label) will be visible to accessibility tools.
41    Unknown,
42    /// A text label with the given contents, usually (but not necessarily) short and fixed
43    Label(&'a str),
44    /// A text label with an access key
45    AccessLabel(&'a str, Key),
46    /// A push button
47    ///
48    /// ### Label
49    ///
50    /// If no label is set explicitly then a label is inferred from children of
51    /// the widget.
52    ///
53    /// ### Messages
54    ///
55    /// [`kas::messages::Activate`] may be used to trigger the button.
56    Button,
57    /// A checkable box
58    ///
59    /// ### Label
60    ///
61    /// If no label is set explicitly then a label is inferred from children of
62    /// the widget.
63    ///
64    /// ### Messages
65    ///
66    /// [`kas::messages::Activate`] may be used to toggle the state.
67    CheckBox(bool),
68    /// A radio button
69    ///
70    /// ### Label
71    ///
72    /// If no label is set explicitly then a label is inferred from children of
73    /// the widget.
74    ///
75    /// ### Messages
76    ///
77    /// [`kas::messages::Activate`] may be used to toggle the state.
78    RadioButton(bool),
79    /// A tab handle
80    ///
81    /// ### Messages
82    ///
83    /// [`kas::messages::Activate`] may be used to activate the tab.
84    Tab,
85    /// A stack / tab page
86    TabPage,
87    /// A visible border surrounding or between other items
88    Border,
89    /// A scrollable region
90    ///
91    /// The widget should support [`Event::Scroll`].
92    ///
93    /// ### Messages
94    ///
95    /// [`kas::messages::SetScrollOffset`] may be used to set the scroll offset.
96    ScrollRegion {
97        /// The current scroll offset (from zero to `max_offset`)
98        offset: Offset,
99        /// The maximum offset (non-negative)
100        max_offset: Offset,
101    },
102    /// A scroll bar
103    ScrollBar {
104        /// Orientation (usually either `Down` or `Right`)
105        direction: Direction,
106        /// The current position (from zero to `max_value`)
107        value: i32,
108        /// The maximum position (non-negative)
109        max_value: i32,
110    },
111    /// A small visual element
112    Indicator,
113    /// An image
114    Image,
115    /// A canvas
116    Canvas,
117    /// A text label supporting selection
118    TextLabel {
119        /// Text contents
120        ///
121        /// NOTE: it is likely that the representation here changes to
122        /// accomodate more complex texts and potentially other details.
123        text: &'a str,
124        /// The cursor index within `contents`
125        cursor: usize,
126        /// The selection index. Equals `cursor` if the selection is empty.
127        /// May be less than or greater than `cursor`. (Aside: some toolkits
128        /// call this the selection anchor but Kas does not; see
129        /// [`kas::text::SelectionHelper`].)
130        sel_index: usize,
131    },
132    /// Editable text
133    ///
134    /// ### Messages
135    ///
136    /// [`kas::messages::SetValueText`] may be used to replace the entire
137    /// text. [`kas::messages::ReplaceSelectedText`] may be used to insert text
138    /// at `cursor`, replacing all text between `cursor` and `sel_index`.
139    TextInput {
140        /// Text contents
141        ///
142        /// NOTE: it is likely that the representation here changes to
143        /// accomodate more complex texts and potentially other details.
144        text: &'a str,
145        /// Whether the text input supports multi-line text
146        multi_line: bool,
147        /// The cursor index within `contents`
148        cursor: usize,
149        /// The selection index. Equals `cursor` if the selection is empty.
150        /// May be less than or greater than `cursor`. (Aside: some toolkits
151        /// call this the selection anchor but Kas does not; see
152        /// [`kas::text::SelectionHelper`].)
153        sel_index: usize,
154    },
155    /// A gripable handle
156    ///
157    /// This is a part of a slider, scroll-bar, splitter or similar widget which
158    /// can be dragged by the mouse. Its [`Layout::rect`] may be queried.
159    Grip,
160    /// A slider input
161    ///
162    /// Note that values may not be finite; for example `max: f64::INFINITY`.
163    ///
164    /// ### Messages
165    ///
166    /// [`SetValueF64`] may be used to set the input value.
167    ///
168    /// [`IncrementStep`] and [`DecrementStep`] change the value by one step.
169    Slider {
170        /// Minimum value
171        min: f64,
172        /// Maximum value
173        max: f64,
174        /// Step
175        step: f64,
176        /// Current value
177        value: f64,
178        /// Orientation (direction of increasing values)
179        direction: Direction,
180    },
181    /// A spinner: numeric edit box with up and down buttons
182    ///
183    /// Note that values may not be finite; for example `max: f64::INFINITY`.
184    ///
185    /// ### Messages
186    ///
187    /// [`SetValueF64`] may be used to set the input value.
188    ///
189    /// [`IncrementStep`] and [`DecrementStep`] change the value by one step.
190    SpinButton {
191        /// Minimum value
192        min: f64,
193        /// Maximum value
194        max: f64,
195        /// Step
196        step: f64,
197        /// Current value
198        value: f64,
199    },
200    /// A progress bar
201    ProgressBar {
202        /// The reported value should be between `0.0` and `1.0`.
203        fraction: f32,
204        /// Orientation (direction of increasing values)
205        direction: Direction,
206    },
207    /// A list of possibly selectable items
208    ///
209    /// Note that this role should only be used where it is desirable to expose
210    /// the list as an element. In other cases (where a list is used merely as
211    /// a tool to place elements next to each other), use [`Role::None`].
212    ///
213    /// Child nodes should (but are not required to) use [`Role::OptionListItem`].
214    OptionList {
215        /// The number of items in the list, if known
216        len: Option<usize>,
217        /// Orientation
218        direction: Direction,
219    },
220    /// An item within a list
221    OptionListItem {
222        /// Index in the list, if known
223        ///
224        /// Note that this may change frequently, thus is not a useful key.
225        index: Option<usize>,
226        /// Whether the item is currently selected, if applicable.
227        ///
228        /// > When deciding whether to set this value to `false` or `None`,
229        /// > consider whether it would be appropriate for a screen reader to
230        /// > announce “not selected”.
231        ///
232        /// See also [`accesskit::Node::is_selected`](https://docs.rs/accesskit/latest/accesskit/struct.Node.html#method.is_selected).
233        selected: Option<bool>,
234    },
235    /// A grid of possibly selectable items
236    ///
237    /// Note that this role should only be used where it is desirable to expose
238    /// the grid as an element. In other cases (where a grid is used merely as
239    /// a tool to place elements next to each other), use [`Role::None`].
240    ///
241    /// Child nodes should (but are not required to) use [`Role::GridCell`].
242    Grid {
243        /// The number of columns in the grid, if known
244        columns: Option<usize>,
245        /// The number of rows in the grid, if known
246        rows: Option<usize>,
247    },
248    /// An item within a list
249    GridCell {
250        /// Grid cell index and span, if known
251        info: Option<GridCellInfo>,
252        /// Whether the item is currently selected, if applicable.
253        ///
254        /// > When deciding whether to set this value to `false` or `None`,
255        /// > consider whether it would be appropriate for a screen reader to
256        /// > announce “not selected”.
257        ///
258        /// See also [`accesskit::Node::is_selected`](https://docs.rs/accesskit/latest/accesskit/struct.Node.html#method.is_selected).
259        selected: Option<bool>,
260    },
261    /// A menu bar
262    MenuBar,
263    /// An openable menu
264    ///
265    /// # Messages
266    ///
267    /// [`kas::messages::Activate`] may be used to open the menu.
268    ///
269    /// [`kas::messages::Expand`] and [`kas::messages::Collapse`] may be used to
270    /// open and close the menu.
271    Menu {
272        /// True if the menu is open
273        expanded: bool,
274    },
275    /// A drop-down combination box
276    ///
277    /// Includes the index and text of the active entry
278    ///
279    /// # Messages
280    ///
281    /// [`kas::messages::SetIndex`] may be used to set the selected entry.
282    ///
283    /// [`kas::messages::Expand`] and [`kas::messages::Collapse`] may be used to
284    /// open and close the menu.
285    ComboBox {
286        /// Index of the current choice
287        active: usize,
288        /// Text of the current choice
289        text: &'a str,
290        /// True if the menu is open
291        expanded: bool,
292    },
293    /// A list of variable-size children with resizing grips
294    Splitter,
295    /// A window
296    Window,
297    /// The special bar at the top of a window titling contents and usually embedding window controls
298    TitleBar,
299}
300
301/// A copy-on-write text value or a reference to another source
302pub enum TextOrSource<'a> {
303    /// Borrowed text
304    Borrowed(&'a str),
305    /// Owned text
306    Owned(String),
307    /// A reference to another widget able to a text value
308    ///
309    /// It is expected that the given [`Id`] refers to a widget with role
310    /// [`Role::Label`] or [`Role::TextLabel`].
311    Source(Id),
312}
313
314impl<'a> From<&'a str> for TextOrSource<'a> {
315    #[inline]
316    fn from(text: &'a str) -> Self {
317        Self::Borrowed(text)
318    }
319}
320
321impl From<String> for TextOrSource<'static> {
322    #[inline]
323    fn from(text: String) -> Self {
324        Self::Owned(text)
325    }
326}
327
328impl<'a> From<&'a String> for TextOrSource<'a> {
329    #[inline]
330    fn from(text: &'a String) -> Self {
331        Self::Borrowed(text)
332    }
333}
334
335impl From<Id> for TextOrSource<'static> {
336    #[inline]
337    fn from(id: Id) -> Self {
338        Self::Source(id)
339    }
340}
341
342#[cfg(feature = "accesskit")]
343impl<'a> Role<'a> {
344    /// Construct an AccessKit [`Role`] from self
345    pub(crate) fn as_accesskit_role(&self) -> accesskit::Role {
346        use accesskit::Role as R;
347
348        match self {
349            Role::None => R::GenericContainer,
350            Role::Unknown | Role::Grip => R::Unknown,
351            Role::Label(_) | Role::AccessLabel(_, _) | Role::TextLabel { .. } => R::Label,
352            Role::Button => R::Button,
353            Role::CheckBox(_) => R::CheckBox,
354            Role::RadioButton(_) => R::RadioButton,
355            Role::Tab => R::Tab,
356            Role::TabPage => R::TabPanel,
357            Role::ScrollRegion { .. } => R::ScrollView,
358            Role::ScrollBar { .. } => R::ScrollBar,
359            Role::Indicator => R::Unknown,
360            Role::Image => R::Image,
361            Role::Canvas => R::Canvas,
362            Role::TextInput {
363                multi_line: false, ..
364            } => R::TextInput,
365            Role::TextInput {
366                multi_line: true, ..
367            } => R::MultilineTextInput,
368            Role::Slider { .. } => R::Slider,
369            Role::SpinButton { .. } => R::SpinButton,
370            Role::ProgressBar { .. } => R::ProgressIndicator,
371            Role::Border => R::Unknown,
372            Role::OptionList { .. } => R::ListBox,
373            Role::OptionListItem { .. } => R::ListBoxOption,
374            Role::Grid { .. } => R::Grid,
375            Role::GridCell { .. } => R::Cell,
376            Role::MenuBar => R::MenuBar,
377            Role::Menu { .. } => R::Menu,
378            Role::ComboBox { .. } => R::ComboBox,
379            Role::Splitter => R::Splitter,
380            Role::Window => R::Window,
381            Role::TitleBar => R::TitleBar,
382        }
383    }
384
385    /// Construct an AccessKit [`Node`] from self
386    ///
387    /// This will set node properties as provided by self, but not those provided by the parent.
388    pub(crate) fn as_accesskit_node(&self, tile: &dyn Tile) -> accesskit::Node {
389        use crate::cast::Cast;
390        use accesskit::Action;
391
392        let mut node = accesskit::Node::new(self.as_accesskit_role());
393        node.set_bounds(tile.rect().cast());
394        if tile.navigable() {
395            node.add_action(Action::Focus);
396        }
397
398        match *self {
399            Role::None | Role::Unknown | Role::Border | Role::Grip | Role::Splitter => (),
400            Role::Button | Role::Tab => {
401                node.add_action(Action::Click);
402            }
403            Role::TabPage => (),
404            Role::Indicator | Role::Image | Role::Canvas => (),
405            Role::MenuBar | Role::Window | Role::TitleBar => (),
406            Role::Label(text) | Role::TextLabel { text, .. } => node.set_value(text),
407            Role::TextInput { text, .. } => {
408                node.add_action(Action::SetValue);
409                node.add_action(Action::ReplaceSelectedText);
410                node.set_value(text)
411            }
412            Role::AccessLabel(text, ref key) => {
413                node.set_value(text);
414                if let Some(text) = key.to_text() {
415                    node.set_access_key(text);
416                }
417            }
418            Role::CheckBox(state) | Role::RadioButton(state) => {
419                node.add_action(Action::Click);
420                node.set_toggled(state.into());
421            }
422            Role::ScrollRegion { offset, max_offset } => {
423                crate::accesskit::apply_scroll_props_to_node(offset, max_offset, &mut node);
424            }
425            Role::ScrollBar {
426                direction,
427                value,
428                max_value,
429            } => {
430                node.set_orientation(direction.into());
431                node.set_numeric_value(value.cast());
432                node.set_min_numeric_value(0.0);
433                node.set_max_numeric_value(max_value.cast());
434            }
435            Role::Slider {
436                min,
437                max,
438                step,
439                value,
440                ..
441            }
442            | Role::SpinButton {
443                min,
444                max,
445                step,
446                value,
447            } => {
448                node.add_action(Action::SetValue);
449                node.add_action(Action::Increment);
450                node.add_action(Action::Decrement);
451                if min.is_finite() {
452                    node.set_min_numeric_value(min);
453                }
454                if max.is_finite() {
455                    node.set_max_numeric_value(max);
456                }
457                if step.is_finite() {
458                    node.set_numeric_value_step(step);
459                }
460                node.set_numeric_value(value);
461                if let Role::Slider { direction, .. } = self {
462                    node.set_orientation((*direction).into());
463                }
464            }
465            Role::ProgressBar {
466                fraction,
467                direction,
468            } => {
469                node.set_max_numeric_value(1.0);
470                node.set_numeric_value(fraction.cast());
471                node.set_orientation(direction.into());
472            }
473            Role::OptionList { len, direction } => {
474                if let Some(len) = len {
475                    node.set_size_of_set(len);
476                }
477                node.set_orientation(direction.into());
478            }
479            Role::OptionListItem { index, selected } => {
480                if let Some(index) = index {
481                    node.set_position_in_set(index);
482                }
483                if let Some(state) = selected {
484                    node.set_selected(state);
485                }
486            }
487            Role::Grid { columns, rows } => {
488                if let Some(cols) = columns {
489                    node.set_column_count(cols);
490                }
491                if let Some(rows) = rows {
492                    node.set_row_count(rows);
493                }
494            }
495            Role::GridCell { info, selected } => {
496                if let Some(info) = info {
497                    node.set_column_index(info.col.cast());
498                    if info.last_col > info.col {
499                        node.set_column_span((info.last_col + 1 - info.col).cast());
500                    }
501                    node.set_row_index(info.row.cast());
502                    if info.last_row > info.row {
503                        node.set_row_span((info.last_row + 1 - info.row).cast());
504                    }
505                }
506                if let Some(state) = selected {
507                    node.set_selected(state);
508                }
509            }
510            Role::ComboBox { expanded, .. } | Role::Menu { expanded } => {
511                node.add_action(Action::Expand);
512                node.add_action(Action::Collapse);
513                node.set_expanded(expanded);
514            }
515        }
516
517        node
518    }
519}
520
521/// Context through which additional role properties may be specified
522///
523/// Unlike other widget method contexts, this is a trait; the caller provides an
524/// implementation.
525pub trait RoleCx {
526    /// Attach a label
527    ///
528    /// Do not use this for [`Role::Label`] and similar items where the label is
529    /// the widget's primary value. Do use this where a label exists which is
530    /// not the primary value, for example an image's alternate text or a label
531    /// next to a control.
532    fn set_label_impl(&mut self, label: TextOrSource<'_>);
533
534    /// Set scroll offset
535    ///
536    /// This sets the current `scroll_offset` and the `max_scroll_offset`. The
537    /// minimum offset is assumed to be zero. It is expected that this supports
538    /// setting the scroll offset and [`Event::Scroll`].
539    fn set_scroll_offset(&mut self, scroll_offset: Offset, max_scroll_offset: Offset);
540}
541
542/// Convenience methods over a [`RoleCx`]
543pub trait RoleCxExt: RoleCx {
544    /// Attach a label
545    ///
546    /// Do not use this for [`Role::Label`] and similar items where the label is
547    /// the widget's primary value. Do use this where a label exists which is
548    /// not the primary value, for example an image's alternate text or a label
549    /// next to a control.
550    fn set_label<'a>(&mut self, label: impl Into<TextOrSource<'a>>) {
551        self.set_label_impl(label.into());
552    }
553}
554
555impl<C: RoleCx + ?Sized> RoleCxExt for C {}