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 {}