kas-core 0.17.0

KAS GUI / core
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License in the LICENSE-APACHE file or at:
//     https://www.apache.org/licenses/LICENSE-2.0

//! Widget roles

use crate::Id;
use crate::dir::Direction;
use crate::event::Key;
#[allow(unused)] use crate::event::{Event, EventState};
use crate::geom::Offset;
use crate::layout::GridCellInfo;
#[allow(unused)]
use crate::messages::{DecrementStep, IncrementStep, SetValueF64};
use crate::text::CursorRange;
#[allow(unused)] use crate::{Layout, Tile};

/// Describes a widget's purpose and capabilities
///
/// This `enum` does not describe children; use [`Tile::child_indices`] for
/// that. This `enum` does not describe associated properties such as a label
/// or labelled-by relationship.
///
/// ### Messages
///
/// Some roles of widget are expected to accept specific messages, as outlined
/// below. See also [`EventState::send`] and related functions.
#[non_exhaustive]
pub enum Role<'a> {
    /// The widget does not present any semantics under introspection
    ///
    /// This is equivalent to the [ARIA presentation role]: the widget will be
    /// ignored by accessibility tools, while child widgets remain visible.
    ///
    /// [ARIA presentation role]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/presentation_role
    None,
    /// Role is unspecified or no listed role is applicable
    ///
    /// Unlike [`Role::None`], the widget and its attached properties (e.g.
    /// label) will be visible to accessibility tools.
    Unknown,
    /// A text label with the given contents, usually (but not necessarily) short and fixed
    Label(&'a str),
    /// A text label with an access key
    AccessLabel(&'a str, Key),
    /// A push button
    ///
    /// ### Label
    ///
    /// If no label is set explicitly then a label is inferred from children of
    /// the widget.
    ///
    /// ### Messages
    ///
    /// [`kas::messages::Activate`] may be used to trigger the button.
    Button,
    /// A checkable box
    ///
    /// ### Label
    ///
    /// If no label is set explicitly then a label is inferred from children of
    /// the widget.
    ///
    /// ### Messages
    ///
    /// [`kas::messages::Activate`] may be used to toggle the state.
    CheckBox(bool),
    /// A radio button
    ///
    /// ### Label
    ///
    /// If no label is set explicitly then a label is inferred from children of
    /// the widget.
    ///
    /// ### Messages
    ///
    /// [`kas::messages::Activate`] may be used to toggle the state.
    RadioButton(bool),
    /// A tab handle
    ///
    /// ### Messages
    ///
    /// [`kas::messages::Activate`] may be used to activate the tab.
    Tab,
    /// A stack / tab page
    TabPage,
    /// A visible border surrounding or between other items
    Border,
    /// A scrollable region
    ///
    /// The widget should support [`Event::Scroll`].
    ///
    /// ### Messages
    ///
    /// [`kas::messages::SetScrollOffset`] may be used to set the scroll offset.
    ScrollRegion {
        /// The current scroll offset (from zero to `max_offset`)
        offset: Offset,
        /// The maximum offset (non-negative)
        max_offset: Offset,
    },
    /// A scroll bar
    ScrollBar {
        /// Orientation (usually either `Down` or `Right`)
        direction: Direction,
        /// The current position (from zero to `max_value`)
        value: i32,
        /// The maximum position (non-negative)
        max_value: i32,
    },
    /// A small visual element
    Indicator,
    /// An image
    Image,
    /// A canvas
    Canvas,
    /// A text label supporting selection
    TextLabel {
        /// Text contents
        ///
        /// NOTE: it is likely that the representation here changes to
        /// accomodate more complex texts and potentially other details.
        text: &'a str,
        /// The cursor index within `contents`
        cursor: usize,
        /// The selection index. Equals `cursor` if the selection is empty.
        /// May be less than or greater than `cursor`. (Aside: some toolkits
        /// call this the selection anchor but Kas does not; see
        /// [`kas::text::SelectionHelper`].)
        sel_index: usize,
    },
    /// Editable text
    ///
    /// ### Messages
    ///
    /// [`kas::messages::SetValueText`] may be used to replace the entire
    /// text. [`kas::messages::ReplaceSelectedText`] may be used to insert text
    /// at `cursor`, replacing all text between `cursor` and `sel_index`.
    TextInput {
        /// Text contents
        ///
        /// NOTE: it is likely that the representation here changes to
        /// accomodate more complex texts and potentially other details.
        text: &'a str,
        /// Whether the text input supports multi-line text
        multi_line: bool,
        /// The cursor index and selection range
        cursor: CursorRange,
    },
    /// A gripable handle
    ///
    /// This is a part of a slider, scroll-bar, splitter or similar widget which
    /// can be dragged by the mouse. Its [`Layout::rect`] may be queried.
    Grip,
    /// A slider input
    ///
    /// Note that values may not be finite; for example `max: f64::INFINITY`.
    ///
    /// ### Messages
    ///
    /// [`SetValueF64`] may be used to set the input value.
    ///
    /// [`IncrementStep`] and [`DecrementStep`] change the value by one step.
    Slider {
        /// Minimum value
        min: f64,
        /// Maximum value
        max: f64,
        /// Step
        step: f64,
        /// Current value
        value: f64,
        /// Orientation (direction of increasing values)
        direction: Direction,
    },
    /// A spinner: numeric edit box with up and down buttons
    ///
    /// Note that values may not be finite; for example `max: f64::INFINITY`.
    ///
    /// ### Messages
    ///
    /// [`SetValueF64`] may be used to set the input value.
    ///
    /// [`IncrementStep`] and [`DecrementStep`] change the value by one step.
    SpinButton {
        /// Minimum value
        min: f64,
        /// Maximum value
        max: f64,
        /// Step
        step: f64,
        /// Current value
        value: f64,
    },
    /// A progress bar
    ProgressBar {
        /// The reported value should be between `0.0` and `1.0`.
        fraction: f32,
        /// Orientation (direction of increasing values)
        direction: Direction,
    },
    /// A list of possibly selectable items
    ///
    /// Note that this role should only be used where it is desirable to expose
    /// the list as an element. In other cases (where a list is used merely as
    /// a tool to place elements next to each other), use [`Role::None`].
    ///
    /// Child nodes should (but are not required to) use [`Role::OptionListItem`].
    OptionList {
        /// The number of items in the list, if known
        len: Option<usize>,
        /// Orientation
        direction: Direction,
    },
    /// An item within a list
    OptionListItem {
        /// Index in the list, if known
        ///
        /// Note that this may change frequently, thus is not a useful key.
        index: Option<usize>,
        /// Whether the item is currently selected, if applicable.
        ///
        /// > When deciding whether to set this value to `false` or `None`,
        /// > consider whether it would be appropriate for a screen reader to
        /// > announce “not selected”.
        ///
        /// See also [`accesskit::Node::is_selected`](https://docs.rs/accesskit/latest/accesskit/struct.Node.html#method.is_selected).
        selected: Option<bool>,
    },
    /// A grid of possibly selectable items
    ///
    /// Note that this role should only be used where it is desirable to expose
    /// the grid as an element. In other cases (where a grid is used merely as
    /// a tool to place elements next to each other), use [`Role::None`].
    ///
    /// Child nodes should (but are not required to) use [`Role::GridCell`].
    Grid {
        /// The number of columns in the grid, if known
        columns: Option<usize>,
        /// The number of rows in the grid, if known
        rows: Option<usize>,
    },
    /// An item within a list
    GridCell {
        /// Grid cell index and span, if known
        info: Option<GridCellInfo>,
        /// Whether the item is currently selected, if applicable.
        ///
        /// > When deciding whether to set this value to `false` or `None`,
        /// > consider whether it would be appropriate for a screen reader to
        /// > announce “not selected”.
        ///
        /// See also [`accesskit::Node::is_selected`](https://docs.rs/accesskit/latest/accesskit/struct.Node.html#method.is_selected).
        selected: Option<bool>,
    },
    /// A menu bar
    MenuBar,
    /// An openable menu
    ///
    /// # Messages
    ///
    /// [`kas::messages::Activate`] may be used to open the menu.
    ///
    /// [`kas::messages::Expand`] and [`kas::messages::Collapse`] may be used to
    /// open and close the menu.
    Menu {
        /// True if the menu is open
        expanded: bool,
    },
    /// A drop-down combination box
    ///
    /// Includes the index and text of the active entry
    ///
    /// # Messages
    ///
    /// [`kas::messages::SetIndex`] may be used to set the selected entry.
    ///
    /// [`kas::messages::Expand`] and [`kas::messages::Collapse`] may be used to
    /// open and close the menu.
    ComboBox {
        /// Index of the current choice
        active: usize,
        /// Text of the current choice
        text: &'a str,
        /// True if the menu is open
        expanded: bool,
    },
    /// A list of variable-size children with resizing grips
    Splitter,
    /// A window
    Window,
    /// The special bar at the top of a window titling contents and usually embedding window controls
    TitleBar,
}

/// A copy-on-write text value or a reference to another source
pub enum TextOrSource<'a> {
    /// Borrowed text
    Borrowed(&'a str),
    /// Owned text
    Owned(String),
    /// A reference to another widget able to a text value
    ///
    /// It is expected that the given [`Id`] refers to a widget with role
    /// [`Role::Label`] or [`Role::TextLabel`].
    Source(Id),
}

impl<'a> From<&'a str> for TextOrSource<'a> {
    #[inline]
    fn from(text: &'a str) -> Self {
        Self::Borrowed(text)
    }
}

