Skip to content

Conversation

@DataTriny
Copy link
Member

@DataTriny DataTriny commented Jan 14, 2026

I think that this property can be completely hidden from the adapters. While AT-SPI has a dedicated active-descendant-changed signal, in practice it seem to be handled like a focus move. Furthermore, the Collection interface is supposed to have a method to retrieve the active descendant but it is not implemented by libatspi. Other platforms don't expose this relationship.

Below an example to test the implementation:

platforms/winit/examples/active_descendant.rs

#[path = "util/fill.rs"]
mod fill;

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

const WINDOW_TITLE: &str = "Active Descendant Example";

const WINDOW_ID: NodeId = NodeId(0);
const LISTBOX_ID: NodeId = NodeId(1);
const CLEAR_BUTTON_ID: NodeId = NodeId(5);
const INITIAL_FOCUS: NodeId = LISTBOX_ID;

const ITEMS: &[(NodeId, &str)] = &[
    (NodeId(2), "Item 1"),
    (NodeId(3), "Item 2"),
    (NodeId(4), "Item 3"),
];

const LISTBOX_X: f64 = 20.0;
const LISTBOX_Y: f64 = 20.0;
const LISTBOX_WIDTH: f64 = 180.0;
const ITEM_HEIGHT: f64 = 30.0;

const LISTBOX_RECT: Rect = Rect {
    x0: LISTBOX_X,
    y0: LISTBOX_Y,
    x1: LISTBOX_X + LISTBOX_WIDTH,
    y1: LISTBOX_Y + ITEM_HEIGHT * ITEMS.len() as f64,
};

const CLEAR_BUTTON_RECT: Rect = Rect {
    x0: LISTBOX_RECT.x0,
    y0: LISTBOX_RECT.y1 + 10.0,
    x1: LISTBOX_RECT.x1,
    y1: LISTBOX_RECT.y1 + 10.0 + ITEM_HEIGHT,
};

fn item_index(id: NodeId) -> Option<usize> {
    ITEMS.iter().position(|(item_id, _)| *item_id == id)
}

struct UiState {
    focus: NodeId,
    active_item: Option<usize>,
    selected_item: Option<usize>,
}

impl UiState {
    fn new() -> Self {
        Self {
            focus: INITIAL_FOCUS,
            active_item: None,
            selected_item: None,
        }
    }

    fn build_root(&self) -> Node {
        let mut node = Node::new(Role::Window);
        node.set_children(vec![LISTBOX_ID, CLEAR_BUTTON_ID]);
        node.set_label(WINDOW_TITLE);
        node
    }

    fn build_listbox(&self) -> Node {
        let mut node = Node::new(Role::ListBox);
        node.set_bounds(LISTBOX_RECT);
        node.set_label("Items");
        node.set_children(ITEMS.iter().map(|(id, _)| *id).collect::<Vec<_>>());
        node.add_action(Action::Focus);
        if let Some(index) = self.active_item {
            node.set_active_descendant(ITEMS[index].0);
        }
        node
    }

    fn build_item(&self, index: usize) -> Node {
        let mut node = Node::new(Role::ListBoxOption);
        node.set_bounds(Rect {
            x0: LISTBOX_RECT.x0,
            y0: LISTBOX_Y + ITEM_HEIGHT * index as f64,
            x1: LISTBOX_RECT.x1,
            y1: LISTBOX_Y + ITEM_HEIGHT * (index + 1) as f64,
        });
        node.set_label(ITEMS[index].1);
        node.add_action(Action::Focus);
        node.add_action(Action::Click);
        node.set_selected(self.selected_item == Some(index));
        node
    }

    fn build_clear_button(&self) -> Node {
        let mut node = Node::new(Role::Button);
        node.set_bounds(CLEAR_BUTTON_RECT);
        node.set_label("Clear Selection");
        node.add_action(Action::Focus);
        if self.selected_item.is_some() {
            node.add_action(Action::Click);
        } else {
            node.set_disabled();
        }
        node
    }

