Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

at main 11 kB View raw
1use std::collections::BTreeSet; 2 3use accesskit::{Node, NodeId, Rect, Tree, TreeId, TreeUpdate}; 4 5pub use accesskit::{Role, Toggled}; 6 7use crate::layout::LayoutRect; 8use crate::strings::{StringKey, StringTable}; 9use crate::widget_id::WidgetId; 10use crate::widgets::LabelText; 11 12#[derive(Copy, Clone, Debug, Default, PartialEq)] 13pub struct AccessState { 14 pub disabled: bool, 15 pub selected: Option<bool>, 16 pub expanded: Option<bool>, 17 pub toggled: Option<Toggled>, 18} 19 20#[derive(Copy, Clone, Debug, PartialEq)] 21pub struct AccessRange { 22 pub value: f64, 23 pub min: f64, 24 pub max: f64, 25 pub step: f64, 26} 27 28#[derive(Clone, Debug, PartialEq)] 29pub struct AccessNode { 30 pub role: Role, 31 pub label: Option<LabelText>, 32 pub description: Option<StringKey>, 33 pub state: AccessState, 34 pub range: Option<AccessRange>, 35} 36 37impl AccessNode { 38 #[must_use] 39 pub const fn new(role: Role) -> Self { 40 Self { 41 role, 42 label: None, 43 description: None, 44 state: AccessState { 45 disabled: false, 46 selected: None, 47 expanded: None, 48 toggled: None, 49 }, 50 range: None, 51 } 52 } 53 54 #[must_use] 55 pub fn with_label(self, key: StringKey) -> Self { 56 self.with_label_text(LabelText::Key(key)) 57 } 58 59 #[must_use] 60 pub fn with_label_text(self, label: LabelText) -> Self { 61 Self { 62 label: Some(label), 63 ..self 64 } 65 } 66 67 #[must_use] 68 pub fn with_description(self, key: StringKey) -> Self { 69 Self { 70 description: Some(key), 71 ..self 72 } 73 } 74 75 #[must_use] 76 pub fn with_disabled(self, disabled: bool) -> Self { 77 Self { 78 state: AccessState { 79 disabled, 80 ..self.state 81 }, 82 ..self 83 } 84 } 85 86 #[must_use] 87 pub fn with_selected(self, selected: bool) -> Self { 88 Self { 89 state: AccessState { 90 selected: Some(selected), 91 ..self.state 92 }, 93 ..self 94 } 95 } 96 97 #[must_use] 98 pub fn with_expanded(self, expanded: bool) -> Self { 99 Self { 100 state: AccessState { 101 expanded: Some(expanded), 102 ..self.state 103 }, 104 ..self 105 } 106 } 107 108 #[must_use] 109 pub fn with_toggled(self, toggled: Toggled) -> Self { 110 Self { 111 state: AccessState { 112 toggled: Some(toggled), 113 ..self.state 114 }, 115 ..self 116 } 117 } 118 119 #[must_use] 120 pub fn with_range(self, range: AccessRange) -> Self { 121 Self { 122 range: Some(range), 123 ..self 124 } 125 } 126} 127 128#[derive(Clone, Debug, PartialEq)] 129struct AccessEntry { 130 id: WidgetId, 131 rect: LayoutRect, 132 node: AccessNode, 133} 134 135#[derive(Clone, Debug, Default)] 136pub struct AccessTreeBuilder { 137 entries: Vec<AccessEntry>, 138 seen: BTreeSet<WidgetId>, 139} 140 141impl AccessTreeBuilder { 142 #[must_use] 143 pub fn new() -> Self { 144 Self::default() 145 } 146 147 pub fn begin_frame(&mut self) { 148 self.entries.clear(); 149 self.seen.clear(); 150 } 151 152 pub fn push(&mut self, id: WidgetId, rect: LayoutRect, node: AccessNode) { 153 assert!( 154 self.seen.insert(id), 155 "AccessTreeBuilder::push duplicate widget id {id:?}; pushes must be unique per frame", 156 ); 157 self.entries.push(AccessEntry { id, rect, node }); 158 } 159 160 #[must_use] 161 pub fn contains(&self, id: WidgetId) -> bool { 162 self.seen.contains(&id) 163 } 164 165 #[must_use] 166 pub fn len(&self) -> usize { 167 self.entries.len() 168 } 169 170 #[must_use] 171 pub fn is_empty(&self) -> bool { 172 self.entries.is_empty() 173 } 174 175 pub fn ids(&self) -> impl Iterator<Item = WidgetId> + '_ { 176 self.entries.iter().map(|e| e.id) 177 } 178 179 #[must_use] 180 pub fn build(&self, strings: &StringTable, focused: Option<WidgetId>) -> TreeUpdate { 181 let root_id = node_id(WidgetId::ROOT); 182 let mut root = Node::new(Role::Window); 183 let children: Vec<NodeId> = self.entries.iter().map(|e| node_id(e.id)).collect(); 184 if !children.is_empty() { 185 root.set_children(children); 186 } 187 let nodes = std::iter::once((root_id, root)) 188 .chain( 189 self.entries 190 .iter() 191 .map(|entry| (node_id(entry.id), build_node(strings, entry))), 192 ) 193 .collect(); 194 TreeUpdate { 195 nodes, 196 tree: Some(Tree::new(root_id)), 197 tree_id: TreeId::ROOT, 198 focus: focused.map_or(root_id, node_id), 199 } 200 } 201} 202 203fn node_id(id: WidgetId) -> NodeId { 204 NodeId::from(id.raw().get()) 205} 206 207fn build_node(strings: &StringTable, entry: &AccessEntry) -> Node { 208 let mut node = Node::new(entry.node.role); 209 node.set_bounds(rect_to_accesskit(entry.rect)); 210 if let Some(label) = &entry.node.label { 211 node.set_label(label.resolve(strings)); 212 } 213 if let Some(key) = entry.node.description { 214 node.set_description(strings.resolve(key)); 215 } 216 if entry.node.state.disabled { 217 node.set_disabled(); 218 } 219 if let Some(selected) = entry.node.state.selected { 220 node.set_selected(selected); 221 } 222 if let Some(expanded) = entry.node.state.expanded { 223 node.set_expanded(expanded); 224 } 225 if let Some(toggled) = entry.node.state.toggled { 226 node.set_toggled(toggled); 227 } 228 if let Some(range) = entry.node.range { 229 node.set_numeric_value(range.value); 230 node.set_min_numeric_value(range.min); 231 node.set_max_numeric_value(range.max); 232 node.set_numeric_value_step(range.step); 233 } 234 node 235} 236 237fn rect_to_accesskit(rect: LayoutRect) -> Rect { 238 Rect { 239 x0: f64::from(rect.min_x().value()), 240 y0: f64::from(rect.min_y().value()), 241 x1: f64::from(rect.max_x().value()), 242 y1: f64::from(rect.max_y().value()), 243 } 244} 245 246#[must_use] 247pub fn root_node_id() -> NodeId { 248 node_id(WidgetId::ROOT) 249} 250 251#[must_use] 252pub fn widget_node_id(id: WidgetId) -> NodeId { 253 node_id(id) 254} 255 256#[cfg(test)] 257mod tests { 258 use accesskit::{Role, Toggled}; 259 260 use super::{AccessNode, AccessRange, AccessTreeBuilder}; 261 use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize}; 262 use crate::strings::{StringKey, StringTable}; 263 use crate::widget_id::{WidgetId, WidgetKey}; 264 265 const LABEL: StringKey = StringKey::new("smoke.label"); 266 const DESC: StringKey = StringKey::new("smoke.desc"); 267 268 fn id(name: &'static str) -> WidgetId { 269 WidgetId::ROOT.child(WidgetKey::new(name)) 270 } 271 272 fn rect() -> LayoutRect { 273 LayoutRect::new( 274 LayoutPos::new(LayoutPx::new(0.0), LayoutPx::new(0.0)), 275 LayoutSize::new(LayoutPx::new(40.0), LayoutPx::new(20.0)), 276 ) 277 } 278 279 #[test] 280 #[should_panic(expected = "AccessTreeBuilder::push duplicate widget id")] 281 fn push_panics_on_duplicate_id_in_debug() { 282 let mut builder = AccessTreeBuilder::new(); 283 let node = AccessNode::new(Role::Button).with_label(LABEL); 284 builder.push(id("a"), rect(), node.clone()); 285 builder.push(id("a"), rect(), node); 286 } 287 288 #[test] 289 fn begin_frame_clears() { 290 let mut builder = AccessTreeBuilder::new(); 291 builder.push(id("a"), rect(), AccessNode::new(Role::Button)); 292 builder.begin_frame(); 293 assert!(builder.is_empty()); 294 assert!(!builder.contains(id("a"))); 295 } 296 297 #[test] 298 fn build_emits_root_with_children() { 299 let mut builder = AccessTreeBuilder::new(); 300 builder.push( 301 id("a"), 302 rect(), 303 AccessNode::new(Role::Button).with_label(LABEL), 304 ); 305 builder.push( 306 id("b"), 307 rect(), 308 AccessNode::new(Role::CheckBox) 309 .with_label(LABEL) 310 .with_toggled(Toggled::True), 311 ); 312 let strings = StringTable::from_entries([(LABEL, "Save".to_owned())]); 313 let update = builder.build(&strings, Some(id("b"))); 314 assert_eq!(update.nodes.len(), 3); 315 assert!(update.tree.is_some()); 316 let (root_id, root_node) = &update.nodes[0]; 317 assert_eq!(*root_id, super::root_node_id()); 318 assert_eq!(root_node.children().len(), 2); 319 assert_eq!(update.focus, super::widget_node_id(id("b"))); 320 } 321 322 #[test] 323 fn build_resolves_label_through_string_table() { 324 let mut builder = AccessTreeBuilder::new(); 325 builder.push( 326 id("a"), 327 rect(), 328 AccessNode::new(Role::Button) 329 .with_label(LABEL) 330 .with_description(DESC), 331 ); 332 let strings = StringTable::from_entries([ 333 (LABEL, "Save".to_owned()), 334 (DESC, "Persist current part".to_owned()), 335 ]); 336 let update = builder.build(&strings, None); 337 let (_, button) = &update.nodes[1]; 338 assert_eq!(button.label(), Some("Save")); 339 assert_eq!(button.description(), Some("Persist current part")); 340 } 341 342 #[test] 343 fn build_emits_numeric_value_for_ranges() { 344 let mut builder = AccessTreeBuilder::new(); 345 builder.push( 346 id("s"), 347 rect(), 348 AccessNode::new(Role::Slider) 349 .with_label(LABEL) 350 .with_range(AccessRange { 351 value: 5.0, 352 min: 0.0, 353 max: 10.0, 354 step: 1.0, 355 }), 356 ); 357 let update = builder.build(StringTable::empty(), None); 358 let (_, slider) = &update.nodes[1]; 359 assert_eq!(slider.numeric_value(), Some(5.0)); 360 assert_eq!(slider.min_numeric_value(), Some(0.0)); 361 assert_eq!(slider.max_numeric_value(), Some(10.0)); 362 assert_eq!(slider.numeric_value_step(), Some(1.0)); 363 } 364 365 #[test] 366 fn build_marks_disabled_state() { 367 let mut builder = AccessTreeBuilder::new(); 368 builder.push( 369 id("a"), 370 rect(), 371 AccessNode::new(Role::Button) 372 .with_label(LABEL) 373 .with_disabled(true), 374 ); 375 let update = builder.build(StringTable::empty(), None); 376 let (_, node) = &update.nodes[1]; 377 assert!(node.is_disabled()); 378 } 379 380 #[test] 381 fn empty_builder_emits_root_only_focus_falls_back() { 382 let builder = AccessTreeBuilder::new(); 383 let update = builder.build(StringTable::empty(), None); 384 assert_eq!(update.nodes.len(), 1); 385 assert_eq!(update.focus, super::root_node_id()); 386 } 387}