Skip to content

Conversation

@DataTriny
Copy link
Member

It appears that #626 broke the "say all" command for Windows Narrator as it would stop at the end of the first line in a paragraph.

@DataTriny
Copy link
Member Author

Here's an example that demonstrates the regression:

platforms/winit/examples/text_run.rs

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

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

const WINDOW_TITLE: &str = "Multiline Text Test";

const WINDOW_ID: NodeId = NodeId(0);
const LABEL_ID: NodeId = NodeId(1);
const TEXT_RUN_1_ID: NodeId = NodeId(2);
const TEXT_RUN_2_ID: NodeId = NodeId(3);
const TEXT_RUN_3_ID: NodeId = NodeId(4);

fn build_label() -> Node {
    let mut node = Node::new(Role::Label);
    node.set_bounds(Rect {
        x0: 10.0,
        y0: 10.0,
        x1: 400.0,
        y1: 150.0,
    });
    node.set_children(vec![TEXT_RUN_1_ID, TEXT_RUN_2_ID, TEXT_RUN_3_ID]);
    node.add_action(Action::Focus);
    node.set_text_direction(TextDirection::LeftToRight);
    node
}

fn build_text_run_1() -> Node {
    let mut node = Node::new(Role::TextRun);
    node.set_bounds(Rect {
        x0: 10.0,
        y0: 10.0,
        x1: 390.0,
        y1: 30.0,
    });
    node.set_value("This paragraph is long enough to wrap ");
    node.set_character_lengths([
        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
        1, 1, 1, 1, 1, 1, 1, 1,
    ]);
    node.set_character_positions([
        10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0, 110.0, 120.0, 130.0, 140.0,
        150.0, 160.0, 170.0, 180.0, 190.0, 200.0, 210.0, 220.0, 230.0, 240.0, 250.0, 260.0, 270.0,
        280.0, 290.0, 300.0, 310.0, 320.0, 330.0, 340.0, 350.0, 360.0, 370.0, 380.0,
    ]);
    node.set_character_widths([
        10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0,
        10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0,
        10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0,
    ]);
    node.set_word_starts([5, 15, 18, 23, 30, 33]);
    node
}

fn build_text_run_2() -> Node {
    let mut node = Node::new(Role::TextRun);
    node.set_bounds(Rect {
        x0: 10.0,
        y0: 30.0,
        x1: 180.0,
        y1: 50.0,
    });
    node.set_value("to another line.\n");
    node.set_character_lengths([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]);
    node.set_character_positions([
        10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0, 110.0, 120.0, 130.0, 140.0,
        150.0, 160.0, 170.0,
    ]);
    node.set_character_widths([
        10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0,
        10.0, 0.0,
    ]);
    node.set_word_starts([0, 3, 11]);
    node
}

fn build_text_run_3() -> Node {
    let mut node = Node::new(Role::TextRun);
    node.set_bounds(Rect {
        x0: 10.0,
        y0: 50.0,
        x1: 190.0,
        y1: 70.0,
    });
    node.set_value("Second paragraph.\n");
    node.set_character_lengths([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]);
    node.set_character_positions([
        10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0, 110.0, 120.0, 130.0, 140.0,
        150.0, 160.0, 170.0, 180.0,
    ]);
    node.set_character_widths([
        10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0,
        10.0, 10.0, 0.0,
    ]);
    node.set_word_starts([7]);
    node
}

struct UiState {
    focus: NodeId,
}

impl UiState {
    fn new() -> Self {
        Self { focus: LABEL_ID }
    }

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

    fn build_initial_tree(&self) -> TreeUpdate {
        let root = self.build_root();
        let label = build_label();
        let text_run_1 = build_text_run_1();
        let text_run_2 = build_text_run_2();
        let text_run_3 = build_text_run_3();

        TreeUpdate {
            nodes: vec![
                (WINDOW_ID, root),
                (LABEL_ID, label),
                (TEXT_RUN_1_ID, text_run_1),
                (TEXT_RUN_2_ID, text_run_2),
                (TEXT_RUN_3_ID, text_run_3),
            ],
            tree: Some(Tree::new(WINDOW_ID)),
            tree_id: TreeId::ROOT,
            focus: self.focus,
        }
    }
}

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;

        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);
            }
            _ => (),
        }
    }

    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,
                data,
                ..
            }) => {
                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>> {
    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 828979e into main Jan 18, 2026
16 checks passed
@mwcampbell mwcampbell deleted the text-boundaries branch January 18, 2026 19:30
@DataTriny DataTriny mentioned this pull request Jan 18, 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