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