diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2025-08-21 15:02:25 +0300 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2025-08-26 21:03:54 +0300 |
| commit | 1f76dce345153d7da95262773c317446c1f6fc32 (patch) | |
| tree | 518ba49abb67f2d9f2f32d8201662c8343aa19fe /src/ui | |
| parent | e1afa712385bc4f2124a3bb7438743d3fdc1854a (diff) | |
| download | niri-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.rs | 15 | ||||
| -rw-r--r-- | src/ui/exit_confirm_dialog.rs | 31 | ||||
| -rw-r--r-- | src/ui/hotkey_overlay.rs | 61 |
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 { |
