Files
servo/components/constellation/constellation_webview.rs
Alice d11fc3a1c6 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>
2026-04-14 13:36:57 +00:00

192 lines
7.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 embedder_traits::user_contents::UserContentManagerId;
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 crate::browsingcontext::BrowsingContext;
use crate::pipeline::Pipeline;
use crate::session_history::JointSessionHistory;
/// The `Constellation`'s view of a `WebView` in the embedding layer. This tracks all of the
/// `Constellation` state for this `WebView`.
pub(crate) struct ConstellationWebView {
/// The [`WebViewId`] of this [`ConstellationWebView`].
webview_id: WebViewId,
/// The [`PipelineId`] of the currently active pipeline at the top level of this WebView.
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
/// context.
pub focused_browsing_context_id: BrowsingContextId,
/// The [`BrowsingContextId`] of the currently hovered browsing context, to use for
/// knowing which frame is currently receiving cursor events.
pub hovered_browsing_context_id: Option<BrowsingContextId>,
/// The last mouse move point in the coordinate space of the Pipeline that it
/// happened int.
pub last_mouse_move_point: Point2D<f32, CSSPixel>,
/// The joint session history for this webview.
pub session_history: JointSessionHistory,
/// The [`UserContentManagerId`] for all pipelines in this `WebView`. This is `Some`
/// if the embedder has set a `UserContentManager` using the WebViewBuilder API and
/// it is `None` otherwise.
pub user_content_manager_id: Option<UserContentManagerId>,
/// The [`Theme`] that this [`ConstellationWebView`] uses. This is communicated to all
/// `ScriptThread`s so that they know how to render the contents of a particular `WebView.
theme: Theme,
/// Whether accessibility is active for this webview.
///
/// Set by [`crate::Constellation::set_accessibility_active()`], and forwarded to the
/// webviews *active* pipelines (of those that represent documents) at any given moment
/// via [`ScriptThreadMessage::SetAccessibilityActive`] in `set_accessibility_active()`
/// and [`crate::Constellation::set_frame_tree_for_webview()`].
pub accessibility_active: bool,
}
impl ConstellationWebView {
pub(crate) fn new(
webview_id: WebViewId,
focused_browsing_context_id: BrowsingContextId,
user_content_manager_id: Option<UserContentManagerId>,
) -> Self {
Self {
webview_id,
user_content_manager_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(),
session_history: JointSessionHistory::new(),
theme: Theme::Light,
accessibility_active: false,
}
}
/// Set the [`Theme`] on this [`ConstellationWebView`] returning true if the theme changed.
pub(crate) fn set_theme(&mut self, new_theme: Theme) -> bool {
let old_theme = std::mem::replace(&mut self.theme, new_theme);
old_theme != self.theme
}
/// Get the [`Theme`] of this [`ConstellationWebView`].
pub(crate) fn theme(&self) -> Theme {
self.theme
}
fn target_pipeline_id_for_input_event(
&self,
event: &ConstellationInputEvent,
browsing_contexts: &FxHashMap<BrowsingContextId, BrowsingContext>,
) -> Option<PipelineId> {
if let Some(hit_test_result) = &event.hit_test_result {
return Some(hit_test_result.pipeline_id);
}
// If there's no hit test, send the event to either the hovered or focused browsing context,
// depending on the event type.
let browsing_context_id = if matches!(event.event.event, InputEvent::MouseLeftViewport(_)) {
self.hovered_browsing_context_id
.unwrap_or(self.focused_browsing_context_id)
} else {
self.focused_browsing_context_id
};
Some(browsing_contexts.get(&browsing_context_id)?.pipeline_id)
}
/// Forward the [`InputEvent`] to this [`ConstellationWebView`]. Returns false if
/// the event could not be forwarded or true otherwise.
pub(crate) fn forward_input_event(
&mut self,
event: ConstellationInputEvent,
pipelines: &FxHashMap<PipelineId, Pipeline>,
browsing_contexts: &FxHashMap<BrowsingContextId, BrowsingContext>,
) -> bool {
let Some(pipeline_id) = self.target_pipeline_id_for_input_event(&event, browsing_contexts)
else {
warn!("Unknown pipeline for input event. Ignoring.");
return false;
};
let Some(pipeline) = pipelines.get(&pipeline_id) else {
warn!("Unknown pipeline id {pipeline_id:?} for input event. Ignoring.");
return false;
};
let mut update_hovered_browsing_context =
|newly_hovered_browsing_context_id, focus_moving_to_another_iframe: bool| {
let old_hovered_context_id = std::mem::replace(
&mut self.hovered_browsing_context_id,
newly_hovered_browsing_context_id,
);
if old_hovered_context_id == newly_hovered_browsing_context_id {
return;
}
let Some(old_hovered_context_id) = old_hovered_context_id else {
return;
};
let Some(pipeline) = browsing_contexts
.get(&old_hovered_context_id)
.and_then(|browsing_context| pipelines.get(&browsing_context.pipeline_id))
else {
return;
};
let mut synthetic_mouse_leave_event = event.clone();
synthetic_mouse_leave_event.event.event =
InputEvent::MouseLeftViewport(MouseLeftViewportEvent {
focus_moving_to_another_iframe,
});
let _ = pipeline
.event_loop
.send(ScriptThreadMessage::SendInputEvent(
self.webview_id,
pipeline.id,
synthetic_mouse_leave_event,
));
};
if let InputEvent::MouseLeftViewport(_) = &event.event.event {
update_hovered_browsing_context(None, false);
return true;
}
if let InputEvent::MouseMove(_) = &event.event.event {
update_hovered_browsing_context(Some(pipeline.browsing_context_id), true);
self.last_mouse_move_point = event
.hit_test_result
.as_ref()
.expect("MouseMove events should always have hit tests.")
.point_in_viewport;
}
let _ = pipeline
.event_loop
.send(ScriptThreadMessage::SendInputEvent(
self.webview_id,
pipeline.id,
event,
));
true
}
}