mirror of
https://github.com/servo/servo
synced 2026-04-25 17:15:48 +02:00
layout: Add a basic accessibility tree implementation for web contents (#42338)
This change introduces the `accessibility_tree` module, containing code to build an in-memory representation of a very basic accessibility tree for web contents. Currently, the tree for a given document contains: - a `RootWebArea` which has the document root node as its sole child, - an `Unknown` node for the root DOM node, - a `GenericContainer` node for each DOM element, and - a `TextRun` node for each text node. This allows us to make basic assertions about the tree contents in the `accessibility` test by doing a tree walk to find text nodes and checking their contents. Right now, the tree is rebuilt from scratch when accessibility is enabled and when a navigation occurs (via `Constellation::set_frame_tree_for_webview()` sending `ScriptThreadMessage::SetAccessibilityActive`); it's not responsive to changes in the page. This change also changes the way we handle updating the graft node between the webview's accessibility tree and its top level pipeline's accessibility tree. Previously, `Constellation::set_frame_tree_for_webview()` would send a `ConstellationToEmbedderMsg::DocumentAccessibilityTreeIdChange` method informing the webview of the accesskit TreeId of the top-level pipeline. However, this resulted in flaky timing as we couldn't depend on that message being handled before the message containing the TreeUpdate from the WebContents, which would lead to a panic as the new TreeId wasn't grafted into the combined tree yet. This change introduces an epoch value which flows from the ConstellationWebview, where it's updated every time the `active_top_level_pipeline_id` changes, to the layout accessibility tree, and finally to the webview with each TreeUpdate. Whenever a TreeUpdate arrives at the webview which has a newer epoch than the last known epoch, the webview-to-contents graft node is updated before the TreeUpdate is forwarded. If a TreeUpdate arrives at the webview with an epoch _older_ than the last known epoch, it's dropped, as it must be for a no-longer-active pipeline. Fixes: Part of #4344 --------- Signed-off-by: delan azabani <dazabani@igalia.com> Signed-off-by: Alice Boxhall <alice@igalia.com> Co-authored-by: delan azabani <dazabani@igalia.com> Co-authored-by: Luke Warlow <lwarlow@igalia.com>
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -8056,6 +8056,7 @@ dependencies = [
|
||||
name = "servo-layout"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"app_units",
|
||||
"atomic_refcell",
|
||||
"bitflags 2.11.0",
|
||||
@@ -8074,6 +8075,7 @@ dependencies = [
|
||||
"rayon",
|
||||
"rustc-hash 2.1.2",
|
||||
"selectors",
|
||||
"serde",
|
||||
"servo-base",
|
||||
"servo-config",
|
||||
"servo-embedder-traits",
|
||||
|
||||
@@ -1164,7 +1164,7 @@ where
|
||||
inherited_secure_context: Option<bool>,
|
||||
throttled: bool,
|
||||
) {
|
||||
debug!("{}: Creating new browsing context", browsing_context_id);
|
||||
debug!("{browsing_context_id}: Creating new browsing context");
|
||||
let bc_group_id = match self
|
||||
.browsing_context_group_set
|
||||
.iter_mut()
|
||||
@@ -3160,23 +3160,19 @@ where
|
||||
};
|
||||
|
||||
webview.accessibility_active = active;
|
||||
self.constellation_to_embedder_proxy.send(
|
||||
ConstellationToEmbedderMsg::DocumentAccessibilityTreeIdChanged(
|
||||
webview_id,
|
||||
webview.active_top_level_pipeline_id.into(),
|
||||
),
|
||||
let Some(pipeline_id) = webview.active_top_level_pipeline_id else {
|
||||
return;
|
||||
};
|
||||
let epoch = webview.active_top_level_pipeline_epoch;
|
||||
// Forward the activation to the webview’s active top-level pipeline, if any. For inactive
|
||||
// pipelines (documents in bfcache), we only need to forward the activation if and when they
|
||||
// become active (see set_frame_tree_for_webview()).
|
||||
// There are two sites like this; this is the a11y activation site.
|
||||
self.send_message_to_pipeline(
|
||||
pipeline_id,
|
||||
ScriptThreadMessage::SetAccessibilityActive(pipeline_id, active, epoch),
|
||||
"Set accessibility active after closure",
|
||||
);
|
||||
|
||||
// Forward the activation to the webview’s active pipelines (of those that represent
|
||||
// documents). For inactive pipelines (documents in bfcache), we only need to forward the
|
||||
// activation if and when they become active (see set_frame_tree_for_webview()).
|
||||
for pipeline_id in self.active_pipelines_for_webview(webview_id) {
|
||||
self.send_message_to_pipeline(
|
||||
pipeline_id,
|
||||
ScriptThreadMessage::SetAccessibilityActive(pipeline_id, active),
|
||||
"Set accessibility active after closure",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn forward_input_event(
|
||||
@@ -3253,13 +3249,7 @@ where
|
||||
// its focused browsing context to be itself.
|
||||
self.webviews.insert(
|
||||
webview_id,
|
||||
ConstellationWebView::new(
|
||||
&self.constellation_to_embedder_proxy,
|
||||
webview_id,
|
||||
pipeline_id,
|
||||
browsing_context_id,
|
||||
user_content_manager_id,
|
||||
),
|
||||
ConstellationWebView::new(webview_id, browsing_context_id, user_content_manager_id),
|
||||
);
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#creating-a-new-browsing-context-group
|
||||
@@ -3649,9 +3639,7 @@ where
|
||||
self.webviews.insert(
|
||||
new_webview_id,
|
||||
ConstellationWebView::new(
|
||||
&self.constellation_to_embedder_proxy,
|
||||
new_webview_id,
|
||||
new_pipeline_id,
|
||||
new_browsing_context_id,
|
||||
user_content_manager_id,
|
||||
),
|
||||
@@ -5773,23 +5761,6 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert a webview to a flat list of active pipeline ids, for activating accessibility.
|
||||
fn active_pipelines_for_webview(&self, webview_id: WebViewId) -> Vec<PipelineId> {
|
||||
let mut result = vec![];
|
||||
let mut browsing_context_ids = vec![BrowsingContextId::from(webview_id)];
|
||||
while let Some(browsing_context_id) = browsing_context_ids.pop() {
|
||||
let Some(browsing_context) = self.browsing_contexts.get(&browsing_context_id) else {
|
||||
continue;
|
||||
};
|
||||
let Some(pipeline) = self.pipelines.get(&browsing_context.pipeline_id) else {
|
||||
continue;
|
||||
};
|
||||
result.push(browsing_context.pipeline_id);
|
||||
browsing_context_ids.extend(pipeline.children.iter().copied());
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Send the frame tree for the given webview to `Paint`.
|
||||
#[servo_tracing::instrument(skip_all)]
|
||||
fn set_frame_tree_for_webview(&mut self, webview_id: WebViewId) {
|
||||
@@ -5797,35 +5768,55 @@ where
|
||||
// avoiding this panic would require a mechanism for dealing
|
||||
// with low-resource scenarios.
|
||||
let browsing_context_id = BrowsingContextId::from(webview_id);
|
||||
if let Some(frame_tree) = self.browsing_context_to_sendable(browsing_context_id) {
|
||||
if let Some(webview) = self.webviews.get_mut(&webview_id) {
|
||||
if frame_tree.pipeline.id != webview.active_top_level_pipeline_id {
|
||||
webview.active_top_level_pipeline_id = frame_tree.pipeline.id;
|
||||
self.constellation_to_embedder_proxy.send(
|
||||
ConstellationToEmbedderMsg::DocumentAccessibilityTreeIdChanged(
|
||||
webview_id,
|
||||
webview.active_top_level_pipeline_id.into(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
debug!("{}: Sending frame tree", browsing_context_id);
|
||||
self.paint_proxy
|
||||
.send(PaintMessage::SetFrameTreeForWebView(webview_id, frame_tree));
|
||||
}
|
||||
|
||||
let Some(webview) = self.webviews.get(&webview_id) else {
|
||||
let Some(frame_tree) = self.browsing_context_to_sendable(browsing_context_id) else {
|
||||
return;
|
||||
};
|
||||
let active = webview.accessibility_active;
|
||||
for pipeline_id in self.active_pipelines_for_webview(webview_id) {
|
||||
|
||||
let new_pipeline_id = frame_tree.pipeline.id;
|
||||
|
||||
debug!("{}: Sending frame tree", browsing_context_id);
|
||||
self.paint_proxy
|
||||
.send(PaintMessage::SetFrameTreeForWebView(webview_id, frame_tree));
|
||||
|
||||
let Some(webview) = self.webviews.get_mut(&webview_id) else {
|
||||
return;
|
||||
};
|
||||
if webview.active_top_level_pipeline_id == Some(new_pipeline_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
let old_pipeline_id = webview.active_top_level_pipeline_id;
|
||||
let old_epoch = webview.active_top_level_pipeline_epoch;
|
||||
let new_epoch = old_epoch.next();
|
||||
|
||||
let accessibility_active = webview.accessibility_active;
|
||||
|
||||
webview.active_top_level_pipeline_id = Some(new_pipeline_id);
|
||||
webview.active_top_level_pipeline_epoch = new_epoch;
|
||||
|
||||
// Deactivate accessibility in the now-inactive top-level document in the WebView.
|
||||
// This ensures that the document stops sending tree updates, since they will be
|
||||
// discarded in libservo anyway, and also ensures that when accessibility is
|
||||
// reactivated, the document sends the whole accessibility tree from scratch.
|
||||
if let Some(old_pipeline_id) = old_pipeline_id {
|
||||
self.send_message_to_pipeline(
|
||||
pipeline_id,
|
||||
ScriptThreadMessage::SetAccessibilityActive(pipeline_id, active),
|
||||
old_pipeline_id,
|
||||
ScriptThreadMessage::SetAccessibilityActive(old_pipeline_id, false, old_epoch),
|
||||
"Set accessibility active after closure",
|
||||
);
|
||||
}
|
||||
|
||||
// Forward activation to layout for the active top-level document in the WebView.
|
||||
// There are two sites like this; this is the navigation (or bfcache traversal) site.
|
||||
self.send_message_to_pipeline(
|
||||
new_pipeline_id,
|
||||
ScriptThreadMessage::SetAccessibilityActive(
|
||||
new_pipeline_id,
|
||||
accessibility_active,
|
||||
new_epoch,
|
||||
),
|
||||
"Set accessibility active after closure",
|
||||
);
|
||||
}
|
||||
|
||||
#[servo_tracing::instrument(skip_all)]
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use embedder_traits::user_contents::UserContentManagerId;
|
||||
use embedder_traits::{GenericEmbedderProxy, InputEvent, MouseLeftViewportEvent, Theme};
|
||||
use embedder_traits::{InputEvent, MouseLeftViewportEvent, Theme};
|
||||
use euclid::Point2D;
|
||||
use log::warn;
|
||||
use rustc_hash::FxHashMap;
|
||||
use script_traits::{ConstellationInputEvent, ScriptThreadMessage};
|
||||
use servo_base::Epoch;
|
||||
use servo_base::id::{BrowsingContextId, PipelineId, WebViewId};
|
||||
use style_traits::CSSPixel;
|
||||
|
||||
use super::embedder::ConstellationToEmbedderMsg;
|
||||
use crate::browsingcontext::BrowsingContext;
|
||||
use crate::pipeline::Pipeline;
|
||||
use crate::session_history::JointSessionHistory;
|
||||
@@ -23,7 +23,9 @@ pub(crate) struct ConstellationWebView {
|
||||
webview_id: WebViewId,
|
||||
|
||||
/// The [`PipelineId`] of the currently active pipeline at the top level of this WebView.
|
||||
pub active_top_level_pipeline_id: PipelineId,
|
||||
pub active_top_level_pipeline_id: Option<PipelineId>,
|
||||
/// A counter for changes to [`Self::active_top_level_pipeline_id`].
|
||||
pub active_top_level_pipeline_epoch: Epoch,
|
||||
|
||||
/// The currently focused browsing context in this webview for key events.
|
||||
/// The focused pipeline is the current entry of the focused browsing
|
||||
@@ -61,23 +63,15 @@ pub(crate) struct ConstellationWebView {
|
||||
|
||||
impl ConstellationWebView {
|
||||
pub(crate) fn new(
|
||||
embedder_proxy: &GenericEmbedderProxy<ConstellationToEmbedderMsg>,
|
||||
webview_id: WebViewId,
|
||||
active_top_level_pipeline_id: PipelineId,
|
||||
focused_browsing_context_id: BrowsingContextId,
|
||||
user_content_manager_id: Option<UserContentManagerId>,
|
||||
) -> Self {
|
||||
embedder_proxy.send(
|
||||
ConstellationToEmbedderMsg::DocumentAccessibilityTreeIdChanged(
|
||||
webview_id,
|
||||
active_top_level_pipeline_id.into(),
|
||||
),
|
||||
);
|
||||
|
||||
Self {
|
||||
webview_id,
|
||||
user_content_manager_id,
|
||||
active_top_level_pipeline_id,
|
||||
active_top_level_pipeline_id: None,
|
||||
active_top_level_pipeline_epoch: Epoch::default(),
|
||||
focused_browsing_context_id,
|
||||
hovered_browsing_context_id: None,
|
||||
last_mouse_move_point: Default::default(),
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use accesskit::TreeId;
|
||||
use embedder_traits::{
|
||||
InputEventOutcome, JSValue, JavaScriptEvaluationError, JavaScriptEvaluationId,
|
||||
MediaSessionEvent, NewWebViewDetails, TraversalId,
|
||||
@@ -48,7 +47,4 @@ pub enum ConstellationToEmbedderMsg {
|
||||
AllowNavigationRequest(WebViewId, PipelineId, ServoUrl),
|
||||
/// The history state has changed.
|
||||
HistoryChanged(WebViewId, Vec<ServoUrl>, usize),
|
||||
/// Notifies the embedder that the AccessKit [`TreeId`] for the top-level document in this
|
||||
/// WebView has been changed (or initially set).
|
||||
DocumentAccessibilityTreeIdChanged(WebViewId, TreeId),
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ doctest = false
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
[dependencies]
|
||||
accesskit = { workspace = true }
|
||||
app_units = { workspace = true }
|
||||
atomic_refcell = { workspace = true }
|
||||
bitflags = { workspace = true }
|
||||
@@ -46,6 +47,7 @@ rustc-hash = { workspace = true }
|
||||
script = { workspace = true }
|
||||
script_traits = { workspace = true }
|
||||
selectors = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
servo_arc = { workspace = true }
|
||||
servo-base = { workspace = true }
|
||||
servo-config = { workspace = true }
|
||||
|
||||
155
components/layout/accessibility_tree.rs
Normal file
155
components/layout/accessibility_tree.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use accesskit::Role;
|
||||
use layout_api::LayoutNode;
|
||||
use log::trace;
|
||||
use rustc_hash::FxHashMap;
|
||||
use script::layout_dom::ServoLayoutNode;
|
||||
use servo_base::Epoch;
|
||||
use style::dom::{NodeInfo, OpaqueNode};
|
||||
|
||||
struct AccessibilityUpdate {
|
||||
accesskit_update: accesskit::TreeUpdate,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct AccessibilityNode {
|
||||
id: accesskit::NodeId,
|
||||
accesskit_node: accesskit::Node,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AccessibilityTree {
|
||||
nodes: FxHashMap<accesskit::NodeId, AccessibilityNode>,
|
||||
accesskit_tree: accesskit::Tree,
|
||||
tree_id: accesskit::TreeId,
|
||||
epoch: Epoch,
|
||||
}
|
||||
|
||||
impl AccessibilityUpdate {
|
||||
fn new(tree: accesskit::Tree, tree_id: accesskit::TreeId) -> Self {
|
||||
Self {
|
||||
accesskit_update: accesskit::TreeUpdate {
|
||||
nodes: Default::default(),
|
||||
tree: Some(tree),
|
||||
focus: accesskit::NodeId(1),
|
||||
tree_id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn add(&mut self, node: &AccessibilityNode) {
|
||||
self.accesskit_update
|
||||
.nodes
|
||||
.push((node.id, node.accesskit_node.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
impl AccessibilityTree {
|
||||
const ROOT_NODE_ID: accesskit::NodeId = accesskit::NodeId(0);
|
||||
|
||||
pub(super) fn new(tree_id: accesskit::TreeId, epoch: Epoch) -> Self {
|
||||
// The root node doesn't correspond to a DOM node, but contains the root DOM node.
|
||||
let mut root_node = AccessibilityNode::new(AccessibilityTree::ROOT_NODE_ID);
|
||||
root_node
|
||||
.accesskit_node
|
||||
.set_role(accesskit::Role::RootWebArea);
|
||||
root_node
|
||||
.accesskit_node
|
||||
.add_action(accesskit::Action::Focus);
|
||||
|
||||
let mut tree = Self {
|
||||
nodes: Default::default(),
|
||||
accesskit_tree: accesskit::Tree::new(root_node.id),
|
||||
tree_id,
|
||||
epoch,
|
||||
};
|
||||
tree.nodes.insert(root_node.id, root_node);
|
||||
|
||||
tree
|
||||
}
|
||||
|
||||
pub(super) fn update_tree(
|
||||
&mut self,
|
||||
root_dom_node: &ServoLayoutNode<'_>,
|
||||
) -> Option<accesskit::TreeUpdate> {
|
||||
let mut tree_update = AccessibilityUpdate::new(self.accesskit_tree.clone(), self.tree_id);
|
||||
|
||||
let root_dom_node_id = Self::to_accesskit_id(&root_dom_node.opaque());
|
||||
let root_node = self
|
||||
.nodes
|
||||
.get_mut(&AccessibilityTree::ROOT_NODE_ID)
|
||||
.unwrap();
|
||||
root_node
|
||||
.accesskit_node
|
||||
.set_children(vec![root_dom_node_id]);
|
||||
|
||||
tree_update.add(root_node);
|
||||
|
||||
self.update_node_and_children(root_dom_node, &mut tree_update);
|
||||
Some(tree_update.accesskit_update)
|
||||
}
|
||||
|
||||
fn update_node_and_children(
|
||||
&mut self,
|
||||
dom_node: &ServoLayoutNode<'_>,
|
||||
tree_update: &mut AccessibilityUpdate,
|
||||
) {
|
||||
// TODO: read accessibility damage from dom_node (right now, assume damage is complete)
|
||||
|
||||
let node = self.get_or_create_node_mut(dom_node);
|
||||
let accesskit_node = &mut node.accesskit_node;
|
||||
|
||||
let mut new_children: Vec<accesskit::NodeId> = vec![];
|
||||
for dom_child in dom_node.flat_tree_children() {
|
||||
let child_id = Self::to_accesskit_id(&dom_child.opaque());
|
||||
new_children.push(child_id);
|
||||
}
|
||||
if new_children != accesskit_node.children() {
|
||||
accesskit_node.set_children(new_children);
|
||||
}
|
||||
|
||||
if dom_node.is_text_node() {
|
||||
accesskit_node.set_role(Role::TextRun);
|
||||
let text_content = dom_node.text_content();
|
||||
trace!("node text content = {text_content:?}");
|
||||
// FIXME: this should take into account editing selection units (grapheme clusters?)
|
||||
accesskit_node.set_value(&*text_content);
|
||||
} else if dom_node.as_element().is_some() {
|
||||
accesskit_node.set_role(Role::GenericContainer);
|
||||
}
|
||||
|
||||
tree_update.add(node);
|
||||
|
||||
for dom_child in dom_node.flat_tree_children() {
|
||||
self.update_node_and_children(&dom_child, tree_update);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_or_create_node_mut(&mut self, dom_node: &ServoLayoutNode<'_>) -> &mut AccessibilityNode {
|
||||
let id = Self::to_accesskit_id(&dom_node.opaque());
|
||||
|
||||
self.nodes
|
||||
.entry(id)
|
||||
.or_insert_with(|| AccessibilityNode::new(id))
|
||||
}
|
||||
|
||||
fn to_accesskit_id(opaque: &OpaqueNode) -> accesskit::NodeId {
|
||||
accesskit::NodeId(opaque.0 as u64)
|
||||
}
|
||||
|
||||
pub(crate) fn epoch(&self) -> Epoch {
|
||||
self.epoch
|
||||
}
|
||||
}
|
||||
|
||||
impl AccessibilityNode {
|
||||
fn new(id: accesskit::NodeId) -> Self {
|
||||
Self {
|
||||
id,
|
||||
accesskit_node: accesskit::Node::new(Role::Unknown),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ use script::layout_dom::{
|
||||
};
|
||||
use script_traits::{DrawAPaintImageResult, PaintWorkletError, Painter, ScriptThreadMessage};
|
||||
use servo_arc::Arc as ServoArc;
|
||||
use servo_base::Epoch;
|
||||
use servo_base::generic_channel::GenericSender;
|
||||
use servo_base::id::{PipelineId, WebViewId};
|
||||
use servo_config::opts::{self, DiagnosticsLogging};
|
||||
@@ -80,6 +81,7 @@ use url::Url;
|
||||
use webrender_api::ExternalScrollId;
|
||||
use webrender_api::units::{DevicePixel, LayoutVector2D};
|
||||
|
||||
use crate::accessibility_tree::AccessibilityTree;
|
||||
use crate::context::{CachedImageOrError, ImageResolver, LayoutContext};
|
||||
use crate::display_list::{DisplayListBuilder, HitTest, PaintTimingHandler, StackingContextTree};
|
||||
use crate::query::{
|
||||
@@ -205,9 +207,12 @@ pub struct LayoutThread {
|
||||
/// Handler for all Paint Timings
|
||||
paint_timing_handler: RefCell<Option<PaintTimingHandler>>,
|
||||
|
||||
/// Whether accessibility is active in this layout.
|
||||
/// (Note: this is a temporary field which will be replaced with an optional accessibility tree member.)
|
||||
accessibility_active: Cell<bool>,
|
||||
/// Layout's internal representation of its accessibility tree.
|
||||
/// This is `None` if accessibility is not active.
|
||||
accessibility_tree: RefCell<Option<AccessibilityTree>>,
|
||||
|
||||
/// See [Layout::needs_accessibility_update()].
|
||||
needs_accessibility_update: Cell<bool>,
|
||||
}
|
||||
|
||||
pub struct LayoutFactoryImpl();
|
||||
@@ -669,12 +674,25 @@ impl Layout for LayoutThread {
|
||||
&mut self.stylist
|
||||
}
|
||||
|
||||
fn set_accessibility_active(&self, active: bool) {
|
||||
if !(pref!(accessibility_enabled)) {
|
||||
fn set_accessibility_active(&self, active: bool, epoch: Epoch) {
|
||||
if !active {
|
||||
self.accessibility_tree.replace(None);
|
||||
return;
|
||||
}
|
||||
self.set_needs_accessibility_update();
|
||||
let mut accessibility_tree = self.accessibility_tree.borrow_mut();
|
||||
if accessibility_tree.is_some() {
|
||||
return;
|
||||
}
|
||||
*accessibility_tree = Some(AccessibilityTree::new(self.id.into(), epoch));
|
||||
}
|
||||
|
||||
self.accessibility_active.replace(active);
|
||||
fn needs_accessibility_update(&self) -> bool {
|
||||
self.needs_accessibility_update.get()
|
||||
}
|
||||
|
||||
fn set_needs_accessibility_update(&self) {
|
||||
self.needs_accessibility_update.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -731,7 +749,8 @@ impl LayoutThread {
|
||||
previously_highlighted_dom_node: Cell::new(None),
|
||||
paint_timing_handler: Default::default(),
|
||||
user_stylesheets: config.user_stylesheets,
|
||||
accessibility_active: Cell::new(false),
|
||||
accessibility_tree: Default::default(),
|
||||
needs_accessibility_update: Cell::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -799,6 +818,10 @@ impl LayoutThread {
|
||||
if self.fragment_tree.borrow().is_none() {
|
||||
return false;
|
||||
}
|
||||
// If accessibility was just activated, we need reflow to build the accessibility tree.
|
||||
if self.needs_accessibility_update() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we have a fragment tree and it's up-to-date and this reflow
|
||||
// doesn't need more reflow results, we can skip the rest of layout.
|
||||
@@ -851,6 +874,33 @@ impl LayoutThread {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_accessibility_tree_update(&self, root_element: &ServoLayoutNode) -> bool {
|
||||
if !self.needs_accessibility_update() {
|
||||
return false;
|
||||
}
|
||||
let mut accessibility_tree = self.accessibility_tree.borrow_mut();
|
||||
let Some(accessibility_tree) = accessibility_tree.as_mut() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let accessibility_tree = &mut *accessibility_tree;
|
||||
if let Some(tree_update) = accessibility_tree.update_tree(root_element) {
|
||||
// TODO(#4344): send directly to embedder over the pipeline_to_embedder_sender cloned from ScriptThread.
|
||||
// FIXME: Handle send error. Could have a method on accessibility tree to
|
||||
// finalise after sending, removing accessibility damage? On fail, retain damage
|
||||
// for next reflow, as well as retaining document.needs_accessibility_update.
|
||||
let _ = self
|
||||
.script_chan
|
||||
.send(ScriptThreadMessage::AccessibilityTreeUpdate(
|
||||
self.webview_id,
|
||||
tree_update,
|
||||
accessibility_tree.epoch(),
|
||||
));
|
||||
}
|
||||
self.needs_accessibility_update.set(false);
|
||||
true
|
||||
}
|
||||
|
||||
/// The high-level routine that performs layout.
|
||||
#[servo_tracing::instrument(skip_all)]
|
||||
fn handle_reflow(&mut self, mut reflow_request: ReflowRequest) -> Option<ReflowResult> {
|
||||
@@ -905,6 +955,9 @@ impl LayoutThread {
|
||||
if self.handle_update_scroll_node_request(&reflow_request) {
|
||||
reflow_phases_run.insert(ReflowPhasesRun::UpdatedScrollNodeOffset);
|
||||
}
|
||||
if self.handle_accessibility_tree_update(&root_element.as_node()) {
|
||||
reflow_phases_run.insert(ReflowPhasesRun::UpdatedAccessibilityTree);
|
||||
}
|
||||
|
||||
let pending_images = std::mem::take(&mut *image_resolver.pending_images.lock());
|
||||
let pending_rasterization_images =
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//! Layout. Performs layout on the DOM, builds display lists and sends them to be
|
||||
//! painted.
|
||||
|
||||
mod accessibility_tree;
|
||||
mod cell;
|
||||
mod context;
|
||||
mod display_list;
|
||||
|
||||
@@ -2840,7 +2840,8 @@ impl Document {
|
||||
}
|
||||
if !self.window().layout_blocked() &&
|
||||
(!self.restyle_reason().is_empty() ||
|
||||
self.window().layout().needs_new_display_list())
|
||||
self.window().layout().needs_new_display_list() ||
|
||||
self.window().layout().needs_accessibility_update())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -80,12 +80,12 @@ use script_traits::{
|
||||
};
|
||||
use servo_arc::Arc as ServoArc;
|
||||
use servo_base::cross_process_instant::CrossProcessInstant;
|
||||
use servo_base::generic_channel;
|
||||
use servo_base::generic_channel::GenericSender;
|
||||
use servo_base::id::{
|
||||
BrowsingContextId, HistoryStateId, PipelineId, PipelineNamespace, ScriptEventLoopId,
|
||||
TEST_WEBVIEW_ID, WebViewId,
|
||||
};
|
||||
use servo_base::{Epoch, generic_channel};
|
||||
use servo_canvas_traits::webgl::WebGLPipeline;
|
||||
use servo_config::{opts, pref, prefs};
|
||||
use servo_constellation_traits::{
|
||||
@@ -1969,16 +1969,16 @@ impl ScriptThread {
|
||||
.borrow_mut()
|
||||
.remove(&user_content_manager_id);
|
||||
},
|
||||
ScriptThreadMessage::AccessibilityTreeUpdate(webview_id, tree_update) => {
|
||||
ScriptThreadMessage::AccessibilityTreeUpdate(webview_id, tree_update, epoch) => {
|
||||
let _ = self.senders.pipeline_to_embedder_sender.send(
|
||||
EmbedderMsg::AccessibilityTreeUpdate(webview_id, tree_update),
|
||||
EmbedderMsg::AccessibilityTreeUpdate(webview_id, tree_update, epoch),
|
||||
);
|
||||
},
|
||||
ScriptThreadMessage::UpdatePinchZoomInfos(id, pinch_zoom_infos) => {
|
||||
self.handle_update_pinch_zoom_infos(id, pinch_zoom_infos, CanGc::from_cx(cx));
|
||||
},
|
||||
ScriptThreadMessage::SetAccessibilityActive(pipeline_id, active) => {
|
||||
self.set_accessibility_active(pipeline_id, active);
|
||||
ScriptThreadMessage::SetAccessibilityActive(pipeline_id, active, epoch) => {
|
||||
self.set_accessibility_active(pipeline_id, active, epoch);
|
||||
},
|
||||
ScriptThreadMessage::TriggerGarbageCollection => unsafe {
|
||||
JS_GC(*GlobalScope::get_cx(), GCReason::API);
|
||||
@@ -3703,7 +3703,7 @@ impl ScriptThread {
|
||||
}
|
||||
|
||||
/// See the docs for [`ScriptThreadMessage::SetAccessibilityActive`].
|
||||
fn set_accessibility_active(&self, pipeline_id: PipelineId, active: bool) {
|
||||
fn set_accessibility_active(&self, pipeline_id: PipelineId, active: bool, epoch: Epoch) {
|
||||
if !(pref!(accessibility_enabled)) {
|
||||
return;
|
||||
}
|
||||
@@ -3713,7 +3713,10 @@ impl ScriptThread {
|
||||
.borrow()
|
||||
.find_document(pipeline_id)
|
||||
.expect("Got pipeline_id from self.documents");
|
||||
document.window().layout().set_accessibility_active(active);
|
||||
document
|
||||
.window()
|
||||
.layout()
|
||||
.set_accessibility_active(active, epoch);
|
||||
}
|
||||
|
||||
/// Handle a "navigate an iframe" message from the constellation.
|
||||
|
||||
@@ -669,11 +669,9 @@ impl ServoInner {
|
||||
warn!("Failed to respond to GetScreenMetrics: {error}");
|
||||
}
|
||||
},
|
||||
EmbedderMsg::AccessibilityTreeUpdate(webview_id, tree_update) => {
|
||||
EmbedderMsg::AccessibilityTreeUpdate(webview_id, tree_update, epoch) => {
|
||||
if let Some(webview) = self.get_webview_handle(webview_id) {
|
||||
webview
|
||||
.delegate()
|
||||
.notify_accessibility_tree_update(webview, tree_update);
|
||||
webview.process_accessibility_tree_update(tree_update, epoch);
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -781,11 +779,6 @@ impl ServoInner {
|
||||
.notify_media_session_event(webview, media_session_event);
|
||||
}
|
||||
},
|
||||
ConstellationToEmbedderMsg::DocumentAccessibilityTreeIdChanged(webview_id, tree_id) => {
|
||||
if let Some(webview) = self.get_webview_handle(webview_id) {
|
||||
webview.notify_document_accessibility_tree_id(tree_id);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,18 +48,159 @@ fn test_basic_accessibility_update() {
|
||||
let load_webview = webview.clone();
|
||||
servo_test.spin(move || load_webview.load_status() != LoadStatus::Complete);
|
||||
|
||||
let updates = wait_for_min_updates(&servo_test, delegate.clone(), 1);
|
||||
let updates = wait_for_min_updates(&servo_test, delegate.clone(), 2);
|
||||
let tree = build_tree(updates);
|
||||
let _ = assert_tree_structure_and_get_root_web_area(&tree);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_activate_accessibility_after_layout() {
|
||||
let servo_test = ServoTest::new_with_builder(|builder| {
|
||||
let mut preferences = Preferences::default();
|
||||
preferences.accessibility_enabled = true;
|
||||
builder.preferences(preferences)
|
||||
});
|
||||
|
||||
let delegate = Rc::new(WebViewDelegateImpl::default());
|
||||
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
|
||||
.delegate(delegate.clone())
|
||||
.url(Url::parse("data:text/html,<!DOCTYPE html>").unwrap())
|
||||
.build();
|
||||
|
||||
let load_webview = webview.clone();
|
||||
servo_test.spin(move || load_webview.load_status() != LoadStatus::Complete);
|
||||
|
||||
webview.set_accessibility_active(true);
|
||||
|
||||
let updates = wait_for_min_updates(&servo_test, delegate.clone(), 2);
|
||||
let tree = build_tree(updates);
|
||||
let _ = assert_tree_structure_and_get_root_web_area(&tree);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_navigate_creates_new_accessibility_update() {
|
||||
let servo_test = ServoTest::new_with_builder(|builder| {
|
||||
let mut preferences = Preferences::default();
|
||||
preferences.accessibility_enabled = true;
|
||||
builder.preferences(preferences)
|
||||
});
|
||||
|
||||
let page_1_url = Url::parse("data:text/html,<!DOCTYPE html> page 1").unwrap();
|
||||
let page_2_url = Url::parse("data:text/html,<!DOCTYPE html> page 2").unwrap();
|
||||
|
||||
let delegate = Rc::new(WebViewDelegateImpl::default());
|
||||
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
|
||||
.delegate(delegate.clone())
|
||||
.url(page_1_url)
|
||||
.build();
|
||||
webview.set_accessibility_active(true);
|
||||
|
||||
let load_webview = webview.clone();
|
||||
servo_test.spin(move || load_webview.load_status() != LoadStatus::Complete);
|
||||
|
||||
let updates = wait_for_min_updates(&servo_test, delegate.clone(), 2);
|
||||
let mut tree = build_tree(updates);
|
||||
|
||||
let root_web_area = assert_tree_structure_and_get_root_web_area(&tree);
|
||||
|
||||
let result = find_first_matching_node(root_web_area, |node| {
|
||||
node.role() == accesskit::Role::TextRun
|
||||
});
|
||||
let text_node = result.expect("Should be exactly one TextRun in the tree");
|
||||
|
||||
assert_eq!(text_node.value().as_deref(), Some("page 1"));
|
||||
|
||||
let load_webview = webview.clone();
|
||||
webview.load(page_2_url.clone());
|
||||
servo_test.spin(move || load_webview.url() != Some(page_2_url.clone()));
|
||||
|
||||
let new_updates = wait_for_min_updates(&servo_test, delegate.clone(), 2);
|
||||
for tree_update in new_updates {
|
||||
tree.update_and_process_changes(tree_update, &mut NoOpChangeHandler);
|
||||
}
|
||||
|
||||
let root_node = tree.state().root();
|
||||
let scroll_view = find_first_matching_node(root_node, |node| node.role() == Role::ScrollView)
|
||||
.expect("Tree should include a scroll view corresponding to the WebView.");
|
||||
let scroll_view_children = scroll_view.children().collect::<Vec<_>>();
|
||||
assert_eq!(scroll_view_children.len(), 1);
|
||||
let graft_node = scroll_view_children[0];
|
||||
assert!(graft_node.is_graft());
|
||||
let result =
|
||||
find_first_matching_node(root_node, |node| node.role() == accesskit::Role::TextRun);
|
||||
let text_node = result.expect("Should be exactly one TextRun in the tree");
|
||||
|
||||
assert_eq!(text_node.value().as_deref(), Some("page 2"));
|
||||
}
|
||||
|
||||
// FIXME(accessibility): when clicking back and forward, we currently rely on
|
||||
// layout and the accessibility tree being rebuilt from scratch, so that the full
|
||||
// a11y tree can be resent.
|
||||
// But if bfcache navigations stop redoing layout, or we implement incremental
|
||||
// a11y tree building, this test will break.
|
||||
#[test]
|
||||
fn test_accessibility_after_navigate_and_back() {
|
||||
let servo_test = ServoTest::new_with_builder(|builder| {
|
||||
let mut preferences = Preferences::default();
|
||||
preferences.accessibility_enabled = true;
|
||||
builder.preferences(preferences)
|
||||
});
|
||||
|
||||
let page_1_url = Url::parse("data:text/html,<!DOCTYPE html> page 1").unwrap();
|
||||
let page_2_url = Url::parse("data:text/html,<!DOCTYPE html> page 2").unwrap();
|
||||
|
||||
let delegate = Rc::new(WebViewDelegateImpl::default());
|
||||
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
|
||||
.delegate(delegate.clone())
|
||||
.url(page_1_url.clone())
|
||||
.build();
|
||||
webview.set_accessibility_active(true);
|
||||
|
||||
let load_webview = webview.clone();
|
||||
servo_test.spin(move || load_webview.load_status() != LoadStatus::Complete);
|
||||
|
||||
let updates = wait_for_min_updates(&servo_test, delegate.clone(), 2);
|
||||
let mut tree = build_tree(updates);
|
||||
|
||||
let root_web_area = assert_tree_structure_and_get_root_web_area(&tree);
|
||||
|
||||
let result = find_all_matching_nodes(root_web_area, |node| {
|
||||
node.role() == accesskit::Role::TextRun
|
||||
});
|
||||
assert_eq!(result.len(), 1);
|
||||
let text_node = result[0];
|
||||
|
||||
assert_eq!(text_node.value().as_deref(), Some("page 1"));
|
||||
|
||||
let load_webview = webview.clone();
|
||||
webview.load(page_2_url.clone());
|
||||
servo_test.spin(move || load_webview.url() != Some(page_2_url.clone()));
|
||||
|
||||
let new_updates = wait_for_min_updates(&servo_test, delegate.clone(), 2);
|
||||
for tree_update in new_updates {
|
||||
tree.update_and_process_changes(tree_update, &mut NoOpChangeHandler);
|
||||
}
|
||||
|
||||
let root_node = tree.state().root();
|
||||
let result = find_all_matching_nodes(root_node, |node| node.role() == accesskit::Role::TextRun);
|
||||
assert_eq!(result.len(), 1);
|
||||
let text_node = result[0];
|
||||
|
||||
assert_eq!(text_node.value().as_deref(), Some("page 2"));
|
||||
|
||||
let back_webview = webview.clone();
|
||||
webview.go_back(1);
|
||||
servo_test.spin(move || back_webview.url() != Some(page_1_url.clone()));
|
||||
|
||||
let new_updates = wait_for_min_updates(&servo_test, delegate.clone(), 2);
|
||||
for tree_update in new_updates {
|
||||
tree.update_and_process_changes(tree_update, &mut NoOpChangeHandler);
|
||||
}
|
||||
|
||||
let root_node = tree.state().root();
|
||||
let result = find_all_matching_nodes(root_node, |node| node.role() == accesskit::Role::TextRun);
|
||||
assert_eq!(result.len(), 1);
|
||||
let text_node = result[0];
|
||||
|
||||
assert_eq!(text_node.value().as_deref(), Some("page 1"));
|
||||
}
|
||||
|
||||
// TODO(accessibility): write test for resend a11y tree when clicking back or forward
|
||||
|
||||
fn wait_for_min_updates(
|
||||
servo_test: &ServoTest,
|
||||
delegate: Rc<WebViewDelegateImpl>,
|
||||
@@ -114,16 +255,51 @@ fn build_tree(tree_updates: Vec<TreeUpdate>) -> accesskit_consumer::Tree {
|
||||
tree
|
||||
}
|
||||
|
||||
fn assert_tree_structure_and_get_root_web_area<'tree>(
|
||||
tree: &'tree accesskit_consumer::Tree,
|
||||
) -> accesskit_consumer::Node<'tree> {
|
||||
let root_node = tree.state().root();
|
||||
let scroll_view = find_first_matching_node(root_node, |node| node.role() == Role::ScrollView)
|
||||
.expect("Tree should include a scroll view corresponding to the WebView.");
|
||||
let scroll_view_children: Vec<accesskit_consumer::Node<'_>> = scroll_view.children().collect();
|
||||
assert_eq!(scroll_view_children.len(), 1);
|
||||
let graft_node = scroll_view_children[0];
|
||||
assert!(graft_node.is_graft());
|
||||
|
||||
let graft_node_children: Vec<accesskit_consumer::Node<'_>> = graft_node.children().collect();
|
||||
assert_eq!(graft_node_children.len(), 1);
|
||||
|
||||
let root_web_area = graft_node_children[0];
|
||||
assert_eq!(root_web_area.role(), Role::RootWebArea);
|
||||
|
||||
root_web_area
|
||||
}
|
||||
|
||||
fn find_first_matching_node(
|
||||
root_node: accesskit_consumer::Node<'_>,
|
||||
mut pred: impl FnMut(&accesskit_consumer::Node) -> bool,
|
||||
) -> Option<accesskit_consumer::Node<'_>> {
|
||||
let mut children = root_node.children().collect::<VecDeque<_>>();
|
||||
let mut result: Option<accesskit_consumer::Node> = None;
|
||||
while let Some(candidate) = children.pop_front() {
|
||||
if pred(&candidate) {
|
||||
result = Some(candidate);
|
||||
break;
|
||||
return Some(candidate);
|
||||
}
|
||||
for child in candidate.children() {
|
||||
children.push_back(child);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn find_all_matching_nodes(
|
||||
root_node: accesskit_consumer::Node<'_>,
|
||||
mut pred: impl FnMut(&accesskit_consumer::Node) -> bool,
|
||||
) -> Vec<accesskit_consumer::Node<'_>> {
|
||||
let mut children = root_node.children().collect::<VecDeque<_>>();
|
||||
let mut result = vec![];
|
||||
while let Some(candidate) = children.pop_front() {
|
||||
if pred(&candidate) {
|
||||
result.push(candidate);
|
||||
}
|
||||
for child in candidate.children() {
|
||||
children.push_back(child);
|
||||
|
||||
@@ -19,8 +19,10 @@ use embedder_traits::{
|
||||
};
|
||||
use euclid::{Scale, Size2D};
|
||||
use image::RgbaImage;
|
||||
use log::debug;
|
||||
use paint_api::WebViewTrait;
|
||||
use paint_api::rendering_context::RenderingContext;
|
||||
use servo_base::Epoch;
|
||||
use servo_base::generic_channel::GenericSender;
|
||||
use servo_base::id::WebViewId;
|
||||
use servo_config::pref;
|
||||
@@ -106,6 +108,9 @@ pub(crate) struct WebViewInner {
|
||||
/// [`TreeId`] of the web contents of this [`WebView`]’s active top-level pipeline,
|
||||
/// which is grafted into the tree for this [`WebView`].
|
||||
pub(crate) grafted_accesskit_tree_id: Option<TreeId>,
|
||||
/// A counter for changes to the grafted accesskit tree for this webview.
|
||||
/// See [`Self::grafted_accesskit_tree_id`].
|
||||
grafted_accesskit_tree_epoch: Option<Epoch>,
|
||||
|
||||
rendering_context: Rc<dyn RenderingContext>,
|
||||
user_content_manager: Option<Rc<UserContentManager>>,
|
||||
@@ -156,6 +161,7 @@ impl WebView {
|
||||
.unwrap_or_else(|| Rc::new(DefaultGamepadDelegate)),
|
||||
accesskit_tree_id: None,
|
||||
grafted_accesskit_tree_id: None,
|
||||
grafted_accesskit_tree_epoch: None,
|
||||
hidpi_scale_factor: builder.hidpi_scale_factor,
|
||||
load_status: LoadStatus::Started,
|
||||
status_text: None,
|
||||
@@ -820,6 +826,8 @@ impl WebView {
|
||||
self.inner_mut().accesskit_tree_id = Some(accesskit_tree_id);
|
||||
} else {
|
||||
self.inner_mut().accesskit_tree_id = None;
|
||||
self.inner_mut().grafted_accesskit_tree_id = None;
|
||||
self.inner_mut().grafted_accesskit_tree_epoch = None;
|
||||
}
|
||||
|
||||
self.inner().servo.constellation_proxy().send(
|
||||
@@ -837,7 +845,8 @@ impl WebView {
|
||||
.inner_mut()
|
||||
.grafted_accesskit_tree_id
|
||||
.replace(grafted_tree_id);
|
||||
// TODO(accessibility): try to avoid duplicate notifications in the first place?
|
||||
// TODO(#4344): try to avoid duplicate notifications in the first place?
|
||||
// (see ConstellationWebView::new for more details)
|
||||
if old_grafted_tree_id == Some(grafted_tree_id) {
|
||||
return;
|
||||
}
|
||||
@@ -861,6 +870,30 @@ impl WebView {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn process_accessibility_tree_update(&self, tree_update: TreeUpdate, epoch: Epoch) {
|
||||
if self
|
||||
.inner()
|
||||
.grafted_accesskit_tree_epoch
|
||||
.is_some_and(|current| epoch < current)
|
||||
{
|
||||
// We expect this to happen occasionally when the constellation navigates, because
|
||||
// deactivating accessibility happens asynchronously, so the script thread of the
|
||||
// previously active document may continue sending updates for a short period of time.
|
||||
debug!("Ignoring stale tree update for {:?}", tree_update.tree_id);
|
||||
return;
|
||||
}
|
||||
if self
|
||||
.inner()
|
||||
.grafted_accesskit_tree_epoch
|
||||
.is_none_or(|current| epoch > current)
|
||||
{
|
||||
self.notify_document_accessibility_tree_id(tree_update.tree_id);
|
||||
self.inner_mut().grafted_accesskit_tree_epoch = Some(epoch);
|
||||
}
|
||||
self.delegate()
|
||||
.notify_accessibility_tree_update(self.clone(), tree_update);
|
||||
}
|
||||
}
|
||||
|
||||
/// A structure used to expose a view of the [`WebView`] to the Servo
|
||||
|
||||
@@ -30,6 +30,7 @@ use malloc_size_of::malloc_size_of_is_0;
|
||||
use malloc_size_of_derive::MallocSizeOf;
|
||||
use pixels::SharedRasterImage;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use servo_base::Epoch;
|
||||
use servo_base::generic_channel::{
|
||||
GenericCallback, GenericSender, GenericSharedMemory, SendResult,
|
||||
};
|
||||
@@ -508,7 +509,7 @@ pub enum EmbedderMsg {
|
||||
/// and the embedder can continue processing it, if necessary.
|
||||
InputEventsHandled(WebViewId, Vec<InputEventOutcome>),
|
||||
/// Send the embedder an accessibility tree update.
|
||||
AccessibilityTreeUpdate(WebViewId, TreeUpdate),
|
||||
AccessibilityTreeUpdate(WebViewId, TreeUpdate, Epoch),
|
||||
}
|
||||
|
||||
impl Debug for EmbedderMsg {
|
||||
|
||||
@@ -387,7 +387,24 @@ pub trait Layout {
|
||||
fn query_effective_overflow(&self, node: TrustedNodeAddress) -> Option<AxesOverflow>;
|
||||
fn stylist_mut(&mut self) -> &mut Stylist;
|
||||
|
||||
fn set_accessibility_active(&self, active: bool);
|
||||
/// Set whether the accessibility tree should be constructed for this Layout.
|
||||
/// This should be called by the embedder when accessibility is requested by the user.
|
||||
fn set_accessibility_active(&self, enabled: bool, epoch: Epoch);
|
||||
|
||||
/// Whether the accessibility tree needs updating. This is set to true when
|
||||
/// - accessibility is activated; or
|
||||
/// - a page is loaded after accesibility is activated.
|
||||
///
|
||||
/// In future, this should be set to true if DOM or style have changed in a way that
|
||||
/// impacts the accessibility tree.
|
||||
///
|
||||
/// Checked in can_skip_reflow_request_entirely(), as a dirty accessibility tree
|
||||
/// should force a reflow, and handle_reflow() to determine whether to update the
|
||||
/// accessibility tree during reflow.
|
||||
fn needs_accessibility_update(&self) -> bool;
|
||||
|
||||
/// See [Self::needs_accessibility_update()].
|
||||
fn set_needs_accessibility_update(&self);
|
||||
}
|
||||
|
||||
/// This trait is part of `layout_api` because it depends on both `script_traits`
|
||||
@@ -593,6 +610,7 @@ bitflags! {
|
||||
/// updating style or layout. This is used when updating canvas contents and
|
||||
/// progressing to a new animated image frame.
|
||||
const UpdatedImageData = 1 << 5;
|
||||
const UpdatedAccessibilityTree = 1 << 6;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ use pixels::PixelFormat;
|
||||
use profile_traits::mem;
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use servo_base::Epoch;
|
||||
use servo_base::cross_process_instant::CrossProcessInstant;
|
||||
use servo_base::generic_channel::{GenericCallback, GenericReceiver, GenericSender};
|
||||
use servo_base::id::{
|
||||
@@ -312,7 +313,7 @@ pub enum ScriptThreadMessage {
|
||||
/// `user_contents_for_manager_id` map.
|
||||
DestroyUserContentManager(UserContentManagerId),
|
||||
/// Send the embedder an accessibility tree update.
|
||||
AccessibilityTreeUpdate(WebViewId, accesskit::TreeUpdate),
|
||||
AccessibilityTreeUpdate(WebViewId, accesskit::TreeUpdate, Epoch),
|
||||
/// Update the pinch zoom details of a pipeline. Each `Window` stores a `VisualViewport` DOM
|
||||
/// instance that gets updated according to the changes from the `Compositor``.
|
||||
UpdatePinchZoomInfos(PipelineId, PinchZoomInfos),
|
||||
@@ -324,7 +325,7 @@ pub enum ScriptThreadMessage {
|
||||
/// those pipelines run in script threads, which complicates things: the pipelines in a webview
|
||||
/// may be split across multiple script threads, and the pipelines in a script thread may belong
|
||||
/// to multiple webviews. So the simplest approach is to activate it for one pipeline at a time.
|
||||
SetAccessibilityActive(PipelineId, bool),
|
||||
SetAccessibilityActive(PipelineId, bool, Epoch),
|
||||
/// Force a garbage collection in this script thread.
|
||||
TriggerGarbageCollection,
|
||||
}
|
||||
|
||||
@@ -678,6 +678,8 @@ impl RunningAppState {
|
||||
|
||||
for window in self.windows().values() {
|
||||
for (_, webview) in window.webviews() {
|
||||
// Activate accessibility in the WebView.
|
||||
// There are two sites like this; this is the a11y activation site.
|
||||
webview.set_accessibility_active(active);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,8 @@ impl ServoShellWindow {
|
||||
self.add_webview(webview.clone());
|
||||
// If `self` is not in `state.windows`, our notify_accessibility_tree_update() will panic.
|
||||
if state.accessibility_active() {
|
||||
// Activate accessibility in the WebView.
|
||||
// There are two sites like this; this is the WebView creation site.
|
||||
webview.set_accessibility_active(true);
|
||||
}
|
||||
webview
|
||||
|
||||
Reference in New Issue
Block a user