Skip to main content

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