accesskit_winit 0.33.0

AccessKit UI accessibility infrastructure: winit adapter
Documentation
#[path = "util/fill.rs"]
mod fill;

use accesskit::{
    Action, ActionRequest, Affine, Live, Node, NodeId, Rect, Role, Tree, TreeId, TreeUpdate, Vec2,
};
use accesskit_winit::{Adapter, Event as AccessKitEvent, WindowEvent as AccessKitWindowEvent};
use std::error::Error;
use std::time::{Duration, Instant};
use winit::{
    application::ApplicationHandler,
    event::{ElementState, KeyEvent, WindowEvent},
    event_loop::ControlFlow,
    event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy},
    keyboard::Key,
    window::{Window, WindowId},
};

const WINDOW_TITLE: &str = "Hello world";

const WINDOW_ID: NodeId = NodeId(0);
const BUTTON_1_ID: NodeId = NodeId(1);
const BUTTON_2_ID: NodeId = NodeId(2);
const ANNOUNCEMENT_ID: NodeId = NodeId(3);
const INITIAL_FOCUS: NodeId = BUTTON_1_ID;

const WINDOW_RECT: Rect = Rect {
    x0: 0.0,
    y0: 0.0,
    x1: 393.0,
    y1: 759.0,
};

const BUTTON_1_RECT: Rect = Rect {
    x0: 20.0,
    y0: 20.0,
    x1: 200.0,
    y1: 64.0,
};

const BUTTON_2_RECT: Rect = Rect {
    x0: 20.0,
    y0: 84.0,
    x1: 200.0,
    y1: 128.0,
};

#[cfg(target_os = "ios")]
fn safe_area_inset(window: &Window) -> Vec2 {
    let Ok(outer) = window.outer_position() else {
        return Vec2::ZERO;
    };
    let Ok(inner) = window.inner_position() else {
        return Vec2::ZERO;
    };
    Vec2::new((inner.x - outer.x) as f64, (inner.y - outer.y) as f64)
}

#[cfg(not(target_os = "ios"))]
fn safe_area_inset(_: &Window) -> Vec2 {
    Vec2::ZERO
}

fn build_button(id: NodeId, label: &str) -> Node {
    let rect = match id {
        BUTTON_1_ID => BUTTON_1_RECT,
        BUTTON_2_ID => BUTTON_2_RECT,
        _ => unreachable!(),
    };
    let mut node = Node::new(Role::Button);
    node.set_bounds(rect);
    node.set_label(label);
    node.add_action(Action::Focus);
    node.add_action(Action::Click);
    node
}

fn build_announcement(text: &str) -> Node {
    let mut node = Node::new(Role::Label);
    node.set_value(text);
    node.set_live(Live::Polite);
    node
}

const ANNOUNCEMENT_DELAY: Duration = Duration::from_millis(150);

struct UiState {
    focus: NodeId,
    announcement: Option<String>,
    pending_announcement: Option<(String, Instant)>,
    scale_factor: f64,
    inset: Vec2,
}

impl UiState {
    fn new(scale_factor: f64, inset: Vec2) -> Self {
        Self {
            focus: INITIAL_FOCUS,
            announcement: None,
            pending_announcement: None,
            scale_factor,
            inset,
        }
    }

    fn build_root(&mut self) -> Node {
        let mut node = Node::new(Role::Window);
        node.set_bounds(WINDOW_RECT);
        node.set_transform(Affine::translate(self.inset) * Affine::scale(self.scale_factor));
        node.set_children(vec![BUTTON_1_ID, BUTTON_2_ID]);
        if self.announcement.is_some() {
            node.push_child(ANNOUNCEMENT_ID);
        }
        node.set_label(WINDOW_TITLE);
        node
    }

    fn build_initial_tree(&mut self) -> TreeUpdate {
        let root = self.build_root();
        let button_1 = build_button(BUTTON_1_ID, "Button 1");
        let button_2 = build_button(BUTTON_2_ID, "Button 2");
        let tree = Tree::new(WINDOW_ID);
        let mut result = TreeUpdate {
            nodes: vec![
                (WINDOW_ID, root),
                (BUTTON_1_ID, button_1),
                (BUTTON_2_ID, button_2),
            ],
            tree: Some(tree),
            tree_id: TreeId::ROOT,
            focus: self.focus,
        };
        if let Some(announcement) = &self.announcement {
            result
                .nodes
                .push((ANNOUNCEMENT_ID, build_announcement(announcement)));
        }
        result
    }

    fn set_focus(&mut self, adapter: &mut Adapter, focus: NodeId) {
        self.focus = focus;
        adapter.update_if_active(|| TreeUpdate {
            nodes: vec![],
            tree: None,
            tree_id: TreeId::ROOT,
            focus,
        });
    }

    fn press_button(&mut self, id: NodeId) {
        let text = if id == BUTTON_1_ID {
            "You pressed button 1"
        } else {
            "You pressed button 2"
        };
        // On iOS, VoiceOver announces the label of the activated button.
        // Postpone the live region update so the messages don't overlap.
        self.pending_announcement = Some((text.into(), Instant::now()));
    }

    fn flush_announcement(&mut self, adapter: &mut Adapter) -> bool {
        let Some((_, queued_at)) = &self.pending_announcement else {
            return false;
        };
        if queued_at.elapsed() < ANNOUNCEMENT_DELAY {
            return true;
        }
        if let Some((text, _)) = self.pending_announcement.take() {
            self.announcement = Some(text.clone());
            adapter.update_if_active(|| {
                let announcement = build_announcement(&text);
                let root = self.build_root();
                TreeUpdate {
                    nodes: vec![(ANNOUNCEMENT_ID, announcement), (WINDOW_ID, root)],
                    tree: None,
                    tree_id: TreeId::ROOT,
                    focus: self.focus,
                }
            });
        }
        false
    }
}