impl From<String> for TextOrSource<'static> {
    #[inline]
    fn from(text: String) -> Self {
        Self::Owned(text)
    }
}

impl<'a> From<&'a String> for TextOrSource<'a> {
    #[inline]
    fn from(text: &'a String) -> Self {
        Self::Borrowed(text)
    }
}

impl From<Id> for TextOrSource<'static> {
    #[inline]
    fn from(id: Id) -> Self {
        Self::Source(id)
    }
}

#[cfg(feature = "accesskit")]
impl<'a> Role<'a> {
    /// Construct an AccessKit [`Role`] from self
    pub(crate) fn as_accesskit_role(&self) -> accesskit::Role {
        use accesskit::Role as R;

        match self {
            Role::None => R::GenericContainer,
            Role::Unknown | Role::Grip => R::Unknown,
            Role::Label(_) | Role::AccessLabel(_, _) | Role::TextLabel { .. } => R::Label,
            Role::Button => R::Button,
            Role::CheckBox(_) => R::CheckBox,
            Role::RadioButton(_) => R::RadioButton,
            Role::Tab => R::Tab,
            Role::TabPage => R::TabPanel,
            Role::ScrollRegion { .. } => R::ScrollView,
            Role::ScrollBar { .. } => R::ScrollBar,
            Role::Indicator => R::Unknown,
            Role::Image => R::Image,
            Role::Canvas => R::Canvas,
            Role::TextInput {
                multi_line: false, ..
            } => R::TextInput,
            Role::TextInput {
                multi_line: true, ..
            } => R::MultilineTextInput,
            Role::Slider { .. } => R::Slider,
            Role::SpinButton { .. } => R::SpinButton,
            Role::ProgressBar { .. } => R::ProgressIndicator,
            Role::Border => R::Unknown,
            Role::OptionList { .. } => R::ListBox,
            Role::OptionListItem { .. } => R::ListBoxOption,
            Role::Grid { .. } => R::Grid,
            Role::GridCell { .. } => R::Cell,
            Role::MenuBar => R::MenuBar,
            Role::Menu { .. } => R::Menu,
            Role::ComboBox { .. } => R::ComboBox,
            Role::Splitter => R::Splitter,
            Role::Window => R::Window,
            Role::TitleBar => R::TitleBar,
        }
    }

