feat: Add screen filter settings

This commit is contained in:
Victoria Brekenfeld 2025-03-17 19:45:00 +01:00 • committed by Michael Murphy
parent 64d013ed38
commit 6cb05e6494
6 changed files with 239 additions and 17 deletions

2
Cargo.lock generated
View file

@ -1704,6 +1704,8 @@ dependencies = [
"locales-rs", "locales-rs",
"mime 0.3.17", "mime 0.3.17",
"notify 6.1.1", "notify 6.1.1",
"num-derive 0.4.2",
"num-traits",
"once_cell", "once_cell",
"regex", "regex",
"ron 0.8.1", "ron 0.8.1",

View file

@ -85,6 +85,8 @@ gettext-rs = { version = "0.7.2", features = [
"gettext-system", "gettext-system",
], optional = true } ], optional = true }
async-fn-stream = "0.2.2" async-fn-stream = "0.2.2"
num-traits = "0.2"
num-derive = "0.4"
[dependencies.cosmic-settings-subscriptions] [dependencies.cosmic-settings-subscriptions]
git = "https://github.com/pop-os/cosmic-settings-subscriptions" git = "https://github.com/pop-os/cosmic-settings-subscriptions"

View file

@ -324,6 +324,9 @@ impl Page {
Message::Event(AccessibilityEvent::Magnifier(value)) => { Message::Event(AccessibilityEvent::Magnifier(value)) => {
self.magnifier_state = value; self.magnifier_state = value;
} }
Message::Event(
AccessibilityEvent::Bound(_) | AccessibilityEvent::ScreenFilter { .. },
) => {}
Message::SetMagnifier(value) => { Message::SetMagnifier(value) => {
if let Some(sender) = self.wayland_thread.as_ref() { if let Some(sender) = self.wayland_thread.as_ref() {
let _ = sender.send(AccessibilityRequest::Magnifier(value)); let _ = sender.send(AccessibilityRequest::Magnifier(value));

View file

@ -1,34 +1,63 @@
use cosmic::{ use cosmic::{
Apply, Task,
cosmic_theme::{CosmicPalette, ThemeBuilder}, cosmic_theme::{CosmicPalette, ThemeBuilder},
iced_core::text::Wrapping, iced_core::text::Wrapping,
theme::{self, CosmicTheme}, theme::{self, CosmicTheme},
widget::{button, container, horizontal_space, icon, settings, text}, widget::{button, container, dropdown, horizontal_space, icon, settings, text, toggler},
Apply, Task,
}; };
pub use cosmic_comp_config::ZoomMovement; pub use cosmic_comp_config::ZoomMovement;
use cosmic_config::CosmicConfigEntry; use cosmic_config::CosmicConfigEntry;
use cosmic_settings_page::{ use cosmic_settings_page::{
self as page, self as page, Insert,
section::{self, Section}, section::{self, Section},
Insert,
}; };
use num_traits::FromPrimitive;
use slotmap::SlotMap; use slotmap::SlotMap;
pub mod magnifier; pub mod magnifier;
mod wayland; mod wayland;
pub use wayland::{AccessibilityEvent, AccessibilityRequest}; pub use wayland::{AccessibilityEvent, AccessibilityRequest, ColorFilter};
#[derive(Debug, Default)] #[derive(Debug)]
pub struct Page { pub struct Page {
entity: page::Entity, entity: page::Entity,
magnifier_state: bool, magnifier_state: bool,
screen_inverted: bool,
screen_filter_active: bool,
screen_filter_selection: ColorFilter,
screen_filter_selections: Vec<String>,
wayland_available: bool, wayland_available: Option<u32>,
wayland_thread: Option<wayland::Sender>, wayland_thread: Option<wayland::Sender>,
theme: Box<cosmic::cosmic_theme::Theme>, theme: Box<cosmic::cosmic_theme::Theme>,
high_contrast: Option<bool>, high_contrast: Option<bool>,
} }
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)] #[derive(Debug, Clone)]
pub enum Message { pub enum Message {
Event(wayland::AccessibilityEvent), Event(wayland::AccessibilityEvent),
@ -36,6 +65,9 @@ pub enum Message {
Return, Return,
HighContrast(bool), HighContrast(bool),
SystemTheme(Box<cosmic::cosmic_theme::Theme>), SystemTheme(Box<cosmic::cosmic_theme::Theme>),
SetScreenInverted(bool),
SetScreenFilterActive(bool),
SetScreenFilterSelection(ColorFilter),
} }
impl page::Page<crate::pages::Message> for Page { impl page::Page<crate::pages::Message> for Page {
@ -62,7 +94,6 @@ impl page::Page<crate::pages::Message> for Page {
if self.wayland_thread.is_none() { if self.wayland_thread.is_none() {
match wayland::spawn_wayland_connection() { match wayland::spawn_wayland_connection() {
Ok((tx, mut rx)) => { Ok((tx, mut rx)) => {
self.wayland_available = true;
self.wayland_thread = Some(tx); self.wayland_thread = Some(tx);
return cosmic::Task::stream(async_fn_stream::fn_stream( return cosmic::Task::stream(async_fn_stream::fn_stream(
@ -88,7 +119,7 @@ impl page::Page<crate::pages::Message> for Page {
"Failed to spawn wayland connection for accessibility page: {}", "Failed to spawn wayland connection for accessibility page: {}",
err err
); );
self.wayland_available = false; self.wayland_available = None;
} }
} }
} }
@ -117,6 +148,9 @@ pub fn vision() -> section::Section<crate::pages::Message> {
off = fl!("accessibility", "off"); off = fl!("accessibility", "off");
unavailable = fl!("accessibility", "unavailable"); unavailable = fl!("accessibility", "unavailable");
high_contrast = fl!("accessibility", "high-contrast"); 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() Section::default()
@ -134,7 +168,7 @@ pub fn vision() -> section::Section<crate::pages::Message> {
.find(|(_, v)| v.id == "accessibility_magnifier") .find(|(_, v)| v.id == "accessibility_magnifier")
.expect("magnifier page not found"); .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 { if page.magnifier_state {
&descriptions[on] &descriptions[on]
} else { } else {
@ -158,6 +192,7 @@ pub fn vision() -> section::Section<crate::pages::Message> {
.class(theme::Button::Transparent) .class(theme::Button::Transparent)
.on_press_maybe( .on_press_maybe(
page.wayland_available page.wayland_available
.is_some()
.then_some(crate::pages::Message::Page(magnifier_entity)), .then_some(crate::pages::Message::Page(magnifier_entity)),
) )
}) })
@ -168,6 +203,50 @@ pub fn vision() -> section::Section<crate::pages::Message> {
) )
.map(crate::pages::Message::Accessibility), .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() .into()
}) })
} }
@ -175,14 +254,25 @@ pub fn vision() -> section::Section<crate::pages::Message> {
impl Page { impl Page {
pub fn update(&mut self, message: Message) -> cosmic::iced::Task<crate::app::Message> { pub fn update(&mut self, message: Message) -> cosmic::iced::Task<crate::app::Message> {
match message { match message {
Message::Event(AccessibilityEvent::Bound(version)) => {
self.wayland_available = Some(version);
}
Message::Event(AccessibilityEvent::Magnifier(value)) => { Message::Event(AccessibilityEvent::Magnifier(value)) => {
self.magnifier_state = 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 => { Message::Event(AccessibilityEvent::Closed) | Message::ProtocolUnavailable => {
self.wayland_available = false; self.wayland_available = None;
self.screen_filter_active = false;
} }
Message::Return => { 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) => { Message::SystemTheme(theme) => {
self.theme = 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() cosmic::iced::Task::none()

View file

@ -1,12 +1,13 @@
use cosmic_protocols::a11y::v1::client::cosmic_a11y_manager_v1; use cosmic_protocols::a11y::v1::client::cosmic_a11y_manager_v1;
use num_derive::{FromPrimitive, ToPrimitive};
use sctk::{ use sctk::{
reexports::{ reexports::{
calloop::{self, channel, LoopSignal}, calloop::{self, LoopSignal, channel},
calloop_wayland_source::WaylandSource, calloop_wayland_source::WaylandSource,
client::{ client::{
globals::{registry_queue_init, GlobalListContents}, ConnectError, Connection, Dispatch, Proxy, WEnum,
globals::{GlobalListContents, registry_queue_init},
protocol::wl_registry, protocol::wl_registry,
ConnectError, Connection, Dispatch, Proxy,
}, },
}, },
registry::RegistryState, registry::RegistryState,
@ -15,13 +16,37 @@ use tokio::sync::mpsc;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum AccessibilityEvent { pub enum AccessibilityEvent {
Bound(u32),
Magnifier(bool), Magnifier(bool),
ScreenFilter {
inverted: bool,
filter: Option<ColorFilter>,
},
Closed, 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)] #[derive(Debug, Clone, Copy)]
pub enum AccessibilityRequest { pub enum AccessibilityRequest {
Magnifier(bool), Magnifier(bool),
ScreenFilter {
inverted: bool,
filter: Option<ColorFilter>,
},
} }
pub type Sender = calloop::channel::Sender<AccessibilityRequest>; pub type Sender = calloop::channel::Sender<AccessibilityRequest>;
@ -40,7 +65,7 @@ pub fn spawn_wayland_connection() -> Result<
std::thread::spawn(move || { std::thread::spawn(move || {
if let Err(err) = wayland_thread(conn, event_tx.clone(), request_rx) { if let Err(err) = wayland_thread(conn, event_tx.clone(), request_rx) {
tracing::warn!("Accessibility protocol wayland thread crashed: {}", err); 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, global: cosmic_a11y_manager_v1::CosmicA11yManagerV1,
magnifier: bool, magnifier: bool,
screen_inverted: bool,
screen_filter: Option<ColorFilter>,
} }
impl Dispatch<cosmic_a11y_manager_v1::CosmicA11yManagerV1, ()> for State { impl Dispatch<cosmic_a11y_manager_v1::CosmicA11yManagerV1, ()> for State {
@ -87,6 +114,41 @@ fn wayland_thread(
state.magnifier = magnifier; 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!(), _ => unreachable!(),
} }
} }
@ -117,12 +179,14 @@ fn wayland_thread(
let registry_state = RegistryState::new(&globals); let registry_state = RegistryState::new(&globals);
let Ok(global) = registry_state.bind_one::<cosmic_a11y_manager_v1::CosmicA11yManagerV1, _, _>( let Ok(global) = registry_state.bind_one::<cosmic_a11y_manager_v1::CosmicA11yManagerV1, _, _>(
&qhandle, &qhandle,
1..=1, 1..=2,
(), (),
) else { ) else {
return Ok(()); return Ok(());
}; };
let _ = tx.blocking_send(AccessibilityEvent::Bound(global.version()));
loop_handle loop_handle
.insert_source(rx, |request, _, state| match request { .insert_source(rx, |request, _, state| match request {
channel::Event::Msg(AccessibilityRequest::Magnifier(val)) => { channel::Event::Msg(AccessibilityRequest::Magnifier(val)) => {
@ -132,6 +196,29 @@ fn wayland_thread(
cosmic_a11y_manager_v1::ActiveState::Disabled 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 => { channel::Event::Closed => {
state.loop_signal.stop(); state.loop_signal.stop();
state.loop_signal.wakeup(); state.loop_signal.wakeup();
@ -145,6 +232,8 @@ fn wayland_thread(
global, global,
magnifier: false, magnifier: false,
screen_inverted: false,
screen_filter: None,
}; };
event_loop.run(None, &mut state, |_| {})?; event_loop.run(None, &mut state, |_| {})?;

View file

@ -135,6 +135,8 @@ accessibility = Accessibility
.off = Off .off = Off
.unavailable = Unavailable .unavailable = Unavailable
.high-contrast = High contrast mode .high-contrast = High contrast mode
.invert-colors = Invert Colors
.color-filters = Color filters
magnifier = Magnifier magnifier = Magnifier
.controls = Or use these shortcuts: { $zoom_in -> .controls = Or use these shortcuts: { $zoom_in ->
[zero] {""} [zero] {""}
@ -153,6 +155,12 @@ magnifier = Magnifier
.continuous = Continuously with pointer .continuous = Continuously with pointer
.onedge = When pointer reaches edge .onedge = When pointer reaches edge
.centered = To keep pointer centered .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 ## Desktop