struct WindowState {
    window: Window,
    adapter: Adapter,
    ui: UiState,
}

impl WindowState {
    fn new(window: Window, adapter: Adapter, ui: UiState) -> Self {
        Self {
            window,
            adapter,
            ui,
        }
    }
}

struct Application {
    event_loop_proxy: EventLoopProxy<AccessKitEvent>,
    window: Option<WindowState>,
}

impl Application {
    fn new(event_loop_proxy: EventLoopProxy<AccessKitEvent>) -> Self {
        Self {
            event_loop_proxy,
            window: None,
        }
    }

    fn create_window(&mut self, event_loop: &ActiveEventLoop) -> Result<(), Box<dyn Error>> {
        let window_attributes = Window::default_attributes()
            .with_title(WINDOW_TITLE)
            .with_visible(false);

        let window = event_loop.create_window(window_attributes)?;
        let ui = UiState::new(window.scale_factor(), safe_area_inset(&window));
        let adapter =
            Adapter::with_event_loop_proxy(event_loop, &window, self.event_loop_proxy.clone());
        window.set_visible(true);

        self.window = Some(WindowState::new(window, adapter, ui));
        Ok(())
    }
}

impl ApplicationHandler<AccessKitEvent> for Application {
    fn window_event(&mut self, _: &ActiveEventLoop, _: WindowId, event: WindowEvent) {
        let window = match &mut self.window {
            Some(window) => window,
            None => return,
        };
        let adapter = &mut window.adapter;
        let state = &mut window.ui;

        adapter.process_event(&window.window, &event);
        match event {
            WindowEvent::CloseRequested => {
                fill::cleanup_window(&window.window);
                self.window = None;
            }
            WindowEvent::Resized(_) => {
                state.scale_factor = window.window.scale_factor();
                state.inset = safe_area_inset(&window.window);
                adapter.update_if_active(|| state.build_initial_tree());
                window.window.request_redraw();
            }
            WindowEvent::RedrawRequested => {
                fill::fill_window(&window.window);
            }
            WindowEvent::KeyboardInput {
                event:
                    KeyEvent {
                        logical_key: virtual_code,
                        state: ElementState::Pressed,
                        ..
                    },
                ..
            } => match virtual_code {
                Key::Named(winit::keyboard::NamedKey::Tab) => {
                    let new_focus = if state.focus == BUTTON_1_ID {
                        BUTTON_2_ID
                    } else {
                        BUTTON_1_ID
                    };
                    state.set_focus(adapter, new_focus);
                    window.window.request_redraw();
                }
                Key::Named(winit::keyboard::NamedKey::Space) => {
                    let id = state.focus;
                    state.press_button(id);
                    window.window.request_redraw();
                }
                _ => (),
            },
            _ => (),
        }
    }

    fn user_event(&mut self, _: &ActiveEventLoop, user_event: AccessKitEvent) {
        let window = match &mut self.window {
            Some(window) => window,
            None => return,
        };
        let adapter = &mut window.adapter;
        let state = &mut window.ui;

        match user_event.window_event {
            AccessKitWindowEvent::InitialTreeRequested => {
                adapter.update_if_active(|| state.build_initial_tree());
            }
            AccessKitWindowEvent::ActionRequested(ActionRequest {
                action,
                target_node,
                ..
            }) => {
                if target_node == BUTTON_1_ID || target_node == BUTTON_2_ID {
                    match action {
                        Action::Focus => {
                            state.set_focus(adapter, target_node);
                        }
                        Action::Click => {
                            state.press_button(target_node);
                        }
                        _ => (),
                    }
                }
                window.window.request_redraw();
            }
            AccessKitWindowEvent::AccessibilityDeactivated => (),
        }
    }

    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        if self.window.is_none() {
            self.create_window(event_loop)
                .expect("failed to create initial window");
        }
        if let Some(window) = self.window.as_ref() {
            window.window.request_redraw();
        }
    }

    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
        if let Some(window) = &mut self.window {
            if window.ui.flush_announcement(&mut window.adapter) {
                event_loop.set_control_flow(ControlFlow::wait_duration(ANNOUNCEMENT_DELAY));
            }
        } else {
            event_loop.exit();
        }
    }
}

fn main() -> Result<(), Box<dyn Error>> {
    println!("This example has no visible GUI, and a keyboard interface:");
    println!("- [Tab] switches focus between two logical buttons.");
    println!(
        "- [Space] 'presses' the button, adding static text in a live region announcing that it was pressed."
    );
    #[cfg(target_os = "windows")]
    println!(
        "Enable Narrator with [Win]+[Ctrl]+[Enter] (or [Win]+[Enter] on older versions of Windows)."
    );
    #[cfg(all(
        feature = "accesskit_unix",
        any(
            target_os = "linux",
            target_os = "dragonfly",
            target_os = "freebsd",
            target_os = "netbsd",
            target_os = "openbsd"
        )
    ))]
    println!("Enable Orca with [Super]+[Alt]+[S].");

    let event_loop = EventLoop::with_user_event().build()?;
    let mut state = Application::new(event_loop.create_proxy());
    event_loop.run_app(&mut state).map_err(Into::into)
}