WebView accessibility internals
As described in the Servo accessibility for embedders section, the Servo accessibility system is exposed to embedders on a per-WebView basis.
The WebView has a minimal tree of its own, which essentially exists to provide a graft node for its top-level Pipeline’s accessibility tree.
The tree for the pipeline is generated based on its document’s structure by layout::AccessibilityTree.
This tree is created when accessibility is activated for the pipeline, and updated each time the document is reflowed.
Any changes to the tree are captured in a TreeUpdate, which is sent to the embedder.
Note
We currently don’t support accessibility trees in IFrames.
WebView subtree
Each WebView has a minimal tree consisting of a ScrollView and a graft node for the top-level pipeline (i.e. the top-level document).
#43029 first introduced the minimal tree for WebViews.
A TreeUpdate with an updated graft node is emitted when accessibility is activated for the WebView, and when the WebView navigates to another top-level Document, causing its top-level Pipeline to change.
#43556 first introduced the graft node between the WebView and the document.
Accessibility activation
When an embedder calls set_accessibility_active(true) on a WebView, the WebView assumes responsibility for ensuring that until accessibility is deactivated or the WebView is destroyed, the embedder will receive TreeUpdates representing the WebView and its current contents at any given time.
In order to do this, it needs to activate accessibility in its top-level Pipeline both immediately, and whenever the top-level Pipeline changes.
It also de-activates accessibility in any inactive pipelines (i.e. any pipelines which don’t correspond to a Document currently being shown, but which are retained by the back/forward cache).
The basic initial flow is:
- The embedder application calls
set_accessibility_active(true)on theWebView.- This causes the
WebViewto randomly generate and store aTreeIdfor itself, which will remain consistent for the life of theWebViewor untilset_accessibility_active(false)is called.
- This causes the
- The
WebViewsends aEmbedderToConstellationMessage::SetAccessibilityActive()message to notify the constellation that accessibility should be activated for its top-level pipeline. - The constellation sends a
ScriptThreadMessage::SetAccessibilityActive()message to notify the script thread that accessibility should be activated for the specified pipeline. - The script thread calls
set_accessibility_active()on the pipeline’sLayoutThread. - On the next reflow, the
LayoutThreadgenerates an initialTreeUpdatefor its accessibility tree, and sends anEmbedderMsg::AccessibilityTreeUpdate()message with the tree update. - The
WebViewretrieves theTreeUpdate’stree_idand stores it in itsgrafted_accesskit_tree_idfield. It then generates aTreeUpdaterepresenting its own minimal tree with the graft node’stree_idset to thegrafted_accesskit_tree_id, and passes thisTreeUpdateto itsWebViewDelegate’snotify_accessibility_tree_update()method. - Once the graft node has been updated, the
WebViewcan then callnotify_accessibility_tree_update()again to forward the web contents’TreeUpdatebuilt in step 5.
Important
The
WebViewmust send theTreeUpdateupdating its graft node before it forwards the firstTreeUpdatefrom the pipeline in order to avoid a panic, due to the ordering requirements for subtree grafting.
After accessibility has been activated on the pipeline, it will continue to send TreeUpdates to the WebView.
After the first, there’s no need to send a separate TreeUpdate for the WebView’s tree; the TreeUpdates from the pipeline can be passed directly to notify_accessibility_tree_update().
Handling navigations: grafted tree epoch
When there is a navigation, such as when a user enters a new URL in the address bar, clicks a link, or uses the Back or Forward buttons, the WebView’s top-level pipeline changes.
This means that it needs to:
- de-activate accessibility in the old top-level pipeline,
- activate accessibility in the new top-level pipeline,
- graft the tree for the new pipeline in place of the tree for the old pipeline,
- begin forwarding the tree updates from the new pipeline, and
- ignore any further tree updates from the old pipeline.
We manage this by tracking an Epoch which is incremented every time the top-level pipeline changes.
This epoch is passed from the Constellation, where the top-level pipeline is set, to ScriptThreadMessage::SetAccessibilityActive() along with the PipelineId.
It’s then sent back to the WebView in EmbedderMsg::AccessibilityTreeUpdate(), so that the WebView can check this against its existing grafted_accesskit_tree_epoch.
- If the epoch hasn’t changed, the
TreeUpdatecan simply be forwarded to the embedder. - If the epoch is greater than the existing
grafted_accesskit_tree_epoch, that indicates that a new tree needs to be grafted, and the epoch value needs to be updated. - If the epoch is less than the existing
grafted_accesskit_tree_epoch, theTreeUpdateshould be ignored.
Important
If the
TreeUpdatewith a stale epoch was forwarded to the embedder, it could cause a panic, as it would contain aTreeIdwhich is no longer grafted in theWebView’s tree.
Note
Only the current document needs to be in the platform’s accessibility tree. Updating the
WebView’s graft node to graft in the tree for the current document will (correctly) cause the previously active document to be removed from the platform’s accessibility tree. If and when the user navigates back to a previously-viewed document, the graft node will be updated again to graft in the accessibility tree for that document.However, deactivating accessibility in inactive pipelines means we destroy all of the accessibility tree information cached in layout for that pipeline. This means that even if the user navigates in the session history, we will always need to re-compute all accessibility data whenever a navigation occurs. Issue #46471 proposes retaining accessibility data instead.
The pipeline epoch was introduced in #42388.
Deterministic TreeId generation for Pipelines
We have a deterministic mapping from a PipelineId to accesskit::TreeId, implemented using the Uuid::new_v5() method, using a static namespace value combined with the pipeline ID.
This was implemented to allow immediately sending a TreeUpdate updating the WebView’s graft node without needing to wait for a message to return from the pipeline.
However, now we update the graft node immediately after receiving the first TreeUpdate from the new pipeline, which also includes the tree id.
We expect it will be still useful when implementing IFrame support: we would only need to have the pipeline ID for the embedded frame to turn the <iframe> element into a graft node.
See #43012 for more discussion of the design.