    fn build_initial_tree(&self) -> TreeUpdate {
        let root = self.build_root();
        let listbox = self.build_listbox();
        let clear_button = self.build_clear_button();
        let tree = Tree::new(WINDOW_ID);
        let mut nodes = vec![(WINDOW_ID, root), (LISTBOX_ID, listbox)];
        for (index, (id, _)) in ITEMS.iter().enumerate() {
            nodes.push((*id, self.build_item(index)));
        }
        nodes.push((CLEAR_BUTTON_ID, clear_button));
        TreeUpdate {
            nodes,
            tree: Some(tree),
            tree_id: TreeId::ROOT,
            focus: self.focus,
        }
    }

    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 select_item(&mut self, adapter: &mut Adapter, index: usize) {
        let old_selected = self.selected_item;
        self.active_item = Some(index);
        self.selected_item = Some(index);
        let focus = self.focus;

        adapter.update_if_active(|| {
            let mut nodes = vec![
                (LISTBOX_ID, self.build_listbox()),
                (ITEMS[index].0, self.build_item(index)),
                (CLEAR_BUTTON_ID, self.build_clear_button()),
            ];
            if let Some(old_index) = old_selected {
                if old_index != index {
                    nodes.push((ITEMS[old_index].0, self.build_item(old_index)));
                }
            }
            TreeUpdate {
                nodes,
                tree: None,
                tree_id: TreeId::ROOT,
                focus,
            }
        });
    }

    fn clear_selection(&mut self, adapter: &mut Adapter) {
        let old_selected = self.selected_item;
        self.active_item = None;
        self.selected_item = None;
        let focus = self.focus;

        adapter.update_if_active(|| {
            let mut nodes = vec![
                (LISTBOX_ID, self.build_listbox()),
                (CLEAR_BUTTON_ID, self.build_clear_button()),
            ];
            if let Some(old_index) = old_selected {
                nodes.push((ITEMS[old_index].0, self.build_item(old_index)));
            }
            TreeUpdate {
                nodes,
                tree: None,
                tree_id: TreeId::ROOT,
                focus,
            }
        });
    }

    fn move_active(&mut self, adapter: &mut Adapter, delta: i32) {
        let max_index = (ITEMS.len() - 1) as i32;
        let new_index = match self.active_item {
            Some(index) => (index as i32 + delta).clamp(0, max_index) as usize,
            None => 0, // No selection: start at first item regardless of direction
        };
        self.select_item(adapter, new_index);
    }
}

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 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, UiState::new()));
        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(_) => {
                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 == LISTBOX_ID {
                        CLEAR_BUTTON_ID
                    } else {
                        LISTBOX_ID
                    };
                    state.set_focus(adapter, new_focus);
                    window.window.request_redraw();
                }
                Key::Named(winit::keyboard::NamedKey::ArrowUp) => {
                    if state.focus == LISTBOX_ID {
                        state.move_active(adapter, -1);
                        window.window.request_redraw();
                    }
                }
                Key::Named(winit::keyboard::NamedKey::ArrowDown) => {
                    if state.focus == LISTBOX_ID {
                        state.move_active(adapter, 1);
                        window.window.request_redraw();
                    }
                }
                Key::Named(winit::keyboard::NamedKey::Space) => {
                    if state.focus == CLEAR_BUTTON_ID && state.selected_item.is_some() {
                        state.clear_selection(adapter);
                        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,
                ..
            }) => {
                match action {
                    Action::Focus => {
                        if target_node == LISTBOX_ID || target_node == CLEAR_BUTTON_ID {
                            state.set_focus(adapter, target_node);
                        } else if let Some(index) = item_index(target_node) {
                            state.set_focus(adapter, LISTBOX_ID);
                            state.select_item(adapter, index);
                        }
                    }
                    Action::Click => {
                        if let Some(index) = item_index(target_node) {
                            state.select_item(adapter, index);
                        } else if target_node == CLEAR_BUTTON_ID && state.selected_item.is_some() {
                            state.clear_selection(adapter);
                        }
                    }
                    _ => (),
                }
                window.window.request_redraw();
            }
            AccessKitWindowEvent::AccessibilityDeactivated => (),
        }
    }

    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        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 self.window.is_none() {
            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 the ListBox and the Clear Selection button.");
    println!("- [Up]/[Down] arrows navigate and select items in the ListBox.");
    println!("- [Space] activates the Clear Selection button (when focused and enabled).");
    #[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)
}

@mwcampbell mwcampbell merged commit 863755d into main Jan 15, 2026
16 checks passed
@mwcampbell mwcampbell deleted the active-descendant branch January 15, 2026 14:15
@DataTriny DataTriny mentioned this pull request Jan 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants