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",
"mime 0.3.17",
"notify 6.1.1",
"num-derive 0.4.2",
"num-traits",
"once_cell",
"regex",
"ron 0.8.1",

View file

@ -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"

View file

@ -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));

View file

@ -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<String>,
wayland_available: bool,
wayland_available: Option<u32>,
wayland_thread: Option<wayland::Sender>,
theme: Box<cosmic::cosmic_theme::Theme>,
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)]
pub enum Message {
Event(wayland::AccessibilityEvent),
@ -36,6 +65,9 @@ pub enum Message {
Return,
HighContrast(bool),
SystemTheme(Box<cosmic::cosmic_theme::Theme>),
SetScreenInverted(bool),
SetScreenFilterActive(bool),
SetScreenFilterSelection(ColorFilter),
}
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() {
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<crate::pages::Message> 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<crate::pages::Message> {
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<crate::pages::Message> {
.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<crate::pages::Message> {
.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<crate::pages::Message> {
)
.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<crate::pages::Message> {
impl Page {
pub fn update(&mut self, message: Message) -> cosmic::iced::Task<crate::app::Message> {
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()

View file

@ -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<ColorFilter>,
},
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<ColorFilter>,
},
}
pub type Sender = calloop::channel::Sender<AccessibilityRequest>;
@ -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<ColorFilter>,
}
impl Dispatch<cosmic_a11y_manager_v1::CosmicA11yManagerV1, ()> 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::<cosmic_a11y_manager_v1::CosmicA11yManagerV1, _, _>(
&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, |_| {})?;

View file

@ -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