    /// Construct an AccessKit [`Node`] from self
    ///
    /// This will set node properties as provided by self, but not those provided by the parent.
    pub(crate) fn as_accesskit_node(&self, tile: &dyn Tile) -> accesskit::Node {
        use crate::cast::Cast;
        use accesskit::Action;

        let mut node = accesskit::Node::new(self.as_accesskit_role());
        node.set_bounds(tile.rect().cast());
        if tile.navigable() {
            node.add_action(Action::Focus);
        }

        match *self {
            Role::None | Role::Unknown | Role::Border | Role::Grip | Role::Splitter => (),
            Role::Button | Role::Tab => {
                node.add_action(Action::Click);
            }
            Role::TabPage => (),
            Role::Indicator | Role::Image | Role::Canvas => (),
            Role::MenuBar | Role::Window | Role::TitleBar => (),
            Role::Label(text) | Role::TextLabel { text, .. } => node.set_value(text),
            Role::TextInput { text, .. } => {
                node.add_action(Action::SetValue);
                node.add_action(Action::ReplaceSelectedText);
                node.set_value(text)
            }
            Role::AccessLabel(text, ref key) => {
                node.set_value(text);
                if let Some(text) = key.to_text() {
                    node.set_access_key(text);
                }
            }
            Role::CheckBox(state) | Role::RadioButton(state) => {
                node.add_action(Action::Click);
                node.set_toggled(state.into());
            }
            Role::ScrollRegion { offset, max_offset } => {
                crate::accesskit::apply_scroll_props_to_node(offset, max_offset, &mut node);
            }
            Role::ScrollBar {
                direction,
                value,
                max_value,
            } => {
                node.set_orientation(direction.into());
                node.set_numeric_value(value.cast());
                node.set_min_numeric_value(0.0);
                node.set_max_numeric_value(max_value.cast());
            }
            Role::Slider {
                min,
                max,
                step,
                value,
                ..
            }
            | Role::SpinButton {
                min,
                max,
                step,
                value,
            } => {
                node.add_action(Action::SetValue);
                node.add_action(Action::Increment);
                node.add_action(Action::Decrement);
                if min.is_finite() {
                    node.set_min_numeric_value(min);
                }
                if max.is_finite() {
                    node.set_max_numeric_value(max);
                }
                if step.is_finite() {
                    node.set_numeric_value_step(step);
                }
                node.set_numeric_value(value);
                if let Role::Slider { direction, .. } = self {
                    node.set_orientation((*direction).into());
                }
            }
            Role::ProgressBar {
                fraction,
                direction,
            } => {
                node.set_max_numeric_value(1.0);
                node.set_numeric_value(fraction.cast());
                node.set_orientation(direction.into());
            }
            Role::OptionList { len, direction } => {
                if let Some(len) = len {
                    node.set_size_of_set(len);
                }
                node.set_orientation(direction.into());
            }
            Role::OptionListItem { index, selected } => {
                if let Some(index) = index {
                    node.set_position_in_set(index);
                }
                if let Some(state) = selected {
                    node.set_selected(state);
                }
            }
            Role::Grid { columns, rows } => {
                if let Some(cols) = columns {
                    node.set_column_count(cols);
                }
                if let Some(rows) = rows {
                    node.set_row_count(rows);
                }
            }
            Role::GridCell { info, selected } => {
                if let Some(info) = info {
                    node.set_column_index(info.col.cast());
                    if info.last_col > info.col {
                        node.set_column_span((info.last_col + 1 - info.col).cast());
                    }
                    node.set_row_index(info.row.cast());
                    if info.last_row > info.row {
                        node.set_row_span((info.last_row + 1 - info.row).cast());
                    }
                }
                if let Some(state) = selected {
                    node.set_selected(state);
                }
            }
            Role::ComboBox { expanded, .. } | Role::Menu { expanded } => {
                node.add_action(Action::Expand);
                node.add_action(Action::Collapse);
                node.set_expanded(expanded);
            }
        }

        node
    }
}

/// Context through which additional role properties may be specified
///
/// Unlike other widget method contexts, this is a trait; the caller provides an
/// implementation.
pub trait RoleCx {
    /// Attach a label
    ///
    /// Do not use this for [`Role::Label`] and similar items where the label is
    /// the widget's primary value. Do use this where a label exists which is
    /// not the primary value, for example an image's alternate text or a label
    /// next to a control.
    fn set_label_impl(&mut self, label: TextOrSource<'_>);
}

/// Convenience methods over a [`RoleCx`]
pub trait RoleCxExt: RoleCx {
    /// Attach a label
    ///
    /// Do not use this for [`Role::Label`] and similar items where the label is
    /// the widget's primary value. Do use this where a label exists which is
    /// not the primary value, for example an image's alternate text or a label
    /// next to a control.
    fn set_label<'a>(&mut self, label: impl Into<TextOrSource<'a>>) {
        self.set_label_impl(label.into());
    }
}

impl<C: RoleCx + ?Sized> RoleCxExt for C {}