feat: Add screen filter settings
This commit is contained in:
parent
64d013ed38
commit
6cb05e6494
6 changed files with 239 additions and 17 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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, |_| {})?;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue