a11y: Add magnifier toggle
This commit is contained in:
parent
257784ebc8
commit
2b88f35991
21 changed files with 461 additions and 191 deletions
|
|
@ -5,6 +5,10 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cosmic-dbus-a11y = { git = "https://github.com/pop-os/dbus-settings-bindings" }
|
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
|
cosmic-time.workspace = true
|
||||||
i18n-embed-fl.workspace = true
|
i18n-embed-fl.workspace = true
|
||||||
i18n-embed.workspace = true
|
i18n-embed.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
accessibility = Tilgængelighed
|
|
||||||
|
|
@ -1 +1,3 @@
|
||||||
accessibility = Zugänglichkeit
|
screen-reader = Bildschirmleser
|
||||||
|
magnifier = Bildschirmlupe
|
||||||
|
settings = Bedienungshilfen Einstellungen...
|
||||||
|
|
|
||||||
|
|
@ -1 +1,3 @@
|
||||||
accessibility = Accessibility
|
screen-reader = Screen reader
|
||||||
|
magnifier = Magnifier
|
||||||
|
settings = Accessibility settings...
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
accessibility = Accesibilidad
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
accessibility = Accessibilité
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
accessibility = Inrochtaineacht
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
accessibility = Akadálymentesítés
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
accessibility = Aksesibilitas
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
accessibility = Accessibilità
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
accessibility = Toegankelijkheid
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
accessibility = Dostępność
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
accessibility = Acessibilidade
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
accessibility = Acessibilidade
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
accessibility = Accesibilitate
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
accessibility = Tillgänglighet
|
|
||||||
|
|
@ -2,27 +2,35 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::{self, A11yRequest},
|
backend::{
|
||||||
|
self,
|
||||||
|
dbus::{DBusRequest, DBusUpdate},
|
||||||
|
wayland::{AccessibilityEvent, AccessibilityRequest, WaylandUpdate},
|
||||||
|
},
|
||||||
fl,
|
fl,
|
||||||
};
|
};
|
||||||
use cosmic::{
|
use cosmic::{
|
||||||
applet::{
|
applet::{
|
||||||
padded_control,
|
menu_button, padded_control,
|
||||||
token::subscription::{activation_token_subscription, TokenRequest, TokenUpdate},
|
token::subscription::{activation_token_subscription, TokenRequest, TokenUpdate},
|
||||||
},
|
},
|
||||||
cctk::sctk::reexports::calloop,
|
cctk::sctk::reexports::calloop::channel,
|
||||||
|
cosmic_theme::Spacing,
|
||||||
iced::{
|
iced::{
|
||||||
platform_specific::shell::wayland::commands::popup::{destroy_popup, get_popup},
|
platform_specific::shell::wayland::commands::popup::{destroy_popup, get_popup},
|
||||||
window, Length, Subscription,
|
window, Length, Subscription,
|
||||||
},
|
},
|
||||||
iced_runtime::core::layout::Limits,
|
iced_runtime::core::layout::Limits,
|
||||||
widget::container,
|
iced_widget::column,
|
||||||
|
theme,
|
||||||
|
widget::{divider, text},
|
||||||
Element, Task,
|
Element, Task,
|
||||||
};
|
};
|
||||||
use cosmic_time::{anim, chain, id, once_cell::sync::Lazy, Instant, Timeline};
|
use cosmic_time::{anim, chain, id, once_cell::sync::Lazy, Instant, Timeline};
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
static ENABLED: Lazy<id::Toggler> = Lazy::new(id::Toggler::unique);
|
static READER_TOGGLE: Lazy<id::Toggler> = Lazy::new(id::Toggler::unique);
|
||||||
|
static MAGNIFIER_TOGGLE: Lazy<id::Toggler> = Lazy::new(id::Toggler::unique);
|
||||||
|
|
||||||
pub fn run() -> cosmic::iced::Result {
|
pub fn run() -> cosmic::iced::Result {
|
||||||
cosmic::applet::run::<CosmicA11yApplet>(())
|
cosmic::applet::run::<CosmicA11yApplet>(())
|
||||||
|
|
@ -31,24 +39,26 @@ pub fn run() -> cosmic::iced::Result {
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
struct CosmicA11yApplet {
|
struct CosmicA11yApplet {
|
||||||
core: cosmic::app::Core,
|
core: cosmic::app::Core,
|
||||||
icon_name: String,
|
reader_enabled: bool,
|
||||||
a11y_enabled: bool,
|
magnifier_enabled: bool,
|
||||||
popup: Option<window::Id>,
|
popup: Option<window::Id>,
|
||||||
a11y_sender: Option<UnboundedSender<backend::A11yRequest>>,
|
dbus_sender: Option<UnboundedSender<DBusRequest>>,
|
||||||
|
wayland_sender: Option<channel::SyncSender<AccessibilityRequest>>,
|
||||||
timeline: Timeline,
|
timeline: Timeline,
|
||||||
token_tx: Option<calloop::channel::Sender<TokenRequest>>,
|
token_tx: Option<channel::Sender<TokenRequest>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum Message {
|
enum Message {
|
||||||
TogglePopup,
|
TogglePopup,
|
||||||
CloseRequested(window::Id),
|
CloseRequested(window::Id),
|
||||||
Errored(String),
|
ScreenReaderEnabled(chain::Toggler, bool),
|
||||||
Enabled(chain::Toggler, bool),
|
MagnifierEnabled(chain::Toggler, bool),
|
||||||
Frame(Instant),
|
Frame(Instant),
|
||||||
Token(TokenUpdate),
|
Token(TokenUpdate),
|
||||||
OpenSettings,
|
OpenSettings,
|
||||||
Update(backend::Update),
|
DBusUpdate(DBusUpdate),
|
||||||
|
WaylandUpdate(WaylandUpdate),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl cosmic::Application for CosmicA11yApplet {
|
impl cosmic::Application for CosmicA11yApplet {
|
||||||
|
|
@ -89,16 +99,23 @@ impl cosmic::Application for CosmicA11yApplet {
|
||||||
) -> cosmic::iced::Task<cosmic::app::Message<Self::Message>> {
|
) -> cosmic::iced::Task<cosmic::app::Message<Self::Message>> {
|
||||||
match message {
|
match message {
|
||||||
Message::Frame(now) => self.timeline.now(now),
|
Message::Frame(now) => self.timeline.now(now),
|
||||||
Message::Enabled(chain, enabled) => {
|
Message::ScreenReaderEnabled(chain, enabled) => {
|
||||||
self.timeline.set_chain(chain).start();
|
if let Some(tx) = &self.dbus_sender {
|
||||||
self.a11y_enabled = enabled;
|
self.timeline.set_chain(chain).start();
|
||||||
|
self.reader_enabled = enabled;
|
||||||
if let Some(tx) = &self.a11y_sender {
|
let _ = tx.send(DBusRequest::Status(enabled));
|
||||||
let _ = tx.send(A11yRequest::Status(enabled));
|
} else {
|
||||||
|
self.reader_enabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::Errored(why) => {
|
Message::MagnifierEnabled(chain, enabled) => {
|
||||||
tracing::error!("{}", why);
|
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 => {
|
Message::TogglePopup => {
|
||||||
if let Some(p) = self.popup.take() {
|
if let Some(p) = self.popup.take() {
|
||||||
|
|
@ -131,7 +148,7 @@ impl cosmic::Application for CosmicA11yApplet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::OpenSettings => {
|
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() {
|
if let Some(tx) = self.token_tx.as_ref() {
|
||||||
let _ = tx.send(TokenRequest {
|
let _ = tx.send(TokenRequest {
|
||||||
app_id: Self::APP_ID.to_string(),
|
app_id: Self::APP_ID.to_string(),
|
||||||
|
|
@ -150,7 +167,7 @@ impl cosmic::Application for CosmicA11yApplet {
|
||||||
}
|
}
|
||||||
TokenUpdate::ActivationToken { token, .. } => {
|
TokenUpdate::ActivationToken { token, .. } => {
|
||||||
let mut cmd = std::process::Command::new("cosmic-settings");
|
let mut cmd = std::process::Command::new("cosmic-settings");
|
||||||
cmd.arg("a11y");
|
cmd.arg("accessibility");
|
||||||
if let Some(token) = token {
|
if let Some(token) = token {
|
||||||
cmd.env("XDG_ACTIVATION_TOKEN", &token);
|
cmd.env("XDG_ACTIVATION_TOKEN", &token);
|
||||||
cmd.env("DESKTOP_STARTUP_ID", &token);
|
cmd.env("DESKTOP_STARTUP_ID", &token);
|
||||||
|
|
@ -158,16 +175,31 @@ impl cosmic::Application for CosmicA11yApplet {
|
||||||
tokio::spawn(cosmic::process::spawn(cmd));
|
tokio::spawn(cosmic::process::spawn(cmd));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Message::Update(update) => match update {
|
Message::DBusUpdate(update) => match update {
|
||||||
backend::Update::Error(err) => {
|
DBusUpdate::Error(err) => {
|
||||||
tracing::error!("{err}");
|
tracing::error!("{err}");
|
||||||
|
let _ = self.dbus_sender.take();
|
||||||
|
self.reader_enabled = false;
|
||||||
}
|
}
|
||||||
backend::Update::Status(enabled) => {
|
DBusUpdate::Status(enabled) => {
|
||||||
self.a11y_enabled = enabled;
|
self.reader_enabled = enabled;
|
||||||
}
|
}
|
||||||
backend::Update::Init(enabled, tx) => {
|
DBusUpdate::Init(enabled, tx) => {
|
||||||
self.a11y_enabled = enabled;
|
self.reader_enabled = enabled;
|
||||||
self.a11y_sender = Some(tx);
|
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<Message> {
|
fn view_window(&self, _id: window::Id) -> Element<Message> {
|
||||||
let toggle = padded_control(
|
let Spacing {
|
||||||
|
space_xxs, space_s, ..
|
||||||
|
} = theme::active().cosmic().spacing;
|
||||||
|
|
||||||
|
let reader_toggle = padded_control(
|
||||||
anim!(
|
anim!(
|
||||||
//toggler
|
READER_TOGGLE,
|
||||||
ENABLED,
|
|
||||||
&self.timeline,
|
&self.timeline,
|
||||||
fl!("accessibility"),
|
fl!("screen-reader"),
|
||||||
self.a11y_enabled,
|
self.reader_enabled,
|
||||||
Message::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)
|
.text_size(14)
|
||||||
.width(Length::Fill),
|
.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
|
self.core
|
||||||
.applet
|
.applet
|
||||||
.popup_container(container(toggle).padding([8, 0]))
|
.popup_container(content_list)
|
||||||
.max_width(372.)
|
.max_width(372.)
|
||||||
.max_height(600.)
|
.max_height(600.)
|
||||||
.into()
|
.into()
|
||||||
|
|
@ -206,7 +259,8 @@ impl cosmic::Application for CosmicA11yApplet {
|
||||||
|
|
||||||
fn subscription(&self) -> Subscription<Message> {
|
fn subscription(&self) -> Subscription<Message> {
|
||||||
Subscription::batch(vec![
|
Subscription::batch(vec![
|
||||||
backend::subscription().map(Message::Update),
|
backend::dbus::subscription().map(Message::DBusUpdate),
|
||||||
|
backend::wayland::a11y_subscription().map(Message::WaylandUpdate),
|
||||||
self.timeline
|
self.timeline
|
||||||
.as_subscription()
|
.as_subscription()
|
||||||
.map(|(_, now)| Message::Frame(now)),
|
.map(|(_, now)| Message::Frame(now)),
|
||||||
|
|
|
||||||
139
cosmic-applet-a11y/src/backend/dbus.rs
Normal file
139
cosmic-applet-a11y/src/backend/dbus.rs
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
// Copyright 2023 System76 <info@system76.com>
|
||||||
|
// 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<DBusRequest>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum DBusRequest {
|
||||||
|
Status(bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum State {
|
||||||
|
Ready,
|
||||||
|
Waiting(Connection, u8, bool, UnboundedReceiver<DBusRequest>),
|
||||||
|
Finished,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subscription() -> iced::Subscription<DBusUpdate> {
|
||||||
|
struct MyId;
|
||||||
|
|
||||||
|
Subscription::run_with_id(
|
||||||
|
std::any::TypeId::of::<MyId>(),
|
||||||
|
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<DBusUpdate>,
|
||||||
|
) -> 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,139 +1,2 @@
|
||||||
// Copyright 2023 System76 <info@system76.com>
|
pub mod dbus;
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
pub mod wayland;
|
||||||
|
|
||||||
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<A11yRequest>),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum A11yRequest {
|
|
||||||
Status(bool),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum State {
|
|
||||||
Ready,
|
|
||||||
Waiting(Connection, u8, bool, UnboundedReceiver<A11yRequest>),
|
|
||||||
Finished,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn subscription() -> iced::Subscription<Update> {
|
|
||||||
struct MyId;
|
|
||||||
|
|
||||||
Subscription::run_with_id(
|
|
||||||
std::any::TypeId::of::<MyId>(),
|
|
||||||
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<Update>,
|
|
||||||
) -> 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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
96
cosmic-applet-a11y/src/backend/wayland/mod.rs
Normal file
96
cosmic-applet-a11y/src/backend/wayland/mod.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
// Copyright 2023 System76 <info@system76.com>
|
||||||
|
// 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<Mutex<Option<mpsc::Receiver<AccessibilityEvent>>>> =
|
||||||
|
Lazy::new(|| Mutex::new(None));
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum WaylandUpdate {
|
||||||
|
State(AccessibilityEvent),
|
||||||
|
Started(SyncSender<AccessibilityRequest>),
|
||||||
|
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<WaylandUpdate> {
|
||||||
|
Subscription::run_with_id(
|
||||||
|
std::any::TypeId::of::<WaylandUpdate>(),
|
||||||
|
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<WaylandUpdate>,
|
||||||
|
) -> 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<AccessibilityEvent>,
|
||||||
|
tx: SyncSender<AccessibilityRequest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WaylandWatcher {
|
||||||
|
pub fn new() -> anyhow::Result<Self> {
|
||||||
|
let (tx, rx) = mpsc::channel(20);
|
||||||
|
let tx = thread::spawn_a11y(tx)?;
|
||||||
|
Ok(Self { tx, rx })
|
||||||
|
}
|
||||||
|
}
|
||||||
123
cosmic-applet-a11y/src/backend/wayland/thread.rs
Normal file
123
cosmic-applet-a11y/src/backend/wayland/thread.rs
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
// Copyright 2025 System76 <info@system76.com>
|
||||||
|
// 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<AccessibilityEvent>,
|
||||||
|
) -> anyhow::Result<SyncSender<AccessibilityRequest>> {
|
||||||
|
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<AccessibilityEvent>,
|
||||||
|
global: cosmic_a11y_manager_v1::CosmicA11yManagerV1,
|
||||||
|
|
||||||
|
magnifier: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch<cosmic_a11y_manager_v1::CosmicA11yManagerV1, ()> for State {
|
||||||
|
fn event(
|
||||||
|
state: &mut Self,
|
||||||
|
_proxy: &cosmic_a11y_manager_v1::CosmicA11yManagerV1,
|
||||||
|
event: <cosmic_a11y_manager_v1::CosmicA11yManagerV1 as Proxy>::Event,
|
||||||
|
_data: &(),
|
||||||
|
_conn: &Connection,
|
||||||
|
_qhandle: &sctk::reexports::client::QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
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<wl_registry::WlRegistry, GlobalListContents> for State {
|
||||||
|
fn event(
|
||||||
|
_state: &mut Self,
|
||||||
|
_proxy: &wl_registry::WlRegistry,
|
||||||
|
_event: <wl_registry::WlRegistry as Proxy>::Event,
|
||||||
|
_data: &GlobalListContents,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qhandle: &sctk::reexports::client::QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
// We don't care about any dynamic globals
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut event_loop = calloop::EventLoop::<State>::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::<cosmic_a11y_manager_v1::CosmicA11yManagerV1, _, _>(&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)
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue