Skip to content

Commit 03d745b

Browse files
authored
feat!: Basic live regions (#128)
1 parent 1c7e5fa commit 03d745b

File tree

7 files changed

+107
-40
lines changed

7 files changed

+107
-40
lines changed

common/src/lib.rs

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,17 @@ pub enum AriaCurrent {
507507
Time,
508508
}
509509

510+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
511+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
512+
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
513+
#[cfg_attr(feature = "serde", serde(crate = "serde"))]
514+
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
515+
pub enum Live {
516+
Off,
517+
Polite,
518+
Assertive,
519+
}
520+
510521
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
511522
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
512523
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
@@ -773,13 +784,6 @@ pub struct Node {
773784
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))]
774785
pub nonatomic_text_field_root: bool,
775786

776-
// Live region attributes.
777-
#[cfg_attr(feature = "serde", serde(default))]
778-
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))]
779-
pub container_live_atomic: bool,
780-
#[cfg_attr(feature = "serde", serde(default))]
781-
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))]
782-
pub container_live_busy: bool,
783787
#[cfg_attr(feature = "serde", serde(default))]
784788
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))]
785789
pub live_atomic: bool,
@@ -961,11 +965,6 @@ pub struct Node {
961965
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
962966
pub class_name: Option<Box<str>>,
963967

964-
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
965-
pub container_live_relevant: Option<Box<str>>,
966-
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
967-
pub container_live_status: Option<Box<str>>,
968-
969968
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
970969
pub css_display: Option<Box<str>>,
971970

@@ -994,7 +993,7 @@ pub struct Node {
994993
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
995994
pub live_relevant: Option<Box<str>>,
996995
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
997-
pub live_status: Option<Box<str>>,
996+
pub live: Option<Live>,
998997

999998
/// Only if not already exposed in [`Node::name`] ([`NameFrom::Placeholder`]).
1000999
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
@@ -1197,8 +1196,6 @@ impl Node {
11971196
visited: false,
11981197
busy: false,
11991198
nonatomic_text_field_root: false,
1200-
container_live_atomic: false,
1201-
container_live_busy: false,
12021199
live_atomic: false,
12031200
modal: false,
12041201
canvas_has_fallback: false,
@@ -1239,8 +1236,6 @@ impl Node {
12391236
checked_state: None,
12401237
checked_state_description: None,
12411238
class_name: None,
1242-
container_live_relevant: None,
1243-
container_live_status: None,
12441239
css_display: None,
12451240
font_family: None,
12461241
html_tag: None,
@@ -1249,7 +1244,7 @@ impl Node {
12491244
key_shortcuts: None,
12501245
language: None,
12511246
live_relevant: None,
1252-
live_status: None,
1247+
live: None,
12531248
placeholder: None,
12541249
aria_role: None,
12551250
role_description: None,

consumer/src/node.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ use std::iter::FusedIterator;
1212
use std::sync::{Arc, Weak};
1313

1414
use accesskit::kurbo::{Affine, Point, Rect};
15-
use accesskit::{Action, ActionData, ActionRequest, CheckedState, DefaultActionVerb, NodeId, Role};
15+
use accesskit::{
16+
Action, ActionData, ActionRequest, CheckedState, DefaultActionVerb, Live, NodeId, Role,
17+
};
1618

1719
use crate::iterators::{
1820
FollowingSiblings, FollowingUnignoredSiblings, PrecedingSiblings, PrecedingUnignoredSiblings,
@@ -479,6 +481,12 @@ impl<'a> Node<'a> {
479481
)
480482
}
481483

484+
pub fn live(&self) -> Live {
485+
self.data()
486+
.live
487+
.unwrap_or_else(|| self.parent().map_or(Live::Off, |parent| parent.live()))
488+
}
489+
482490
pub(crate) fn first_unignored_child(self) -> Option<Node<'a>> {
483491
for child in self.children() {
484492
if !child.is_ignored() {

platforms/windows/examples/hello_world.rs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ use std::{cell::RefCell, convert::TryInto, mem::drop, num::NonZeroU128, rc::Rc};
44

55
use accesskit::kurbo::Rect;
66
use accesskit::{
7-
Action, ActionHandler, ActionRequest, DefaultActionVerb, Node, NodeId, Role, Tree, TreeUpdate,
7+
Action, ActionHandler, ActionRequest, DefaultActionVerb, Live, Node, NodeId, Role, Tree,
8+
TreeUpdate,
89
};
910
use lazy_static::lazy_static;
1011
use windows::{
@@ -52,6 +53,7 @@ const WINDOW_TITLE: &str = "Hello world";
5253
const WINDOW_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(1) });
5354
const BUTTON_1_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(2) });
5455
const BUTTON_2_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(3) });
56+
const PRESSED_TEXT_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(4) });
5557
const INITIAL_FOCUS: NodeId = BUTTON_1_ID;
5658

