-
Notifications
You must be signed in to change notification settings - Fork 619
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Initial AccessKit support #2865
Conversation
0a7c6f0
to
59dfa0c
Compare
internal/backends/winit/Cargo.toml
Outdated
accesskit = { version = "0.11.0" } | ||
accesskit_winit = { version = "0.14.0" } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we make it an optional feature?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my opinion it should not be optional. Afaics it comes at no runtime cost until it is used. It does come at a compile time cost though. But by the time an end-user discovers that the application is not accessible it is IMO too late.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another option: we could add it do the default features, so it's possible to opt out of it. How about that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd have this as a default feature: That way users can opt out when needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because of how heavy accessibility support is on Linux, a problem for which I haven't yet found a good solution, having a way to opt out is acceptable to me. I'm glad it's enabled by default.
Looks like the CI tests the merge :(. That means I have to rebase this - sorry, that makes incremental review harder. |
f69cac2
to
d4f61f9
Compare
I'm not sure I understand the implications of this. It's important that the same UI element (button, checkbox, list item, etc.) have the same node ID across tree updates, regardless of what elements may have been added or removed. I'm sorry if that wasn't clear already. Does your current solution mean that the same element might have different node IDs at different times? |
For simple state changes (checkbox ticked or unticked) remains the same. But when say an item is added to a list view then we end up re-creating the entire tree. What's the implication of replacing the entire tree and introducing new IDs? Or differently asked: How can I test the implication? :) |
When you add or remove items in a list view, I think it's not so bad for other items in the list view to have different node IDs. But if it means that items outside the list view get different node IDs as a side effect, then depending on the details of the platform accessibility API and the assistive technology being used, the result could be disorienting or at least distracting for the user. For example, if some other widget is focused when you add or remove items in a list, then the assistive technology (e.g. screen reader) will think that the focus changed when it actually didn't, and the user will get an unnecessary re-announcement of focus. I honestly don't know if there are worse side effects in practice; I just know it's important for widgets to have stable node IDs. |
The worst effect of changing an existing item's node ID that I can now think of is this: If a screen reader's cursor is on an item, but the keyboard focus is not, and the item's node ID changes, then the screen reader loses its place in the UI. |
Yeah, I can imagine that if the focus is erratically changed that would be bad. Suppose there's an "Add" button that adds items to a list and that it's okay for the user to press this button many times. So when the button is focused, and then pressed, an item is added to the list, but the button should remain focused. What would happen right now with Slint is that when the button is pressed, the assistive tech would announce an unnecessary change of focus to a button that happens to be in the same location, with the same text, and the same (expected) state). I tried this on the todo example and it "works". It isn't ideal, but it seems not too bad either. |
Yes, I agree that re-announcing focus is an acceptable bug, at least for now. |
internal/backends/winit/accesskit.rs
Outdated
/// The AccessKit adapter tries to keep the given window adapter's item tree in sync with accesskit's node tree. | ||
/// | ||
/// The entire item tree is mapped to accesskit's node tree. Any changes to an individual accessible item results | ||
/// in an access kit tree update with just changed nodes. Any changes in the tree structure result in a complete | ||
/// tree rebuild. This could be implemented more efficiently, but it requires encoding the item index, raw vrc pointer, | ||
/// and tree generation into the nodeid. | ||
/// | ||
/// For unix it's necessary to inform accesskit about any changes to the position or size of the window, hence | ||
/// the `on_event` function that needs calling. | ||
/// | ||
/// Similarly, when the window adapter is informed about a focus change, handle_focus_change must be called. | ||
/// Finally, when a component is destroyed, `unregister_component` must be called, which rebuilds the entire | ||
/// tree at the moment. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice documentation! 👍
Unfortunately, focus handling is broken.
The I'm sorry I apparently didn't document this clearly enough. This is all implemented correctly in the example for the AccessKit winit adapter. |
My bad. You’re right about the focus handling. I meant to implement it the way you describe. Will do :) |
With the latest commit, I get the following panic when running the todo example on Windows with a screen reader active:
I think you might have just uncovered a bug in AccessKit. |
What I've figured out so far is that the panic is occurring because, in a single tree update, a node is changed, then removed. Clearly AccessKit should be able to handle that without panicking. What I don't know yet is if the removal of the node is also a bug in AccessKit. |
While I believe I've found a couple of bugs in AccessKit, I also noticed some behavior in slint which seems problematic. I see that it's possible for |
Ouch, indeed, our lazy creation of components bites us here. That would explain the problem you're seeing (and odd that I didn't see it with VoiceOver). I'll add a guard for that. |
Or what if |
That's also a good idea. Initially I hadn't done that because the NodeId contained the tree generation and the moment we know the tree structure changed, we must bump the generation to invalidate any node id that we might see for example due to an incoming action request. But now that the node id encoding is simpler, what you propose makes indeed more sense :) |
Well, the node ID still contains the tree generation. Did you intend to remove that? |
But even with the node ID containing the tree generation as it does now, I don't see why you need to invalidate all node IDs immediately, before the timer runs. Consider this scenario:
So as far as I can tell, the necessary invalidation is done anyway, without requiring |
I pushed a fix that implements this now. We'll still end up doing a couple of unnecessary tree rebuilds on startup. But maybe that's not too bad. The todo example is pretty bad in terms of keyboard navigation - the popup on close has no keyboard focus and there's also no correct initial focus. I'll try to fix that separate from this PR. |
Yep, your analysis is entirely correct :) |
No, I think it's still needed to ensure that the index in all_node that's also in NodeId is valid. Of course we could just do an out-of-bounds check when accessing it, but I'd rather not use it at all instead of performing an action on the wrong item (for example). |
OK, that last commit did fix the problem I was seeing on Windows. But now I'm seeing something else that concerns me: The todo example seems to be triggering full-tree rebuilds much more often than it does. For example, toggling the checkbox of one of the todo-list items increments the tree generation, even though, as far as I can tell, doing this with the "hide done items" checkbox unchecked doesn't cause any items to be added or removed. More strangely, even just pressing Alt+F4 (which should close the window, but doesn't) causes the tree generation to be incremented. |
That makes sense. In that case, we just need to make sure that the tree generation isn't incremented more often than is necessary. |
Yeah, any conditional element for example would cause this. For example a checkbox may look like this:
And so toggling that state will trigger register_component and unregister_component. |
Invalidating all node IDs this often, when merely toggling a checkbox or even doing something that doesn't make any meaningful change to the UI at all (like my Alt+F4 example), is unacceptable. I'll see if I can come up with a way to give each item a node ID that is guaranteed to be unique and stable for the life of that item. |
Perhaps the simplest was would be to use a slab instead of a vector for the nodes. Then, when register_component as well as unregister_component is called, we trigger a timer-driven update. In that update, we first go through all existing nodes we reported earlier, try to upgrade the ItemWeak and if it's None, we delete the entry from the slab and must remember include the parent node in the TreeUpdate with update children. Then we walk Slint's item tree and we need to determine which items are new, so that we can create new nodes for them and update the parent to include them. Finding new items could perhaps be done on an per-parent basis: We look at the children (accessible descendants) and for each we try to see if it's a known one by looking for it in the existing |
That solution, using a slab, sounds good to me. I'll be happy to review it when it's ready. |
Actually I have an idea that I think will be simpler. Please give me some time to implement it and see if it works. I just accepted the CLA, so I can provide a patch. |
On the issue of stable node IDs, please look at my branch. Basically, a node ID now consists of a component ID and the item index within the component. Component IDs are monotonically increasing. There are two hash maps, one from component pointers to component IDs, and another from component IDs to component weak references. A component ID is lazily assigned when a node is created for an item under that component. When a component is unregistered, its hash map entries (if any) are removed.
|
Nice idea! Would you prefer creating a PR for this branch or shall I merge this PR and then you create a PR for the master branch? |
OK, now this looks good to me, bearing in mind that it's only the beginning of accessibility support. |
Wait, I found a bug: trying to handle an action request panics on Windows, because the AccessKit Windows adapter (usually) sends action requests from a non-main thread. To reproduce, run an example with the Narrator screen reader on Windows, and use Caps Lock+Left Arrow or Caps Lock+Right Arrow to move the Narrator cursor between items. By default, Narrator will sync the system focus with the Narrator cursor, so as soon as you reach a focusable item, you'll get the panic. |
Good catch! I was mistakenly assuming that a send wrapper can always be cloned, even if the thread it wasn't created in. I'll push a fix (once I found one ;) |
Alright, that works for me now. I noticed one interesting bug with Narrator: When I move the slint example window, the narrator's blue focus remains at the original position. It's almost as if something like set_root_window_bounds is missing for the Windows adapter. |
It's common for the Narrator highlight cursor to get out of sync when a window is moved; it doesn't only happen with AccessKit. I wouldn't worry about that. |
Ah that’s good to know :). Alright, I’ll go ahead the merge this then (will squash some of my fix up commits). |
Yup, I don't see any more critical, i.e. fundamental, issues. Of course, there's more work to do to implement complete accessibility support, but this is a good start. |
This change adds initial accessibility support for the winit backend through use of AccessKit.
The action requests on Windows typically come in from a non-main thread, so we'd try to clone the send wrapper in `do_action` in a non-main thread. That panics unfortunately - send-wrapper requires the clone to be done in the original thread. To work around this, wrap the send-wrapper in an Arc, which we can safely clone. Then inside the closure invoked from within the main thread, we can clone the send wrapper safely and then take out the Weak from it (safely as well).
4d2565e
to
77c8240
Compare
|
For the record of this, @mwcampbell signed the CLA version 1.1 before the change to CLA 2.0 today. So the bot's message here is safe to ignore. |
This change adds initial accessibility support for the winit backend
through use of AccessKit.
cc #32