Compare commits

...

7 Commits

Author SHA1 Message Date
Luke Warlow
64041ee394 Print action requests from our subtree
Co-authored-by: Delan Azabani <dazabani@igalia.com>
Signed-off-by: Luke Warlow <lwarlow@igalia.com>
2026-01-13 17:04:45 +08:00
delan azabani
6bab93ae4c Fix crash when requesting text
Signed-off-by: delan azabani <dazabani@igalia.com>
2026-01-13 17:04:45 +08:00
Alice Boxhall
2b0b180eb2 Add accessibility mappings for h1-h6; remove several redundant mappings.
Co-authored-by: Delan Azabani <dazabani@igalia.com>
Signed-off-by: Alice Boxhall <alice@igalia.com>
2026-01-13 17:04:44 +08:00
Luke Warlow
84798a5ee6 Send accessibility tree updates to accesskit
Signed-off-by: Luke Warlow <lwarlow@igalia.com>
Signed-off-by: Delan Azabani <dazabani@igalia.com>
2026-01-13 17:04:44 +08:00
Delan Azabani
08965f2149 Add some rough HTML element role mappings
Co-authored-by: Luke Warlow <lwarlow@igalia.com>
Signed-off-by: Delan Azabani <dazabani@igalia.com>
2026-01-13 16:59:02 +08:00
Delan Azabani
eb90fcc8f9 Hacky accessibility tree for testing accesskit patch
Signed-off-by: Delan Azabani <dazabani@igalia.com>
2026-01-13 16:59:01 +08:00
Delan Azabani
463a1fc631 Patch deps
Co-authored-by: Alice Boxhall <alice@igalia.com>
Signed-off-by: Delan Azabani <dazabani@igalia.com>
2026-01-13 16:56:58 +08:00
24 changed files with 458 additions and 52 deletions

116
Cargo.lock generated
View File