5759
const BUTTON_1_RECT: Rect = Rect {
@@ -114,13 +116,13 @@ struct WindowState {
114116

115117
impl WindowState {
116118
fn press_button(&self, id: NodeId) {
117-
// This is a pretty hacky way of updating a node.
119+
// This is a pretty hacky way of adding or updating a node.
118120
// A real GUI framework would have a consistent way
119-
// of building a node from underlying data.
121+
// of building nodes from underlying data.
120122
// Also, this update isn't as lazy as it could be;
121123
// we force the AccessKit tree to be initialized.
122124
// This is expedient in this case, because that tree
123-
// is the only place where the state of the buttons
125+
// is the only place where the state of the announcement
124126
// is stored. It's not a problem because we're really
125127
// only concerned with testing lazy updates in the context
126128
// of focus changes.
@@ -133,9 +135,18 @@ impl WindowState {
133135
} else {
134136
"You pressed button 2"
135137
};
136-
let node = make_button(id, name);
138+
let node = Node {
139+
name: Some(name.into()),
140+
live: Some(Live::Polite),
141+
..Node::new(PRESSED_TEXT_ID, Role::StaticText)
142+
};
143+
let root = Node {
144+
children: vec![BUTTON_1_ID, BUTTON_2_ID, PRESSED_TEXT_ID],
145+
name: Some(WINDOW_TITLE.into()),
146+
..Node::new(WINDOW_ID, Role::Window)
147+
};
137148
let update = TreeUpdate {
138-
nodes: vec![node],
149+
nodes: vec![node, root],
139150
tree: None,
140151
focus: is_window_focused.then(|| focus),
141152
};
@@ -338,7 +349,7 @@ fn create_window(title: &str, initial_state: TreeUpdate, initial_focus: NodeId)
338349
fn main() -> Result<()> {
339350
println!("This example has no visible GUI, and a keyboard interface:");
340351
println!("- [Tab] switches focus between two logical buttons.");
341-
println!("- [Space] 'presses' the button, permanently renaming it.");
352+
println!("- [Space] 'presses' the button, adding static text in a live region announcing that it was pressed.");
342353
println!("Enable Narrator with [Win]+[Ctrl]+[Enter] (or [Win]+[Enter] on older versions of Windows).");
343354

344355
let window = create_window(WINDOW_TITLE, get_initial_state(), INITIAL_FOCUS)?;

platforms/windows/src/adapter.rs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
use std::sync::Arc;
77

8-
use accesskit::{ActionHandler, TreeUpdate};
8+
use accesskit::{ActionHandler, Live, TreeUpdate};
99
use accesskit_consumer::{Tree, TreeChange};
1010
use lazy_init::LazyTransform;
1111
use windows::Win32::{
@@ -110,10 +110,36 @@ impl Adapter {
110110
event_id: UIA_AutomationFocusChangedEventId,
111111
});
112112
}
113+
TreeChange::NodeAdded(node) => {
114+
if !node.is_invisible_or_ignored()
115+
&& node.name().is_some()
116+
&& node.live() != Live::Off
117+
{
118+
let platform_node = PlatformNode::new(&node, self.hwnd);
119+
let element: IRawElementProviderSimple = platform_node.into();
120+
queue.push(QueuedEvent::Simple {
121+
element,
122+
event_id: UIA_LiveRegionChangedEventId,
123+
});
124+
}
125+
}
113126
TreeChange::NodeUpdated { old_node, new_node } => {
114-
let old_node = ResolvedPlatformNode::new(old_node, self.hwnd);
115-
let new_node = ResolvedPlatformNode::new(new_node, self.hwnd);
116-
new_node.enqueue_property_changes(&mut queue, &old_node);
127+
let old_platform_node = ResolvedPlatformNode::new(old_node, self.hwnd);
128+
let new_platform_node = ResolvedPlatformNode::new(new_node, self.hwnd);
129+
new_platform_node.enqueue_property_changes(&mut queue, &old_platform_node);
130+
if !new_node.is_invisible_or_ignored()
131+
&& new_node.name().is_some()
132+
&& new_node.live() != Live::Off
133+
&& (new_node.name() != old_node.name()
134+
|| new_node.live() != old_node.live())
135+
{
136+
let element: IRawElementProviderSimple =
137+
new_platform_node.downgrade().into();
138+
queue.push(QueuedEvent::Simple {
139+
element,
140+
event_id: UIA_LiveRegionChangedEventId,
141+
});
142+
}
117143
}
118144
// TODO: handle other events (#20)
119145
_ => (),

platforms/windows/src/node.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
#![allow(non_upper_case_globals)]
1212

1313
use accesskit::kurbo::Point;
14-
use accesskit::{CheckedState, NodeIdContent, Role};
14+
use accesskit::{CheckedState, Live, NodeIdContent, Role};
1515
use accesskit_consumer::{Node, WeakNode};
1616
use arrayvec::ArrayVec;
1717
use paste::paste;
@@ -37,7 +37,7 @@ impl ResolvedPlatformNode<'_> {
3737
ResolvedPlatformNode::new(node, self.hwnd)
3838
}
3939

40-
fn downgrade(&self) -> PlatformNode {
40+
pub(crate) fn downgrade(&self) -> PlatformNode {
4141
PlatformNode::new(&self.node, self.hwnd)
4242
}
4343

@@ -279,6 +279,14 @@ impl ResolvedPlatformNode<'_> {
279279
self.node.is_focused()
280280
}
281281

282+
fn live_setting(&self) -> LiveSetting {
283+
match self.node.live() {
284+
Live::Off => Off,
285+
Live::Polite => Polite,
286+
Live::Assertive => Assertive,
287+
}
288+
}
289+
282290
fn is_toggle_pattern_supported(&self) -> bool {
283291
self.node.checked_state().is_some() && !self.is_selection_item_pattern_supported()
284292
}
@@ -740,7 +748,8 @@ properties! {
740748
(IsControlElement, is_content_element),
741749
(IsEnabled, is_enabled),
742750
(IsKeyboardFocusable, is_focusable),
743-
(HasKeyboardFocus, is_focused)
751+
(HasKeyboardFocus, is_focused),
752+
(LiveSetting, live_setting)
744753
}
745754

746755
#[implement(Windows::Win32::UI::Accessibility::IToggleProvider)]

platforms/windows/src/util.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ impl From<ToggleState> for VariantFactory {
8282
}
8383
}
8484

85+
impl From<LiveSetting> for VariantFactory {
86+
fn from(value: LiveSetting) -> Self {
87+
value.0.into()
88+
}
89+
}
90+
8591
const VARIANT_FALSE: i16 = 0i16;
8692
const VARIANT_TRUE: i16 = -1i16;
8793

platforms/winit/examples/simple.rs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use accesskit::kurbo::Rect;
2-
use accesskit::{Action, ActionRequest, DefaultActionVerb, Node, NodeId, Role, Tree, TreeUpdate};
2+
use accesskit::{
3+
Action, ActionRequest, DefaultActionVerb, Live, Node, NodeId, Role, Tree, TreeUpdate,
4+
};
35
use accesskit_winit::{ActionRequestEvent, Adapter};
46
use std::{
57
num::NonZeroU128,
@@ -16,6 +18,7 @@ const WINDOW_TITLE: &str = "Hello world";
1618
const WINDOW_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(1) });
1719
const BUTTON_1_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(2) });
1820
const BUTTON_2_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(3) });
21+
const PRESSED_TEXT_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(4) });
1922
const INITIAL_FOCUS: NodeId = BUTTON_1_ID;
2023

