From 2b88f359917604cb14f9ad8667b4b242580d4a8b Mon Sep 17 00:00:00 2001 From: Victoria Brekenfeld Date: Thu, 20 Feb 2025 16:03:09 +0100 Subject: [PATCH] a11y: Add magnifier toggle --- cosmic-applet-a11y/Cargo.toml | 4 + .../i18n/da/cosmic_applet_a11y.ftl | 1 - .../i18n/de/cosmic_applet_a11y.ftl | 4 +- .../i18n/en/cosmic_applet_a11y.ftl | 4 +- .../i18n/es/cosmic_applet_a11y.ftl | 1 - .../i18n/fr/cosmic_applet_a11y.ftl | 1 - .../i18n/ga/cosmic_applet_a11y.ftl | 1 - .../i18n/hu/cosmic_applet_a11y.ftl | 1 - .../i18n/id/cosmic_applet_a11y.ftl | 1 - .../i18n/it/cosmic_applet_a11y.ftl | 1 - .../i18n/nl/cosmic_applet_a11y.ftl | 1 - .../i18n/pl/cosmic_applet_a11y.ftl | 1 - .../i18n/pt-BR/cosmic_applet_a11y.ftl | 1 - .../i18n/pt/cosmic_applet_a11y.ftl | 1 - .../i18n/ro/cosmic_applet_a11y.ftl | 1 - .../i18n/sv/cosmic_applet_a11y.ftl | 1 - cosmic-applet-a11y/src/app.rs | 128 +++++++++++----- cosmic-applet-a11y/src/backend/dbus.rs | 139 +++++++++++++++++ cosmic-applet-a11y/src/backend/mod.rs | 141 +----------------- cosmic-applet-a11y/src/backend/wayland/mod.rs | 96 ++++++++++++ .../src/backend/wayland/thread.rs | 123 +++++++++++++++ 21 files changed, 461 insertions(+), 191 deletions(-) delete mode 100644 cosmic-applet-a11y/i18n/da/cosmic_applet_a11y.ftl delete mode 100644 cosmic-applet-a11y/i18n/es/cosmic_applet_a11y.ftl delete mode 100644 cosmic-applet-a11y/i18n/fr/cosmic_applet_a11y.ftl delete mode 100644 cosmic-applet-a11y/i18n/ga/cosmic_applet_a11y.ftl delete mode 100644 cosmic-applet-a11y/i18n/hu/cosmic_applet_a11y.ftl delete mode 100644 cosmic-applet-a11y/i18n/id/cosmic_applet_a11y.ftl delete mode 100644 cosmic-applet-a11y/i18n/it/cosmic_applet_a11y.ftl delete mode 100644 cosmic-applet-a11y/i18n/nl/cosmic_applet_a11y.ftl delete mode 100644 cosmic-applet-a11y/i18n/pl/cosmic_applet_a11y.ftl delete mode 100644 cosmic-applet-a11y/i18n/pt-BR/cosmic_applet_a11y.ftl delete mode 100644 cosmic-applet-a11y/i18n/pt/cosmic_applet_a11y.ftl delete mode 100644 cosmic-applet-a11y/i18n/ro/cosmic_applet_a11y.ftl delete mode 100644 cosmic-applet-a11y/i18n/sv/cosmic_applet_a11y.ftl create mode 100644 cosmic-applet-a11y/src/backend/dbus.rs create mode 100644 cosmic-applet-a11y/src/backend/wayland/mod.rs create mode 100644 cosmic-applet-a11y/src/backend/wayland/thread.rs diff --git a/cosmic-applet-a11y/Cargo.toml b/cosmic-applet-a11y/Cargo.toml index c85ce820..b7e831a7 100644 --- a/cosmic-applet-a11y/Cargo.toml +++ b/cosmic-applet-a11y/Cargo.toml @@ -5,6 +5,10 @@ edition = "2021" [dependencies] cosmic-dbus-a11y = { git = "https://github.com/pop-os/dbus-settings-bindings" } + +anyhow.workspace = true +cctk.workspace = true +cosmic-protocols.workspace = true cosmic-time.workspace = true i18n-embed-fl.workspace = true i18n-embed.workspace = true diff --git a/cosmic-applet-a11y/i18n/da/cosmic_applet_a11y.ftl b/cosmic-applet-a11y/i18n/da/cosmic_applet_a11y.ftl deleted file mode 100644 index 69270f5d..00000000 --- a/cosmic-applet-a11y/i18n/da/cosmic_applet_a11y.ftl +++ /dev/null @@ -1 +0,0 @@ -accessibility = Tilgængelighed diff --git a/cosmic-applet-a11y/i18n/de/cosmic_applet_a11y.ftl b/cosmic-applet-a11y/i18n/de/cosmic_applet_a11y.ftl index e1ea752c..6580910b 100644 --- a/cosmic-applet-a11y/i18n/de/cosmic_applet_a11y.ftl +++ b/cosmic-applet-a11y/i18n/de/cosmic_applet_a11y.ftl @@ -1 +1,3 @@ -accessibility = Zugänglichkeit +screen-reader = Bildschirmleser +magnifier = Bildschirmlupe +settings = Bedienungshilfen Einstellungen... diff --git a/cosmic-applet-a11y/i18n/en/cosmic_applet_a11y.ftl b/cosmic-applet-a11y/i18n/en/cosmic_applet_a11y.ftl index 902732ed..c386db3e 100644 --- a/cosmic-applet-a11y/i18n/en/cosmic_applet_a11y.ftl +++ b/cosmic-applet-a11y/i18n/en/cosmic_applet_a11y.ftl @@ -1 +1,3 @@ -accessibility = Accessibility +screen-reader = Screen reader +magnifier = Magnifier +settings = Accessibility settings... \ No newline at end of file diff --git a/cosmic-applet-a11y/i18n/es/cosmic_applet_a11y.ftl b/cosmic-applet-a11y/i18n/es/cosmic_applet_a11y.ftl deleted file mode 100644 index 72cf7d90..00000000 --- a/cosmic-applet-a11y/i18n/es/cosmic_applet_a11y.ftl +++ /dev/null @@ -1 +0,0 @@ -accessibility = Accesibilidad diff --git a/cosmic-applet-a11y/i18n/fr/cosmic_applet_a11y.ftl b/cosmic-applet-a11y/i18n/fr/cosmic_applet_a11y.ftl deleted file mode 100644 index cdf70e19..00000000 --- a/cosmic-applet-a11y/i18n/fr/cosmic_applet_a11y.ftl +++ /dev/null @@ -1 +0,0 @@ -accessibility = Accessibilité diff --git a/cosmic-applet-a11y/i18n/ga/cosmic_applet_a11y.ftl b/cosmic-applet-a11y/i18n/ga/cosmic_applet_a11y.ftl deleted file mode 100644 index 5aecc7f2..00000000 --- a/cosmic-applet-a11y/i18n/ga/cosmic_applet_a11y.ftl +++ /dev/null @@ -1 +0,0 @@ -accessibility = Inrochtaineacht diff --git a/cosmic-applet-a11y/i18n/hu/cosmic_applet_a11y.ftl b/cosmic-applet-a11y/i18n/hu/cosmic_applet_a11y.ftl deleted file mode 100644 index 7a41d892..00000000 --- a/cosmic-applet-a11y/i18n/hu/cosmic_applet_a11y.ftl +++ /dev/null @@ -1 +0,0 @@ -accessibility = Akadálymentesítés diff --git a/cosmic-applet-a11y/i18n/id/cosmic_applet_a11y.ftl b/cosmic-applet-a11y/i18n/id/cosmic_applet_a11y.ftl deleted file mode 100644 index 71ef3fdc..00000000 --- a/cosmic-applet-a11y/i18n/id/cosmic_applet_a11y.ftl +++ /dev/null @@ -1 +0,0 @@ -accessibility = Aksesibilitas diff --git a/cosmic-applet-a11y/i18n/it/cosmic_applet_a11y.ftl b/cosmic-applet-a11y/i18n/it/cosmic_applet_a11y.ftl deleted file mode 100644 index dd95ddc9..00000000 --- a/cosmic-applet-a11y/i18n/it/cosmic_applet_a11y.ftl +++ /dev/null @@ -1 +0,0 @@ -accessibility = Accessibilità diff --git a/cosmic-applet-a11y/i18n/nl/cosmic_applet_a11y.ftl b/cosmic-applet-a11y/i18n/nl/cosmic_applet_a11y.ftl deleted file mode 100644 index eca2a580..00000000 --- a/cosmic-applet-a11y/i18n/nl/cosmic_applet_a11y.ftl +++ /dev/null @@ -1 +0,0 @@ -accessibility = Toegankelijkheid diff --git a/cosmic-applet-a11y/i18n/pl/cosmic_applet_a11y.ftl b/cosmic-applet-a11y/i18n/pl/cosmic_applet_a11y.ftl deleted file mode 100644 index a98d2c48..00000000 --- a/cosmic-applet-a11y/i18n/pl/cosmic_applet_a11y.ftl +++ /dev/null @@ -1 +0,0 @@ -accessibility = Dostępność diff --git a/cosmic-applet-a11y/i18n/pt-BR/cosmic_applet_a11y.ftl b/cosmic-applet-a11y/i18n/pt-BR/cosmic_applet_a11y.ftl deleted file mode 100644 index bcad2b6e..00000000 --- a/cosmic-applet-a11y/i18n/pt-BR/cosmic_applet_a11y.ftl +++ /dev/null @@ -1 +0,0 @@ -accessibility = Acessibilidade diff --git a/cosmic-applet-a11y/i18n/pt/cosmic_applet_a11y.ftl b/cosmic-applet-a11y/i18n/pt/cosmic_applet_a11y.ftl deleted file mode 100644 index bcad2b6e..00000000 --- a/cosmic-applet-a11y/i18n/pt/cosmic_applet_a11y.ftl +++ /dev/null @@ -1 +0,0 @@ -accessibility = Acessibilidade diff --git a/cosmic-applet-a11y/i18n/ro/cosmic_applet_a11y.ftl b/cosmic-applet-a11y/i18n/ro/cosmic_applet_a11y.ftl deleted file mode 100644 index 4a969617..00000000 --- a/cosmic-applet-a11y/i18n/ro/cosmic_applet_a11y.ftl +++ /dev/null @@ -1 +0,0 @@ -accessibility = Accesibilitate diff --git a/cosmic-applet-a11y/i18n/sv/cosmic_applet_a11y.ftl b/cosmic-applet-a11y/i18n/sv/cosmic_applet_a11y.ftl deleted file mode 100644 index 8e361989..00000000 --- a/cosmic-applet-a11y/i18n/sv/cosmic_applet_a11y.ftl +++ /dev/null @@ -1 +0,0 @@ -accessibility = Tillgänglighet diff --git a/cosmic-applet-a11y/src/app.rs b/cosmic-applet-a11y/src/app.rs index 34b89322..b267238b 100644 --- a/cosmic-applet-a11y/src/app.rs +++ b/cosmic-applet-a11y/src/app.rs @@ -2,27 +2,35 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::{ - backend::{self, A11yRequest}, + backend::{ + self, + dbus::{DBusRequest, DBusUpdate}, + wayland::{AccessibilityEvent, AccessibilityRequest, WaylandUpdate}, + }, fl, }; use cosmic::{ applet::{ - padded_control, + menu_button, padded_control, token::subscription::{activation_token_subscription, TokenRequest, TokenUpdate}, }, - cctk::sctk::reexports::calloop, + cctk::sctk::reexports::calloop::channel, + cosmic_theme::Spacing, iced::{ platform_specific::shell::wayland::commands::popup::{destroy_popup, get_popup}, window, Length, Subscription, }, iced_runtime::core::layout::Limits, - widget::container, + iced_widget::column, + theme, + widget::{divider, text}, Element, Task, }; use cosmic_time::{anim, chain, id, once_cell::sync::Lazy, Instant, Timeline}; use tokio::sync::mpsc::UnboundedSender; -static ENABLED: Lazy = Lazy::new(id::Toggler::unique); +static READER_TOGGLE: Lazy = Lazy::new(id::Toggler::unique); +static MAGNIFIER_TOGGLE: Lazy = Lazy::new(id::Toggler::unique); pub fn run() -> cosmic::iced::Result { cosmic::applet::run::(()) @@ -31,24 +39,26 @@ pub fn run() -> cosmic::iced::Result { #[derive(Clone, Default)] struct CosmicA11yApplet { core: cosmic::app::Core, - icon_name: String, - a11y_enabled: bool, + reader_enabled: bool, + magnifier_enabled: bool, popup: Option, - a11y_sender: Option>, + dbus_sender: Option>, + wayland_sender: Option>, timeline: Timeline, - token_tx: Option>, + token_tx: Option>, } #[derive(Debug, Clone)] enum Message { TogglePopup, CloseRequested(window::Id), - Errored(String), - Enabled(chain::Toggler, bool), + ScreenReaderEnabled(chain::Toggler, bool), + MagnifierEnabled(chain::Toggler, bool), Frame(Instant), Token(TokenUpdate), OpenSettings, - Update(backend::Update), + DBusUpdate(DBusUpdate), + WaylandUpdate(WaylandUpdate), } impl cosmic::Application for CosmicA11yApplet { @@ -89,16 +99,23 @@ impl cosmic::Application for CosmicA11yApplet { ) -> cosmic::iced::Task> { match message { Message::Frame(now) => self.timeline.now(now), - Message::Enabled(chain, enabled) => { - self.timeline.set_chain(chain).start(); - self.a11y_enabled = enabled; - - if let Some(tx) = &self.a11y_sender { - let _ = tx.send(A11yRequest::Status(enabled)); + Message::ScreenReaderEnabled(chain, enabled) => { + if let Some(tx) = &self.dbus_sender { + self.timeline.set_chain(chain).start(); + self.reader_enabled = enabled; + let _ = tx.send(DBusRequest::Status(enabled)); + } else { + self.reader_enabled = false; } } - Message::Errored(why) => { - tracing::error!("{}", why); + Message::MagnifierEnabled(chain, enabled) => { + if let Some(tx) = &self.wayland_sender { + self.timeline.set_chain(chain).start(); + self.magnifier_enabled = enabled; + let _ = tx.send(AccessibilityRequest::Magnifier(enabled)); + } else { + self.magnifier_enabled = false; + } } Message::TogglePopup => { if let Some(p) = self.popup.take() { @@ -131,7 +148,7 @@ impl cosmic::Application for CosmicA11yApplet { } } Message::OpenSettings => { - let exec = "cosmic-settings a11y".to_string(); + let exec = "cosmic-settings accessibility".to_string(); if let Some(tx) = self.token_tx.as_ref() { let _ = tx.send(TokenRequest { app_id: Self::APP_ID.to_string(), @@ -150,7 +167,7 @@ impl cosmic::Application for CosmicA11yApplet { } TokenUpdate::ActivationToken { token, .. } => { let mut cmd = std::process::Command::new("cosmic-settings"); - cmd.arg("a11y"); + cmd.arg("accessibility"); if let Some(token) = token { cmd.env("XDG_ACTIVATION_TOKEN", &token); cmd.env("DESKTOP_STARTUP_ID", &token); @@ -158,16 +175,31 @@ impl cosmic::Application for CosmicA11yApplet { tokio::spawn(cosmic::process::spawn(cmd)); } }, - Message::Update(update) => match update { - backend::Update::Error(err) => { + Message::DBusUpdate(update) => match update { + DBusUpdate::Error(err) => { tracing::error!("{err}"); + let _ = self.dbus_sender.take(); + self.reader_enabled = false; } - backend::Update::Status(enabled) => { - self.a11y_enabled = enabled; + DBusUpdate::Status(enabled) => { + self.reader_enabled = enabled; } - backend::Update::Init(enabled, tx) => { - self.a11y_enabled = enabled; - self.a11y_sender = Some(tx); + DBusUpdate::Init(enabled, tx) => { + self.reader_enabled = enabled; + self.dbus_sender = Some(tx); + } + }, + Message::WaylandUpdate(update) => match update { + WaylandUpdate::Errored => { + tracing::error!("Wayland error"); + let _ = self.wayland_sender.take(); + self.magnifier_enabled = false; + } + WaylandUpdate::State(AccessibilityEvent::Magnifier(enabled)) => { + self.magnifier_enabled = enabled; + } + WaylandUpdate::Started(tx) => { + self.wayland_sender = Some(tx); } }, } @@ -183,22 +215,43 @@ impl cosmic::Application for CosmicA11yApplet { } fn view_window(&self, _id: window::Id) -> Element { - let toggle = padded_control( + let Spacing { + space_xxs, space_s, .. + } = theme::active().cosmic().spacing; + + let reader_toggle = padded_control( anim!( - //toggler - ENABLED, + READER_TOGGLE, &self.timeline, - fl!("accessibility"), - self.a11y_enabled, - Message::Enabled, + fl!("screen-reader"), + self.reader_enabled, + Message::ScreenReaderEnabled, + ) + .text_size(14) + .width(Length::Fill), + ); + let magnifier_toggle = padded_control( + anim!( + MAGNIFIER_TOGGLE, + &self.timeline, + fl!("magnifier"), + self.magnifier_enabled, + Message::MagnifierEnabled, ) .text_size(14) .width(Length::Fill), ); + let content_list = column![ + reader_toggle, + magnifier_toggle, + padded_control(divider::horizontal::default()).padding([space_xxs, space_s]), + menu_button(text::body(fl!("settings"))).on_press(Message::OpenSettings) + ] + .padding([8, 0]); self.core .applet - .popup_container(container(toggle).padding([8, 0])) + .popup_container(content_list) .max_width(372.) .max_height(600.) .into() @@ -206,7 +259,8 @@ impl cosmic::Application for CosmicA11yApplet { fn subscription(&self) -> Subscription { Subscription::batch(vec![ - backend::subscription().map(Message::Update), + backend::dbus::subscription().map(Message::DBusUpdate), + backend::wayland::a11y_subscription().map(Message::WaylandUpdate), self.timeline .as_subscription() .map(|(_, now)| Message::Frame(now)), diff --git a/cosmic-applet-a11y/src/backend/dbus.rs b/cosmic-applet-a11y/src/backend/dbus.rs new file mode 100644 index 00000000..1a1b0d1e --- /dev/null +++ b/cosmic-applet-a11y/src/backend/dbus.rs @@ -0,0 +1,139 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: GPL-3.0-only + +use cosmic::iced::futures::FutureExt; +use cosmic::{ + iced::{ + self, + futures::{self, select, SinkExt, StreamExt}, + Subscription, + }, + iced_futures::stream, +}; +use cosmic_dbus_a11y::*; +use std::fmt::Debug; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; +use zbus::Connection; + +#[derive(Debug, Clone)] +pub enum DBusUpdate { + Error(String), + Status(bool), + Init(bool, UnboundedSender), +} + +pub enum DBusRequest { + Status(bool), +} + +#[derive(Debug)] +pub enum State { + Ready, + Waiting(Connection, u8, bool, UnboundedReceiver), + Finished, +} + +pub fn subscription() -> iced::Subscription { + struct MyId; + + Subscription::run_with_id( + std::any::TypeId::of::(), + stream::channel(50, move |mut output| async move { + let mut state = State::Ready; + + loop { + state = start_listening(state, &mut output).await; + } + }), + ) +} + +async fn start_listening( + state: State, + output: &mut futures::channel::mpsc::Sender, +) -> State { + match state { + State::Ready => { + let conn = match Connection::session().await.map_err(|e| e.to_string()) { + Ok(conn) => conn, + Err(e) => { + _ = output.send(DBusUpdate::Error(e)).await; + return State::Finished; + } + }; + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + let mut enabled = false; + if let Ok(proxy) = StatusProxy::new(&conn).await { + if let Ok(status) = proxy.screen_reader_enabled().await { + enabled = status; + } + } + _ = output.send(DBusUpdate::Init(enabled, tx)).await; + State::Waiting(conn, 20, enabled, rx) + } + State::Waiting(conn, mut retry, mut enabled, mut rx) => { + let Ok(proxy) = StatusProxy::new(&conn).await else { + if retry == 0 { + tracing::error!("Accessibility Status is unavailable."); + return State::Finished; + } else { + _ = tokio::time::sleep(tokio::time::Duration::from_secs( + 2_u64.pow(retry as u32), + )) + .await; + retry -= 1; + return State::Waiting(conn, retry, enabled, rx); + } + }; + retry = 20; + + let mut watch_changes = proxy.receive_screen_reader_enabled_changed().await; + + if let Ok(status) = proxy.screen_reader_enabled().await { + if enabled != status { + _ = output.send(DBusUpdate::Status(enabled)); + } + enabled = status; + } + + loop { + if let Ok(status) = proxy.screen_reader_enabled().await { + if enabled != status { + _ = output.send(DBusUpdate::Status(enabled)); + } + enabled = status; + } + + let mut next_change = Box::pin(watch_changes.next()).fuse(); + let mut next_request = Box::pin(rx.recv()).fuse(); + + select! { + v = next_request => { + match v { + Some(DBusRequest::Status(is_enabled)) => { + // Set status + enabled = is_enabled; + _ = proxy.set_is_enabled(is_enabled).await; + _ = proxy.set_screen_reader_enabled(is_enabled).await; + } + None => return State::Finished, + } + } + v = next_change => { + match v { + Some(f) => { + if let Ok(enabled) = f.get().await { + _ = output.send(DBusUpdate::Status(enabled)); + } + } + None => break, + }; + } + } + } + + State::Waiting(conn, retry, enabled, rx) + } + State::Finished => iced::futures::future::pending().await, + } +} diff --git a/cosmic-applet-a11y/src/backend/mod.rs b/cosmic-applet-a11y/src/backend/mod.rs index 7b80a14a..fd96a1f5 100644 --- a/cosmic-applet-a11y/src/backend/mod.rs +++ b/cosmic-applet-a11y/src/backend/mod.rs @@ -1,139 +1,2 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: GPL-3.0-only - -use cosmic::iced::futures::FutureExt; -use cosmic::{ - iced::{ - self, - futures::{self, select, SinkExt, StreamExt}, - Subscription, - }, - iced_futures::stream, -}; -use cosmic_dbus_a11y::*; -use std::fmt::Debug; -use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; -use zbus::Connection; - -#[derive(Debug, Clone)] -pub enum Update { - Error(String), - Status(bool), - Init(bool, UnboundedSender), -} - -pub enum A11yRequest { - Status(bool), -} - -#[derive(Debug)] -pub enum State { - Ready, - Waiting(Connection, u8, bool, UnboundedReceiver), - Finished, -} - -pub fn subscription() -> iced::Subscription { - struct MyId; - - Subscription::run_with_id( - std::any::TypeId::of::(), - stream::channel(50, move |mut output| async move { - let mut state = State::Ready; - - loop { - state = start_listening(state, &mut output).await; - } - }), - ) -} - -async fn start_listening( - state: State, - output: &mut futures::channel::mpsc::Sender, -) -> State { - match state { - State::Ready => { - let conn = match Connection::session().await.map_err(|e| e.to_string()) { - Ok(conn) => conn, - Err(e) => { - _ = output.send(Update::Error(e)).await; - return State::Finished; - } - }; - let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); - let mut enabled = false; - if let Ok(proxy) = StatusProxy::new(&conn).await { - if let Ok(status) = proxy.screen_reader_enabled().await { - enabled = status; - } - } - _ = output.send(Update::Init(enabled, tx)).await; - State::Waiting(conn, 20, enabled, rx) - } - State::Waiting(conn, mut retry, mut enabled, mut rx) => { - let Ok(proxy) = StatusProxy::new(&conn).await else { - if retry == 0 { - tracing::error!("Accessibility Status is unavailable."); - return State::Finished; - } else { - _ = tokio::time::sleep(tokio::time::Duration::from_secs( - 2_u64.pow(retry as u32), - )) - .await; - retry -= 1; - return State::Waiting(conn, retry, enabled, rx); - } - }; - retry = 20; - - let mut watch_changes = proxy.receive_screen_reader_enabled_changed().await; - - if let Ok(status) = proxy.screen_reader_enabled().await { - if enabled != status { - _ = output.send(Update::Status(enabled)); - } - enabled = status; - } - - loop { - if let Ok(status) = proxy.screen_reader_enabled().await { - if enabled != status { - _ = output.send(Update::Status(enabled)); - } - enabled = status; - } - - let mut next_change = Box::pin(watch_changes.next()).fuse(); - let mut next_request = Box::pin(rx.recv()).fuse(); - - select! { - v = next_request => { - match v { - Some(A11yRequest::Status(is_enabled)) => { - // Set status - enabled = is_enabled; - _ = proxy.set_is_enabled(is_enabled).await; - _ = proxy.set_screen_reader_enabled(is_enabled).await; - } - None => return State::Finished, - } - } - v = next_change => { - match v { - Some(f) => { - if let Ok(enabled) = f.get().await { - _ = output.send(Update::Status(enabled)); - } - } - None => break, - }; - } - } - } - - State::Waiting(conn, retry, enabled, rx) - } - State::Finished => iced::futures::future::pending().await, - } -} +pub mod dbus; +pub mod wayland; diff --git a/cosmic-applet-a11y/src/backend/wayland/mod.rs b/cosmic-applet-a11y/src/backend/wayland/mod.rs new file mode 100644 index 00000000..0981b112 --- /dev/null +++ b/cosmic-applet-a11y/src/backend/wayland/mod.rs @@ -0,0 +1,96 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: GPL-3.0-only + +use anyhow; +use cctk::sctk::reexports::calloop::channel::SyncSender; +use cosmic::iced::{ + self, + futures::{self, channel::mpsc, SinkExt, StreamExt}, + stream, Subscription, +}; +use once_cell::sync::Lazy; +use tokio::sync::Mutex; + +mod thread; + +pub static WAYLAND_RX: Lazy>>> = + Lazy::new(|| Mutex::new(None)); + +#[derive(Debug, Clone)] +pub enum WaylandUpdate { + State(AccessibilityEvent), + Started(SyncSender), + Errored, +} + +#[derive(Debug, Clone, Copy)] +pub enum AccessibilityEvent { + Magnifier(bool), +} + +#[derive(Debug, Clone, Copy)] +pub enum AccessibilityRequest { + Magnifier(bool), +} + +pub fn a11y_subscription() -> iced::Subscription { + Subscription::run_with_id( + std::any::TypeId::of::(), + stream::channel(50, move |mut output| async move { + let mut state = State::Waiting; + + loop { + state = start_listening(state, &mut output).await; + } + }), + ) +} + +async fn start_listening( + state: State, + output: &mut futures::channel::mpsc::Sender, +) -> State { + match state { + State::Waiting => { + let mut guard = WAYLAND_RX.lock().await; + let rx = { + if guard.is_none() { + if let Ok(WaylandWatcher { rx, tx }) = WaylandWatcher::new() { + *guard = Some(rx); + _ = output.send(WaylandUpdate::Started(tx)).await; + } else { + _ = output.send(WaylandUpdate::Errored).await; + return State::Error; + } + } + guard.as_mut().unwrap() + }; + if let Some(w) = rx.next().await { + _ = output.send(WaylandUpdate::State(w)).await; + State::Waiting + } else { + _ = output.send(WaylandUpdate::Errored).await; + State::Error + } + } + State::Error => cosmic::iced::futures::future::pending().await, + } +} + +pub enum State { + Waiting, + Error, +} + +pub struct WaylandWatcher { + rx: mpsc::Receiver, + tx: SyncSender, +} + +impl WaylandWatcher { + pub fn new() -> anyhow::Result { + let (tx, rx) = mpsc::channel(20); + let tx = thread::spawn_a11y(tx)?; + Ok(Self { tx, rx }) + } +} diff --git a/cosmic-applet-a11y/src/backend/wayland/thread.rs b/cosmic-applet-a11y/src/backend/wayland/thread.rs new file mode 100644 index 00000000..85d1e4a1 --- /dev/null +++ b/cosmic-applet-a11y/src/backend/wayland/thread.rs @@ -0,0 +1,123 @@ +// Copyright 2025 System76 +// SPDX-License-Identifier: GPL-3.0-only + +use calloop::channel::*; +use cctk::{ + sctk::{ + self, + reexports::{ + calloop::{self, channel}, + calloop_wayland_source::WaylandSource, + }, + registry::RegistryState, + }, + wayland_client::{self, globals::GlobalListContents, protocol::wl_registry, Dispatch, Proxy}, +}; +use cosmic::iced::futures::{self, SinkExt}; +use cosmic_protocols::a11y::v1::client::cosmic_a11y_manager_v1; +use futures::{channel::mpsc, executor::block_on}; +use wayland_client::{globals::registry_queue_init, Connection}; + +use super::{AccessibilityEvent, AccessibilityRequest}; + +pub fn spawn_a11y( + tx: mpsc::Sender, +) -> anyhow::Result> { + let (a11y_tx, a11y_rx) = calloop::channel::sync_channel(100); + let conn = Connection::connect_to_env()?; + + std::thread::spawn(move || { + struct State { + loop_signal: calloop::LoopSignal, + tx: mpsc::Sender, + global: cosmic_a11y_manager_v1::CosmicA11yManagerV1, + + magnifier: bool, + } + + impl Dispatch for State { + fn event( + state: &mut Self, + _proxy: &cosmic_a11y_manager_v1::CosmicA11yManagerV1, + event: ::Event, + _data: &(), + _conn: &Connection, + _qhandle: &sctk::reexports::client::QueueHandle, + ) { + match event { + cosmic_a11y_manager_v1::Event::Magnifier { active } => { + let magnifier = active + .into_result() + .unwrap_or(cosmic_a11y_manager_v1::ActiveState::Disabled) + == cosmic_a11y_manager_v1::ActiveState::Enabled; + if magnifier != state.magnifier { + if block_on(state.tx.send(AccessibilityEvent::Magnifier(magnifier))) + .is_err() + { + state.loop_signal.stop(); + state.loop_signal.wakeup(); + }; + state.magnifier = magnifier; + } + } + _ => unreachable!(), + } + } + } + impl Dispatch for State { + fn event( + _state: &mut Self, + _proxy: &wl_registry::WlRegistry, + _event: ::Event, + _data: &GlobalListContents, + _conn: &Connection, + _qhandle: &sctk::reexports::client::QueueHandle, + ) { + // We don't care about any dynamic globals + } + } + + let mut event_loop = calloop::EventLoop::::try_new().unwrap(); + + let loop_handle = event_loop.handle(); + let (globals, event_queue) = registry_queue_init(&conn).unwrap(); + let qhandle = event_queue.handle(); + + WaylandSource::new(conn, event_queue) + .insert(loop_handle.clone()) + .unwrap(); + + let registry_state = RegistryState::new(&globals); + let global = registry_state + .bind_one::(&qhandle, 1..=1, ()) + .unwrap(); + + loop_handle + .insert_source(a11y_rx, |request, _, state| match request { + channel::Event::Msg(AccessibilityRequest::Magnifier(val)) => { + state.global.set_magnifier(if val { + cosmic_a11y_manager_v1::ActiveState::Enabled + } else { + cosmic_a11y_manager_v1::ActiveState::Disabled + }); + } + channel::Event::Closed => { + state.loop_signal.stop(); + state.loop_signal.wakeup(); + } + }) + .unwrap(); + + let mut state = State { + loop_signal: event_loop.get_signal(), + tx, + global, + + magnifier: false, + }; + + event_loop.run(None, &mut state, |_| {}).unwrap(); + }); + + Ok(a11y_tx) +}