aboutsummaryrefslogtreecommitdiff
path: root/src/ui
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2025-08-21 15:02:25 +0300
committerIvan Molodetskikh <yalterz@gmail.com>2025-08-26 21:03:54 +0300
commit1f76dce345153d7da95262773c317446c1f6fc32 (patch)
tree518ba49abb67f2d9f2f32d8201662c8343aa19fe /src/ui
parente1afa712385bc4f2124a3bb7438743d3fdc1854a (diff)
downloadniri-1f76dce345153d7da95262773c317446c1f6fc32.tar.gz
niri-1f76dce345153d7da95262773c317446c1f6fc32.tar.bz2
niri-1f76dce345153d7da95262773c317446c1f6fc32.zip
Implement screen reader announcements via AccessKit
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/config_error_notification.rs15
-rw-r--r--src/ui/exit_confirm_dialog.rs31
-rw-r--r--src/ui/hotkey_overlay.rs61
3 files changed, 86 insertions, 21 deletions
diff --git a/src/ui/config_error_notification.rs b/src/ui/config_error_notification.rs
index 20667172..c6928b6b 100644
--- a/src/ui/config_error_notification.rs
+++ b/src/ui/config_error_notification.rs
@@ -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.")
+}
diff --git a/src/ui/exit_confirm_dialog.rs b/src/ui/exit_confirm_dialog.rs
index e0cc9398..0147cf39 100644
--- a/src/ui/exit_confirm_dialog.rs
+++ b/src/ui/exit_confirm_dialog.rs
@@ -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
+}
diff --git a/src/ui/hotkey_overlay.rs b/src/ui/hotkey_overlay.rs
index 247f44f6..6b7a4cd4 100644
--- a/src/ui/hotkey_overlay.rs
+++ b/src/ui/hotkey_overlay.rs
@@ -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 {