2124
const BUTTON_1_RECT: Rect = Rect {
@@ -71,13 +74,13 @@ impl State {
7174
}
7275

7376
fn press_button(&self, adapter: &Adapter, id: NodeId) {
74-
// This is a pretty hacky way of updating a node.
77+
// This is a pretty hacky way of adding or updating a node.
7578
// A real GUI framework would have a consistent way
76-
// of building a node from underlying data.
79+
// of building nodes from underlying data.
7780
// Also, this update isn't as lazy as it could be;
7881
// we force the AccessKit tree to be initialized.
7982
// This is expedient in this case, because that tree
80-
// is the only place where the state of the buttons
83+
// is the only place where the state of the announcement
8184
// is stored. It's not a problem because we're really
8285
// only concerned with testing lazy updates in the context
8386
// of focus changes.
@@ -86,9 +89,18 @@ impl State {
8689
} else {
8790
"You pressed button 2"
8891
};
89-
let node = make_button(id, name);
92+
let node = Node {
93+
name: Some(name.into()),
94+
live: Some(Live::Polite),
95+
..Node::new(PRESSED_TEXT_ID, Role::StaticText)
96+
};
97+
let root = Node {
98+
children: vec![BUTTON_1_ID, BUTTON_2_ID, PRESSED_TEXT_ID],
99+
name: Some(WINDOW_TITLE.into()),
100+
..Node::new(WINDOW_ID, Role::Window)
101+
};
90102
let update = TreeUpdate {
91-
nodes: vec![node],
103+
nodes: vec![node, root],
92104
tree: None,
93105
focus: self.is_window_focused.then_some(self.focus),
94106
};
@@ -114,7 +126,7 @@ fn initial_tree_update(state: &State) -> TreeUpdate {
114126
fn main() {
115127
println!("This example has no visible GUI, and a keyboard interface:");
116128
println!("- [Tab] switches focus between two logical buttons.");
117-
println!("- [Space] 'presses' the button, permanently renaming it.");
129+
println!("- [Space] 'presses' the button, adding static text in a live region announcing that it was pressed.");
118130
#[cfg(target_os = "windows")]
119131
println!("Enable Narrator with [Win]+[Ctrl]+[Enter] (or [Win]+[Enter] on older versions of Windows).");
120132

0 commit comments

Comments
 (0)