1use crate::{shell, *};
2use accesskit::{Action, ActionRequest, NodeId, Role, TreeId};
3use accesskit_consumer::{NodeRef, Tree, common_filter};
4use std::time::Duration;
5use winit::keyboard::NamedKey;
6
7thread_local! {
8 static START: Instant = Instant::now();
9 static FRAMES: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
10}
11
12fn new_ui() -> Ui {
13 Ui::new(Theme::dark(), Duration::from_millis(500))
14}
15
16/// Builds a frame 16 ms after this thread's previous one.
17fn frame(ui: &mut Ui, build: impl FnOnce(&mut Ui)) {
18 let frames = FRAMES.with(|frames| {
19 frames.set(frames.get() + 1);
20 frames.get()
21 });
22 let now = START.with(|start| *start) + Duration::from_millis(16) * frames;
23 ui.begin([600.0, 300.0], 2.0, now);
24 build(ui);
25 ui.end();
26}
27
28/// The tree as the platform sees it, generic containers left out: a line per node of its
29/// role, name, value, states, shortcut and actions, with the focus marked.
30fn snapshot(tree: &Tree) -> String {
31 fn write(node: &NodeRef, depth: usize, out: &mut String) {
32 let data = node.data();
33 let mut line = format!("{}{:?}", " ".repeat(depth), node.role());
34 if let Some(label) = data.label() {
35 line += &format!(" {label:?}");
36 }
37 if let Some(value) = data.value() {
38 line += &format!(" = {value:?}");
39 }
40 if let Some(placeholder) = data.placeholder() {
41 line += &format!(" ({placeholder})");
42 }
43 for (state, on) in [
44 ("toggled", data.toggled() == Some(accesskit::Toggled::True)),
45 (
46 "untoggled",
47 data.toggled() == Some(accesskit::Toggled::False),
48 ),
49 ("expanded", data.is_expanded() == Some(true)),
50 ("collapsed", data.is_expanded() == Some(false)),
51 ("selected", data.is_selected() == Some(true)),
52 ("disabled", data.is_disabled()),
53 ("focused", node.is_focused()),
54 ] {
55 if on {
56 line += &format!(" [{state}]");
57 }
58 }
59 if let Some(keys) = data.keyboard_shortcut() {
60 line += &format!(" <{keys}>");
61 }
62 if let Some(description) = data.description() {
63 line += &format!(" -- {description}");
64 }
65 let actions: Vec<_> = [
66 (Action::Click, "click"),
67 (Action::Focus, "focus"),
68 (Action::SetValue, "set"),
69 ]
70 .into_iter()
71 .filter(|(action, _)| data.supports_action(*action))
72 .map(|(_, name)| name)
73 .collect();
74 if !actions.is_empty() {
75 line += &format!(" {{{}}}", actions.join(" "));
76 }
77 out.push_str(&line);
78 out.push('\n');
79 for child in node.filtered_children(common_filter) {
80 write(&child, depth + 1, out);
81 }
82 }
83 let mut out = String::new();
84 write(&tree.state().root(), 0, &mut out);
85 out
86}
87
88/// The tree built from scratch, as assistive technology first asks for it.
89fn tree(ui: &mut Ui) -> Tree {
90 ui.deactivate_accessibility();
91 Tree::new(ui.accessibility("Notes", 2.0), true)
92}
93
94fn key(ui: &mut Ui, named: NamedKey) {
95 ui.event(Event::Key {
96 key: winit::keyboard::Key::Named(named),
97 text: None,
98 });
99}
100
101fn toolbar_id() -> Id {
102 Id::ROOT.child("toolbar")
103}
104
105fn font_menu() -> Id {
106 Id::ROOT.child("font menu")
107}
108
109fn more_menu() -> Id {
110 Id::ROOT.child("more menu")
111}
112
113/// A toolbar of the kit's controls, as the app builds its own; returns what was clicked.
114fn toolbar(ui: &mut Ui, bold: bool) -> Vec<&'static str> {
115 let mut clicked = Vec::new();
116 let text = ui.theme.text;
117 ui.open_as(
118 toolbar_id(),
119 Spec {
120 size: [fill(), px(30.0)],
121 gap: 4.0,
122 role: Some(Role::Toolbar),
123 ..Spec::default()
124 },
125 );
126 if shell::tool_button(ui, "undo", shell::CHEVRON, text, None).clicked {
127 clicked.push("undo");
128 }
129 popup::tooltip(ui, "Undo", "⌘Z", None);
130 shell::unavailable(ui, "cut", shell::CHEVRON, text, false);
131 popup::tooltip(ui, "Cut", "⌘X", None);
132 if shell::tool_button(ui, "bold", shell::CHEVRON, text, Some(bold)).clicked {
133 clicked.push("bold");
134 }
135 popup::tooltip(ui, "Bold", "⌘B", Some("Makes the selected text bold."));
136 if shell::split_button(
137 ui,
138 "paste",
139 "Paste",
140 shell::CHEVRON,
141 None,
142 None,
143 more_menu(),
144 )
145 .clicked
146 {
147 clicked.push("paste");
148 }
149 popup::tooltip(ui, "Paste", "⌘V", None);
150 let combo = ui.id("font");
151 shell::combo(ui, "font", "Font", "Calibri", 120.0, font_menu(), true);
152 let level = ui.open(
153 "level",
154 Spec {
155 flags: Flags::CLICKABLE,
156 size: [px(44.0), px(22.0)],
157 text: Some("100%"),
158 role: Some(Role::Button),
159 ..Spec::default()
160 },
161 );
162 ui.close();
163 popup::tooltip(ui, "Actual Size", "", None);
164 if let Some(node) = ui.access(level) {
165 node.set_value("100%");
166 }
167 ui.close();
168 let items = [
169 popup::Item {
170 text: "Recent",
171 heading: true,
172 ..popup::Item::default()
173 },
174 popup::Item {
175 text: "Calibri",
176 checked: Some(true),
177 current: true,
178 ..popup::Item::default()
179 },
180 popup::Item {
181 text: "Arial",
182 ..popup::Item::default()
183 },
184 popup::Item {
185 text: "Wingdings",
186 disabled: true,
187 ..popup::Item::default()
188 },
189 ];
190 popup::menu(ui, font_menu(), Anchor::Over(combo), &items, Some("Font"));
191 let paste = [popup::Item {
192 text: "Keep Text Only",
193 shortcut: "⌘⇧V",
194 ..popup::Item::default()
195 }];
196 let anchor = Anchor::Below(ui.id("paste"));
197 popup::menu(ui, more_menu(), anchor, &paste, None);
198 clicked
199}
200
201#[test]
202fn the_toolbar_names_its_controls_from_their_tooltips() {
203 let mut ui = new_ui();
204 for _ in 0..2 {
205 frame(&mut ui, |ui| {
206 toolbar(ui, true);
207 });
208 }
209 assert_eq!(
210 snapshot(&tree(&mut ui)),
211 r#"Window "Notes" [focused]
212 Toolbar
213 Button "Undo" <⌘Z> {click focus}
214 Button "Cut" [disabled] <⌘X>
215 Button "Bold" [toggled] <⌘B> -- Makes the selected text bold. {click focus}
216 Button "Paste" <⌘V> {click focus}
217 Button "Paste Options" [collapsed] {click focus}
218 ComboBox "Font" = "Calibri" [collapsed] {click focus}
219 Button "Actual Size" = "100%" {click focus}
220"#
221 );
222}
223
224#[test]
225fn each_toolbar_control_has_a_name_of_its_own() {
226 let mut ui = new_ui();
227 for _ in 0..2 {
228 frame(&mut ui, |ui| {
229 toolbar(ui, true);
230 });
231 }
232 let tree = tree(&mut ui);
233 let toolbar = tree
234 .state()
235 .node_by_tree_local_id(toolbar_id().node(), TreeId::ROOT)
236 .unwrap();
237 let names: Vec<_> = toolbar
238 .filtered_children(common_filter)
239 .map(|node| node.label().unwrap_or_default())
240 .collect();
241 for (at, name) in names.iter().enumerate() {
242 assert!(
243 !name.is_empty() && !names[at + 1..].contains(name),
244 "{names:?}"
245 );
246 }
247}
248
249#[test]
250fn a_disabled_combo_shows_its_value_but_takes_no_clicks_or_focus() {
251 let mut ui = new_ui();
252 let build = |ui: &mut Ui| {
253 frame(ui, |ui| {
254 shell::combo(ui, "font", "Font", "Calibri", 120.0, font_menu(), false);
255 });
256 };
257 build(&mut ui);
258 let [left, top, right, bottom] = ui.laid_out(ui.id("font")).unwrap();
259 let middle = [(left + right) / 2.0, (top + bottom) / 2.0];
260 ui.event(Event::PointerMoved(middle));
261 for pressed in [true, false] {
262 ui.event(Event::Button {
263 button: winit::event::MouseButton::Left,
264 pressed,
265 at: Instant::now(),
266 });
267 build(&mut ui);
268 }
269 assert!(!ui.popup_open(font_menu()));
270 key(&mut ui, NamedKey::Tab);
271 build(&mut ui);
272 assert_ne!(ui.focused(), Some(ui.id("font")));
273 assert!(
274 snapshot(&tree(&mut ui)).contains(r#"ComboBox "Font" = "Calibri" [collapsed] [disabled]"#),
275 "{}",
276 snapshot(&tree(&mut ui))
277 );
278}
279
280#[test]
281fn a_toggle_reads_as_one_while_off_and_a_plain_button_as_none() {
282 let mut ui = new_ui();
283 for _ in 0..2 {
284 frame(&mut ui, |ui| {
285 toolbar(ui, false);
286 });
287 }
288 let snapshot = snapshot(&tree(&mut ui));
289 assert!(
290 snapshot.contains(r#"Button "Bold" [untoggled] <⌘B>"#),
291 "{snapshot}"
292 );
293 assert!(
294 snapshot.contains(r#"Button "Undo" <⌘Z> {click focus}"#),
295 "{snapshot}"
296 );
297}
298
299#[test]
300fn a_row_opening_a_menu_says_so_instead_of_naming_keys() {
301 let mut ui = new_ui();
302 let menu = Id::ROOT.child("menu");
303 let items = [
304 popup::Item {
305 text: "Bullet Library",
306 submenu: true,
307 ..popup::Item::default()
308 },
309 popup::Item {
310 text: "Bold",
311 shortcut: "⌘B",
312 checked: Some(false),
313 ..popup::Item::default()
314 },
315 ];
316 ui.open_popup(menu);
317 for _ in 0..2 {
318 frame(&mut ui, |ui| {
319 popup::menu(ui, menu, Anchor::Point([0.0; 2]), &items, None);
320 });
321 }
322 let tree = tree(&mut ui);
323 let snapshot = snapshot(&tree);
324 assert!(
325 snapshot.contains(r#"MenuItem "Bullet Library" [collapsed]"#),
326 "{snapshot}"
327 );
328 assert!(!snapshot.contains('›'), "{snapshot}");
329 assert!(
330 snapshot.contains(r#"MenuItemCheckBox "Bold" [untoggled]"#),
331 "{snapshot}"
332 );
333 let update = ui.accessibility_tree("Notes", 2.0);
334 let (_, row) = update
335 .nodes
336 .iter()
337 .find(|(_, node)| node.label() == Some("Bullet Library"))
338 .expect("the row");
339 assert_eq!(row.has_popup(), Some(accesskit::HasPopup::Menu));
340}
341
342#[test]
343fn swatches_are_named_by_their_colour_names_or_hex() {
344 let mut ui = new_ui();
345 let grid = Id::ROOT.child("colours");
346 let swatches = [([1.0, 1.0, 0.0, 1.0], "Yellow"), ([0.0, 0.0, 0.0, 1.0], "")];
347 ui.open_popup(grid);
348 for _ in 0..2 {
349 frame(&mut ui, |ui| {
350 popup::colors(ui, grid, Anchor::Point([0.0; 2]), "No Color", &swatches, 2);
351 });
352 }
353 let snapshot = snapshot(&tree(&mut ui));
354 assert!(snapshot.contains(r#"MenuItem "Yellow""#), "{snapshot}");
355 assert!(snapshot.contains(r##"MenuItem "#000000""##), "{snapshot}");
356}
357
358#[test]
359fn an_open_menu_takes_the_focus_and_its_highlight_is_the_focused_item() {
360 let mut ui = new_ui();
361 frame(&mut ui, |ui| {
362 toolbar(ui, false);
363 });
364 // Tab enters the toolbar, and the arrows reach the font box, which Space opens.
365 key(&mut ui, NamedKey::Tab);
366 for _ in 0..4 {
367 key(&mut ui, NamedKey::ArrowRight);
368 }
369 frame(&mut ui, |ui| {
370 toolbar(ui, false);
371 });
372 assert_eq!(ui.focused(), Some(toolbar_id().child("font")));
373 key(&mut ui, NamedKey::Space);
374 for _ in 0..3 {
375 frame(&mut ui, |ui| {
376 toolbar(ui, false);
377 });
378 }
379 assert!(ui.popup_open(font_menu()));
380 let snapshot = snapshot(&tree(&mut ui));
381 assert!(
382 snapshot.contains(r#"ComboBox "Font" = "Calibri" [expanded]"#),
383 "{snapshot}"
384 );
385 assert!(
386 snapshot.ends_with(
387 r#" Menu
388 SearchInput = "" (Font) {focus set}
389 Heading "Recent"
390 MenuItemCheckBox "Calibri" [toggled] [selected] [focused] {click focus}
391 MenuItem "Arial" {click focus}
392 MenuItem "Wingdings" [disabled]
393"#
394 ),
395 "the checked item starts highlighted, and is the focus within the filter:\n{snapshot}"
396 );
397 key(&mut ui, NamedKey::ArrowDown);
398 frame(&mut ui, |ui| {
399 toolbar(ui, false);
400 });
401 let snapshot = self::snapshot(&tree(&mut ui));
402 assert!(
403 snapshot.contains(r#"MenuItem "Arial" [selected] [focused]"#),
404 "{snapshot}"
405 );
406 // Escape closes it, and the focus returns to the box it opened from.
407 key(&mut ui, NamedKey::Escape);
408 frame(&mut ui, |ui| {
409 toolbar(ui, false);
410 });
411 assert!(!ui.popup_open(font_menu()));
412 assert_eq!(ui.focused(), Some(toolbar_id().child("font")));
413 let snapshot = self::snapshot(&tree(&mut ui));
414 assert!(
415 snapshot.contains(r#"ComboBox "Font" = "Calibri" [collapsed] [focused]"#),
416 "{snapshot}"
417 );
418}
419
420#[test]
421fn keys_step_through_the_toolbar_press_its_buttons_and_return() {
422 let mut ui = new_ui();
423 let page = Id::ROOT.child("page");
424 let build = |ui: &mut Ui| {
425 let clicked = toolbar(ui, false);
426 ui.leaf(
427 "page",
428 Spec {
429 flags: Flags::CUSTOM | Flags::FOCUSABLE,
430 size: [fill(), fill()],
431 role: Some(Role::GenericContainer),
432 ..Spec::default()
433 },
434 );
435 clicked
436 };
437 frame(&mut ui, |ui| {
438 build(ui);
439 });
440 ui.set_focus(Some(page));
441 // The page takes Tab for itself; F6 moves to the toolbar, entered at its first button.
442 key(&mut ui, NamedKey::Tab);
443 frame(&mut ui, |ui| {
444 build(ui);
445 });
446 assert_eq!(ui.focused(), Some(page));
447 key(&mut ui, NamedKey::F6);
448 frame(&mut ui, |ui| {
449 build(ui);
450 });
451 assert_eq!(ui.focused(), Some(toolbar_id().child("undo")));
452 // The disabled button is passed over, and the arrows wrap.
453 key(&mut ui, NamedKey::ArrowRight);
454 frame(&mut ui, |ui| {
455 build(ui);
456 });
457 assert_eq!(ui.focused(), Some(toolbar_id().child("bold")));
458 key(&mut ui, NamedKey::ArrowLeft);
459 key(&mut ui, NamedKey::ArrowLeft);
460 frame(&mut ui, |ui| {
461 build(ui);
462 });
463 assert_eq!(ui.focused(), Some(toolbar_id().child("level")));
464 key(&mut ui, NamedKey::Home);
465 key(&mut ui, NamedKey::ArrowRight);
466 key(&mut ui, NamedKey::Enter);
467 let mut clicked = Vec::new();
468 frame(&mut ui, |ui| clicked = build(ui));
469 assert_eq!(clicked, ["bold"]);
470 // Tab leaves the toolbar as one step, for the page.
471 key(&mut ui, NamedKey::Tab);
472 frame(&mut ui, |ui| {
473 build(ui);
474 });
475 assert_eq!(ui.focused(), Some(page));
476 key(&mut ui, NamedKey::F6);
477 key(&mut ui, NamedKey::Escape);
478 frame(&mut ui, |ui| {
479 build(ui);
480 });
481 assert_eq!(ui.focused(), Some(page), "Escape returns to the page");
482}
483
484fn dialog_id() -> Id {
485 Id::ROOT.child("dialog")
486}
487
488/// A dialog of a labelled field, a check box and two buttons; returns the buttons clicked.
489fn dialog(ui: &mut Ui, name: &mut String, checked: bool) -> Vec<&'static str> {
490 let mut clicked = Vec::new();
491 if !ui.popup_open(dialog_id()) {
492 return clicked;
493 }
494 ui.open_as(
495 dialog_id(),
496 Spec {
497 axis: Axis::Y,
498 size: [px(300.0), children()],
499 anchor: Some(Anchor::Dialog),
500 role: Some(Role::Dialog),
501 ..Spec::default()
502 },
503 );
504 if let Some(node) = ui.access(dialog_id()) {
505 node.set_label("Rename");
506 }
507 ui.leaf(
508 "heading",
509 Spec {
510 size: [fit(), px(20.0)],
511 text: Some("Choose a name for the section."),
512 ..Spec::default()
513 },
514 );
515 let field = dialog_id().child("name");
516 text_field(
517 ui,
518 field,
519 name,
520 "Untitled",
521 Spec {
522 size: [fill(), px(22.0)],
523 ..Spec::default()
524 },
525 );
526 if let Some(node) = ui.access(field) {
527 node.set_label("Name");
528 }
529 if check_box(ui, "keep", "Keep a copy", checked).clicked {
530 clicked.push("keep");
531 }
532 for label in ["OK", "Cancel"] {
533 if button(ui, label, label).clicked {
534 clicked.push(label);
535 }
536 }
537 ui.close();
538 clicked
539}
540
541#[test]
542fn a_dialog_holds_the_focus_and_tab_steps_through_its_controls() {
543 let mut ui = new_ui();
544 let mut name = "Notes".to_owned();
545 let build = |ui: &mut Ui, name: &mut String| {
546 ui.leaf(
547 "behind",
548 Spec {
549 flags: Flags::CLICKABLE,
550 size: [px(40.0), px(20.0)],
551 text: Some("Behind"),
552 role: Some(Role::Button),
553 ..Spec::default()
554 },
555 );
556 dialog(ui, name, true)
557 };
558 frame(&mut ui, |ui| {
559 build(ui, &mut name);
560 });
561 ui.open_popup(dialog_id());
562 frame(&mut ui, |ui| {
563 build(ui, &mut name);
564 });
565 assert_eq!(
566 snapshot(&tree(&mut ui)),
567 r#"Window "Notes"
568 Button "Behind" {click focus}
569 Dialog "Rename" [focused]
570 Label = "Choose a name for the section."
571 TextInput "Name" = "Notes" (Untitled) {focus set}
572 CheckBox "Keep a copy" [toggled] {click focus}
573 Button "OK" {click focus}
574 Button "Cancel" {click focus}
575"#
576 );
577 let field = dialog_id().child("name");
578 let mut focused = Vec::new();
579 for _ in 0..5 {
580 key(&mut ui, NamedKey::Tab);
581 frame(&mut ui, |ui| {
582 build(ui, &mut name);
583 });
584 focused.push(ui.focused());
585 }
586 let [keep, ok, cancel] = ["keep", "OK", "Cancel"].map(|part| Some(dialog_id().child(part)));
587 assert_eq!(
588 focused,
589 [Some(field), keep, ok, cancel, Some(field)],
590 "Tab stays within the dialog"
591 );
592 ui.event(Event::Modifiers(ModifiersState::SHIFT));
593 key(&mut ui, NamedKey::Tab);
594 ui.event(Event::Modifiers(ModifiersState::empty()));
595 key(&mut ui, NamedKey::Space);
596 let mut clicked = Vec::new();
597 frame(&mut ui, |ui| clicked = build(ui, &mut name));
598 assert_eq!(clicked, ["Cancel"]);
599}
600
601#[test]
602fn assistive_technology_presses_focuses_and_sets_values() {
603 let mut ui = new_ui();
604 let mut name = "Notes".to_owned();
605 frame(&mut ui, |ui| {
606 dialog(ui, &mut name, false);
607 });
608 ui.open_popup(dialog_id());
609 frame(&mut ui, |ui| {
610 dialog(ui, &mut name, false);
611 });
612 let request = |action, id: Id, data| {
613 Event::Access(ActionRequest {
614 action,
615 target_tree: TreeId::ROOT,
616 target_node: id.node(),
617 data,
618 })
619 };
620 let field = dialog_id().child("name");
621 ui.event(request(
622 Action::SetValue,
623 field,
624 Some(accesskit::ActionData::Value("Journal".into())),
625 ));
626 ui.event(request(Action::Click, dialog_id().child("OK"), None));
627 let mut clicked = Vec::new();
628 frame(&mut ui, |ui| clicked = dialog(ui, &mut name, false));
629 assert_eq!(name, "Journal");
630 assert_eq!(clicked, ["OK"]);
631 assert_eq!(ui.focused(), Some(field));
632 ui.event(request(Action::Focus, dialog_id().child("keep"), None));
633 frame(&mut ui, |ui| {
634 dialog(ui, &mut name, false);
635 });
636 assert_eq!(ui.focused(), Some(dialog_id().child("keep")));
637}
638
639#[test]
640fn the_palette_is_a_dialog_whose_field_leads_through_its_options() {
641 let mut ui = new_ui();
642 let id = Id::ROOT.child("palette");
643 let items = [
644 popup::Item {
645 text: "Bold",
646 shortcut: "⌘B",
647 ..popup::Item::default()
648 },
649 popup::Item {
650 text: "New Page",
651 ..popup::Item::default()
652 },
653 popup::Item {
654 text: "Italic",
655 ..popup::Item::default()
656 },
657 ];
658 frame(&mut ui, |ui| {
659 popup::palette(ui, id, &[("", &items)], "Search commands");
660 });
661 ui.open_popup(id);
662 frame(&mut ui, |ui| {
663 popup::palette(ui, id, &[("", &items)], "Search commands");
664 });
665 for text in ["i", "t"] {
666 ui.event(Event::Key {
667 key: winit::keyboard::Key::Character(text.into()),
668 text: Some(text.into()),
669 });
670 frame(&mut ui, |ui| {
671 popup::palette(ui, id, &[("", &items)], "Search commands");
672 });
673 }
674 frame(&mut ui, |ui| {
675 popup::palette(ui, id, &[("", &items)], "Search commands");
676 });
677 assert_eq!(
678 snapshot(&tree(&mut ui)),
679 r#"Window "Notes"
680 Dialog "Search commands"
681 SearchInput = "it" (Search commands) {focus set}
682 ListBox
683 ListBoxOption "Italic" [selected] [focused] {click focus}
684"#
685 );
686}
687
688#[test]
689fn updates_send_only_what_changed_and_grafts_hold_the_host_s_tree() {
690 let mut ui = new_ui();
691 let page = Id::ROOT.child("page");
692 let tree_id = TreeId(accesskit::Uuid::from_u128(7));
693 let build = |ui: &mut Ui, bold: bool| {
694 toolbar(ui, bold);
695 ui.open_as(
696 page,
697 Spec {
698 flags: Flags::CUSTOM | Flags::FOCUSABLE,
699 size: [fill(), fill()],
700 role: Some(Role::GenericContainer),
701 ..Spec::default()
702 },
703 );
704 if let Some(node) = ui.access(page) {
705 node.set_tree_id(tree_id);
706 }
707 // Built inside the page, the scrollbar shows beside it.
708 button(ui, "scroll", "Scroll");
709 ui.close();
710 };
711 frame(&mut ui, |ui| build(ui, false));
712 let mut tree = tree(&mut ui);
713 let mut page_node = accesskit::Node::new(Role::Group);
714 page_node.set_label("Page");
715 tree.update_and_process_changes(
716 accesskit::TreeUpdate {
717 nodes: vec![(NodeId(0), page_node)],
718 tree: Some(accesskit::TreeInfo::new(NodeId(0))),
719 tree_id,
720 focus: NodeId(0),
721 },
722 &mut NoChanges,
723 );
724 ui.set_focus(Some(page));
725 frame(&mut ui, |ui| build(ui, true));
726 let update = ui.accessibility("Notes", 2.0);
727 let changed: Vec<_> = update.nodes.iter().map(|(id, _)| *id).collect();
728 assert_eq!(changed, [toolbar_id().child("bold").node()]);
729 assert_eq!(update.focus, page.node());
730 tree.update_and_process_changes(update, &mut NoChanges);
731 let snapshot = snapshot(&tree);
732 assert!(
733 snapshot.ends_with(
734 r#" Group "Page" [focused]
735 Button "Scroll" {click focus}
736"#
737 ),
738 "{snapshot}"
739 );
740}
741
742struct NoChanges;
743
744impl accesskit_consumer::TreeChangeHandler for NoChanges {
745 fn node_added(&mut self, _: &NodeRef) {}
746 fn node_updated(&mut self, _: &NodeRef, _: &NodeRef) {}
747 fn focus_moved(&mut self, _: Option<&NodeRef>, _: Option<&NodeRef>) {}
748 fn node_removed(&mut self, _: &NodeRef) {}
749}
750
751/// A segmented control of sides with `chosen` picked, which a pick moves.
752fn sides(ui: &mut Ui, chosen: &mut usize) {
753 if let Some(picked) = segmented(ui, "sides", "Page tabs", &["Left", "Right"], *chosen) {
754 *chosen = picked;
755 }
756}
757
758#[test]
759fn a_segmented_control_is_a_radio_group_tab_enters_at_its_choice_and_arrows_pick() {
760 let mut ui = new_ui();
761 let mut chosen = 1;
762 for _ in 0..2 {
763 frame(&mut ui, |ui| sides(ui, &mut chosen));
764 }
765 assert_eq!(
766 snapshot(&tree(&mut ui)),
767 r#"Window "Notes" [focused]
768 RadioGroup "Page tabs"
769 RadioButton "Left" [untoggled] {click focus}
770 RadioButton "Right" [toggled] {click focus}
771"#
772 );
773 let segment = |index: usize| Some(Id::ROOT.child("sides").child(index));
774 key(&mut ui, NamedKey::Tab);
775 frame(&mut ui, |ui| sides(ui, &mut chosen));
776 assert_eq!(ui.focused(), segment(1), "Tab enters at the choice");
777 assert_eq!(chosen, 1);
778 key(&mut ui, NamedKey::ArrowLeft);
779 for _ in 0..2 {
780 frame(&mut ui, |ui| sides(ui, &mut chosen));
781 }
782 assert_eq!((ui.focused(), chosen), (segment(0), 0), "an arrow picks");
783 // A click picks without moving the focus, which stays put on later frames.
784 let [left, top, right, bottom] = ui.laid_out(segment(1).unwrap()).unwrap();
785 ui.event(Event::PointerMoved([
786 (left + right) / 2.0,
787 (top + bottom) / 2.0,
788 ]));
789 for pressed in [true, false] {
790 ui.event(Event::Button {
791 button: winit::event::MouseButton::Left,
792 pressed,
793 at: Instant::now(),
794 });
795 frame(&mut ui, |ui| sides(ui, &mut chosen));
796 }
797 for _ in 0..2 {
798 frame(&mut ui, |ui| sides(ui, &mut chosen));
799 }
800 assert_eq!(chosen, 1);
801}