diff --git a/Cargo.lock b/Cargo.lock index 32b7334..9b58af8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1704,6 +1704,8 @@ dependencies = [ "locales-rs", "mime 0.3.17", "notify 6.1.1", + "num-derive 0.4.2", + "num-traits", "once_cell", "regex", "ron 0.8.1", diff --git a/cosmic-settings/Cargo.toml b/cosmic-settings/Cargo.toml index f9fdc72..39ea1d2 100644 --- a/cosmic-settings/Cargo.toml +++ b/cosmic-settings/Cargo.toml @@ -85,6 +85,8 @@ gettext-rs = { version = "0.7.2", features = [ "gettext-system", ], optional = true } async-fn-stream = "0.2.2" +num-traits = "0.2" +num-derive = "0.4" [dependencies.cosmic-settings-subscriptions] git = "https://github.com/pop-os/cosmic-settings-subscriptions" diff --git a/cosmic-settings/src/pages/accessibility/magnifier.rs b/cosmic-settings/src/pages/accessibility/magnifier.rs index c15ca68..4080fb1 100644 --- a/cosmic-settings/src/pages/accessibility/magnifier.rs +++ b/cosmic-settings/src/pages/accessibility/magnifier.rs @@ -324,6 +324,9 @@ impl Page { Message::Event(AccessibilityEvent::Magnifier(value)) => { self.magnifier_state = value; } + Message::Event( + AccessibilityEvent::Bound(_) | AccessibilityEvent::ScreenFilter { .. }, + ) => {} Message::SetMagnifier(value) => { if let Some(sender) = self.wayland_thread.as_ref() { let _ = sender.send(AccessibilityRequest::Magnifier(value)); diff --git a/cosmic-settings/src/pages/accessibility/mod.rs b/cosmic-settings/src/pages/accessibility/mod.rs index 2af7fca..42e10e0 100644 --- a/cosmic-settings/src/pages/accessibility/mod.rs +++ b/cosmic-settings/src/pages/accessibility/mod.rs @@ -1,34 +1,63 @@ use cosmic::{ + Apply, Task, cosmic_theme::{CosmicPalette, ThemeBuilder}, iced_core::text::Wrapping, theme::{self, CosmicTheme}, - widget::{button, container, horizontal_space, icon, settings, text}, - Apply, Task, + widget::{button, container, dropdown, horizontal_space, icon, settings, text, toggler}, }; pub use cosmic_comp_config::ZoomMovement; use cosmic_config::CosmicConfigEntry; use cosmic_settings_page::{ - self as page, + self as page, Insert, section::{self, Section}, - Insert, }; +use num_traits::FromPrimitive; use slotmap::SlotMap; pub mod magnifier; mod wayland; -pub use wayland::{AccessibilityEvent, AccessibilityRequest}; +pub use wayland::{AccessibilityEvent, AccessibilityRequest, ColorFilter}; -#[derive(Debug, Default)] +#[derive(Debug)] pub struct Page { entity: page::Entity, magnifier_state: bool, + screen_inverted: bool, + screen_filter_active: bool, + screen_filter_selection: ColorFilter, + screen_filter_selections: Vec, - wayland_available: bool, + wayland_available: Option, wayland_thread: Option, theme: Box, high_contrast: Option, } +impl Default for Page { + fn default() -> Self { + Page { + entity: page::Entity::default(), + magnifier_state: false, + screen_inverted: false, + screen_filter_active: false, + screen_filter_selection: ColorFilter::Greyscale, + screen_filter_selections: vec![ + // This order has to match the representation of `ColorFilter` + fl!("color-filter", "greyscale"), + fl!("color-filter", "deuteranopia"), + fl!("color-filter", "protanopia"), + fl!("color-filter", "tritanopia"), + fl!("color-filter", "unknown"), + ], + + wayland_available: None, + wayland_thread: None, + theme: Box::default(), + high_contrast: None, + } + } +} + #[derive(Debug, Clone)] pub enum Message { Event(wayland::AccessibilityEvent), @@ -36,6 +65,9 @@ pub enum Message { Return, HighContrast(bool), SystemTheme(Box), + SetScreenInverted(bool), + SetScreenFilterActive(bool), + SetScreenFilterSelection(ColorFilter), } impl page::Page for Page { @@ -62,7 +94,6 @@ impl page::Page for Page { if self.wayland_thread.is_none() { match wayland::spawn_wayland_connection() { Ok((tx, mut rx)) => { - self.wayland_available = true; self.wayland_thread = Some(tx); return cosmic::Task::stream(async_fn_stream::fn_stream( @@ -88,7 +119,7 @@ impl page::Page for Page { "Failed to spawn wayland connection for accessibility page: {}", err ); - self.wayland_available = false; + self.wayland_available = None; } } } @@ -117,6 +148,9 @@ pub fn vision() -> section::Section { off = fl!("accessibility", "off"); unavailable = fl!("accessibility", "unavailable"); high_contrast = fl!("accessibility", "high-contrast"); + invert_colors = fl!("accessibility", "invert-colors"); + color_filters = fl!("accessibility", "color-filters"); + color_filter_type = fl!("color-filter"); }); Section::default() @@ -134,7 +168,7 @@ pub fn vision() -> section::Section { .find(|(_, v)| v.id == "accessibility_magnifier") .expect("magnifier page not found"); - let status_text = if page.wayland_available { + let status_text = if page.wayland_available.is_some() { if page.magnifier_state { &descriptions[on] } else { @@ -158,6 +192,7 @@ pub fn vision() -> section::Section { .class(theme::Button::Transparent) .on_press_maybe( page.wayland_available + .is_some() .then_some(crate::pages::Message::Page(magnifier_entity)), ) }) @@ -168,6 +203,50 @@ pub fn vision() -> section::Section { ) .map(crate::pages::Message::Accessibility), ) + .add( + cosmic::Element::from( + settings::item::builder(&descriptions[invert_colors]).control( + toggler(page.screen_inverted).on_toggle_maybe( + page.wayland_available + .is_some_and(|ver| ver >= 2) + .then_some(Message::SetScreenInverted), + ), + ), + ) + .map(crate::pages::Message::Accessibility), + ) + .add({ + cosmic::Element::from( + settings::item::builder(&descriptions[color_filters]).control( + toggler(page.screen_filter_active).on_toggle_maybe( + page.wayland_available + .is_some_and(|ver| ver >= 2) + .then_some(Message::SetScreenFilterActive), + ), + ), + ) + .map(crate::pages::Message::Accessibility) + }) + .add({ + let selections = if page.screen_filter_selection == ColorFilter::Unknown { + &page.screen_filter_selections + } else { + &page.screen_filter_selections[0..4] + }; + cosmic::Element::from( + settings::item::builder(&descriptions[color_filter_type]).control( + dropdown( + selections, + Some(page.screen_filter_selection as usize), + move |idx| { + let filter = ColorFilter::from_usize(idx).unwrap_or_default(); + Message::SetScreenFilterSelection(filter) + }, + ), + ), + ) + .map(crate::pages::Message::Accessibility) + }) .into() }) } @@ -175,14 +254,25 @@ pub fn vision() -> section::Section { impl Page { pub fn update(&mut self, message: Message) -> cosmic::iced::Task { match message { + Message::Event(AccessibilityEvent::Bound(version)) => { + self.wayland_available = Some(version); + } Message::Event(AccessibilityEvent::Magnifier(value)) => { self.magnifier_state = value; } + Message::Event(AccessibilityEvent::ScreenFilter { inverted, filter }) => { + self.screen_inverted = inverted; + self.screen_filter_active = filter.is_some(); + if let Some(filter) = filter { + self.screen_filter_selection = filter; + } + } Message::Event(AccessibilityEvent::Closed) | Message::ProtocolUnavailable => { - self.wayland_available = false; + self.wayland_available = None; + self.screen_filter_active = false; } Message::Return => { - return cosmic::iced::Task::done(crate::app::Message::Page(self.entity)) + return cosmic::iced::Task::done(crate::app::Message::Page(self.entity)); } Message::SystemTheme(theme) => { self.theme = theme; @@ -243,6 +333,34 @@ impl Page { } }); } + Message::SetScreenInverted(inverted) => { + if let Some(sender) = self.wayland_thread.as_ref() { + let _ = sender.send(AccessibilityRequest::ScreenFilter { + inverted, + filter: Some(wayland::ColorFilter::Unknown), + }); + } + } + Message::SetScreenFilterActive(active) => { + if let Some(sender) = self.wayland_thread.as_ref() { + let _ = sender.send(AccessibilityRequest::ScreenFilter { + inverted: self.screen_inverted, + filter: active.then_some(self.screen_filter_selection), + }); + } + } + Message::SetScreenFilterSelection(filter) => { + if self.screen_filter_active && self.wayland_available.is_some_and(|ver| ver >= 2) { + if let Some(sender) = self.wayland_thread.as_ref() { + let _ = sender.send(AccessibilityRequest::ScreenFilter { + inverted: self.screen_inverted, + filter: Some(filter), + }); + } + } else { + self.screen_filter_selection = filter; + } + } } cosmic::iced::Task::none() diff --git a/cosmic-settings/src/pages/accessibility/wayland.rs b/cosmic-settings/src/pages/accessibility/wayland.rs index 45e4b58..486f7fa 100644 --- a/cosmic-settings/src/pages/accessibility/wayland.rs +++ b/cosmic-settings/src/pages/accessibility/wayland.rs @@ -1,12 +1,13 @@ use cosmic_protocols::a11y::v1::client::cosmic_a11y_manager_v1; +use num_derive::{FromPrimitive, ToPrimitive}; use sctk::{ reexports::{ - calloop::{self, channel, LoopSignal}, + calloop::{self, LoopSignal, channel}, calloop_wayland_source::WaylandSource, client::{ - globals::{registry_queue_init, GlobalListContents}, + ConnectError, Connection, Dispatch, Proxy, WEnum, + globals::{GlobalListContents, registry_queue_init}, protocol::wl_registry, - ConnectError, Connection, Dispatch, Proxy, }, }, registry::RegistryState, @@ -15,13 +16,37 @@ use tokio::sync::mpsc; #[derive(Debug, Clone, Copy)] pub enum AccessibilityEvent { + Bound(u32), Magnifier(bool), + ScreenFilter { + inverted: bool, + filter: Option, + }, Closed, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)] +pub enum ColorFilter { + Greyscale, + Deuteranopia, + Protanopia, + Tritanopia, + Unknown, +} + +impl Default for ColorFilter { + fn default() -> Self { + ColorFilter::Unknown + } +} + #[derive(Debug, Clone, Copy)] pub enum AccessibilityRequest { Magnifier(bool), + ScreenFilter { + inverted: bool, + filter: Option, + }, } pub type Sender = calloop::channel::Sender; @@ -40,7 +65,7 @@ pub fn spawn_wayland_connection() -> Result< std::thread::spawn(move || { if let Err(err) = wayland_thread(conn, event_tx.clone(), request_rx) { tracing::warn!("Accessibility protocol wayland thread crashed: {}", err); - let _ = event_tx.send(AccessibilityEvent::Closed); + let _ = event_tx.blocking_send(AccessibilityEvent::Closed); } }); @@ -58,6 +83,8 @@ fn wayland_thread( global: cosmic_a11y_manager_v1::CosmicA11yManagerV1, magnifier: bool, + screen_inverted: bool, + screen_filter: Option, } impl Dispatch for State { @@ -87,6 +114,41 @@ fn wayland_thread( state.magnifier = magnifier; } } + cosmic_a11y_manager_v1::Event::ScreenFilter { inverted, filter } => { + let inverted = inverted + .into_result() + .unwrap_or(cosmic_a11y_manager_v1::ActiveState::Disabled) + == cosmic_a11y_manager_v1::ActiveState::Enabled; + let filter = match filter { + WEnum::Value(cosmic_a11y_manager_v1::Filter::Disabled) => None, + WEnum::Value(cosmic_a11y_manager_v1::Filter::Greyscale) => { + Some(ColorFilter::Greyscale) + } + WEnum::Value(cosmic_a11y_manager_v1::Filter::DaltonizeProtanopia) => { + Some(ColorFilter::Protanopia) + } + WEnum::Value(cosmic_a11y_manager_v1::Filter::DaltonizeDeuteranopia) => { + Some(ColorFilter::Deuteranopia) + } + WEnum::Value(cosmic_a11y_manager_v1::Filter::DaltonizeTritanopia) => { + Some(ColorFilter::Tritanopia) + } + WEnum::Value(_) | WEnum::Unknown(_) => Some(ColorFilter::Unknown), + }; + + if inverted != state.screen_inverted || filter != state.screen_filter { + if state + .tx + .blocking_send(AccessibilityEvent::ScreenFilter { inverted, filter }) + .is_err() + { + state.loop_signal.stop(); + state.loop_signal.wakeup(); + }; + state.screen_inverted = inverted; + state.screen_filter = filter; + } + } _ => unreachable!(), } } @@ -117,12 +179,14 @@ fn wayland_thread( let registry_state = RegistryState::new(&globals); let Ok(global) = registry_state.bind_one::( &qhandle, - 1..=1, + 1..=2, (), ) else { return Ok(()); }; + let _ = tx.blocking_send(AccessibilityEvent::Bound(global.version())); + loop_handle .insert_source(rx, |request, _, state| match request { channel::Event::Msg(AccessibilityRequest::Magnifier(val)) => { @@ -132,6 +196,29 @@ fn wayland_thread( cosmic_a11y_manager_v1::ActiveState::Disabled }); } + channel::Event::Msg(AccessibilityRequest::ScreenFilter { inverted, filter }) => { + state.global.set_screen_filter( + if inverted { + cosmic_a11y_manager_v1::ActiveState::Enabled + } else { + cosmic_a11y_manager_v1::ActiveState::Disabled + }, + match filter { + None => cosmic_a11y_manager_v1::Filter::Disabled, + Some(ColorFilter::Greyscale) => cosmic_a11y_manager_v1::Filter::Greyscale, + Some(ColorFilter::Protanopia) => { + cosmic_a11y_manager_v1::Filter::DaltonizeProtanopia + } + Some(ColorFilter::Deuteranopia) => { + cosmic_a11y_manager_v1::Filter::DaltonizeDeuteranopia + } + Some(ColorFilter::Tritanopia) => { + cosmic_a11y_manager_v1::Filter::DaltonizeTritanopia + } + Some(ColorFilter::Unknown) => cosmic_a11y_manager_v1::Filter::Unknown, + }, + ); + } channel::Event::Closed => { state.loop_signal.stop(); state.loop_signal.wakeup(); @@ -145,6 +232,8 @@ fn wayland_thread( global, magnifier: false, + screen_inverted: false, + screen_filter: None, }; event_loop.run(None, &mut state, |_| {})?; diff --git a/i18n/en/cosmic_settings.ftl b/i18n/en/cosmic_settings.ftl index 3ab4728..0c29328 100644 --- a/i18n/en/cosmic_settings.ftl +++ b/i18n/en/cosmic_settings.ftl @@ -135,6 +135,8 @@ accessibility = Accessibility .off = Off .unavailable = Unavailable .high-contrast = High contrast mode + .invert-colors = Invert Colors + .color-filters = Color filters magnifier = Magnifier .controls = Or use these shortcuts: { $zoom_in -> [zero] {""} @@ -153,6 +155,12 @@ magnifier = Magnifier .continuous = Continuously with pointer .onedge = When pointer reaches edge .centered = To keep pointer centered +color-filter = Color filter type + .unknown = Unknown Filter active + .greyscale = Greyscale + .deuteranopia = Green/Red (green weakness, Deuteranopia) + .protanopia = Red/Green (red weakness, Protanopia) + .tritanopia = Blue/Yellow (blue weakness, Tritanopia) ## Desktop