mirror of
https://github.com/YaLTeR/niri.git
synced 2025-10-06 00:23:14 +02:00
Implement screen reader announcements via AccessKit
This commit is contained in:
158
Cargo.lock
generated
158
Cargo.lock
generated
@@ -2,6 +2,54 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "accesskit"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c0690ad6e6f9597b8439bd3c95e8c6df5cd043afd950c6d68f3b37df641e27c"
|
||||
|
||||
[[package]]
|
||||
name = "accesskit_atspi_common"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fb511e093896d3cae0efba40322087dff59ea322308a3e6edf70f28d22f2607"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"accesskit_consumer",
|
||||
"atspi-common",
|
||||
"serde",
|
||||
"thiserror 1.0.69",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "accesskit_consumer"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec27574c1baeb7747c802a194566b46b602461e81dc4957949580ea8da695038"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "accesskit_unix"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2abbfb16144cca5bb2ea6acad5865b7c1e70d4fa171ceba1a52ea8e78a7515f4"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"accesskit_atspi_common",
|
||||
"async-channel",
|
||||
"async-executor",
|
||||
"async-task",
|
||||
"atspi",
|
||||
"futures-lite",
|
||||
"futures-util",
|
||||
"serde",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
@@ -323,6 +371,56 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "628d228f918ac3b82fe590352cc719d30664a0c13ca3a60266fe02c7132d480a"
|
||||
|
||||
[[package]]
|
||||
name = "atspi"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c83247582e7508838caf5f316c00791eee0e15c0bf743e6880585b867e16815c"
|
||||
dependencies = [
|
||||
"atspi-common",
|
||||
"atspi-connection",
|
||||
"atspi-proxies",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atspi-common"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33dfc05e7cdf90988a197803bf24f5788f94f7c94a69efa95683e8ffe76cfdfb"
|
||||
dependencies = [
|
||||
"enumflags2",
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"zbus",
|
||||
"zbus-lockstep",
|
||||
"zbus-lockstep-macros",
|
||||
"zbus_names",
|
||||
"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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2eebcb9e7e76f26d0bcfd6f0295e1cd1e6f33bedbc5698a971db8dc43d7751c"
|
||||
dependencies = [
|
||||
"atspi-common",
|
||||
"serde",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
@@ -1059,6 +1157,12 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.5.0"
|
||||
@@ -1584,6 +1688,9 @@ name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
@@ -2246,6 +2353,8 @@ dependencies = [
|
||||
name = "niri"
|
||||
version = "25.5.1"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"accesskit_unix",
|
||||
"anyhow",
|
||||
"approx 0.5.1",
|
||||
"arrayvec",
|
||||
@@ -3030,6 +3139,16 @@ version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.36.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.37.5"
|
||||
@@ -4215,7 +4334,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quick-xml",
|
||||
"quick-xml 0.37.5",
|
||||
"quote",
|
||||
]
|
||||
|
||||
@@ -4929,6 +5048,30 @@ dependencies = [
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus-lockstep"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29e96e38ded30eeab90b6ba88cb888d70aef4e7489b6cd212c5e5b5ec38045b6"
|
||||
dependencies = [
|
||||
"zbus_xml",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus-lockstep-macros"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc6821851fa840b708b4cbbaf6241868cabc85a2dc22f426361b0292bfc0b836"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"zbus-lockstep",
|
||||
"zbus_xml",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_macros"
|
||||
version = "5.9.0"
|
||||
@@ -4956,6 +5099,19 @@ dependencies = [
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_xml"
|
||||
version = "5.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589e9a02bfafb9754bb2340a9e3b38f389772684c63d9637e76b1870377bec29"
|
||||
dependencies = [
|
||||
"quick-xml 0.36.2",
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.26"
|
||||
|
@@ -50,6 +50,8 @@ readme = "README.md"
|
||||
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
|
||||
|
||||
[dependencies]
|
||||
accesskit = { version = "0.21.0", optional = true }
|
||||
accesskit_unix = { version = "0.17.0", optional = true }
|
||||
anyhow.workspace = true
|
||||
arrayvec = "0.7.6"
|
||||
async-channel = "2.5.0"
|
||||
@@ -124,8 +126,8 @@ xshell = "0.2.7"
|
||||
|
||||
[features]
|
||||
default = ["dbus", "systemd", "xdp-gnome-screencast"]
|
||||
# Enables D-Bus support (serve various freedesktop and GNOME interfaces, power button handling).
|
||||
dbus = ["dep:zbus", "dep:async-io", "dep:url"]
|
||||
# Enables D-Bus support (serve various freedesktop and GNOME interfaces, accessibility tree, power button handling).
|
||||
dbus = ["dep:zbus", "dep:async-io", "dep:url", "dep:accesskit", "dep:accesskit_unix"]
|
||||
# Enables systemd integration (global environment, apps in transient scopes).
|
||||
systemd = ["dbus"]
|
||||
# Enables screencasting support through xdg-desktop-portal-gnome.
|
||||
|
292
src/a11y.rs
Normal file
292
src/a11y.rs
Normal file
@@ -0,0 +1,292 @@
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
|
||||
use accesskit::{
|
||||
ActionHandler, ActionRequest, ActivationHandler, DeactivationHandler, Live, Node, NodeId, Role,
|
||||
Tree, TreeUpdate,
|
||||
};
|
||||
use accesskit_unix::Adapter;
|
||||
use calloop::LoopHandle;
|
||||
|
||||
use crate::layout::workspace::WorkspaceId;
|
||||
use crate::niri::{KeyboardFocus, Niri, State};
|
||||
|
||||
const ID_ROOT: NodeId = NodeId(0);
|
||||
const ID_ANNOUNCEMENT: NodeId = NodeId(1);
|
||||
const ID_SCREENSHOT_UI: NodeId = NodeId(2);
|
||||
const ID_EXIT_CONFIRM_DIALOG: NodeId = NodeId(3);
|
||||
const ID_OVERVIEW: NodeId = NodeId(4);
|
||||
|
||||
pub struct A11y {
|
||||
event_loop: LoopHandle<'static, State>,
|
||||
focus: NodeId,
|
||||
workspace_id: Option<WorkspaceId>,
|
||||
last_announcement: String,
|
||||
to_accesskit: Option<mpsc::SyncSender<TreeUpdate>>,
|
||||
}
|
||||
|
||||
enum Msg {
|
||||
InitialTree,
|
||||
Deactivate,
|
||||
Action(ActionRequest),
|
||||
}
|
||||
|
||||
impl A11y {
|
||||
pub fn new(event_loop: LoopHandle<'static, State>) -> Self {
|
||||
Self {
|
||||
event_loop,
|
||||
focus: ID_ROOT,
|
||||
workspace_id: None,
|
||||
last_announcement: String::new(),
|
||||
to_accesskit: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self) {
|
||||
let (tx, rx) = calloop::channel::channel();
|
||||
let (to_accesskit, from_main) = mpsc::sync_channel::<TreeUpdate>(8);
|
||||
|
||||
// The adapter has a tendency to deadlock, so put it on a thread for now...
|
||||
let handler = Handler { tx };
|
||||
let res = thread::Builder::new()
|
||||
.name("AccessKit Adapter".to_owned())
|
||||
.spawn(move || {
|
||||
let mut adapter = Adapter::new(handler.clone(), handler.clone(), handler);
|
||||
while let Ok(tree) = from_main.recv() {
|
||||
let is_focused = tree.focus != ID_ROOT;
|
||||
adapter.update_if_active(move || tree);
|
||||
adapter.update_window_focus_state(is_focused);
|
||||
}
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(_handle) => {}
|
||||
Err(err) => {
|
||||
warn!("error spawning the AccessKit adapter thread: {err:?}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.event_loop
|
||||
.insert_source(rx, |e, _, state| match e {
|
||||
calloop::channel::Event::Msg(msg) => state.niri.on_a11y_msg(msg),
|
||||
calloop::channel::Event::Closed => (),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
self.to_accesskit = Some(to_accesskit);
|
||||
}
|
||||
|
||||
fn update_tree(&mut self, tree: TreeUpdate) {
|
||||
trace!("updating tree: {tree:?}");
|
||||
self.focus = tree.focus;
|
||||
|
||||
let Some(tx) = &mut self.to_accesskit else {
|
||||
return;
|
||||
};
|
||||
match tx.try_send(tree) {
|
||||
Ok(()) => {}
|
||||
Err(mpsc::TrySendError::Full(_)) => {
|
||||
warn!("AccessKit channel is full, it probably deadlocked; disconnecting");
|
||||
self.to_accesskit = None;
|
||||
}
|
||||
Err(mpsc::TrySendError::Disconnected(_)) => {
|
||||
warn!("AccessKit channel disconnected");
|
||||
self.to_accesskit = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Niri {
|
||||
pub fn refresh_a11y(&mut self) {
|
||||
if self.a11y.to_accesskit.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
let _span = tracy_client::span!("refresh_a11y");
|
||||
|
||||
let mut announcement = None;
|
||||
let ws_id = self.layout.active_workspace().map(|ws| ws.id());
|
||||
if let Some(ws_id) = ws_id {
|
||||
if self.a11y.workspace_id != Some(ws_id) {
|
||||
let (_, idx, ws) = self
|
||||
.layout
|
||||
.workspaces()
|
||||
.find(|(_, _, ws)| ws.id() == ws_id)
|
||||
.unwrap();
|
||||
|
||||
let mut buf = format!("Workspace {}", idx + 1);
|
||||
if let Some(name) = ws.name() {
|
||||
buf.push(' ');
|
||||
buf.push_str(name);
|
||||
}
|
||||
|
||||
announcement = Some(buf);
|
||||
}
|
||||
}
|
||||
self.a11y.workspace_id = ws_id;
|
||||
|
||||
let focus = self.a11y_focus();
|
||||
let update_focus = self.a11y.focus != focus;
|
||||
|
||||
if !(announcement.is_some() || update_focus) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut nodes = Vec::new();
|
||||
|
||||
if let Some(mut announcement) = announcement {
|
||||
// Work around having to change node value for it to get announced.
|
||||
if announcement == self.a11y.last_announcement {
|
||||
announcement.push(' ');
|
||||
}
|
||||
self.a11y.last_announcement = announcement.clone();
|
||||
|
||||
let mut node = Node::new(Role::Label);
|
||||
node.set_value(announcement);
|
||||
node.set_live(Live::Polite);
|
||||
nodes.push((ID_ANNOUNCEMENT, node));
|
||||
}
|
||||
|
||||
let update = TreeUpdate {
|
||||
nodes,
|
||||
tree: None,
|
||||
focus,
|
||||
};
|
||||
|
||||
self.a11y.update_tree(update);
|
||||
}
|
||||
|
||||
pub fn a11y_announce(&mut self, mut announcement: String) {
|
||||
if self.a11y.to_accesskit.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
let _span = tracy_client::span!("a11y_announce");
|
||||
|
||||
// Work around having to change node value for it to get announced.
|
||||
if announcement == self.a11y.last_announcement {
|
||||
announcement.push(' ');
|
||||
}
|
||||
self.a11y.last_announcement = announcement.clone();
|
||||
|
||||
let mut node = Node::new(Role::Label);
|
||||
node.set_value(announcement);
|
||||
node.set_live(Live::Polite);
|
||||
|
||||
let update = TreeUpdate {
|
||||
nodes: vec![(ID_ANNOUNCEMENT, node)],
|
||||
tree: None,
|
||||
focus: self.a11y.focus,
|
||||
};
|
||||
|
||||
self.a11y.update_tree(update);
|
||||
}
|
||||
|
||||
pub fn a11y_announce_config_error(&mut self) {
|
||||
if self.a11y.to_accesskit.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.a11y_announce(crate::ui::config_error_notification::error_text(false));
|
||||
}
|
||||
|
||||
pub fn a11y_announce_hotkey_overlay(&mut self) {
|
||||
if self.a11y.to_accesskit.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.a11y_announce(self.hotkey_overlay.a11y_text());
|
||||
}
|
||||
|
||||
fn a11y_focus(&self) -> NodeId {
|
||||
match self.keyboard_focus {
|
||||
KeyboardFocus::ScreenshotUi => ID_SCREENSHOT_UI,
|
||||
KeyboardFocus::ExitConfirmDialog => ID_EXIT_CONFIRM_DIALOG,
|
||||
KeyboardFocus::Overview => ID_OVERVIEW,
|
||||
_ => ID_ROOT,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_a11y_msg(&mut self, msg: Msg) {
|
||||
match msg {
|
||||
Msg::InitialTree => {
|
||||
let tree = self.a11y_build_full_tree();
|
||||
trace!("sending initial tree: {tree:?}");
|
||||
self.a11y.update_tree(tree);
|
||||
}
|
||||
Msg::Deactivate => {
|
||||
trace!("deactivate");
|
||||
}
|
||||
Msg::Action(request) => {
|
||||
trace!("request: {request:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn a11y_build_full_tree(&self) -> TreeUpdate {
|
||||
let mut node = Node::new(Role::Label);
|
||||
node.set_live(Live::Polite);
|
||||
|
||||
let mut screenshot_ui = Node::new(Role::Group);
|
||||
screenshot_ui.set_label("Screenshot UI");
|
||||
|
||||
let exit_confirm_dialog = crate::ui::exit_confirm_dialog::a11y_node();
|
||||
|
||||
let mut overview = Node::new(Role::Group);
|
||||
overview.set_label("Overview");
|
||||
|
||||
let mut root = Node::new(Role::Window);
|
||||
root.set_children(vec![
|
||||
ID_ANNOUNCEMENT,
|
||||
ID_SCREENSHOT_UI,
|
||||
ID_EXIT_CONFIRM_DIALOG,
|
||||
ID_OVERVIEW,
|
||||
]);
|
||||
|
||||
let tree = Tree {
|
||||
root: ID_ROOT,
|
||||
toolkit_name: Some(String::from("niri")),
|
||||
toolkit_version: None,
|
||||
};
|
||||
|
||||
let focus = self.a11y_focus();
|
||||
|
||||
TreeUpdate {
|
||||
nodes: vec![
|
||||
(ID_ROOT, root),
|
||||
(ID_ANNOUNCEMENT, node),
|
||||
(ID_SCREENSHOT_UI, screenshot_ui),
|
||||
(ID_EXIT_CONFIRM_DIALOG, exit_confirm_dialog),
|
||||
(ID_OVERVIEW, overview),
|
||||
],
|
||||
tree: Some(tree),
|
||||
focus,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Handler {
|
||||
tx: calloop::channel::Sender<Msg>,
|
||||
}
|
||||
|
||||
impl ActivationHandler for Handler {
|
||||
fn request_initial_tree(&mut self) -> Option<TreeUpdate> {
|
||||
let _ = self.tx.send(Msg::InitialTree);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl DeactivationHandler for Handler {
|
||||
fn deactivate_accessibility(&mut self) {
|
||||
let _ = self.tx.send(Msg::Deactivate);
|
||||
}
|
||||
}
|
||||
|
||||
impl ActionHandler for Handler {
|
||||
fn do_action(&mut self, request: ActionRequest) {
|
||||
let _ = self.tx.send(Msg::Action(request));
|
||||
}
|
||||
}
|
@@ -1842,6 +1842,9 @@ impl State {
|
||||
Action::ShowHotkeyOverlay => {
|
||||
if self.niri.hotkey_overlay.show() {
|
||||
self.niri.queue_redraw_all();
|
||||
|
||||
#[cfg(feature = "dbus")]
|
||||
self.niri.a11y_announce_hotkey_overlay();
|
||||
}
|
||||
}
|
||||
Action::MoveWorkspaceToMonitorLeft => {
|
||||
|
@@ -1,6 +1,8 @@
|
||||
#[macro_use]
|
||||
extern crate tracing;
|
||||
|
||||
#[cfg(feature = "dbus")]
|
||||
pub mod a11y;
|
||||
pub mod animation;
|
||||
pub mod backend;
|
||||
pub mod cli;
|
||||
|
@@ -218,6 +218,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
#[cfg(feature = "dbus")]
|
||||
dbus::DBusServers::start(&mut state, cli.session);
|
||||
|
||||
#[cfg(feature = "dbus")]
|
||||
if cli.session {
|
||||
state.niri.a11y.start();
|
||||
}
|
||||
|
||||
if env::var_os("NIRI_DISABLE_SYSTEM_MANAGER_NOTIFY").map_or(true, |x| x != "1") {
|
||||
// Notify systemd we're ready.
|
||||
if let Err(err) = sd_notify::notify(true, &[NotifyState::Ready]) {
|
||||
|
17
src/niri.rs
17
src/niri.rs
@@ -110,6 +110,8 @@ use smithay::wayland::virtual_keyboard::VirtualKeyboardManagerState;
|
||||
use smithay::wayland::xdg_activation::XdgActivationState;
|
||||
use smithay::wayland::xdg_foreign::XdgForeignState;
|
||||
|
||||
#[cfg(feature = "dbus")]
|
||||
use crate::a11y::A11y;
|
||||
use crate::animation::Clock;
|
||||
use crate::backend::tty::SurfaceDmabufFeedback;
|
||||
use crate::backend::{Backend, Headless, RenderResult, Tty, Winit};
|
||||
@@ -390,6 +392,8 @@ pub struct Niri {
|
||||
#[cfg(feature = "dbus")]
|
||||
pub a11y_keyboard_monitor: Option<crate::dbus::freedesktop_a11y::KeyboardMonitor>,
|
||||
#[cfg(feature = "dbus")]
|
||||
pub a11y: A11y,
|
||||
#[cfg(feature = "dbus")]
|
||||
pub inhibit_power_key_fd: Option<zbus::zvariant::OwnedFd>,
|
||||
|
||||
pub ipc_server: Option<IpcServer>,
|
||||
@@ -760,6 +764,10 @@ impl State {
|
||||
self.refresh_ipc_outputs();
|
||||
self.ipc_refresh_layout();
|
||||
self.ipc_refresh_keyboard_layout_index();
|
||||
|
||||
// Needs to be called after updating the keyboard focus.
|
||||
#[cfg(feature = "dbus")]
|
||||
self.niri.refresh_a11y();
|
||||
}
|
||||
|
||||
fn notify_blocker_cleared(&mut self) {
|
||||
@@ -1326,6 +1334,10 @@ impl State {
|
||||
Err(()) => {
|
||||
self.niri.config_error_notification.show();
|
||||
self.niri.queue_redraw_all();
|
||||
|
||||
#[cfg(feature = "dbus")]
|
||||
self.niri.a11y_announce_config_error();
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -2462,6 +2474,9 @@ impl Niri {
|
||||
|
||||
let exit_confirm_dialog = ExitConfirmDialog::new(animation_clock.clone(), config.clone());
|
||||
|
||||
#[cfg(feature = "dbus")]
|
||||
let a11y = A11y::new(event_loop.clone());
|
||||
|
||||
event_loop
|
||||
.insert_source(
|
||||
Timer::from_duration(Duration::from_secs(1)),
|
||||
@@ -2661,6 +2676,8 @@ impl Niri {
|
||||
#[cfg(feature = "dbus")]
|
||||
a11y_keyboard_monitor: None,
|
||||
#[cfg(feature = "dbus")]
|
||||
a11y,
|
||||
#[cfg(feature = "dbus")]
|
||||
inhibit_power_key_fd: None,
|
||||
|
||||
ipc_server,
|
||||
|
@@ -20,9 +20,6 @@ use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement};
|
||||
use crate::utils::{output_size, to_physical_precise_round};
|
||||
|
||||
const TEXT: &str = "Failed to parse the config file. \
|
||||
Please run <span face='monospace' bgcolor='#000000'>niri validate</span> \
|
||||
to see the errors.";
|
||||
const PADDING: i32 = 8;
|
||||
const FONT: &str = "sans 14px";
|
||||
const BORDER: i32 = 4;
|
||||
@@ -186,7 +183,7 @@ fn render(
|
||||
|
||||
let padding: i32 = to_physical_precise_round(scale, PADDING);
|
||||
|
||||
let mut text = String::from(TEXT);
|
||||
let mut text = error_text(true);
|
||||
let mut border_color = (1., 0.3, 0.3);
|
||||
if let Some(path) = created_path {
|
||||
text = format!(
|
||||
@@ -249,3 +246,13 @@ fn render(
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
pub fn error_text(markup: bool) -> String {
|
||||
let command = if markup {
|
||||
"<span face='monospace' bgcolor='#000000'>niri validate</span>"
|
||||
} else {
|
||||
"niri validate"
|
||||
};
|
||||
|
||||
format!("Failed to parse the config file. Please run {command} to see the errors.")
|
||||
}
|
||||
|
@@ -23,8 +23,7 @@ use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderEleme
|
||||
use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement};
|
||||
use crate::utils::{output_size, to_physical_precise_round};
|
||||
|
||||
const TEXT: &str = "Are you sure you want to exit niri?\n\n\
|
||||
Press <span face='mono' bgcolor='#2C2C2C'> Enter </span> to confirm.";
|
||||
const KEY_NAME: &str = "Enter";
|
||||
const PADDING: i32 = 16;
|
||||
const FONT: &str = "sans 14px";
|
||||
const BORDER: i32 = 8;
|
||||
@@ -228,6 +227,8 @@ impl ExitConfirmDialog {
|
||||
fn render(scale: f64) -> anyhow::Result<MemoryBuffer> {
|
||||
let _span = tracy_client::span!("exit_confirm_dialog::render");
|
||||
|
||||
let markup = text(true);
|
||||
|
||||
let padding: i32 = to_physical_precise_round(scale, PADDING);
|
||||
|
||||
let mut font = FontDescription::from_string(FONT);
|
||||
@@ -239,7 +240,7 @@ fn render(scale: f64) -> anyhow::Result<MemoryBuffer> {
|
||||
layout.context().set_round_glyph_positions(false);
|
||||
layout.set_font_description(Some(&font));
|
||||
layout.set_alignment(Alignment::Center);
|
||||
layout.set_markup(TEXT);
|
||||
layout.set_markup(&markup);
|
||||
|
||||
let (mut width, mut height) = layout.pixel_size();
|
||||
width += padding * 2;
|
||||
@@ -255,7 +256,7 @@ fn render(scale: f64) -> anyhow::Result<MemoryBuffer> {
|
||||
layout.context().set_round_glyph_positions(false);
|
||||
layout.set_font_description(Some(&font));
|
||||
layout.set_alignment(Alignment::Center);
|
||||
layout.set_markup(TEXT);
|
||||
layout.set_markup(&markup);
|
||||
|
||||
cr.set_source_rgb(1., 1., 1.);
|
||||
pangocairo::functions::show_layout(&cr, &layout);
|
||||
@@ -282,3 +283,25 @@ fn render(scale: f64) -> anyhow::Result<MemoryBuffer> {
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
fn text(markup: bool) -> String {
|
||||
let key = if markup {
|
||||
format!("<span face='mono' bgcolor='#2C2C2C'> {KEY_NAME} </span>")
|
||||
} else {
|
||||
String::from(KEY_NAME)
|
||||
};
|
||||
|
||||
format!(
|
||||
"Are you sure you want to exit niri?\n\n\
|
||||
Press {key} to confirm."
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "dbus")]
|
||||
pub fn a11y_node() -> accesskit::Node {
|
||||
let mut node = accesskit::Node::new(accesskit::Role::AlertDialog);
|
||||
node.set_label("Exit niri");
|
||||
node.set_description(text(false));
|
||||
node.set_modal();
|
||||
node
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
use std::cell::RefCell;
|
||||
use std::cmp::max;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Write as _;
|
||||
use std::iter::zip;
|
||||
use std::rc::Rc;
|
||||
|
||||
@@ -123,6 +124,32 @@ impl HotkeyOverlay {
|
||||
|
||||
Some(PrimaryGpuTextureRenderElement(elem))
|
||||
}
|
||||
|
||||
pub fn a11y_text(&self) -> String {
|
||||
let config = self.config.borrow();
|
||||
let actions = collect_actions(&config);
|
||||
|
||||
let mut buf = String::new();
|
||||
writeln!(&mut buf, "{TITLE}").unwrap();
|
||||
|
||||
for action in actions {
|
||||
let Some((key, action)) = format_bind(&config.binds.0, action) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let key = key.map(|key| key_name(true, self.mod_key, &key));
|
||||
let key = key.as_deref().unwrap_or("not bound");
|
||||
|
||||
let action = match pango::parse_markup(&action, '\0') {
|
||||
Ok((_attrs, text, _accel)) => text,
|
||||
Err(_) => action.into(),
|
||||
};
|
||||
|
||||
writeln!(&mut buf, "{key} {action}").unwrap();
|
||||
}
|
||||
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
fn format_bind(binds: &[Bind], action: &Action) -> Option<(Option<Key>, String)> {
|
||||
@@ -298,7 +325,7 @@ fn render(
|
||||
.into_iter()
|
||||
.filter_map(|action| format_bind(&config.binds.0, action))
|
||||
.map(|(key, action)| {
|
||||
let key = key.map(|key| key_name(mod_key, &key));
|
||||
let key = key.map(|key| key_name(false, mod_key, &key));
|
||||
let key = key.as_deref().unwrap_or("(not bound)");
|
||||
let key = format!(" {key} ");
|
||||
(key, action)
|
||||
@@ -466,7 +493,7 @@ fn action_name(action: &Action) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn key_name(mod_key: ModKey, key: &Key) -> String {
|
||||
fn key_name(screen_reader: bool, mod_key: ModKey, key: &Key) -> String {
|
||||
let mut name = String::new();
|
||||
|
||||
let has_comp_mod = key.modifiers.contains(Modifiers::COMPOSITOR);
|
||||
@@ -519,7 +546,7 @@ fn key_name(mod_key: ModKey, key: &Key) -> String {
|
||||
}
|
||||
|
||||
let pretty = match key.trigger {
|
||||
Trigger::Keysym(keysym) => prettify_keysym_name(&keysym_get_name(keysym)),
|
||||
Trigger::Keysym(keysym) => prettify_keysym_name(screen_reader, &keysym_get_name(keysym)),
|
||||
Trigger::MouseLeft => String::from("Mouse Left"),
|
||||
Trigger::MouseRight => String::from("Mouse Right"),
|
||||
Trigger::MouseMiddle => String::from("Mouse Middle"),
|
||||
@@ -539,16 +566,24 @@ fn key_name(mod_key: ModKey, key: &Key) -> String {
|
||||
name
|
||||
}
|
||||
|
||||
fn prettify_keysym_name(name: &str) -> String {
|
||||
fn prettify_keysym_name(screen_reader: bool, name: &str) -> String {
|
||||
let name = if screen_reader {
|
||||
name
|
||||
} else {
|
||||
match name {
|
||||
"slash" => "/",
|
||||
"comma" => ",",
|
||||
"period" => ".",
|
||||
"minus" => "-",
|
||||
"equal" => "=",
|
||||
"grave" => "`",
|
||||
"bracketleft" => "[",
|
||||
"bracketright" => "]",
|
||||
_ => name,
|
||||
}
|
||||
};
|
||||
|
||||
let name = match name {
|
||||
"slash" => "/",
|
||||
"comma" => ",",
|
||||
"period" => ".",
|
||||
"minus" => "-",
|
||||
"equal" => "=",
|
||||
"grave" => "`",
|
||||
"bracketleft" => "[",
|
||||
"bracketright" => "]",
|
||||
"Next" => "Page Down",
|
||||
"Prior" => "Page Up",
|
||||
"Print" => "PrtSc",
|
||||
@@ -574,7 +609,7 @@ mod tests {
|
||||
fn check(config: &str, action: Action) -> String {
|
||||
let config = Config::parse("test.kdl", config).unwrap();
|
||||
if let Some((key, title)) = format_bind(&config.binds.0, &action) {
|
||||
let key = key.map(|key| key_name(ModKey::Super, &key));
|
||||
let key = key.map(|key| key_name(false, ModKey::Super, &key));
|
||||
let key = key.as_deref().unwrap_or("(not bound)");
|
||||
format!(" {key} : {title}")
|
||||
} else {
|
||||
|
Reference in New Issue
Block a user