pages: Add Accessibility/Magnifier page
This commit is contained in:
parent
186698ff5b
commit
8e7ed01fe6
13 changed files with 773 additions and 1 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
|
@ -1671,6 +1671,7 @@ dependencies = [
|
|||
"cosmic-idle-config",
|
||||
"cosmic-mime-apps",
|
||||
"cosmic-panel-config",
|
||||
"cosmic-protocols",
|
||||
"cosmic-randr",
|
||||
"cosmic-randr-shell",
|
||||
"cosmic-settings-config",
|
||||
|
|
@ -1710,6 +1711,7 @@ dependencies = [
|
|||
"serde",
|
||||
"slab",
|
||||
"slotmap",
|
||||
"smithay-client-toolkit",
|
||||
"static_init",
|
||||
"sunrise",
|
||||
"system",
|
||||
|
|
@ -8039,7 +8041,7 @@ version = "0.1.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ cosmic-config.workspace = true
|
|||
cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
|
||||
cosmic-idle-config.workspace = true
|
||||
cosmic-panel-config = { workspace = true, optional = true }
|
||||
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", optional = true }
|
||||
cosmic-randr-shell.workspace = true
|
||||
cosmic-randr = { workspace = true, optional = true }
|
||||
cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true }
|
||||
|
|
@ -55,6 +56,7 @@ once_cell = "1.20.3"
|
|||
regex = "1.11.1"
|
||||
ron = "0.8"
|
||||
rust-embed = "8.5.0"
|
||||
sctk = { workspace = true, optional = true }
|
||||
secure-string = "0.3.0"
|
||||
serde = { version = "1.0.217", features = ["derive"] }
|
||||
slab = "0.4.9"
|
||||
|
|
@ -110,6 +112,7 @@ gettext = ["dep:gettext-rs"]
|
|||
|
||||
# Default features for Linux
|
||||
linux = [
|
||||
"page-accessibility",
|
||||
"page-about",
|
||||
"page-bluetooth",
|
||||
"page-date",
|
||||
|
|
@ -127,6 +130,7 @@ linux = [
|
|||
]
|
||||
|
||||
# Pages
|
||||
page-accessibility = ["dep:cosmic-protocols", "dep:sctk"]
|
||||
page-about = ["dep:cosmic-settings-system", "dep:hostname1-zbus", "dep:zbus"]
|
||||
page-bluetooth = ["dep:bluez-zbus", "dep:zbus", "dep:cosmic-settings-subscriptions"]
|
||||
page-date = ["dep:timedate-zbus", "dep:zbus"]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::config::Config;
|
||||
#[cfg(feature = "page-accessibility")]
|
||||
use crate::pages::accessibility;
|
||||
#[cfg(feature = "page-bluetooth")]
|
||||
use crate::pages::bluetooth;
|
||||
use crate::pages::desktop::{self, appearance};
|
||||
|
|
@ -74,6 +76,12 @@ pub struct SettingsApp {
|
|||
impl SettingsApp {
|
||||
fn subtask_to_page(&self, cmd: &PageCommands) -> Option<Entity> {
|
||||
match cmd {
|
||||
#[cfg(feature = "page-accessibility")]
|
||||
PageCommands::Accessibility => self.pages.page_id::<accessibility::Page>(),
|
||||
#[cfg(feature = "page-accessibility")]
|
||||
PageCommands::AccessibilityMagnifier => {
|
||||
self.pages.page_id::<accessibility::magnifier::Page>()
|
||||
}
|
||||
#[cfg(feature = "page-about")]
|
||||
PageCommands::About => self.pages.page_id::<system::about::Page>(),
|
||||
PageCommands::Appearance => self.pages.page_id::<desktop::appearance::Page>(),
|
||||
|
|
@ -194,6 +202,8 @@ impl cosmic::Application for SettingsApp {
|
|||
app.insert_page::<networking::Page>();
|
||||
#[cfg(feature = "page-bluetooth")]
|
||||
app.insert_page::<bluetooth::Page>();
|
||||
#[cfg(feature = "page-accessibility")]
|
||||
app.insert_page::<accessibility::Page>();
|
||||
let desktop_id = app.insert_page::<desktop::Page>().id();
|
||||
app.insert_page::<display::Page>();
|
||||
#[cfg(feature = "page-sound")]
|
||||
|
|
@ -368,6 +378,18 @@ impl cosmic::Application for SettingsApp {
|
|||
}
|
||||
|
||||
Message::PageMessage(message) => match message {
|
||||
#[cfg(feature = "page-accessibility")]
|
||||
crate::pages::Message::Accessibility(message) => {
|
||||
if let Some(page) = self.pages.page_mut::<accessibility::Page>() {
|
||||
return page.update(message).map(Into::into);
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "page-accessibility")]
|
||||
crate::pages::Message::AccessibilityMagnifier(message) => {
|
||||
if let Some(page) = self.pages.page_mut::<accessibility::magnifier::Page>() {
|
||||
return page.update(self.active_page, message).map(Into::into);
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "page-about")]
|
||||
crate::pages::Message::About(message) => {
|
||||
page::update!(self.pages, message, system::about::Page);
|
||||
|
|
|
|||
|
|
@ -38,6 +38,12 @@ pub struct Args {
|
|||
|
||||
#[derive(Subcommand, Debug, Serialize, Deserialize, Clone)]
|
||||
pub enum PageCommands {
|
||||
/// Accessibility settings page
|
||||
#[cfg(feature = "page-accessibility")]
|
||||
Accessibility,
|
||||
/// Accessibility Magnifier settings page
|
||||
#[cfg(feature = "page-accessibility")]
|
||||
AccessibilityMagnifier,
|
||||
/// About settings page
|
||||
#[cfg(feature = "page-about")]
|
||||
About,
|
||||
|
|
|
|||
339
cosmic-settings/src/pages/accessibility/magnifier.rs
Normal file
339
cosmic-settings/src/pages/accessibility/magnifier.rs
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
use std::collections::HashSet;
|
||||
|
||||
use cosmic::{
|
||||
iced::{Element, Length},
|
||||
iced_core::text::Wrapping,
|
||||
widget::{self, icon, settings, svg, text},
|
||||
Apply,
|
||||
};
|
||||
use cosmic_comp_config::{ZoomConfig, ZoomMovement};
|
||||
use cosmic_config::{ConfigGet, ConfigSet};
|
||||
use cosmic_settings_page::{
|
||||
self as page,
|
||||
section::{self, Section},
|
||||
Entity,
|
||||
};
|
||||
use slotmap::SlotMap;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::error;
|
||||
|
||||
use super::{wayland, AccessibilityEvent, AccessibilityRequest};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Page {
|
||||
entity: Entity,
|
||||
|
||||
accessibility_config: cosmic_config::Config,
|
||||
zoom_config: ZoomConfig,
|
||||
increment_values: Vec<String>,
|
||||
increment_idx: Option<usize>,
|
||||
|
||||
wayland_thread: Option<wayland::Sender>,
|
||||
magnifier_state: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
Event(wayland::AccessibilityEvent),
|
||||
ProtocolUnavailable,
|
||||
SetMagnifier(bool),
|
||||
SetIncrement(usize),
|
||||
SetSignin(bool),
|
||||
SetMovement(ZoomMovement),
|
||||
}
|
||||
|
||||
impl Default for Page {
|
||||
fn default() -> Self {
|
||||
let comp_config = cosmic_config::Config::new("com.system76.CosmicComp", 1).unwrap();
|
||||
let zoom_config: ZoomConfig = comp_config
|
||||
.get("accessibility_zoom")
|
||||
.inspect_err(|err| {
|
||||
if err.is_err() {
|
||||
error!(?err, "Failed to read config 'accessibility_zoom'");
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut values = HashSet::<u32>::from_iter([25, 50, 100, 150, 200, zoom_config.increment])
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
values.sort();
|
||||
let increment_values = values
|
||||
.into_iter()
|
||||
.map(|val| {
|
||||
format!(
|
||||
"{}%{}",
|
||||
val,
|
||||
if val == ZoomConfig::default().increment {
|
||||
" (Default)"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let increment_idx = increment_values.iter().position(|s| {
|
||||
s.split("%").next().and_then(|val| str::parse(val).ok()) == Some(zoom_config.increment)
|
||||
});
|
||||
|
||||
Page {
|
||||
entity: Entity::default(),
|
||||
|
||||
accessibility_config: comp_config,
|
||||
zoom_config,
|
||||
increment_values,
|
||||
increment_idx,
|
||||
|
||||
wayland_thread: None,
|
||||
magnifier_state: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn set_id(&mut self, entity: Entity) {
|
||||
self.entity = entity;
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new(
|
||||
"accessibility_magnifier",
|
||||
"preferences-desktop-accessibility",
|
||||
)
|
||||
.title(fl!("magnifier"))
|
||||
}
|
||||
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut SlotMap<section::Entity, page::Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![
|
||||
sections.insert(magnifier()),
|
||||
sections.insert(tip()),
|
||||
sections.insert(view_movement()),
|
||||
])
|
||||
}
|
||||
|
||||
fn on_enter(
|
||||
&mut self,
|
||||
sender: mpsc::Sender<crate::pages::Message>,
|
||||
) -> cosmic::Task<crate::pages::Message> {
|
||||
if self.wayland_thread.is_none() {
|
||||
match wayland::spawn_wayland_connection() {
|
||||
Ok((tx, mut rx)) => {
|
||||
self.wayland_thread = Some(tx);
|
||||
tokio::task::spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
let _ = sender
|
||||
.send(crate::pages::Message::AccessibilityMagnifier(
|
||||
Message::Event(event),
|
||||
))
|
||||
.await;
|
||||
}
|
||||
let _ = sender
|
||||
.send(crate::pages::Message::AccessibilityMagnifier(
|
||||
Message::ProtocolUnavailable,
|
||||
))
|
||||
.await;
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"Failed to spawn wayland connection for magnifier page: {}",
|
||||
err
|
||||
);
|
||||
return cosmic::Task::done(crate::pages::Message::Accessibility(
|
||||
super::Message::Return,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cosmic::Task::none()
|
||||
}
|
||||
|
||||
fn on_leave(&mut self) -> cosmic::Task<crate::pages::Message> {
|
||||
let _ = self.wayland_thread.take();
|
||||
|
||||
cosmic::Task::none()
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
||||
pub fn magnifier() -> section::Section<crate::pages::Message> {
|
||||
crate::slab!(descriptions {
|
||||
magnifier = fl!("magnifier");
|
||||
controls = fl!("magnifier", "controls");
|
||||
increment = fl!("magnifier", "increment");
|
||||
signin = fl!("magnifier", "signin");
|
||||
});
|
||||
|
||||
Section::default()
|
||||
.title(&descriptions[magnifier])
|
||||
.descriptions(descriptions)
|
||||
.view::<Page>(move |_binder, page, section| {
|
||||
let descriptions = §ion.descriptions;
|
||||
|
||||
settings::section()
|
||||
.title(§ion.title)
|
||||
.add(
|
||||
settings::item::builder(&descriptions[magnifier])
|
||||
.description(&descriptions[controls])
|
||||
.control(
|
||||
widget::toggler(page.magnifier_state).on_toggle(Message::SetMagnifier),
|
||||
),
|
||||
)
|
||||
.add(settings::item(
|
||||
&descriptions[increment],
|
||||
widget::dropdown(
|
||||
&page.increment_values,
|
||||
page.increment_idx,
|
||||
Message::SetIncrement,
|
||||
),
|
||||
))
|
||||
.add(settings::item(
|
||||
&descriptions[signin],
|
||||
widget::toggler(page.zoom_config.start_on_login).on_toggle(Message::SetSignin),
|
||||
))
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::AccessibilityMagnifier)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn tip() -> section::Section<crate::pages::Message> {
|
||||
crate::slab!(descriptions {
|
||||
applet = fl!("magnifier", "applet");
|
||||
});
|
||||
let applet_illustration = icon::from_name("illustration-accessibility-magnifier-applet")
|
||||
.icon()
|
||||
.into_svg_handle();
|
||||
|
||||
Section::default()
|
||||
.descriptions(descriptions)
|
||||
.view::<Page>(move |_binder, _page, section| {
|
||||
let descriptions = §ion.descriptions;
|
||||
|
||||
let mut items = vec![text::body(&descriptions[applet])
|
||||
.wrapping(Wrapping::Word)
|
||||
.width(Length::Shrink)
|
||||
.into()];
|
||||
if let Some(illustration) = applet_illustration.clone() {
|
||||
items.push(svg(illustration).width(Length::Fill).into());
|
||||
}
|
||||
|
||||
settings::section()
|
||||
.add(settings::flex_item_row(items))
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn view_movement() -> section::Section<crate::pages::Message> {
|
||||
crate::slab!(descriptions {
|
||||
movement = fl!("magnifier", "movement");
|
||||
continuous = fl!("magnifier", "continuous");
|
||||
onedge = fl!("magnifier", "onedge");
|
||||
centered = fl!("magnifier", "centered");
|
||||
});
|
||||
Section::default()
|
||||
.title(&descriptions[movement])
|
||||
.descriptions(descriptions)
|
||||
.view::<Page>(move |_binder, page, section| {
|
||||
let descriptions = §ion.descriptions;
|
||||
|
||||
settings::section()
|
||||
.title(§ion.title)
|
||||
.add(widget::settings::item_row(vec![widget::radio(
|
||||
text::body(&descriptions[continuous]),
|
||||
ZoomMovement::Continuously,
|
||||
Some(page.zoom_config.view_moves),
|
||||
Message::SetMovement,
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.into()]))
|
||||
.add(widget::settings::item_row(vec![widget::radio(
|
||||
text::body(&descriptions[onedge]),
|
||||
ZoomMovement::OnEdge,
|
||||
Some(page.zoom_config.view_moves),
|
||||
Message::SetMovement,
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.into()]))
|
||||
.add(widget::settings::item_row(vec![widget::radio(
|
||||
text::body(&descriptions[centered]),
|
||||
ZoomMovement::Centered,
|
||||
Some(page.zoom_config.view_moves),
|
||||
Message::SetMovement,
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.into()]))
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::AccessibilityMagnifier)
|
||||
})
|
||||
}
|
||||
|
||||
impl Page {
|
||||
pub fn update(
|
||||
&mut self,
|
||||
active_page: page::Entity,
|
||||
message: Message,
|
||||
) -> cosmic::iced::Task<crate::app::Message> {
|
||||
match message {
|
||||
Message::Event(AccessibilityEvent::Magnifier(value)) => {
|
||||
self.magnifier_state = value;
|
||||
}
|
||||
Message::SetMagnifier(value) => {
|
||||
if let Some(sender) = self.wayland_thread.as_ref() {
|
||||
let _ = sender.send(AccessibilityRequest::Magnifier(value));
|
||||
}
|
||||
}
|
||||
Message::SetIncrement(idx) => {
|
||||
self.increment_idx = Some(idx);
|
||||
let value = self.increment_values[idx]
|
||||
.split("%")
|
||||
.next()
|
||||
.unwrap()
|
||||
.parse::<u32>()
|
||||
.unwrap();
|
||||
self.zoom_config.increment = value;
|
||||
|
||||
if let Err(err) = self
|
||||
.accessibility_config
|
||||
.set("accessibility_zoom", self.zoom_config)
|
||||
{
|
||||
error!(?err, "Failed to set config 'accessibility_zoom'");
|
||||
}
|
||||
}
|
||||
Message::SetSignin(value) => {
|
||||
self.zoom_config.start_on_login = value;
|
||||
|
||||
if let Err(err) = self
|
||||
.accessibility_config
|
||||
.set("accessibility_zoom", self.zoom_config)
|
||||
{
|
||||
error!(?err, "Failed to set config 'accessibility_zoom'");
|
||||
}
|
||||
}
|
||||
Message::SetMovement(zoom_movement) => {
|
||||
self.zoom_config.view_moves = zoom_movement;
|
||||
|
||||
if let Err(err) = self
|
||||
.accessibility_config
|
||||
.set("accessibility_zoom", self.zoom_config)
|
||||
{
|
||||
error!(?err, "Failed to set config 'accessibility_zoom'");
|
||||
}
|
||||
}
|
||||
// We shouldn't have gotten into this page in that case
|
||||
Message::Event(AccessibilityEvent::Closed) | Message::ProtocolUnavailable => {
|
||||
if active_page == self.entity {
|
||||
return cosmic::iced::Task::done(crate::app::Message::PageMessage(
|
||||
crate::pages::Message::Accessibility(super::Message::Return),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cosmic::iced::Task::none()
|
||||
}
|
||||
}
|
||||
175
cosmic-settings/src/pages/accessibility/mod.rs
Normal file
175
cosmic-settings/src/pages/accessibility/mod.rs
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
use cosmic::{
|
||||
iced_core::text::Wrapping,
|
||||
theme,
|
||||
widget::{button, container, horizontal_space, icon, settings, text},
|
||||
Apply,
|
||||
};
|
||||
pub use cosmic_comp_config::ZoomMovement;
|
||||
use cosmic_settings_page::{
|
||||
self as page,
|
||||
section::{self, Section},
|
||||
Insert,
|
||||
};
|
||||
use slotmap::SlotMap;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
pub mod magnifier;
|
||||
mod wayland;
|
||||
pub use wayland::{AccessibilityEvent, AccessibilityRequest};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Page {
|
||||
entity: page::Entity,
|
||||
magnifier_state: bool,
|
||||
|
||||
wayland_available: bool,
|
||||
wayland_thread: Option<wayland::Sender>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
Event(wayland::AccessibilityEvent),
|
||||
ProtocolUnavailable,
|
||||
Return,
|
||||
}
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn set_id(&mut self, entity: page::Entity) {
|
||||
self.entity = entity;
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new(
|
||||
"accessibility",
|
||||
"preferences-desktop-accessibility-symbolic",
|
||||
)
|
||||
.title(fl!("accessibility"))
|
||||
}
|
||||
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut SlotMap<section::Entity, page::Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![sections.insert(vision())])
|
||||
}
|
||||
|
||||
fn on_enter(
|
||||
&mut self,
|
||||
sender: mpsc::Sender<crate::pages::Message>,
|
||||
) -> cosmic::Task<crate::pages::Message> {
|
||||
if self.wayland_thread.is_none() {
|
||||
match wayland::spawn_wayland_connection() {
|
||||
Ok((tx, mut rx)) => {
|
||||
self.wayland_available = true;
|
||||
self.wayland_thread = Some(tx);
|
||||
tokio::task::spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
let _ = sender
|
||||
.send(crate::pages::Message::Accessibility(Message::Event(event)))
|
||||
.await;
|
||||
}
|
||||
let _ = sender
|
||||
.send(crate::pages::Message::Accessibility(
|
||||
Message::ProtocolUnavailable,
|
||||
))
|
||||
.await;
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"Failed to spawn wayland connection for accessibility page: {}",
|
||||
err
|
||||
);
|
||||
self.wayland_available = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cosmic::Task::none()
|
||||
}
|
||||
|
||||
fn on_leave(&mut self) -> cosmic::Task<crate::pages::Message> {
|
||||
let _ = self.wayland_thread.take();
|
||||
|
||||
cosmic::Task::none()
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {
|
||||
fn sub_pages(page: Insert<crate::pages::Message>) -> Insert<crate::pages::Message> {
|
||||
page.sub_page::<magnifier::Page>()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vision() -> section::Section<crate::pages::Message> {
|
||||
crate::slab!(descriptions {
|
||||
magnifier = fl!("magnifier");
|
||||
vision = fl!("accessibility", "vision");
|
||||
on = fl!("accessibility", "on");
|
||||
off = fl!("accessibility", "off");
|
||||
unavailable = fl!("accessibility", "unavailable");
|
||||
});
|
||||
|
||||
Section::default()
|
||||
.title(&descriptions[vision])
|
||||
.descriptions(descriptions)
|
||||
.view::<Page>(move |binder, page, section| {
|
||||
let descriptions = §ion.descriptions;
|
||||
|
||||
settings::section()
|
||||
.title(§ion.title)
|
||||
.add({
|
||||
let (magnifier_entity, _magnifier_info) = binder
|
||||
.info
|
||||
.iter()
|
||||
.find(|(_, v)| v.id == "accessibility_magnifier")
|
||||
.expect("magnifier page not found");
|
||||
|
||||
let status_text = if page.wayland_available {
|
||||
if page.magnifier_state {
|
||||
&descriptions[on]
|
||||
} else {
|
||||
&descriptions[off]
|
||||
}
|
||||
} else {
|
||||
&descriptions[unavailable]
|
||||
};
|
||||
|
||||
settings::item_row(vec![
|
||||
text::body(&descriptions[magnifier])
|
||||
.wrapping(Wrapping::Word)
|
||||
.into(),
|
||||
horizontal_space().into(),
|
||||
text::body(status_text).wrapping(Wrapping::Word).into(),
|
||||
icon::from_name("go-next-symbolic").size(16).into(),
|
||||
])
|
||||
.apply(container)
|
||||
.class(cosmic::theme::Container::List)
|
||||
.apply(button::custom)
|
||||
.class(theme::Button::Transparent)
|
||||
.on_press_maybe(
|
||||
page.wayland_available
|
||||
.then_some(crate::pages::Message::Page(magnifier_entity)),
|
||||
)
|
||||
})
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
impl Page {
|
||||
pub fn update(&mut self, message: Message) -> cosmic::iced::Task<crate::app::Message> {
|
||||
match message {
|
||||
Message::Event(AccessibilityEvent::Magnifier(value)) => {
|
||||
self.magnifier_state = value;
|
||||
}
|
||||
Message::Event(AccessibilityEvent::Closed) | Message::ProtocolUnavailable => {
|
||||
self.wayland_available = false;
|
||||
}
|
||||
Message::Return => {
|
||||
return cosmic::iced::Task::done(crate::app::Message::Page(self.entity))
|
||||
}
|
||||
}
|
||||
|
||||
cosmic::iced::Task::none()
|
||||
}
|
||||
}
|
||||
152
cosmic-settings/src/pages/accessibility/wayland.rs
Normal file
152
cosmic-settings/src/pages/accessibility/wayland.rs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
use cosmic_protocols::a11y::v1::client::cosmic_a11y_manager_v1;
|
||||
use sctk::{
|
||||
reexports::{
|
||||
calloop::{self, channel, LoopSignal},
|
||||
calloop_wayland_source::WaylandSource,
|
||||
client::{
|
||||
globals::{registry_queue_init, GlobalListContents},
|
||||
protocol::wl_registry,
|
||||
ConnectError, Connection, Dispatch, Proxy,
|
||||
},
|
||||
},
|
||||
registry::RegistryState,
|
||||
};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum AccessibilityEvent {
|
||||
Magnifier(bool),
|
||||
Closed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum AccessibilityRequest {
|
||||
Magnifier(bool),
|
||||
}
|
||||
|
||||
pub type Sender = calloop::channel::Sender<AccessibilityRequest>;
|
||||
|
||||
pub fn spawn_wayland_connection() -> Result<
|
||||
(
|
||||
channel::Sender<AccessibilityRequest>,
|
||||
mpsc::Receiver<AccessibilityEvent>,
|
||||
),
|
||||
ConnectError,
|
||||
> {
|
||||
let (event_tx, event_rx) = mpsc::channel(10);
|
||||
let (request_tx, request_rx) = channel::channel();
|
||||
let conn = Connection::connect_to_env()?;
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
Ok((request_tx, event_rx))
|
||||
}
|
||||
|
||||
fn wayland_thread(
|
||||
conn: Connection,
|
||||
tx: mpsc::Sender<AccessibilityEvent>,
|
||||
rx: channel::Channel<AccessibilityRequest>,
|
||||
) -> anyhow::Result<()> {
|
||||
struct State {
|
||||
loop_signal: 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 state
|
||||
.tx
|
||||
.blocking_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())
|
||||
.map_err(|err| err.error)?;
|
||||
|
||||
let registry_state = RegistryState::new(&globals);
|
||||
let Ok(global) = registry_state.bind_one::<cosmic_a11y_manager_v1::CosmicA11yManagerV1, _, _>(
|
||||
&qhandle,
|
||||
1..=1,
|
||||
(),
|
||||
) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
loop_handle
|
||||
.insert_source(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();
|
||||
}
|
||||
})
|
||||
.map_err(|err| err.error)?;
|
||||
|
||||
let mut state = State {
|
||||
loop_signal: event_loop.get_signal(),
|
||||
tx,
|
||||
global,
|
||||
|
||||
magnifier: false,
|
||||
};
|
||||
|
||||
event_loop.run(None, &mut state, |_| {})?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
use cosmic_settings_page::Entity;
|
||||
|
||||
#[cfg(feature = "page-accessibility")]
|
||||
pub mod accessibility;
|
||||
#[cfg(feature = "page-bluetooth")]
|
||||
pub mod bluetooth;
|
||||
pub mod desktop;
|
||||
|
|
@ -20,6 +22,10 @@ pub mod time;
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
#[cfg(feature = "page-accessibility")]
|
||||
Accessibility(accessibility::Message),
|
||||
#[cfg(feature = "page-accessibility")]
|
||||
AccessibilityMagnifier(accessibility::magnifier::Message),
|
||||
#[cfg(feature = "page-about")]
|
||||
About(system::about::Message),
|
||||
Appearance(desktop::appearance::Message),
|
||||
|
|
|
|||
1
debian/install
vendored
1
debian/install
vendored
|
|
@ -1,6 +1,7 @@
|
|||
/usr/bin/cosmic-settings
|
||||
/usr/share/applications/com.system76.CosmicSettings.desktop
|
||||
/usr/share/applications/com.system76.CosmicSettings.About.desktop
|
||||
/usr/share/applications/com.system76.CosmicSettings.Accessibility.desktop
|
||||
/usr/share/applications/com.system76.CosmicSettings.Appearance.desktop
|
||||
/usr/share/applications/com.system76.CosmicSettings.Bluetooth.desktop
|
||||
/usr/share/applications/com.system76.CosmicSettings.DateTime.desktop
|
||||
|
|
|
|||
|
|
@ -127,6 +127,27 @@ bluetooth-available = Nearby Devices
|
|||
|
||||
bluetooth-adapters = Bluetooth Adapters
|
||||
|
||||
## Accessibility
|
||||
|
||||
accessibility = Accessibility
|
||||
.vision = Vision
|
||||
.on = On
|
||||
.off = Off
|
||||
.unavailable = Unavailable
|
||||
magnifier = Magnifier
|
||||
.controls =
|
||||
Or use keyboard shortcuts:
|
||||
Super + = to zoom in,
|
||||
Super + - to zoom out,
|
||||
Super + scroll with your mouse
|
||||
.increment = Zoom increment
|
||||
.signin = Start magnifier on sign in
|
||||
.applet = Toggle magnifier on/off in applet on the panel
|
||||
.movement = Zoomed view moves
|
||||
.continuous = Continuously with pointer
|
||||
.onedge = When pointer reaches edge
|
||||
.centered = To keep pointer centered
|
||||
|
||||
## Desktop
|
||||
|
||||
desktop = Desktop
|
||||
|
|
|
|||
3
justfile
3
justfile
|
|
@ -32,6 +32,7 @@ polkit-rules-dst := clean(rootdir / prefix) / 'share' / 'polkit-1' / 'rules.d' /
|
|||
# Desktop entries
|
||||
entry-settings := appid + '.desktop'
|
||||
entry-about := appid + '.About.desktop'
|
||||
entry-accessibility := appid + '.Accessibility.desktop'
|
||||
entry-appear := appid + '.Appearance.desktop'
|
||||
entry-bluetooth := appid + '.Bluetooth.desktop'
|
||||
entry-date-time := appid + '.DateTime.desktop'
|
||||
|
|
@ -68,6 +69,7 @@ default: build-release
|
|||
install-desktop-entries:
|
||||
install -Dm0644 'resources/{{entry-settings}}' '{{appdir}}/{{entry-settings}}'
|
||||
install -Dm0644 'resources/{{entry-about}}' '{{appdir}}/{{entry-about}}'
|
||||
install -Dm0644 'resources/{{entry-accessibility}}' '{{appdir}}/{{entry-accessibility}}'
|
||||
install -Dm0644 'resources/{{entry-appear}}' '{{appdir}}/{{entry-appear}}'
|
||||
install -Dm0644 'resources/{{entry-bluetooth}}' '{{appdir}}/{{entry-bluetooth}}'
|
||||
install -Dm0644 'resources/{{entry-date-time}}' '{{appdir}}/{{entry-date-time}}'
|
||||
|
|
@ -118,6 +120,7 @@ uninstall:
|
|||
rm -rf {{bin-dest}} \
|
||||
'{{appdir}}/{{entry-settings}}' \
|
||||
'{{appdir}}/{{entry-about}}' \
|
||||
'{{appdir}}/{{entry-accessibility}}' \
|
||||
'{{appdir}}/{{entry-appear}}' \
|
||||
'{{appdir}}/{{entry-bluetooth}}' \
|
||||
'{{appdir}}/{{entry-date-time}}' \
|
||||
|
|
|
|||
12
resources/com.system76.CosmicSettings.Accessibility.desktop
Normal file
12
resources/com.system76.CosmicSettings.Accessibility.desktop
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[Desktop Entry]
|
||||
Name=Accessibility
|
||||
Comment=Accessibility settings.
|
||||
Type=Settings
|
||||
Exec=cosmic-settings accessibility
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
Keywords=COSMIC;theme;
|
||||
NoDisplay=true
|
||||
OnlyShowIn=COSMIC
|
||||
Icon=preferences-desktop-accessibility
|
||||
StartupNotify=true
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<svg width="240" height="126" viewBox="0 0 240 126" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_174_7927)">
|
||||
<path d="M0 0H240V126H0V0Z" fill="#666666"/>
|
||||
<path d="M0 0H240V32H0V0Z" fill="#1B1B1B"/>
|
||||
<path d="M104 16C104 7.16344 111.163 0 120 0V0C128.837 0 136 7.16344 136 16V16C136 24.8366 128.837 32 120 32V32C111.163 32 104 24.8366 104 16V16Z" fill="#2A2A2A"/>
|
||||
<g clip-path="url(#clip1_174_7927)">
|
||||
<path d="M120 8C117.878 8 115.843 8.84285 114.343 10.3431C112.843 11.8434 112 13.8783 112 16C112 18.1217 112.843 20.1566 114.343 21.6569C115.843 23.1571 117.878 24 120 24C122.122 24 124.157 23.1571 125.657 21.6569C127.157 20.1566 128 18.1217 128 16C128 13.8783 127.157 11.8434 125.657 10.3431C124.157 8.84285 122.122 8 120 8ZM119.969 9C120.232 9 120.492 9.05173 120.734 9.15224C120.977 9.25275 121.197 9.40007 121.383 9.58579C121.569 9.7715 121.716 9.99198 121.817 10.2346C121.917 10.4773 121.969 10.7374 121.969 11C121.969 11.5304 121.758 12.0391 121.383 12.4142C121.008 12.7893 120.499 13 119.969 13C119.439 13 118.93 12.7893 118.555 12.4142C118.18 12.0391 117.969 11.5304 117.969 11C117.969 10.4696 118.18 9.96086 118.555 9.58579C118.93 9.21071 119.439 9 119.969 9ZM114.469 14H125.469V16H122.469V22H120.469V18H119.469V22H117.469V16H114.469V14Z" fill="#C4C4C4"/>
|
||||
</g>
|
||||
<rect x="16" y="34" width="208" height="66" rx="8" fill="#1B1B1B"/>
|
||||
<path d="M168 67C168 60.3726 173.373 55 180 55H204C210.627 55 216 60.3726 216 67V67C216 73.6274 210.627 79 204 79H180C173.373 79 168 73.6274 168 67V67Z" fill="#636363"/>
|
||||
<path d="M34.0239 72H32.7359L32.3999 67.646C32.2599 65.938 32.1806 64.6267 32.1619 63.712L30.0759 70.908H28.8299L26.6319 63.698C26.6319 64.8647 26.5713 66.2134 26.4499 67.744L26.1279 72H24.8679L25.6659 62.354H27.4579L29.4879 69.368L31.4199 62.354H33.2259L34.0239 72Z" fill="#C4C4C4"/>
|
||||
<path d="M41.1964 70.278C41.1964 70.5767 41.2477 70.8007 41.3504 70.95C41.4531 71.09 41.6071 71.1974 41.8124 71.272L41.5184 72.168C41.1357 72.1214 40.8277 72.014 40.5944 71.846C40.3611 71.678 40.1884 71.4167 40.0764 71.062C39.5817 71.7994 38.8491 72.168 37.8784 72.168C37.1504 72.168 36.5764 71.9627 36.1564 71.552C35.7364 71.1414 35.5264 70.6047 35.5264 69.942C35.5264 69.158 35.8064 68.556 36.3664 68.136C36.9357 67.716 37.7384 67.506 38.7744 67.506H39.9084V66.96C39.9084 66.4374 39.7824 66.064 39.5304 65.84C39.2784 65.616 38.8911 65.504 38.3684 65.504C37.8271 65.504 37.1644 65.6347 36.3804 65.896L36.0584 64.958C36.9731 64.622 37.8224 64.454 38.6064 64.454C39.4744 64.454 40.1231 64.6687 40.5524 65.098C40.9817 65.518 41.1964 66.12 41.1964 66.904V70.278ZM38.1724 71.202C38.9097 71.202 39.4884 70.8194 39.9084 70.054V68.36H38.9424C37.5797 68.36 36.8984 68.864 36.8984 69.872C36.8984 70.3107 37.0057 70.642 37.2204 70.866C37.4351 71.09 37.7524 71.202 38.1724 71.202Z" fill="#C4C4C4"/>
|
||||
<path d="M49.6549 64.958C49.3749 65.0514 49.0669 65.112 48.7309 65.14C48.3949 65.168 47.9843 65.182 47.4989 65.182C48.3669 65.574 48.8009 66.1947 48.8009 67.044C48.8009 67.7814 48.5489 68.3834 48.0449 68.85C47.5409 69.3167 46.8549 69.55 45.9869 69.55C45.6509 69.55 45.3383 69.5034 45.0489 69.41C44.9369 69.4847 44.8483 69.5874 44.7829 69.718C44.7176 69.8394 44.6849 69.9654 44.6849 70.096C44.6849 70.4974 45.0069 70.698 45.6509 70.698H46.8269C47.3216 70.698 47.7603 70.7867 48.1429 70.964C48.5256 71.1414 48.8196 71.384 49.0249 71.692C49.2396 72 49.3469 72.35 49.3469 72.742C49.3469 73.4607 49.0529 74.0114 48.4649 74.394C47.8769 74.786 47.0183 74.982 45.8889 74.982C45.0956 74.982 44.4656 74.898 43.9989 74.73C43.5416 74.5714 43.2149 74.3287 43.0189 74.002C42.8229 73.6754 42.7249 73.2554 42.7249 72.742H43.8869C43.8869 73.0407 43.9429 73.274 44.0549 73.442C44.1669 73.6194 44.3676 73.75 44.6569 73.834C44.9463 73.9274 45.3569 73.974 45.8889 73.974C46.6636 73.974 47.2143 73.876 47.5409 73.68C47.8769 73.4934 48.0449 73.2087 48.0449 72.826C48.0449 72.4807 47.9143 72.2194 47.6529 72.042C47.3916 71.8647 47.0276 71.776 46.5609 71.776H45.3989C44.7736 71.776 44.2976 71.6454 43.9709 71.384C43.6536 71.1134 43.4949 70.7774 43.4949 70.376C43.4949 70.1334 43.5649 69.9 43.7049 69.676C43.8449 69.452 44.0456 69.2514 44.3069 69.074C43.8776 68.85 43.5603 68.5747 43.3549 68.248C43.1589 67.912 43.0609 67.506 43.0609 67.03C43.0609 66.5354 43.1823 66.092 43.4249 65.7C43.6769 65.308 44.0176 65.0047 44.4469 64.79C44.8856 64.566 45.3709 64.454 45.9029 64.454C46.4816 64.4634 46.9669 64.4447 47.3589 64.398C47.7509 64.342 48.0729 64.272 48.3249 64.188C48.5863 64.0947 48.9036 63.964 49.2769 63.796L49.6549 64.958ZM45.9029 65.378C45.4176 65.378 45.0396 65.532 44.7689 65.84C44.5076 66.1387 44.3769 66.5354 44.3769 67.03C44.3769 67.534 44.5123 67.94 44.7829 68.248C45.0536 68.5467 45.4363 68.696 45.9309 68.696C46.4349 68.696 46.8176 68.5514 47.0789 68.262C47.3496 67.9634 47.4849 67.548 47.4849 67.016C47.4849 65.924 46.9576 65.378 45.9029 65.378Z" fill="#C4C4C4"/>
|
||||
<path d="M54.4504 64.454C55.1224 64.454 55.645 64.6547 56.0184 65.056C56.401 65.4574 56.5924 66.008 56.5924 66.708V72H55.3044V66.89C55.3044 66.3674 55.2064 65.9987 55.0104 65.784C54.8144 65.5694 54.525 65.462 54.1424 65.462C53.7504 65.462 53.405 65.574 53.1064 65.798C52.8077 66.022 52.5277 66.344 52.2664 66.764V72H50.9784V64.622H52.0844L52.1964 65.714C52.4577 65.322 52.7797 65.014 53.1624 64.79C53.5544 64.566 53.9837 64.454 54.4504 64.454Z" fill="#C4C4C4"/>
|
||||
<path d="M60.4695 64.622V72H59.1815V64.622H60.4695ZM59.8115 61.08C60.0821 61.08 60.3015 61.164 60.4695 61.332C60.6375 61.5 60.7215 61.71 60.7215 61.962C60.7215 62.214 60.6375 62.424 60.4695 62.592C60.3015 62.7507 60.0821 62.83 59.8115 62.83C59.5501 62.83 59.3355 62.7507 59.1675 62.592C58.9995 62.424 58.9155 62.214 58.9155 61.962C58.9155 61.71 58.9995 61.5 59.1675 61.332C59.3355 61.164 59.5501 61.08 59.8115 61.08Z" fill="#C4C4C4"/>
|
||||
<path d="M65.6667 61.5C66.0587 61.5 66.4133 61.542 66.7307 61.626C67.0573 61.71 67.384 61.836 67.7107 62.004L67.2627 62.9C66.7773 62.6667 66.2593 62.55 65.7087 62.55C65.214 62.55 64.8733 62.6387 64.6867 62.816C64.5 62.9934 64.4067 63.2687 64.4067 63.642V64.622H68.5227V72H67.2347V65.616H64.4067V72H63.1187V65.616H61.9427V64.622H63.1187V63.656C63.1187 63.0027 63.3287 62.48 63.7487 62.088C64.1687 61.696 64.808 61.5 65.6667 61.5Z" fill="#C4C4C4"/>
|
||||
<path d="M76.7574 68.094C76.7574 68.3087 76.7481 68.528 76.7294 68.752H72.0254C72.0814 69.564 72.2867 70.1614 72.6414 70.544C72.9961 70.9267 73.4534 71.118 74.0134 71.118C74.3681 71.118 74.6947 71.0667 74.9934 70.964C75.2921 70.8614 75.6047 70.698 75.9314 70.474L76.4914 71.244C75.7074 71.86 74.8487 72.168 73.9154 72.168C72.8887 72.168 72.0861 71.832 71.5074 71.16C70.9381 70.488 70.6534 69.564 70.6534 68.388C70.6534 67.6227 70.7747 66.946 71.0174 66.358C71.2694 65.7607 71.6241 65.294 72.0814 64.958C72.5481 64.622 73.0941 64.454 73.7194 64.454C74.6994 64.454 75.4507 64.776 75.9734 65.42C76.4961 66.064 76.7574 66.9554 76.7574 68.094ZM75.4834 67.716C75.4834 66.988 75.3387 66.4327 75.0494 66.05C74.7601 65.6674 74.3261 65.476 73.7474 65.476C72.6927 65.476 72.1187 66.2507 72.0254 67.8H75.4834V67.716Z" fill="#C4C4C4"/>
|
||||
<path d="M82.0203 64.454C82.2816 64.454 82.5243 64.482 82.7483 64.538L82.5103 65.798C82.2863 65.742 82.0716 65.714 81.8663 65.714C81.409 65.714 81.0403 65.882 80.7603 66.218C80.4803 66.554 80.261 67.0767 80.1023 67.786V72H78.8143V64.622H79.9203L80.0463 66.12C80.2423 65.5694 80.5083 65.154 80.8443 64.874C81.1803 64.594 81.5723 64.454 82.0203 64.454Z" fill="#C4C4C4"/>
|
||||
<path d="M190 67C190 72.5228 185.523 77 180 77C174.477 77 170 72.5228 170 67C170 61.4772 174.477 57 180 57C185.523 57 190 61.4772 190 67Z" fill="#030303"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_174_7927">
|
||||
<rect width="240" height="126" rx="8" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_174_7927">
|
||||
<rect width="16" height="16" fill="white" transform="translate(112 8)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.6 KiB |
Loading…
Add table
Add a link
Reference in a new issue