Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

Diagram showing subtree grafting between the minimal tree for a WebView, containing just a ScrollView node and graft node, and the tree for a pipeline for a document named webpage.html

#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:

  1. The embedder application calls set_accessibility_active(true) on the WebView.
    • This causes the WebView to randomly generate and store a TreeId for itself, which will remain consistent for the life of the WebView or until set_accessibility_active(false) is called.
  2. The WebView sends a EmbedderToConstellationMessage::SetAccessibilityActive() message to notify the constellation that accessibility should be activated for its top-level pipeline.
  3. The constellation sends a ScriptThreadMessage::SetAccessibilityActive() message to notify the script thread that accessibility should be activated for the specified pipeline.
  4. The script thread calls set_accessibility_active() on the pipeline’s LayoutThread.
  5. On the next reflow, the LayoutThread generates an initial TreeUpdate for its accessibility tree, and sends an EmbedderMsg::AccessibilityTreeUpdate() message with the tree update.
  6. The WebView retrieves the TreeUpdate’s tree_id and stores it in its grafted_accesskit_tree_id field. It then generates a TreeUpdate representing its own minimal tree with the graft node’s tree_id set to the grafted_accesskit_tree_id, and passes this TreeUpdate to its WebViewDelegate’s notify_accessibility_tree_update() method.
  7. Once the graft node has been updated, the WebView can then call notify_accessibility_tree_update() again to forward the web contents’ TreeUpdate built in step 5.

Important

The WebView must send the TreeUpdate updating its graft node before it forwards the first TreeUpdate from 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 TreeUpdate can 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, the TreeUpdate should be ignored.

Important

If the TreeUpdate with a stale epoch was forwarded to the embedder, it could cause a panic, as it would contain a TreeId which is no longer grafted in the WebView’s tree.

Diagram showing activating accessibility and navigating to a new URL, illustrating the previous two sections.

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.