@@ -18,45 +18,60 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
[[package]]
name = "accessibility_traits"
version = "0.0.1"
dependencies = [
"accesskit",
"base",
"crossbeam-channel",
"ipc-channel",
"malloc_size_of_derive",
"rustc-hash 2.1.1",
"serde",
"servo_malloc_size_of",
"strum",
]
[[package]]
name = "accesskit"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf203f9d3bd8f29f98833d1fbef628df18f759248a547e7e01cfbf63cda36a99"
source = "git+https://github.com/AccessKit/accesskit.git?rev=783c1e8fea164f337a0cd62739fb6e59f159d77c#783c1e8fea164f337a0cd62739fb6e59f159d77c"
dependencies = [
"enumn",
"serde",
"uuid",
]
[[package]]
name = "accesskit_atspi_common"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "890d241cf51fc784f0ac5ac34dfc847421f8d39da6c7c91a0fcc987db62a8267"
source = "git+https://github.com/AccessKit/accesskit.git?rev=783c1e8fea164f337a0cd62739fb6e59f159d77c#783c1e8fea164f337a0cd62739fb6e59f159d77c"
dependencies = [
"accesskit",
"accesskit_consumer",
"atspi-common",
"serde",
"thiserror 1.0.69",
"zvariant",
]
[[package]]
name = "accesskit_consumer"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db81010a6895d8707f9072e6ce98070579b43b717193d2614014abd5cb17dd43"
source = "git+https://github.com/AccessKit/accesskit.git?rev=783c1e8fea164f337a0cd62739fb6e59f159d77c#783c1e8fea164f337a0cd62739fb6e59f159d77c"
dependencies = [
"accesskit",
"hashbrown 0.15.5",
"hashbrown 0.16.0",
]
[[package]]
name = "accesskit_macos"
version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0089e5c0ac0ca281e13ea374773898d9354cc28d15af9f0f7394d44a495b575"
source = "git+https://github.com/AccessKit/accesskit.git?rev=783c1e8fea164f337a0cd62739fb6e59f159d77c#783c1e8fea164f337a0cd62739fb6e59f159d77c"
dependencies = [
"accesskit",
"accesskit_consumer",
"hashbrown 0.15.5",
"hashbrown 0.16.0",
"objc2 0.5.2",
"objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2",
@@ -65,8 +80,7 @@ dependencies = [
[[package]]
name = "accesskit_unix"
version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301e55b39cfc15d9c48943ce5f572204a551646700d0e8efa424585f94fec528"
source = "git+https://github.com/AccessKit/accesskit.git?rev=783c1e8fea164f337a0cd62739fb6e59f159d77c#783c1e8fea164f337a0cd62739fb6e59f159d77c"
dependencies = [
"accesskit",
"accesskit_atspi_common",
@@ -83,12 +97,11 @@ dependencies = [
[[package]]
name = "accesskit_windows"
version = "0.29.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d63dd5041e49c363d83f5419a896ecb074d309c414036f616dc0b04faca971"
source = "git+https://github.com/AccessKit/accesskit.git?rev=783c1e8fea164f337a0cd62739fb6e59f159d77c#783c1e8fea164f337a0cd62739fb6e59f159d77c"
dependencies = [
"accesskit",
"accesskit_consumer",
"hashbrown 0.15.5",
"hashbrown 0.16.0",
"static_assertions",
"windows 0.61.3",
"windows-core 0.61.2",
@@ -631,20 +644,19 @@ checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c"
[[package]]
name = "atspi"
version = "0.25.0"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c83247582e7508838caf5f316c00791eee0e15c0bf743e6880585b867e16815c"
checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d"
dependencies = [
"atspi-common",
"atspi-connection",
"atspi-proxies",
]
[[package]]
name = "atspi-common"
version = "0.9.0"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33dfc05e7cdf90988a197803bf24f5788f94f7c94a69efa95683e8ffe76cfdfb"
checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d"
dependencies = [
"enumflags2",
"serde",
@@ -656,23 +668,11 @@ dependencies = [
"zvariant",
]
[[package]]
name = "atspi-connection"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4193d51303d8332304056ae0004714256b46b6635a5c556109b319c0d3784938"
dependencies = [
"atspi-common",
"atspi-proxies",
"futures-lite",
"zbus",
]
[[package]]
name = "atspi-proxies"
version = "0.9.0"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2eebcb9e7e76f26d0bcfd6f0295e1cd1e6f33bedbc5698a971db8dc43d7751c"
checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc"
dependencies = [
"atspi-common",
"serde",
@@ -2353,8 +2353,7 @@ dependencies = [
[[package]]
name = "ecolor"
version = "0.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71ddb8ac7643d1dba1bb02110e804406dd459a838efcb14011ced10556711a8e"
source = "git+https://github.com/emilk/egui.git?rev=82199c1c449ec58aca3e8f0252e2d68f2ea5f5da#82199c1c449ec58aca3e8f0252e2d68f2ea5f5da"
dependencies = [
"bytemuck",
"emath",
@@ -2363,8 +2362,7 @@ dependencies = [
[[package]]
name = "egui"
version = "0.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a9b567d356674e9a5121ed3fedfb0a7c31e059fe71f6972b691bcd0bfc284e3"
source = "git+https://github.com/emilk/egui.git?rev=82199c1c449ec58aca3e8f0252e2d68f2ea5f5da#82199c1c449ec58aca3e8f0252e2d68f2ea5f5da"
dependencies = [
"accesskit",
"ahash",
@@ -2394,8 +2392,7 @@ dependencies = [
[[package]]
name = "egui-winit"
version = "0.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec6687e5bb551702f4ad10ac428bab12acf9d53047ebb1082d4a0ed8c6251a29"
source = "git+https://github.com/emilk/egui.git?rev=82199c1c449ec58aca3e8f0252e2d68f2ea5f5da#82199c1c449ec58aca3e8f0252e2d68f2ea5f5da"
dependencies = [
"accesskit_winit",
"arboard",
@@ -2460,8 +2457,7 @@ dependencies = [
[[package]]
name = "emath"
version = "0.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "491bdf728bf25ddd9ad60d4cf1c48588fa82c013a2440b91aa7fc43e34a07c32"
source = "git+https://github.com/emilk/egui.git?rev=82199c1c449ec58aca3e8f0252e2d68f2ea5f5da#82199c1c449ec58aca3e8f0252e2d68f2ea5f5da"
dependencies = [
"bytemuck",
]
@@ -2470,6 +2466,7 @@ dependencies = [
name = "embedder_traits"
version = "0.0.1"
dependencies = [
"accessibility_traits",
"base",
"bitflags 2.10.0",
"cookie 0.18.1",
@@ -2552,6 +2549,17 @@ dependencies = [
"syn",
]
[[package]]
name = "enumn"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "env_filter"
version = "0.1.4"
@@ -2588,8 +2596,7 @@ dependencies = [
[[package]]
name = "epaint"
version = "0.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "009d0dd3c2163823a0abdb899451ecbc78798dec545ee91b43aff1fa790bab62"
source = "git+https://github.com/emilk/egui.git?rev=82199c1c449ec58aca3e8f0252e2d68f2ea5f5da#82199c1c449ec58aca3e8f0252e2d68f2ea5f5da"
dependencies = [
"ab_glyph",
"ahash",
@@ -2606,8 +2613,7 @@ dependencies = [
[[package]]
name = "epaint_default_fonts"
version = "0.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c4fbe202b6578d3d56428fa185cdf114a05e49da05f477b3c7f0fbb221f1862"
source = "git+https://github.com/emilk/egui.git?rev=82199c1c449ec58aca3e8f0252e2d68f2ea5f5da#82199c1c449ec58aca3e8f0252e2d68f2ea5f5da"
[[package]]
name = "equator"
@@ -2808,6 +2814,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "font-types"
version = "0.10.1"
@@ -3804,7 +3816,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
"foldhash 0.1.5",
"serde",
]
@@ -3813,6 +3825,9 @@ name = "hashbrown"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
dependencies = [
"foldhash 0.2.0",
]
[[package]]
name = "hashlink"
@@ -4973,6 +4988,8 @@ dependencies = [
name = "layout"
version = "0.0.1"
dependencies = [
"accessibility_traits",
"accesskit",
"app_units",
"atomic_refcell",
"base",
@@ -5133,6 +5150,7 @@ dependencies = [
name = "libservo"
version = "0.0.1"
dependencies = [
"accessibility_traits",
"arboard",
"background_hang_monitor",
"base",
@@ -7876,6 +7894,7 @@ dependencies = [
name = "script_traits"
version = "0.0.1"
dependencies = [
"accessibility_traits",
"background_hang_monitor_api",
"base",
"bluetooth_traits",
@@ -8399,6 +8418,7 @@ dependencies = [
name = "servoshell"
version = "0.0.4"
dependencies = [
"accessibility_traits",
"android_logger",
"backtrace",
"base",

View File

@@ -1,6 +1,7 @@
[workspace]
resolver = "2"
members = [
"components/shared/accessibility",
"components/xpath",
"ports/servoshell",
"tests/unit/*",
@@ -23,6 +24,8 @@ publish = false
rust-version = "1.86.0"
[workspace.dependencies]
accessibility_traits = { path = "components/shared/accessibility" }
accesskit = "0.21.1"
accountable-refcell = "0.2.2"
aes = "0.8.4"
aes-gcm = "0.10.3"
@@ -277,6 +280,14 @@ lto = "thin"
codegen-units = 1
[patch.crates-io]
accesskit = { git = "https://github.com/AccessKit/accesskit.git", rev = "783c1e8fea164f337a0cd62739fb6e59f159d77c" }
accesskit_atspi_common = { git = "https://github.com/AccessKit/accesskit.git", rev = "783c1e8fea164f337a0cd62739fb6e59f159d77c" }
accesskit_consumer = { git = "https://github.com/AccessKit/accesskit.git", rev = "783c1e8fea164f337a0cd62739fb6e59f159d77c" }
accesskit_unix = { git = "https://github.com/AccessKit/accesskit.git", rev = "783c1e8fea164f337a0cd62739fb6e59f159d77c" }
accesskit_macos = { git = "https://github.com/AccessKit/accesskit.git", rev = "783c1e8fea164f337a0cd62739fb6e59f159d77c" }
accesskit_windows = { git = "https://github.com/AccessKit/accesskit.git", rev = "783c1e8fea164f337a0cd62739fb6e59f159d77c" }
egui = { git = "https://github.com/emilk/egui.git", rev = "82199c1c449ec58aca3e8f0252e2d68f2ea5f5da" }
egui-winit = { git = "https://github.com/emilk/egui.git", rev = "82199c1c449ec58aca3e8f0252e2d68f2ea5f5da" }
# If you need to temporarily test Servo with a local fork of some upstream
# crate, add that here. Use the form:
#

View File

@@ -17,6 +17,8 @@ doctest = false
tracing = ["dep:tracing"]
[dependencies]
accessibility_traits = { workspace = true }
accesskit = { workspace = true }
app_units = { workspace = true }
atomic_refcell = { workspace = true }
base = { workspace = true }

View File

@@ -0,0 +1,200 @@
/* 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 std::rc::Rc;
use std::sync::LazyLock;
pub(crate) use accessibility_traits::AccessibilityTree;
use accesskit::{Action, Node as AxNode, NodeId as AxNodeId, Role, Tree as AxTree};
use html5ever::{LocalName, local_name};
use layout_api::wrapper_traits::{LayoutNode, ThreadSafeLayoutNode};
use log::trace;
use rustc_hash::FxHashMap;
use script::layout_dom::ServoLayoutDocument;
use style::dom::{NodeInfo, TDocument, TElement, TNode};
use crate::FragmentTree;
// #[derive(MallocSizeOf)]
pub(crate) struct AccessibilityTreeCalculator {}
impl AccessibilityTreeCalculator {
pub(crate) fn construct(
document: ServoLayoutDocument<'_>,
fragment_tree: Rc<FragmentTree>,
) -> AccessibilityTree {
let mut ax_nodes: FxHashMap<AxNodeId, AxNode> = FxHashMap::default();
let ax_document_id = AxNodeId(document.as_node().opaque().0 as u64);
let ax_document = AxNode::new(Role::Document);
ax_nodes.insert(ax_document_id, ax_document);
let dom_root = document.root_element().unwrap().as_node();
let mut dom_queue = vec![dom_root];
'outer: loop {
// find the next node in the queue that is not display: none.
let dom_next = loop {
let Some(next) = dom_queue.pop() else {
break 'outer;
};
if next
.style_data()
.is_none_or(|style| !style.element_data.borrow().styles.is_display_none())
{
break next;
}
};
// push its children for later traversal.
// reverse order ensures that the first child will get popped first.
for child in RevDomChildren::of(dom_next) {
dom_queue.push(child);
}
// now process the node.
trace!("DOM node: {dom_next:?}");
let dom_parent = dom_next
.parent_node()
.expect("Even the root element must have a parent");
let ax_parent_id = AxNodeId(dom_parent.opaque().0 as u64);
let ax_next_id = AxNodeId(dom_next.opaque().0 as u64);
let ax_parent = ax_nodes.get_mut(&ax_parent_id).expect("Guaranteed by us");
ax_parent.push_child(ax_next_id);
let mut ax_next = AxNode::default();
if dom_next.is_text_node() {
let text_content = dom_next.to_threadsafe().text_content();
trace!("node text content = {:?}", text_content);
ax_next.set_value(&*text_content);
// FIXME: this should take into account editing selection units (grapheme clusters?)
ax_next.set_character_lengths(
text_content
.chars()
.map(|c| c.len_utf8() as u8)
.collect::<Box<[u8]>>(),
);
ax_next.set_role(Role::TextRun);
} else if let Some(element) = dom_next.as_element() {
if element.is_html_element() {
if let Some(role) = HTML_ELEMENT_ROLE_MAPPINGS.get(element.local_name()) {
ax_next.set_role(*role);
}
}
}
ax_next.add_action(Action::Click);
ax_nodes.insert(ax_next_id, ax_next);
}
AccessibilityTree {
ax_nodes,
ax_tree: AxTree {
root: ax_document_id,
toolkit_name: None,
toolkit_version: None,
},
}
}
}
/// Like [`style::dom::DomChildren`], but reversed.
///
/// Do not use the tuple constructor; use [`Self::of`].
pub(crate) struct RevDomChildren<N>(Option<N>);
impl<N: TNode> RevDomChildren<N> {
fn of(node: N) -> Self {
Self(node.last_child())
}
}
impl<N: TNode> Iterator for RevDomChildren<N> {
type Item = N;
fn next(&mut self) -> Option<N> {
let n = self.0.take()?;
self.0 = n.prev_sibling();
Some(n)
}
}
/// <https://www.w3.org/TR/html-aam-1.0/#html-element-role-mappings>
///
/// FIXME: converted mechanically for now, so this will have many errors
static HTML_ELEMENT_ROLE_MAPPINGS: LazyLock<FxHashMap<LocalName, Role>> = LazyLock::new(|| {
[
(local_name!("a"), Role::Link),
(local_name!("address"), Role::Group),
(local_name!("area"), Role::Link),
(local_name!("area"), Role::GenericContainer),
(local_name!("article"), Role::Article),
(local_name!("aside"), Role::Complementary),
(local_name!("b"), Role::GenericContainer),
(local_name!("bdi"), Role::GenericContainer),
(local_name!("bdo"), Role::GenericContainer),
(local_name!("blockquote"), Role::Blockquote),
(local_name!("body"), Role::GenericContainer),
(local_name!("button"), Role::Button),
(local_name!("caption"), Role::Caption),
(local_name!("code"), Role::Code),
(local_name!("data"), Role::GenericContainer),
(local_name!("datalist"), Role::ListBox),
(local_name!("dd"), Role::Definition),
(local_name!("del"), Role::ContentDeletion),
(local_name!("details"), Role::Group),
(local_name!("dfn"), Role::Term),
(local_name!("dialog"), Role::Dialog),
(local_name!("dir"), Role::List),
(local_name!("div"), Role::GenericContainer),
(local_name!("dl"), Role::List),
(local_name!("dt"), Role::Term),
(local_name!("em"), Role::Emphasis),
(local_name!("fieldset"), Role::Group),
(local_name!("figcaption"), Role::Caption),
(local_name!("figure"), Role::Figure),
(local_name!("footer"), Role::ContentInfo),
(local_name!("form"), Role::Form),
(local_name!("h1"), Role::Heading),
(local_name!("h2"), Role::Heading),
(local_name!("h3"), Role::Heading),
(local_name!("h4"), Role::Heading),
(local_name!("h5"), Role::Heading),
(local_name!("h6"), Role::Heading),
(local_name!("header"), Role::Banner),
(local_name!("hgroup"), Role::Group),
(local_name!("hr"), Role::Splitter),
(local_name!("html"), Role::GenericContainer),
(local_name!("i"), Role::GenericContainer),
(local_name!("img"), Role::Image),
(local_name!("ins"), Role::ContentInsertion),
(local_name!("li"), Role::ListItem),
(local_name!("main"), Role::Main),
(local_name!("mark"), Role::Mark),
(local_name!("menu"), Role::List),
(local_name!("meter"), Role::Meter),
(local_name!("nav"), Role::Navigation),
(local_name!("ol"), Role::List),
(local_name!("optgroup"), Role::Group),
(local_name!("option"), Role::ListBoxOption),
(local_name!("output"), Role::Status),
(local_name!("p"), Role::Paragraph),
(local_name!("pre"), Role::GenericContainer),
(local_name!("progress"), Role::ProgressIndicator),
(local_name!("q"), Role::GenericContainer),
(local_name!("s"), Role::ContentDeletion),
(local_name!("samp"), Role::GenericContainer),
(local_name!("search"), Role::Search),
(local_name!("section"), Role::Region),
(local_name!("select"), Role::ListBox),
(local_name!("select"), Role::ComboBox),
(local_name!("small"), Role::GenericContainer),
(local_name!("span"), Role::GenericContainer),
(local_name!("strong"), Role::Strong),
(local_name!("table"), Role::Table),
(local_name!("tbody"), Role::RowGroup),
(local_name!("td"), Role::Cell),
(local_name!("textarea"), Role::TextInput),
(local_name!("tfoot"), Role::RowGroup),
(local_name!("th"), Role::Cell),
(local_name!("th"), Role::ColumnHeader),
(local_name!("th"), Role::RowHeader),
(local_name!("thead"), Role::RowGroup),
(local_name!("time"), Role::Time),
(local_name!("tr"), Role::Row),
(local_name!("u"), Role::GenericContainer),
(local_name!("ul"), Role::List),
]
.into_iter()
.collect()
});

View File

@@ -86,6 +86,7 @@ use url::Url;
use webrender_api::ExternalScrollId;
use webrender_api::units::{DevicePixel, LayoutVector2D};
use crate::accessibility_tree::{AccessibilityTree, AccessibilityTreeCalculator};
use crate::context::{CachedImageOrError, ImageResolver, LayoutContext};
use crate::display_list::{
DisplayListBuilder, HitTest, LargestContentfulPaintCandidateCollector, StackingContextTree,
@@ -180,6 +181,7 @@ pub struct LayoutThread {
/// The fragment tree.
fragment_tree: RefCell<Option<Rc<FragmentTree>>>,
accessibility_tree: RefCell<Option<AccessibilityTree>>,
/// The [`StackingContextTree`] cached from previous layouts.
stacking_context_tree: RefCell<Option<StackingContextTree>>,
@@ -749,6 +751,7 @@ impl LayoutThread {
need_new_stacking_context_tree: Cell::new(false),
box_tree: Default::default(),
fragment_tree: Default::default(),
accessibility_tree: Default::default(),
stacking_context_tree: Default::default(),
paint_api: config.paint_api,
stylist: Stylist::new(device, QuirksMode::NoQuirks),
@@ -1150,6 +1153,15 @@ impl LayoutThread {
run_layout()
});
let accessibility_tree =
AccessibilityTreeCalculator::construct(document, fragment_tree.clone());
self.script_chan
.send(ScriptThreadMessage::HackySendAccessibilityTree(
self.webview_id,
accessibility_tree,
))
.expect("TODO: panic message");
*self.fragment_tree.borrow_mut() = Some(fragment_tree);
if self.debug.style_tree {

View File

@@ -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;

View File

@@ -104,6 +104,7 @@ impl MixedMessage {
ScriptThreadMessage::EmbedderControlResponse(id, _) => Some(id.pipeline_id),
ScriptThreadMessage::SetUserContents(..) => None,
ScriptThreadMessage::DestroyUserContentManager(..) => None,
ScriptThreadMessage::HackySendAccessibilityTree(..) => None,
},
MixedMessage::FromScript(inner_msg) => match inner_msg {
MainThreadScriptMsg::Common(CommonScriptMsg::Task(_, _, pipeline_id, _)) => {

View File

@@ -1923,6 +1923,15 @@ impl ScriptThread {
.borrow_mut()
.remove(&user_content_manager_id);
},
ScriptThreadMessage::HackySendAccessibilityTree(webview_id, accessibility_tree) => {
self.senders
.pipeline_to_embedder_sender
.send(EmbedderMsg::HackyAccessibilityTreeUpdate(
webview_id,
accessibility_tree,
))
.expect("TODO: panic message");
},
}
}

View File

@@ -73,6 +73,7 @@ webxr = [
]
[dependencies]
accessibility_traits = { workspace = true }
background_hang_monitor = { path = "../background_hang_monitor" }
base = { workspace = true }
bitflags = { workspace = true }

View File

@@ -638,6 +638,13 @@ impl ServoInner {
warn!("Failed to respond to GetScreenMetrics: {error}");
}
},
EmbedderMsg::HackyAccessibilityTreeUpdate(webview_id, accessibility_tree) => {
if let Some(webview) = self.get_webview_handle(webview_id) {
webview
.delegate()
.hacky_accessibility_tree_update(webview, accessibility_tree);
}
},
}
}
}

View File

@@ -5,6 +5,7 @@
use std::path::PathBuf;
use std::rc::Rc;
use accessibility_traits::AccessibilityTree;
use base::generic_channel::GenericSender;
use base::id::PipelineId;
use compositing_traits::rendering_context::RenderingContext;
@@ -974,6 +975,13 @@ pub trait WebViewDelegate {
/// A console message was logged by content in this [`WebView`].
/// <https://developer.mozilla.org/en-US/docs/Web/API/Console_API>
fn show_console_message(&self, _webview: WebView, _level: ConsoleLogLevel, _message: String) {}
fn hacky_accessibility_tree_update(
&self,
_webview: WebView,
_accessibility_tree: AccessibilityTree,
) {
}
}
pub(crate) struct DefaultWebViewDelegate;

View File

@@ -0,0 +1,23 @@
[package]
name = "accessibility_traits"
version.workspace = true
authors.workspace = true
license.workspace = true
edition.workspace = true
publish.workspace = true
rust-version.workspace = true
[lib]
name = "accessibility_traits"
path = "lib.rs"
[dependencies]
accesskit = { workspace = true, features = ["serde"] }
rustc-hash = { workspace = true }
base = { workspace = true }
crossbeam-channel = { workspace = true }
ipc-channel = { workspace = true }
malloc_size_of = { workspace = true }
malloc_size_of_derive = { workspace = true }
serde = { workspace = true }
strum = { workspace = true }

View File

@@ -0,0 +1,34 @@
/* 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::{Node as AxNode, NodeId as AxNodeId, Tree as AxTree};
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Debug)]
pub struct AccessibilityTree {
pub ax_nodes: FxHashMap<AxNodeId, AxNode>,
pub ax_tree: AxTree,
}
impl AccessibilityTree {
pub fn descendants(&self) -> AxDescendants<'_> {
AxDescendants(self, vec![self.ax_tree.root])
}
}
pub struct AxDescendants<'tree>(&'tree AccessibilityTree, Vec<AxNodeId>);
impl<'tree> Iterator for AxDescendants<'tree> {
type Item = (AxNodeId, &'tree AxNode);
fn next(&mut self) -> Option<Self::Item> {
let Some(result_id) = self.1.pop() else {
return None;
};
let result_node = self.0.ax_nodes.get(&result_id).unwrap();
for child_id in result_node.children().iter().rev() {
self.1.push(*child_id);
}
Some((result_id, result_node))
}
}

View File

@@ -18,6 +18,7 @@ baked-default-resources = []
gamepad = []
[dependencies]
accessibility_traits = { workspace = true }
base = { workspace = true }
bitflags = { workspace = true }
cookie = { workspace = true }

View File

@@ -22,6 +22,7 @@ use std::ops::Range;
use std::path::PathBuf;
use std::sync::Arc;
use accessibility_traits::AccessibilityTree;
use base::generic_channel::{GenericCallback, GenericSender, GenericSharedMemory, SendResult};
use base::id::{PipelineId, WebViewId};
use crossbeam_channel::Sender;
@@ -530,6 +531,8 @@ pub enum EmbedderMsg {
/// Inform the embedding layer that a particular `InputEvent` was handled by Servo
/// and the embedder can continue processing it, if necessary.
InputEventHandled(WebViewId, InputEventId, InputEventResult),
/// Placeholder
HackyAccessibilityTreeUpdate(WebViewId, AccessibilityTree),
}
impl Debug for EmbedderMsg {

View File

@@ -16,6 +16,7 @@ bluetooth = ["bluetooth_traits"]
webgpu = ["webgpu_traits"]
[dependencies]
accessibility_traits = { workspace = true }
background_hang_monitor_api = { workspace = true }
base = { workspace = true }
bluetooth_traits = { workspace = true, optional = true }

View File

@@ -11,6 +11,7 @@
use std::fmt;
use accessibility_traits::AccessibilityTree;
use base::cross_process_instant::CrossProcessInstant;
use base::generic_channel::{GenericCallback, GenericReceiver, GenericSender};
use base::id::{
@@ -305,6 +306,8 @@ pub enum ScriptThreadMessage {
/// Release all data for the given `UserContentManagerId` from the `ScriptThread`'s
/// `user_contents_for_manager_id` map.
DestroyUserContentManager(UserContentManagerId),
/// Placeholder
HackySendAccessibilityTree(WebViewId, AccessibilityTree),
}
impl fmt::Debug for ScriptThreadMessage {

View File

@@ -56,6 +56,7 @@ webgpu = ["libservo/webgpu"]
webxr = ["libservo/webxr"]
[dependencies]
accessibility_traits = { workspace = true }
bpaf = { version = "0.9.20", features = ["derive"] }
cfg-if = { workspace = true }
crossbeam-channel = { workspace = true }

View File

@@ -213,7 +213,7 @@ impl ApplicationHandler<AppEvent> for App {
.and_then(|window_id| state.window(ServoShellWindowId::from(u64::from(window_id))))
{
if let Some(headed_window) = window.platform_window().as_headed_window() {
headed_window.handle_winit_app_event(app_event);
headed_window.handle_winit_app_event(&window, app_event);
}
}

View File

@@ -7,11 +7,12 @@ use std::rc::Rc;
use std::sync::Arc;
use dpi::PhysicalSize;
use egui::accesskit::{Role, TreeId, TreeUpdate, Uuid};
use egui::text::{CCursor, CCursorRange};
use egui::text_edit::TextEditState;
use egui::{
Button, Key, Label, LayerId, Modifiers, PaintCallback, TopBottomPanel, Vec2, WidgetInfo,
WidgetType, pos2,
WidgetType, accesskit, pos2,
};
use egui_glow::{CallbackFn, EguiGlow};
use egui_winit::EventResponse;
@@ -464,6 +465,18 @@ impl Gui {
// If the top parts of the GUI changed size, then update the size of the WebView and also
// the size of its RenderingContext.
let rect = ctx.available_rect();
let tree_id = TreeId(Uuid::from_bytes([1; 16]));
let id = egui::Id::new("webview");
ctx.accesskit_node_builder(id, |node| {
node.set_role(Role::Group);
node.set_tree_id(tree_id);
node.set_bounds(accesskit::Rect {
x0: rect.left() as f64,
y0: rect.top() as f64,
x1: rect.right() as f64,
y1: rect.bottom() as f64,
});
});
let size = Size2D::new(rect.width(), rect.height()) * scale;
if let Some(webview) = window.active_webview() &&
size != webview.size()
@@ -608,6 +621,12 @@ impl Gui {
pub(crate) fn set_zoom_factor(&self, factor: f32) {
self.context.egui_ctx.set_zoom_factor(factor);
}
pub(crate) fn hacky_accessibility_tree_update(&mut self, updater: impl FnOnce() -> TreeUpdate) {
if let Some(adapter) = self.context.egui_winit.accesskit.as_mut() {
adapter.update_if_active(updater);
}
}
}
fn embedder_image_to_egui_image(image: &Image) -> egui::ColorImage {

View File

@@ -13,6 +13,8 @@ use std::env;
use std::rc::Rc;
use std::time::Duration;
use accessibility_traits::AccessibilityTree;
use egui::accesskit::{Node, NodeId, TreeId, TreeUpdate, Uuid};
use euclid::{Angle, Length, Point2D, Rect, Rotation3D, Scale, Size2D, UnknownUnit, Vector3D};
use keyboard_types::ShortcutMatcher;
use log::{debug, info};
@@ -756,8 +758,21 @@ impl HeadedWindow {
}
}
pub(crate) fn handle_winit_app_event(&self, app_event: AppEvent) {
pub(crate) fn handle_winit_app_event(&self, _window: &ServoShellWindow, app_event: AppEvent) {
if let AppEvent::Accessibility(ref event) = app_event {
match &event.window_event {
egui_winit::accesskit_winit::WindowEvent::InitialTreeRequested => {
// TODO get the initial tree from servo
},
egui_winit::accesskit_winit::WindowEvent::ActionRequested(req) => {
// TODO use window to get active webview, then do something with action request.
if req.target_tree != TreeId::ROOT {
println!("{:?}", req);
}
},
_ => {},
}
if self
.gui
.borrow_mut()
@@ -1131,6 +1146,26 @@ impl PlatformWindow for HeadedWindow {
println!("{message}");
log::log!(level.into(), "{message}");
}
fn hacky_accessibility_tree_update(
&self,
_webview: WebView,
accessibility_tree: AccessibilityTree,
) {
let nodes: Vec<(NodeId, Node)> = accessibility_tree
.ax_nodes
.iter()
.map(|(k, v)| (*k, v.clone()))
.collect();
self.gui
.borrow_mut()
.hacky_accessibility_tree_update(|| TreeUpdate {
nodes,
tree: Some(accessibility_tree.ax_tree),
tree_id: TreeId(Uuid::from_bytes([1; 16])),
focus: NodeId(1),
});
}
}
fn winit_phase_to_touch_event_type(phase: TouchPhase) -> TouchEventType {

View File

@@ -4,11 +4,12 @@
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use accessibility_traits::AccessibilityTree;
use base::generic_channel::GenericCallback;
use dpi::PhysicalSize;
use euclid::{Rect, Scale};
use keyboard_types::{CompositionEvent, CompositionState, Key, KeyState, NamedKey};
use log::{info, warn};
use log::{info, trace, warn};
use raw_window_handle::{RawWindowHandle, WindowHandle};
use servo::{
DeviceIndependentIntRect, DeviceIndependentPixel, DeviceIntSize, DevicePixel, DevicePoint,

View File

@@ -9,6 +9,7 @@ use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::rc::Rc;
use accessibility_traits::AccessibilityTree;
use crossbeam_channel::{Receiver, Sender, unbounded};
use euclid::Rect;
use image::{DynamicImage, ImageFormat, RgbaImage};
@@ -819,6 +820,15 @@ impl WebViewDelegate for RunningAppState {
self.platform_window_for_webview_id(webview.id())
.show_console_message(level, &message);
}
fn hacky_accessibility_tree_update(
&self,
webview: WebView,
accessibility_tree: AccessibilityTree,
) {
self.platform_window_for_webview_id(webview.id())
.hacky_accessibility_tree_update(webview, accessibility_tree);
}
}
struct ServoShellServoDelegate;

View File

@@ -5,6 +5,7 @@
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use accessibility_traits::AccessibilityTree;
use euclid::Scale;
use log::warn;
use servo::{
@@ -433,4 +434,6 @@ pub(crate) trait PlatformWindow {
fn as_headed_window(&self) -> Option<&crate::egl::app::EmbeddedPlatformWindow> {
None
}
fn hacky_accessibility_tree_update(&self, _: WebView, _: AccessibilityTree) {}
}