mirror of
https://github.com/servo/servo
synced 2026-04-25 17:15:48 +02:00
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>
192 lines
7.6 KiB
Rust
192 lines
7.6 KiB
Rust
/* 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
|
||
/// webview’s *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
|
||
}
|
||
}
|