Skip to content

Commit 015ba8e

Browse files
committed
fix: Add an example
1 parent 8eb2936 commit 015ba8e

File tree

1 file changed

+386
-0
lines changed

1 file changed

+386
-0
lines changed

platforms/winit/examples/combobox.rs

Lines changed: 386 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,386 @@
1+
use accesskit::{Action, ActionRequest, Node, NodeId, Rect, Role, Tree, TreeUpdate};
2+
use accesskit_winit::{Adapter, Event as AccessKitEvent, WindowEvent as AccessKitWindowEvent};
3+
use std::error::Error;
4+
use winit::{
5+
application::ApplicationHandler,
6+
dpi::LogicalSize,
7+
event::{ElementState, KeyEvent, WindowEvent},
8+
event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy},
9+
keyboard::Key,
10+
window::{Window, WindowId},
11+
};
12+
13+
const WINDOW_TITLE: &str = "ComboBox example";
14+
15+
const WINDOW_ID: NodeId = NodeId(0);
16+
const COMBOBOX_ID: NodeId = NodeId(1);
17+
const POPUP_ID: NodeId = NodeId(2);
18+
const INITIAL_FOCUS: NodeId = COMBOBOX_ID;
19+
20+
const LANGUAGES: &[(&str, NodeId)] = &[
21+
("English", NodeId(3)),
22+
("Esperanto", NodeId(4)),
23+
("French", NodeId(5)),
24+
("Spanish", NodeId(6)),
25+
];
26+
27+
const ITEM_HEIGHT: f64 = 40.0;
28+
29+
const COMBOBOX_RECT: Rect = Rect {
30+
x0: 100.0,
31+
y0: 20.0,
32+
x1: 300.0,
33+
y1: 20.0 + ITEM_HEIGHT,
34+
};
35+
36+
const POPUP_RECT: Rect = Rect {
37+
x0: COMBOBOX_RECT.x0,
38+
y0: COMBOBOX_RECT.y0,
39+
x1: COMBOBOX_RECT.x1,
40+
y1: COMBOBOX_RECT.y0 + ITEM_HEIGHT * (LANGUAGES.len() as f64),
41+
};
42+
43+
fn build_combobox(expanded: bool) -> Node {
44+
let mut node = Node::new(Role::ComboBox);
45+
node.set_bounds(COMBOBOX_RECT);
46+
node.set_children([POPUP_ID]);
47+
node.set_expanded(expanded);
48+
node.set_label("Select your language");
49+
node.add_action(Action::Click);
50+
node.add_action(Action::Focus);
51+
node.add_action(if expanded {
52+
Action::Collapse
53+
} else {
54+
Action::Expand
55+
});
56+
node
57+
}
58+
59+
fn build_popup() -> Node {
60+
let mut node = Node::new(Role::MenuListPopup);
61+
node.set_bounds(POPUP_RECT);
62+
node.set_children(
63+
LANGUAGES
64+
.iter()
65+
.map(|(_, id)| id)
66+
.copied()
67+
.collect::<Vec<NodeId>>(),
68+
);
69+
node.set_size_of_set(LANGUAGES.len());
70+
node
71+
}
72+
73+
fn build_option(index: usize, selected: bool) -> Node {
74+
let mut node = Node::new(Role::MenuListOption);
75+
node.set_bounds(Rect {
76+
x0: POPUP_RECT.x0,
77+
y0: POPUP_RECT.y0 + (index as f64) * ITEM_HEIGHT,
78+
x1: POPUP_RECT.x1,
79+
y1: POPUP_RECT.y0 + (index as f64) * ITEM_HEIGHT + ITEM_HEIGHT,
80+
});
81+
node.set_label(LANGUAGES[index].0);
82+
node.set_position_in_set(index);
83+
node.set_selected(selected);
84+
node.add_action(Action::Click);
85+
node.add_action(Action::Focus);
86+
node
87+
}
88+
89+
struct UiState {
90+
focus: NodeId,
91+
selected_index: usize,
92+
previous_selected_index: Option<usize>,
93+
}
94+
95+
impl UiState {
96+
fn new() -> Self {
97+
Self {
98+
focus: INITIAL_FOCUS,
99+
selected_index: 0,
100+
previous_selected_index: None,
101+
}
102+
}
103+
104+
fn build_root(&mut self) -> Node {
105+
let mut node = Node::new(Role::Window);
106+
node.set_children([COMBOBOX_ID]);
107+
node.set_label(WINDOW_TITLE);
108+
node
109+
}
110+
111+
fn is_expanded(&self) -> bool {
112+
self.previous_selected_index.is_some()
113+
}
114+
115+
fn build_initial_tree(&mut self) -> TreeUpdate {
116+
let root = self.build_root();
117+
let combobox = build_combobox(self.is_expanded());
118+
let popup = build_popup();
119+
let tree = Tree::new(WINDOW_ID);
120+
let mut update = TreeUpdate {
121+
nodes: vec![
122+
(WINDOW_ID, root),
123+
(COMBOBOX_ID, combobox),
124+
(POPUP_ID, popup),
125+
],
126+
tree: Some(tree),
127+
focus: self.focus,
128+
};
129+
for (i, (_, id)) in LANGUAGES.iter().enumerate() {
130+
let is_selected = i == self.selected_index;
131+
update.nodes.push((*id, build_option(i, is_selected)));
132+
}
133+
update
134+
}
135+
136+
fn set_focus(&mut self, adapter: &mut Adapter, id: NodeId) {
137+
self.focus = id;
138+
adapter.update_if_active(|| TreeUpdate {
139+
nodes: vec![],
140+
tree: None,
141+
focus: self.focus,
142+
});
143+
}
144+
145+
fn expand(&mut self, adapter: &mut Adapter) {
146+
self.previous_selected_index = Some(self.selected_index);
147+
self.focus = LANGUAGES[self.selected_index].1;
148+
adapter.update_if_active(|| {
149+
let combobox = build_combobox(self.is_expanded());
150+
TreeUpdate {
151+
nodes: vec![(COMBOBOX_ID, combobox)],
152+
tree: None,
153+
focus: self.focus,
154+
}
155+
});
156+
}
157+
158+
fn confirm_selection_and_collapse(&mut self, adapter: &mut Adapter) {
159+
self.previous_selected_index = None;
160+
self.focus = COMBOBOX_ID;
161+
adapter.update_if_active(|| {
162+
let combobox = build_combobox(self.is_expanded());
163+
TreeUpdate {
164+
nodes: vec![(COMBOBOX_ID, combobox)],
165+
tree: None,
166+
focus: self.focus,
167+
}
168+
});
169+
}
170+
171+
fn discard_selection_and_collapse(&mut self, adapter: &mut Adapter) {
172+
let previous_selection = self.previous_selected_index.take();
173+
if let Some(previous_selection) = previous_selection {
174+
self.selected_index = previous_selection;
175+
}
176+
self.focus = COMBOBOX_ID;
177+
adapter.update_if_active(|| {
178+
let combobox = build_combobox(self.is_expanded());
179+
let selected_option = build_option(self.selected_index, true);
180+
let mut update = TreeUpdate {
181+
nodes: vec![
182+
(COMBOBOX_ID, combobox),
183+
(LANGUAGES[self.selected_index].1, selected_option),
184+
],
185+
tree: None,
186+
focus: self.focus,
187+
};
188+
if let Some(previous_selection) = previous_selection
189+
.filter(|previous_selection| *previous_selection != self.selected_index)
190+
{
191+
update.nodes.push((
192+
LANGUAGES[previous_selection].1,
193+
build_option(previous_selection, false),
194+
));
195+
}
196+
update
197+
});
198+
}
199+
200+
fn update_selection(&mut self, adapter: &mut Adapter, index: usize) {
201+
let previous_selection = self.selected_index;
202+
self.selected_index = index;
203+
if self.is_expanded() {
204+
self.focus = LANGUAGES[index].1;
205+
}
206+
adapter.update_if_active(|| TreeUpdate {
207+
nodes: vec![
208+
(LANGUAGES[index].1, build_option(index, true)),
209+
(
210+
LANGUAGES[previous_selection].1,
211+
build_option(previous_selection, false),
212+
),
213+
],
214+
tree: None,
215+
focus: self.focus,
216+
});
217+
}
218+
}
219+
220+
struct WindowState {
221+
window: Window,
222+
adapter: Adapter,
223+
ui: UiState,
224+
}
225+
226+
impl WindowState {
227+
fn new(window: Window, adapter: Adapter, ui: UiState) -> Self {
228+
Self {
229+
window,
230+
adapter,
231+
ui,
232+
}
233+
}
234+
}
235+
236+
struct Application {
237+
event_loop_proxy: EventLoopProxy<AccessKitEvent>,
238+
window: Option<WindowState>,
239+
}
240+
241+
impl Application {
242+
fn new(event_loop_proxy: EventLoopProxy<AccessKitEvent>) -> Self {
243+
Self {
244+
event_loop_proxy,
245+
window: None,
246+
}
247+
}
248+
249+
fn create_window(&mut self, event_loop: &ActiveEventLoop) -> Result<(), Box<dyn Error>> {
250+
let window_attributes = Window::default_attributes()
251+
.with_title(WINDOW_TITLE)
252+
.with_visible(false)
253+
.with_inner_size(LogicalSize::new(400, 300));
254+
255+
let window = event_loop.create_window(window_attributes)?;
256+
let adapter =
257+
Adapter::with_event_loop_proxy(event_loop, &window, self.event_loop_proxy.clone());
258+
window.set_visible(true);
259+
260+
self.window = Some(WindowState::new(window, adapter, UiState::new()));
261+
Ok(())
262+
}
263+
}
264+
265+
impl ApplicationHandler<AccessKitEvent> for Application {
266+
fn window_event(&mut self, _: &ActiveEventLoop, _: WindowId, event: WindowEvent) {
267+
let window = match &mut self.window {
268+
Some(window) => window,
269+
None => return,
270+
};
271+
let adapter = &mut window.adapter;
272+
let state = &mut window.ui;
273+
274+
adapter.process_event(&window.window, &event);
275+
match event {
276+
WindowEvent::CloseRequested => {
277+
self.window = None;
278+
}
279+
WindowEvent::KeyboardInput {
280+
event:
281+
KeyEvent {
282+
logical_key: virtual_code,
283+
state: ElementState::Pressed,
284+
..
285+
},
286+
..
287+
} => match virtual_code {
288+
Key::Named(winit::keyboard::NamedKey::ArrowDown) => {
289+
if state.selected_index < LANGUAGES.len() - 1 {
290+
state.update_selection(adapter, state.selected_index + 1);
291+
}
292+
}
293+
Key::Named(winit::keyboard::NamedKey::ArrowUp) => {
294+
if state.selected_index > 0 {
295+
state.update_selection(adapter, state.selected_index - 1);
296+
}
297+
}
298+
Key::Named(winit::keyboard::NamedKey::Space) if state.is_expanded() => {
299+
state.confirm_selection_and_collapse(adapter);
300+
}
301+
Key::Named(winit::keyboard::NamedKey::Space) => {
302+
state.expand(adapter);
303+
}
304+
Key::Named(winit::keyboard::NamedKey::Escape) if state.is_expanded() => {
305+
state.discard_selection_and_collapse(adapter);
306+
}
307+
_ => (),
308+
},
309+
_ => (),
310+
}
311+
}
312+
313+
fn user_event(&mut self, _: &ActiveEventLoop, user_event: AccessKitEvent) {
314+
let window = match &mut self.window {
315+
Some(window) => window,
316+
None => return,
317+
};
318+
let adapter = &mut window.adapter;
319+
let state = &mut window.ui;
320+
321+
match user_event.window_event {
322+
AccessKitWindowEvent::InitialTreeRequested => {
323+
adapter.update_if_active(|| state.build_initial_tree());
324+
}
325+
AccessKitWindowEvent::ActionRequested(ActionRequest { action, target, .. }) => {
326+
if target == COMBOBOX_ID {
327+
if (action == Action::Click || action == Action::Expand) && !state.is_expanded()
328+
{
329+
state.expand(adapter);
330+
} else if (action == Action::Click || action == Action::Collapse)
331+
&& state.is_expanded()
332+
{
333+
state.discard_selection_and_collapse(adapter);
334+
} else if action == Action::Focus {
335+
state.set_focus(adapter, COMBOBOX_ID);
336+
}
337+
} else if action == Action::Click && state.is_expanded() {
338+
if let Some(selection) = LANGUAGES.iter().position(|(_, id)| *id == target) {
339+
state.selected_index = selection;
340+
state.confirm_selection_and_collapse(adapter);
341+
}
342+
} else if action == Action::Focus && state.is_expanded() {
343+
if LANGUAGES.iter().any(|(_, id)| *id == target) {
344+
state.set_focus(adapter, target);
345+
}
346+
}
347+
}
348+
AccessKitWindowEvent::AccessibilityDeactivated => (),
349+
}
350+
}
351+
352+
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
353+
self.create_window(event_loop)
354+
.expect("failed to create initial window");
355+
}
356+
357+
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
358+
if self.window.is_none() {
359+
event_loop.exit();
360+
}
361+
}
362+
}
363+
364+
fn main() -> Result<(), Box<dyn Error>> {
365+
println!("This example has no visible GUI, and a keyboard interface:");
366+
println!("- [Space] expands the combobox.");
367+
println!("- [Up] and [Down] arrow keys update the selection.");
368+
println!("- [Escape] collapses the combobox, discarding the selection.");
369+
#[cfg(target_os = "windows")]
370+
println!("Enable Narrator with [Win]+[Ctrl]+[Enter] (or [Win]+[Enter] on older versions of Windows).");
371+
#[cfg(all(
372+
feature = "accesskit_unix",
373+
any(
374+
target_os = "linux",
375+
target_os = "dragonfly",
376+
target_os = "freebsd",
377+
target_os = "netbsd",
378+
target_os = "openbsd"
379+
)
380+
))]
381+
println!("Enable Orca with [Super]+[Alt]+[S].");
382+
383+
let event_loop = EventLoop::with_user_event().build()?;
384+
let mut state = Application::new(event_loop.create_proxy());
385+
event_loop.run_app(&mut state).map_err(Into::into)
386+
}

0 commit comments

Comments
 (0)