feat: add desktop entries for each settings page
This commit is contained in:
parent
a40b1e9a1a
commit
1c61885428
65 changed files with 294 additions and 37 deletions
55
cosmic-settings/Cargo.toml
Normal file
55
cosmic-settings/Cargo.toml
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
[package]
|
||||
name = "cosmic-settings"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0"
|
||||
rust-version = "1.65.0"
|
||||
|
||||
[dependencies]
|
||||
async-channel = "2.1.1"
|
||||
color-eyre = "0.6.2"
|
||||
cosmic-bg-config = { workspace = true }
|
||||
cosmic-randr-shell = { workspace = true }
|
||||
cosmic-settings-wallpaper = { path = "../pages/wallpapers" }
|
||||
cosmic-settings-page = { path = "../page" }
|
||||
cosmic-settings-system = { path = "../pages/system" }
|
||||
cosmic-settings-time = { path = "../pages/time" }
|
||||
derivative = "2.2.0"
|
||||
derive_setters = "0.1.6"
|
||||
dirs = "5.0.1"
|
||||
generator = "=0.7.5"
|
||||
i18n-embed-fl = "0.7.0"
|
||||
itertools = "0.12.0"
|
||||
libcosmic = {workspace = true}
|
||||
once_cell = "1.19.0"
|
||||
regex = "1.10.3"
|
||||
rust-embed = "8.2.0"
|
||||
slotmap = "1.0.7"
|
||||
tokio = "1.35.1"
|
||||
downcast-rs = "1.2.0"
|
||||
cosmic-comp-config = { workspace = true }
|
||||
# TODO: migrate this dependency to the pages/desktop crate.
|
||||
cosmic-panel-config = { workspace = true }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
url = "2.5.0"
|
||||
freedesktop-desktop-entry = "0.5.0"
|
||||
notify = "6.1.1"
|
||||
anyhow = "1.0"
|
||||
image = "0.25"
|
||||
serde = { version = "1.0.196", features = ["derive"] }
|
||||
ashpd = { version = "0.7", default-features = false }
|
||||
ron = "0.8"
|
||||
static_init = "1.0.3"
|
||||
clap = {version = "4.4.18", features = ["derive"] }
|
||||
itoa = "1.0.10"
|
||||
futures = { package = "futures-lite", version = "2.2.0" }
|
||||
|
||||
[dependencies.i18n-embed]
|
||||
version = "0.14.1"
|
||||
features = ["fluent-system", "desktop-requester"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
wgpu = ["libcosmic/wgpu"]
|
||||
test = []
|
||||
0
cosmic-settings/README.md
Normal file
0
cosmic-settings/README.md
Normal file
4
cosmic-settings/i18n.toml
Normal file
4
cosmic-settings/i18n.toml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
fallback_language = "en"
|
||||
|
||||
[fluent]
|
||||
assets_dir = "../i18n"
|
||||
1
cosmic-settings/profile.json
Normal file
1
cosmic-settings/profile.json
Normal file
File diff suppressed because one or more lines are too long
736
cosmic-settings/src/app.rs
Normal file
736
cosmic-settings/src/app.rs
Normal file
|
|
@ -0,0 +1,736 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::pages::desktop::{
|
||||
self, appearance,
|
||||
dock::{self, applets::ADD_DOCK_APPLET_DIALOGUE_ID},
|
||||
panel::{
|
||||
self,
|
||||
applets_inner::{self, AppletsPage, APPLET_DND_ICON_ID},
|
||||
inner as _panel,
|
||||
},
|
||||
};
|
||||
use crate::pages::input::{self, keyboard};
|
||||
use crate::pages::{self, display, networking, sound, system, time};
|
||||
use crate::subscription::desktop_files;
|
||||
use crate::widget::{page_title, search_header};
|
||||
use crate::PageCommands;
|
||||
use cosmic::app::DbusActivationMessage;
|
||||
use cosmic::iced::Subscription;
|
||||
use cosmic::widget::{button, row, text_input};
|
||||
use cosmic::{
|
||||
app::{Command, Core},
|
||||
cosmic_config::{config_state_subscription, config_subscription},
|
||||
iced::{
|
||||
self,
|
||||
event::{self, wayland, PlatformSpecific},
|
||||
window, Length,
|
||||
},
|
||||
prelude::*,
|
||||
widget::{column, container, icon, nav_bar, scrollable, segmented_button, settings},
|
||||
Element,
|
||||
};
|
||||
use cosmic_panel_config::CosmicPanelConfig;
|
||||
use cosmic_settings_page::{self as page, section};
|
||||
use page::Entity;
|
||||
use std::{borrow::Cow, str::FromStr};
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
pub struct SettingsApp {
|
||||
active_page: page::Entity,
|
||||
config: Config,
|
||||
core: Core,
|
||||
nav_model: nav_bar::Model,
|
||||
pages: page::Binder<crate::pages::Message>,
|
||||
search_active: bool,
|
||||
search_id: cosmic::widget::Id,
|
||||
search_input: String,
|
||||
search_selections: Vec<(page::Entity, section::Entity)>,
|
||||
}
|
||||
|
||||
impl SettingsApp {
|
||||
fn subcommand_to_page(&self, cmd: &PageCommands) -> Option<Entity> {
|
||||
match cmd {
|
||||
PageCommands::About => self.pages.page_id::<system::about::Page>(),
|
||||
PageCommands::Appearance => self.pages.page_id::<desktop::appearance::Page>(),
|
||||
PageCommands::Bluetooth => None,
|
||||
PageCommands::DateTime => self.pages.page_id::<time::date::Page>(),
|
||||
PageCommands::DesktopPanel => self.pages.page_id::<desktop::options::Page>(),
|
||||
PageCommands::Displays => self.pages.page_id::<display::Page>(),
|
||||
PageCommands::Firmware => self.pages.page_id::<system::firmware::Page>(),
|
||||
PageCommands::Keyboard => self.pages.page_id::<input::keyboard::Page>(),
|
||||
PageCommands::Mouse => self.pages.page_id::<input::mouse::Page>(),
|
||||
PageCommands::Network => None,
|
||||
PageCommands::Notifications => self.pages.page_id::<desktop::notifications::Page>(),
|
||||
PageCommands::Power => None,
|
||||
PageCommands::RegionLanguage => self.pages.page_id::<time::region::Page>(),
|
||||
PageCommands::Sound => self.pages.page_id::<sound::Page>(),
|
||||
PageCommands::Time => self.pages.page_id::<time::Page>(),
|
||||
PageCommands::Touchpad => self.pages.page_id::<input::touchpad::Page>(),
|
||||
PageCommands::Users => self.pages.page_id::<system::users::Page>(),
|
||||
PageCommands::Wallpaper => self.pages.page_id::<desktop::wallpaper::Page>(),
|
||||
PageCommands::Workspaces => self.pages.page_id::<desktop::workspaces::Page>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
DesktopInfo,
|
||||
Error(String),
|
||||
Page(page::Entity),
|
||||
PageMessage(crate::pages::Message),
|
||||
PanelConfig(CosmicPanelConfig),
|
||||
SearchActivate,
|
||||
SearchChanged(String),
|
||||
SearchClear,
|
||||
SearchSubmit,
|
||||
SetWindowTitle,
|
||||
OpenContextDrawer(Cow<'static, str>),
|
||||
CloseContextDrawer,
|
||||
SetTheme(cosmic::theme::Theme),
|
||||
}
|
||||
|
||||
impl cosmic::Application for SettingsApp {
|
||||
type Executor = cosmic::executor::single::Executor;
|
||||
type Flags = crate::Args;
|
||||
type Message = Message;
|
||||
|
||||
const APP_ID: &'static str = "com.system76.CosmicSettings";
|
||||
|
||||
fn core(&self) -> &Core {
|
||||
&self.core
|
||||
}
|
||||
|
||||
fn core_mut(&mut self) -> &mut Core {
|
||||
&mut self.core
|
||||
}
|
||||
|
||||
fn init(core: Core, flags: Self::Flags) -> (Self, Command<Self::Message>) {
|
||||
let mut app = SettingsApp {
|
||||
active_page: page::Entity::default(),
|
||||
config: Config::new(),
|
||||
core,
|
||||
nav_model: nav_bar::Model::default(),
|
||||
pages: page::Binder::default(),
|
||||
search_active: false,
|
||||
search_id: cosmic::widget::Id::unique(),
|
||||
search_input: String::new(),
|
||||
search_selections: Vec::default(),
|
||||
};
|
||||
|
||||
let desktop_id = app.insert_page::<desktop::Page>().id();
|
||||
app.insert_page::<input::Page>();
|
||||
app.insert_page::<display::Page>();
|
||||
app.insert_page::<sound::Page>();
|
||||
app.insert_page::<system::Page>();
|
||||
app.insert_page::<time::Page>();
|
||||
|
||||
let active_id = match flags.subcommand {
|
||||
Some(p) => app.subcommand_to_page(&p),
|
||||
None => app
|
||||
.pages
|
||||
.find_page_by_id(&app.config.active_page)
|
||||
.map(|(id, _info)| id),
|
||||
}
|
||||
.unwrap_or(desktop_id);
|
||||
|
||||
let command = app.activate_page(active_id);
|
||||
|
||||
(app, command)
|
||||
}
|
||||
|
||||
fn nav_model(&self) -> Option<&nav_bar::Model> {
|
||||
Some(&self.nav_model)
|
||||
}
|
||||
|
||||
fn header_start(&self) -> Vec<Element<Self::Message>> {
|
||||
let mut widgets = Vec::new();
|
||||
|
||||
widgets.push(if self.search_active {
|
||||
text_input::search_input("", &self.search_input)
|
||||
.width(Length::Fixed(240.0))
|
||||
.id(self.search_id.clone())
|
||||
.on_clear(Message::SearchClear)
|
||||
.on_input(Message::SearchChanged)
|
||||
.on_submit(Message::SearchSubmit)
|
||||
.into()
|
||||
} else {
|
||||
icon::from_name("system-search-symbolic")
|
||||
.apply(button::icon)
|
||||
.padding([0, cosmic::theme::active().cosmic().space_s()])
|
||||
.on_press(Message::SearchActivate)
|
||||
.into()
|
||||
});
|
||||
|
||||
widgets
|
||||
}
|
||||
|
||||
fn on_close_requested(&self, id: window::Id) -> Option<Self::Message> {
|
||||
let message = if id == *applets_inner::ADD_PANEL_APPLET_DIALOGUE_ID {
|
||||
Message::PageMessage(crate::pages::Message::PanelApplet(
|
||||
applets_inner::Message::ClosedAppletDialog,
|
||||
))
|
||||
} else if id == *ADD_DOCK_APPLET_DIALOGUE_ID {
|
||||
Message::PageMessage(crate::pages::Message::DockApplet(dock::applets::Message(
|
||||
applets_inner::Message::ClosedAppletDialog,
|
||||
)))
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(message)
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> Command<Self::Message> {
|
||||
if self.search_active {
|
||||
self.search_active = false;
|
||||
self.search_clear();
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn on_nav_select(&mut self, id: nav_bar::Id) -> Command<Self::Message> {
|
||||
if let Some(page) = self.nav_model.data::<page::Entity>(id).copied() {
|
||||
return self.activate_page(page);
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn on_search(&mut self) -> Command<Self::Message> {
|
||||
self.search_active = true;
|
||||
cosmic::widget::text_input::focus(self.search_id.clone())
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
let window_break = event::listen_with(|event, _| match event {
|
||||
iced::Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Output(
|
||||
wayland::OutputEvent::Created(Some(info)),
|
||||
o,
|
||||
))) if info.name.is_some() => Some(Message::PageMessage(crate::pages::Message::Panel(
|
||||
panel::Message(_panel::Message::OutputAdded(info.name.unwrap(), o)),
|
||||
))),
|
||||
iced::Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Output(
|
||||
wayland::OutputEvent::Removed,
|
||||
o,
|
||||
))) => Some(Message::PageMessage(crate::pages::Message::Panel(
|
||||
panel::Message(_panel::Message::OutputRemoved(o)),
|
||||
))),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
Subscription::batch(vec![
|
||||
window_break,
|
||||
desktop_files(0).map(|_| Message::DesktopInfo),
|
||||
config_subscription(0, "com.system76.CosmicPanel.Panel".into(), 1).map(|update| {
|
||||
for why in update.errors {
|
||||
tracing::error!(?why, "panel config load error");
|
||||
}
|
||||
|
||||
Message::PanelConfig(update.config)
|
||||
}),
|
||||
config_subscription(0, "com.system76.CosmicPanel.Dock".into(), 1).map(|update| {
|
||||
for why in update.errors {
|
||||
tracing::error!(?why, "dock config load error");
|
||||
}
|
||||
|
||||
Message::PanelConfig(update.config)
|
||||
}),
|
||||
config_state_subscription(0, cosmic_bg_config::NAME.into(), 1).map(|update| {
|
||||
Message::PageMessage(pages::Message::DesktopWallpaper(
|
||||
pages::desktop::wallpaper::Message::UpdateState(update.config),
|
||||
))
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn update(&mut self, message: Message) -> Command<Self::Message> {
|
||||
match message {
|
||||
Message::Page(page) => return self.activate_page(page),
|
||||
|
||||
Message::SetWindowTitle => return self.set_title(),
|
||||
|
||||
Message::SearchChanged(phrase) => {
|
||||
self.search_changed(phrase);
|
||||
}
|
||||
|
||||
Message::SearchActivate => {
|
||||
self.search_active = true;
|
||||
return cosmic::widget::text_input::focus(self.search_id.clone());
|
||||
}
|
||||
|
||||
Message::SearchClear => {
|
||||
self.search_clear();
|
||||
}
|
||||
|
||||
Message::SearchSubmit => {
|
||||
self.search_active = true;
|
||||
}
|
||||
|
||||
Message::PageMessage(message) => match message {
|
||||
crate::pages::Message::About(message) => {
|
||||
page::update!(self.pages, message, system::about::Page);
|
||||
}
|
||||
|
||||
crate::pages::Message::DateAndTime(message) => {
|
||||
page::update!(self.pages, message, time::date::Page);
|
||||
}
|
||||
|
||||
crate::pages::Message::Desktop(message) => {
|
||||
page::update!(self.pages, message, desktop::Page);
|
||||
}
|
||||
|
||||
crate::pages::Message::DesktopWallpaper(message) => {
|
||||
if let Some(page) = self.pages.page_mut::<desktop::wallpaper::Page>() {
|
||||
return page.update(message).map(cosmic::app::Message::App);
|
||||
}
|
||||
}
|
||||
|
||||
crate::pages::Message::DesktopWorkspaces(message) => {
|
||||
page::update!(self.pages, message, desktop::workspaces::Page);
|
||||
}
|
||||
|
||||
crate::pages::Message::Displays(message) => {
|
||||
if let Some(page) = self.pages.page_mut::<display::Page>() {
|
||||
return page.update(message).map(cosmic::app::Message::App);
|
||||
}
|
||||
}
|
||||
|
||||
crate::pages::Message::Input(message) => {
|
||||
if let Some(page) = self.pages.page_mut::<input::Page>() {
|
||||
return page.update(message).map(cosmic::app::Message::App);
|
||||
}
|
||||
}
|
||||
|
||||
crate::pages::Message::External { .. } => {
|
||||
todo!("external plugins not supported yet");
|
||||
}
|
||||
|
||||
crate::pages::Message::Page(page) => {
|
||||
return self.activate_page(page);
|
||||
}
|
||||
|
||||
crate::pages::Message::Panel(message) => {
|
||||
page::update!(self.pages, message, panel::Page);
|
||||
}
|
||||
|
||||
crate::pages::Message::PanelApplet(message) => {
|
||||
if let Some(page) = self.pages.page_mut::<applets_inner::Page>() {
|
||||
return page
|
||||
.update(message, *applets_inner::ADD_PANEL_APPLET_DIALOGUE_ID)
|
||||
.map(cosmic::app::Message::App);
|
||||
}
|
||||
}
|
||||
|
||||
crate::pages::Message::Dock(message) => {
|
||||
page::update!(self.pages, message, dock::Page);
|
||||
}
|
||||
|
||||
crate::pages::Message::DockApplet(message) => {
|
||||
if let Some(page) = self.pages.page_mut::<dock::applets::Page>() {
|
||||
return page.update(message).map(cosmic::app::Message::App);
|
||||
}
|
||||
}
|
||||
|
||||
crate::pages::Message::Appearance(message) => {
|
||||
if let Some(page) = self.pages.page_mut::<appearance::Page>() {
|
||||
return page.update(message).map(cosmic::app::Message::App);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Message::PanelConfig(config) if config.name.to_lowercase().contains("panel") => {
|
||||
page::update!(
|
||||
self.pages,
|
||||
panel::Message(_panel::Message::PanelConfig(config.clone())),
|
||||
panel::Page
|
||||
);
|
||||
|
||||
if let Some(page) = self.pages.page_mut::<applets_inner::Page>() {
|
||||
return page
|
||||
.update(
|
||||
applets_inner::Message::PanelConfig(config),
|
||||
*applets_inner::ADD_PANEL_APPLET_DIALOGUE_ID,
|
||||
)
|
||||
.map(cosmic::app::Message::App);
|
||||
}
|
||||
}
|
||||
|
||||
Message::PanelConfig(config) if config.name.to_lowercase().contains("dock") => {
|
||||
page::update!(
|
||||
self.pages,
|
||||
dock::Message::Inner(_panel::Message::PanelConfig(config.clone(),)),
|
||||
dock::Page
|
||||
);
|
||||
page::update!(
|
||||
self.pages,
|
||||
dock::applets::Message(applets_inner::Message::PanelConfig(config,)),
|
||||
dock::applets::Page
|
||||
);
|
||||
}
|
||||
|
||||
Message::DesktopInfo => {
|
||||
let info_list: Vec<_> = freedesktop_desktop_entry::Iter::new(
|
||||
freedesktop_desktop_entry::default_paths(),
|
||||
)
|
||||
.filter_map(|p| applets_inner::Applet::try_from(Cow::from(p)).ok())
|
||||
.collect();
|
||||
|
||||
page::update!(
|
||||
self.pages,
|
||||
dock::applets::Message(applets_inner::Message::Applets(info_list.clone())),
|
||||
dock::applets::Page
|
||||
);
|
||||
if let Some(page) = self.pages.page_mut::<applets_inner::Page>() {
|
||||
return page
|
||||
.update(
|
||||
applets_inner::Message::Applets(info_list),
|
||||
*applets_inner::ADD_PANEL_APPLET_DIALOGUE_ID,
|
||||
)
|
||||
.map(cosmic::app::Message::App);
|
||||
}
|
||||
}
|
||||
|
||||
Message::PanelConfig(_) => {}
|
||||
|
||||
Message::SetTheme(t) => return cosmic::app::command::set_theme(t),
|
||||
|
||||
Message::OpenContextDrawer(title) => {
|
||||
self.core.window.show_context = true;
|
||||
self.set_context_title(title.to_string());
|
||||
}
|
||||
|
||||
Message::CloseContextDrawer => {
|
||||
self.core.window.show_context = false;
|
||||
}
|
||||
|
||||
Message::Error(error) => {
|
||||
tracing::error!(error, "error occurred");
|
||||
}
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn dbus_activation(&mut self, msg: DbusActivationMessage) -> Command<Self::Message> {
|
||||
match msg.msg {
|
||||
cosmic::app::DbusActivationDetails::Activate
|
||||
| cosmic::app::DbusActivationDetails::Open { .. } => None,
|
||||
cosmic::app::DbusActivationDetails::ActivateAction { action, .. } => {
|
||||
PageCommands::from_str(&action)
|
||||
.ok()
|
||||
.and_then(|action| self.subcommand_to_page(&action))
|
||||
.map(|e| self.activate_page(e))
|
||||
}
|
||||
}
|
||||
.unwrap_or_else(Command::none)
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
let theme = cosmic::theme::active();
|
||||
|
||||
let page_view = if self.search_active {
|
||||
self.search_view()
|
||||
} else if let Some(content) = self.pages.content(self.active_page) {
|
||||
self.page_view(content)
|
||||
} else if let Some(sub_pages) = self.pages.sub_pages(self.active_page) {
|
||||
self.sub_page_view(sub_pages)
|
||||
} else {
|
||||
panic!("page without sub-pages or content");
|
||||
};
|
||||
|
||||
let padding = if self.core.is_condensed() {
|
||||
0
|
||||
} else {
|
||||
theme.cosmic().space_l()
|
||||
};
|
||||
|
||||
container(page_view)
|
||||
.max_width(800)
|
||||
.width(Length::Fill)
|
||||
.apply(container)
|
||||
.center_x()
|
||||
.padding([theme.cosmic().space_xxs(), padding])
|
||||
.width(Length::Fill)
|
||||
.apply(scrollable)
|
||||
.into()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn view_window(&self, id: window::Id) -> Element<Message> {
|
||||
if let Some(Some(page)) =
|
||||
(id == *APPLET_DND_ICON_ID).then(|| self.pages.page::<applets_inner::Page>())
|
||||
{
|
||||
return page.dnd_icon();
|
||||
}
|
||||
|
||||
if let Some(Some(page)) = (id == *applets_inner::ADD_PANEL_APPLET_DIALOGUE_ID)
|
||||
.then(|| self.pages.page::<applets_inner::Page>())
|
||||
{
|
||||
return page.add_applet_view(crate::pages::Message::PanelApplet);
|
||||
}
|
||||
|
||||
if let Some(Some(page)) =
|
||||
(id == *ADD_DOCK_APPLET_DIALOGUE_ID).then(|| self.pages.page::<dock::applets::Page>())
|
||||
{
|
||||
return page.inner().add_applet_view(|msg| {
|
||||
crate::pages::Message::DockApplet(dock::applets::Message(msg))
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(Some(page)) = (id == *keyboard::ADD_INPUT_SOURCE_DIALOGUE_ID)
|
||||
.then(|| self.pages.page::<input::Page>())
|
||||
{
|
||||
return page.add_input_source_view();
|
||||
}
|
||||
|
||||
if let Some(Some(page)) = (id == *keyboard::SPECIAL_CHARACTER_DIALOGUE_ID)
|
||||
.then(|| self.pages.page::<input::Page>())
|
||||
{
|
||||
return page.special_character_key_view();
|
||||
}
|
||||
|
||||
if let Some(page) = self.pages.page::<desktop::wallpaper::Page>() {
|
||||
if id == page.color_dialog {
|
||||
return page.show_color_dialog();
|
||||
}
|
||||
}
|
||||
|
||||
panic!("unknown window ID: {id:?}");
|
||||
}
|
||||
|
||||
fn context_drawer(&self) -> Option<Element<Message>> {
|
||||
if self.core.window.show_context {
|
||||
self.pages
|
||||
.context_drawer(self.active_page)
|
||||
.map(|e| e.map(Message::PageMessage))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SettingsApp {
|
||||
/// Activates a page.
|
||||
fn activate_page(&mut self, page: page::Entity) -> Command<crate::Message> {
|
||||
let current_page = self.active_page;
|
||||
self.active_page = page;
|
||||
|
||||
let mut leave_command = iced::Command::none()
|
||||
.map(Message::PageMessage)
|
||||
.map(cosmic::app::Message::App);
|
||||
|
||||
if current_page != page {
|
||||
leave_command = self
|
||||
.pages
|
||||
.on_leave(current_page)
|
||||
.unwrap_or(iced::Command::none())
|
||||
.map(Message::PageMessage)
|
||||
.map(cosmic::app::Message::App);
|
||||
self.config.active_page = Box::from(&*self.pages.info[page].id);
|
||||
self.config
|
||||
.set_active_page(Box::from(&*self.pages.info[page].id));
|
||||
}
|
||||
|
||||
self.search_clear();
|
||||
self.search_active = false;
|
||||
self.activate_navbar(page);
|
||||
|
||||
let page_command = self
|
||||
.pages
|
||||
.page_reload(page)
|
||||
.map(Message::PageMessage)
|
||||
.map(cosmic::app::Message::App);
|
||||
|
||||
Command::batch(vec![
|
||||
leave_command,
|
||||
page_command,
|
||||
cosmic::command::future(async { Message::SetWindowTitle })
|
||||
.map(cosmic::app::Message::App),
|
||||
])
|
||||
}
|
||||
|
||||
fn set_title(&mut self) -> Command<crate::Message> {
|
||||
self.set_window_title(
|
||||
format!(
|
||||
"{} - COSMIC Settings",
|
||||
self.pages.info[self.active_page].title
|
||||
),
|
||||
window::Id::MAIN,
|
||||
)
|
||||
}
|
||||
|
||||
/// Activates the navbar item associated with a page.
|
||||
fn activate_navbar(&mut self, mut page: page::Entity) {
|
||||
if let Some(parent) = self.pages.info[page].parent {
|
||||
page = parent;
|
||||
}
|
||||
|
||||
if let Some(nav_id) = self.pages.data(page) {
|
||||
self.nav_model.activate(*nav_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a main page to the settings application.
|
||||
fn insert_page<P: page::AutoBind<crate::pages::Message>>(
|
||||
&mut self,
|
||||
) -> page::Insert<crate::pages::Message> {
|
||||
let id = self.pages.register::<P>().id();
|
||||
self.navbar_insert(id);
|
||||
|
||||
page::Insert {
|
||||
model: &mut self.pages,
|
||||
id,
|
||||
}
|
||||
}
|
||||
|
||||
fn navbar_insert(&mut self, id: page::Entity) -> segmented_button::SingleSelectEntityMut {
|
||||
let page = &self.pages.info[id];
|
||||
|
||||
self.nav_model
|
||||
.insert()
|
||||
.text(page.title.clone())
|
||||
.icon(icon::from_name(&*page.icon_name).into())
|
||||
.data(id)
|
||||
.with_id(|nav_id| self.pages.data_set(id, nav_id))
|
||||
}
|
||||
|
||||
/// Displays the view of a page.
|
||||
fn page_view(&self, content: &[section::Entity]) -> cosmic::Element<Message> {
|
||||
let page = &self.pages.page[self.active_page];
|
||||
let page_info = &self.pages.info[self.active_page];
|
||||
let mut column_widgets = Vec::with_capacity(1);
|
||||
|
||||
if let Some(parent) = page_info.parent {
|
||||
let page_header = crate::widget::sub_page_header(
|
||||
page_info.title.as_str(),
|
||||
self.pages.info[parent].title.as_str(),
|
||||
Message::Page(parent),
|
||||
);
|
||||
|
||||
let mut page_header_content: cosmic::iced_widget::Row<'_, Message, Theme> =
|
||||
row::with_capacity(2)
|
||||
.align_items(iced::Alignment::End)
|
||||
.push(page_header);
|
||||
|
||||
if let Some(element) = page.header_view() {
|
||||
page_header_content = page_header_content.push(element.map(Message::from));
|
||||
}
|
||||
|
||||
column_widgets.push(page_header_content.into());
|
||||
}
|
||||
|
||||
column_widgets.reserve_exact(1 + content.len());
|
||||
for id in content.iter().copied() {
|
||||
let section = &self.pages.sections[id];
|
||||
let model = &self.pages.page[self.active_page];
|
||||
|
||||
if section
|
||||
.show_while
|
||||
.as_ref()
|
||||
.map_or(true, |func| func(model.as_ref()))
|
||||
{
|
||||
column_widgets.push(
|
||||
(section.view_fn)(&self.pages, model.as_ref(), section)
|
||||
.map(Message::PageMessage),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
settings::view_column(column_widgets).into()
|
||||
}
|
||||
|
||||
fn search_changed(&mut self, phrase: String) {
|
||||
// If the text was cleared, clear the search results too.
|
||||
if phrase.is_empty() {
|
||||
self.search_clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a case-insensitive regular expression for the search function.
|
||||
let search_expression = regex::RegexBuilder::new(&phrase)
|
||||
.case_insensitive(true)
|
||||
.unicode(true)
|
||||
.ignore_whitespace(true)
|
||||
.size_limit(16 * 1024)
|
||||
.build();
|
||||
|
||||
if let Ok(expression) = search_expression {
|
||||
// With the new search expression, generate new search results.
|
||||
let results: Vec<_> = self.pages.search(&expression).collect();
|
||||
|
||||
// Use the results if results were found.
|
||||
if !results.is_empty() {
|
||||
self.search_selections = results;
|
||||
}
|
||||
}
|
||||
|
||||
self.search_input = phrase;
|
||||
}
|
||||
|
||||
/// Clears the search results so that the search page will not be shown.
|
||||
fn search_clear(&mut self) {
|
||||
self.search_selections.clear();
|
||||
self.search_input.clear();
|
||||
}
|
||||
|
||||
/// Displays the search view.
|
||||
fn search_view(&self) -> cosmic::Element<Message> {
|
||||
let mut sections: Vec<cosmic::Element<Message>> = Vec::new();
|
||||
|
||||
let mut current_page = page::Entity::default();
|
||||
for (page, section) in self.search_selections.iter().copied() {
|
||||
let section = &self.pages.sections[section];
|
||||
let model = &self.pages.page[page];
|
||||
|
||||
if page != current_page {
|
||||
current_page = page;
|
||||
sections.push(search_header(&self.pages, page));
|
||||
}
|
||||
|
||||
if section
|
||||
.show_while
|
||||
.as_ref()
|
||||
.map_or(true, |func| func(model.as_ref()))
|
||||
{
|
||||
let section = (section.view_fn)(&self.pages, model.as_ref(), section)
|
||||
.map(Message::PageMessage)
|
||||
.apply(iced::widget::container)
|
||||
.padding([0, 0, 0, cosmic::theme::active().cosmic().space_xl()]);
|
||||
|
||||
sections.push(section.into());
|
||||
}
|
||||
}
|
||||
|
||||
settings::view_column(sections).into()
|
||||
}
|
||||
|
||||
/// Displays the sub-pages view of a page.
|
||||
fn sub_page_view(&self, sub_pages: &[page::Entity]) -> cosmic::Element<Message> {
|
||||
let theme = cosmic::theme::active();
|
||||
let mut page_list =
|
||||
column::with_capacity(sub_pages.len()).spacing(theme.cosmic().space_s());
|
||||
|
||||
for entity in sub_pages.iter().copied() {
|
||||
let sub_page = &self.pages.info[entity];
|
||||
page_list = page_list.push(crate::widget::page_list_item(
|
||||
sub_page.title.as_str(),
|
||||
sub_page.description.as_str(),
|
||||
&sub_page.icon_name,
|
||||
entity,
|
||||
));
|
||||
}
|
||||
|
||||
column::with_capacity(2)
|
||||
.push(page_title(&self.pages.info[self.active_page]))
|
||||
.push(Element::from(page_list).map(Message::Page))
|
||||
.spacing(theme.cosmic().space_m())
|
||||
.padding(0)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
56
cosmic-settings/src/config.rs
Normal file
56
cosmic-settings/src/config.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::cosmic_config::{self, ConfigGet, ConfigSet};
|
||||
|
||||
const NAME: &str = "com.system76.CosmicSettings";
|
||||
|
||||
const ACTIVE_PAGE: &str = "active-page";
|
||||
|
||||
#[must_use]
|
||||
#[derive(Debug)]
|
||||
pub struct Config {
|
||||
pub cosmic_config: Option<cosmic_config::Config>,
|
||||
pub active_page: Box<str>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new() -> Self {
|
||||
let mut config = Self::default();
|
||||
|
||||
let context = match cosmic_config::Config::new(NAME, 1) {
|
||||
Ok(context) => context,
|
||||
Err(why) => {
|
||||
tracing::warn!(?why, "failed to get config");
|
||||
return Self::default();
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(page) = context.get::<Box<str>>(ACTIVE_PAGE) {
|
||||
config.active_page = page;
|
||||
}
|
||||
|
||||
config.cosmic_config = Some(context);
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
pub fn set_active_page(&mut self, page: Box<str>) {
|
||||
if let Some(context) = self.cosmic_config.as_ref() {
|
||||
if let Err(why) = context.set::<Box<str>>(ACTIVE_PAGE, page.clone()) {
|
||||
tracing::error!(?why, "failed to store active page ID");
|
||||
}
|
||||
}
|
||||
|
||||
self.active_page = page;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
cosmic_config: None,
|
||||
active_page: Box::from("desktop"),
|
||||
}
|
||||
}
|
||||
}
|
||||
40
cosmic-settings/src/localize.rs
Normal file
40
cosmic-settings/src/localize.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use i18n_embed::{
|
||||
fluent::{fluent_language_loader, FluentLanguageLoader},
|
||||
DefaultLocalizer, LanguageLoader, Localizer,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use rust_embed::RustEmbed;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "../i18n/"]
|
||||
struct Localizations;
|
||||
|
||||
pub static LANGUAGE_LOADER: Lazy<FluentLanguageLoader> = Lazy::new(|| {
|
||||
let loader: FluentLanguageLoader = fluent_language_loader!();
|
||||
|
||||
loader
|
||||
.load_fallback_language(&Localizations)
|
||||
.expect("Error while loading fallback language");
|
||||
|
||||
loader
|
||||
});
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! fl {
|
||||
($message_id:literal) => {{
|
||||
i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id)
|
||||
}};
|
||||
|
||||
($message_id:literal, $($args:expr),*) => {{
|
||||
i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id, $($args), *)
|
||||
}};
|
||||
}
|
||||
|
||||
// Get the `Localizer` to be used for localizing this library.
|
||||
#[must_use]
|
||||
pub fn localizer() -> Box<dyn Localizer> {
|
||||
Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations))
|
||||
}
|
||||
172
cosmic-settings/src/main.rs
Normal file
172
cosmic-settings/src/main.rs
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#![allow(clippy::cast_precision_loss)]
|
||||
#![allow(clippy::cast_sign_loss)]
|
||||
#![allow(clippy::cast_possible_truncation)]
|
||||
#![allow(clippy::cast_lossless)]
|
||||
|
||||
pub mod app;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub use app::{Message, SettingsApp};
|
||||
pub mod config;
|
||||
|
||||
#[macro_use]
|
||||
pub mod localize;
|
||||
pub mod pages;
|
||||
pub mod subscription;
|
||||
pub mod theme;
|
||||
pub mod utils;
|
||||
pub mod widget;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use cosmic::{app::CosmicFlags, iced::Limits};
|
||||
use i18n_embed::DesktopLanguageRequester;
|
||||
use ron::error::SpannedError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
||||
#[derive(Parser, Debug, Serialize, Deserialize, Clone)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
#[command(propagate_version = true)]
|
||||
pub struct Args {
|
||||
#[command(subcommand)]
|
||||
subcommand: Option<PageCommands>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, Serialize, Deserialize, Clone)]
|
||||
pub enum PageCommands {
|
||||
/// About settings page
|
||||
About,
|
||||
/// Appearance settings page
|
||||
Appearance,
|
||||
/// Bluetooth settings page
|
||||
Bluetooth,
|
||||
/// DateTime settings page
|
||||
DateTime,
|
||||
/// Desktop and panel settings page
|
||||
DesktopPanel,
|
||||
/// Displays settings page
|
||||
Displays,
|
||||
/// Firmware settings page
|
||||
Firmware,
|
||||
/// Keyboard settings page
|
||||
Keyboard,
|
||||
/// Mouse settings page
|
||||
Mouse,
|
||||
/// Network settings page
|
||||
Network,
|
||||
/// Notifications settings page
|
||||
Notifications,
|
||||
/// Power settings page
|
||||
Power,
|
||||
/// Region & Language settings page
|
||||
RegionLanguage,
|
||||
/// Sound settings page
|
||||
Sound,
|
||||
/// Time settings page
|
||||
Time,
|
||||
/// Touchpad settings page
|
||||
Touchpad,
|
||||
/// Users settings page
|
||||
Users,
|
||||
/// Wallpaper settings page
|
||||
Wallpaper,
|
||||
/// Workspaces settings page
|
||||
Workspaces,
|
||||
}
|
||||
|
||||
impl FromStr for PageCommands {
|
||||
type Err = SpannedError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
ron::de::from_str(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for PageCommands {
|
||||
fn to_string(&self) -> String {
|
||||
ron::ser::to_string(self).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl CosmicFlags for Args {
|
||||
type SubCommand = PageCommands;
|
||||
type Args = Vec<String>;
|
||||
|
||||
fn action(&self) -> Option<&PageCommands> {
|
||||
self.subcommand.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns error if iced fails to run the application.
|
||||
pub fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
|
||||
if std::env::var("RUST_SPANTRACE").is_err() {
|
||||
std::env::set_var("RUST_SPANTRACE", "0");
|
||||
}
|
||||
|
||||
std::env::set_var("WGPU_POWER_PREF", "low");
|
||||
|
||||
init_logger();
|
||||
init_localizer();
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
let settings = cosmic::app::Settings::default()
|
||||
.size_limits(Limits::NONE.min_width(400.0).min_height(300.0));
|
||||
|
||||
cosmic::app::run_single_instance::<app::SettingsApp>(settings, args)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_localizer() {
|
||||
let localizer = crate::localize::localizer();
|
||||
let requested_languages = DesktopLanguageRequester::requested_languages();
|
||||
|
||||
if let Err(why) = localizer.select(&requested_languages) {
|
||||
tracing::error!(%why, "error while loading fluent localizations");
|
||||
}
|
||||
}
|
||||
|
||||
fn init_logger() {
|
||||
let log_level = std::env::var("RUST_LOG")
|
||||
.ok()
|
||||
.and_then(|level| level.parse::<tracing::Level>().ok())
|
||||
.unwrap_or(tracing::Level::INFO);
|
||||
|
||||
let log_format = tracing_subscriber::fmt::format()
|
||||
.pretty()
|
||||
.without_time()
|
||||
.with_line_number(true)
|
||||
.with_file(true)
|
||||
.with_target(false)
|
||||
.with_thread_names(true);
|
||||
|
||||
let log_filter = tracing_subscriber::fmt::Layer::default()
|
||||
.with_writer(std::io::stderr)
|
||||
.event_format(log_format)
|
||||
.with_filter(tracing_subscriber::filter::filter_fn(move |metadata| {
|
||||
let target = metadata.target();
|
||||
metadata.level() == &tracing::Level::ERROR
|
||||
|| ((target.starts_with("cosmic_settings") || target.starts_with("cosmic_bg"))
|
||||
&& metadata.level() <= &log_level)
|
||||
}));
|
||||
|
||||
tracing_subscriber::registry().with(log_filter).init();
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! cache_dynamic_lazy {
|
||||
( $( $visible:vis static $variable:ident: $type:ty = $expression:expr; )+ ) => {
|
||||
$(
|
||||
#[static_init::dynamic(lazy)]
|
||||
$visible static $variable: $type = $expression;
|
||||
)+
|
||||
};
|
||||
}
|
||||
1582
cosmic-settings/src/pages/desktop/appearance.rs
Normal file
1582
cosmic-settings/src/pages/desktop/appearance.rs
Normal file
File diff suppressed because it is too large
Load diff
83
cosmic-settings/src/pages/desktop/dock/applets.rs
Normal file
83
cosmic-settings/src/pages/desktop/dock/applets.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
use cosmic::{cosmic_config::CosmicConfigEntry, iced::window, iced_runtime::Command};
|
||||
use cosmic_panel_config::CosmicPanelConfig;
|
||||
use cosmic_settings_page::{self as page, section, Section};
|
||||
use once_cell::sync::Lazy;
|
||||
use slotmap::SlotMap;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::{
|
||||
app,
|
||||
pages::{
|
||||
self,
|
||||
desktop::panel::applets_inner::{self, lists, AppletsPage, ReorderWidgetState},
|
||||
},
|
||||
};
|
||||
|
||||
pub static ADD_DOCK_APPLET_DIALOGUE_ID: Lazy<window::Id> = Lazy::new(window::Id::unique);
|
||||
|
||||
pub(crate) struct Page {
|
||||
inner: applets_inner::Page,
|
||||
}
|
||||
|
||||
impl Default for Page {
|
||||
fn default() -> Self {
|
||||
let config_helper = CosmicPanelConfig::cosmic_config("Dock").ok();
|
||||
let current_config = config_helper.as_ref().and_then(|config_helper| {
|
||||
let panel_config = CosmicPanelConfig::get_entry(config_helper).ok()?;
|
||||
// If the config is not present, it will be created with the default values and the name will not match
|
||||
(panel_config.name == "Dock").then_some(panel_config)
|
||||
});
|
||||
Self {
|
||||
inner: applets_inner::Page {
|
||||
available_entries: freedesktop_desktop_entry::Iter::new(
|
||||
freedesktop_desktop_entry::default_paths(),
|
||||
)
|
||||
.filter_map(|p| applets_inner::Applet::try_from(Cow::from(p)).ok())
|
||||
.collect(),
|
||||
config_helper,
|
||||
current_config,
|
||||
reorder_widget_state: ReorderWidgetState::default(),
|
||||
search: String::new(),
|
||||
has_dialog: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppletsPage for Page {
|
||||
fn inner(&self) -> &applets_inner::Page {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
fn inner_mut(&mut self) -> &mut applets_inner::Page {
|
||||
&mut self.inner
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Message(pub applets_inner::Message);
|
||||
|
||||
impl Page {
|
||||
pub fn update(&mut self, message: Message) -> Command<app::Message> {
|
||||
self.inner.update(message.0, *ADD_DOCK_APPLET_DIALOGUE_ID)
|
||||
}
|
||||
}
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![sections.insert(lists::<Page, _>(|msg| {
|
||||
pages::Message::DockApplet(Message(msg))
|
||||
}))])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("dock_applets", "preferences-dock-symbolic")
|
||||
// .title(fl!("applets"))
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
201
cosmic-settings/src/pages/desktop/dock/mod.rs
Normal file
201
cosmic-settings/src/pages/desktop/dock/mod.rs
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use cosmic::Apply;
|
||||
use cosmic::{
|
||||
cosmic_config::{ConfigSet, CosmicConfigEntry},
|
||||
widget::{settings, text, toggler},
|
||||
Element,
|
||||
};
|
||||
use cosmic_panel_config::{CosmicPanelConfig, CosmicPanelContainerConfig};
|
||||
use cosmic_settings_page::{self as page, section, Section};
|
||||
use slotmap::SlotMap;
|
||||
use tracing::error;
|
||||
|
||||
use crate::pages::desktop::panel::inner::{
|
||||
add_panel, behavior_and_position, configuration, reset_button, style,
|
||||
};
|
||||
|
||||
use super::panel::inner::{self, PageInner, PanelPage};
|
||||
|
||||
pub mod applets;
|
||||
|
||||
pub struct Page {
|
||||
inner: PageInner,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
EnableDock(bool),
|
||||
Inner(inner::Message),
|
||||
}
|
||||
|
||||
impl Page {
|
||||
pub fn update(&mut self, message: Message) {
|
||||
match message {
|
||||
Message::EnableDock(enabled) => {
|
||||
let Some(container_config) = self.inner.container_config.as_mut() else {
|
||||
return;
|
||||
};
|
||||
let Some(panel_config) = self.inner.panel_config.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Ok(helper) = CosmicPanelContainerConfig::cosmic_config() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if enabled {
|
||||
container_config.config_list.push(panel_config.clone());
|
||||
} else {
|
||||
container_config
|
||||
.config_list
|
||||
.retain(|c| c.name.as_str() != "Dock");
|
||||
}
|
||||
|
||||
let entry_names = container_config
|
||||
.config_list
|
||||
.iter()
|
||||
.map(|c| c.name.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Err(err) = helper.set("entries", entry_names) {
|
||||
error!("{:?}", err);
|
||||
}
|
||||
}
|
||||
Message::Inner(inner) => {
|
||||
self.inner.update(inner);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {
|
||||
fn sub_pages(page: page::Insert<crate::pages::Message>) -> page::Insert<crate::pages::Message> {
|
||||
page.sub_page::<applets::Page>()
|
||||
}
|
||||
}
|
||||
|
||||
impl PanelPage for Page {
|
||||
fn inner(&self) -> &PageInner {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
fn inner_mut(&mut self) -> &mut PageInner {
|
||||
&mut self.inner
|
||||
}
|
||||
|
||||
fn autohide_label(&self) -> String {
|
||||
fl!("panel-behavior-and-position", "dock-autohide")
|
||||
}
|
||||
|
||||
fn gap_label(&self) -> String {
|
||||
fl!("panel-style", "dock-anchor-gap")
|
||||
}
|
||||
|
||||
fn extend_label(&self) -> String {
|
||||
fl!("panel-style", "dock-extend")
|
||||
}
|
||||
|
||||
fn configure_applets_label(&self) -> String {
|
||||
fl!("panel-applets", "dock-desc")
|
||||
}
|
||||
|
||||
fn applets_page_id(&self) -> &'static str {
|
||||
"dock_applets"
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Page {
|
||||
fn default() -> Self {
|
||||
// TODO CosmicPanelConfig should return its own version
|
||||
let config_helper = CosmicPanelConfig::cosmic_config("Dock").ok();
|
||||
let panel_config = config_helper.as_ref().and_then(|config_helper| {
|
||||
let panel_config = CosmicPanelConfig::get_entry(config_helper).ok()?;
|
||||
// If the config is not present, it will be created with the default values and the name will not match
|
||||
(panel_config.name == "Dock").then_some(panel_config)
|
||||
});
|
||||
let system_default = cosmic::cosmic_config::Config::system(
|
||||
&format!("{}.Dock", cosmic_panel_config::NAME),
|
||||
CosmicPanelConfig::VERSION,
|
||||
)
|
||||
.map(|c| match CosmicPanelConfig::get_entry(&c) {
|
||||
Ok(c) => c,
|
||||
Err((errs, c)) => {
|
||||
for err in errs {
|
||||
tracing::error!(?err, "Failed to load Dock system config.");
|
||||
}
|
||||
c
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
let container_config = CosmicPanelContainerConfig::load().ok();
|
||||
Self {
|
||||
inner: PageInner {
|
||||
config_helper,
|
||||
panel_config,
|
||||
container_config,
|
||||
outputs_map: HashMap::new(),
|
||||
system_default,
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn enable() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.descriptions(vec![fl!("dock").into()])
|
||||
.view::<Page>(|_binder, page, section| {
|
||||
let descriptions = §ion.descriptions;
|
||||
let Some(container_config) = page.inner.container_config.as_ref() else {
|
||||
return Element::from(text(fl!("unknown")));
|
||||
};
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(
|
||||
&*descriptions[0],
|
||||
toggler(
|
||||
None,
|
||||
container_config
|
||||
.config_list
|
||||
.iter()
|
||||
.any(|e| e.name.as_str() == "Dock"),
|
||||
Message::EnableDock,
|
||||
),
|
||||
))
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Dock)
|
||||
})
|
||||
}
|
||||
// TODO cleanup
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(if self.inner.panel_config.is_some() {
|
||||
vec![
|
||||
sections.insert(enable()),
|
||||
sections.insert(behavior_and_position::<Page, _>(self, |m| {
|
||||
crate::pages::Message::Dock(Message::Inner(m))
|
||||
})),
|
||||
sections.insert(style::<Page, _>(self, |m| {
|
||||
crate::pages::Message::Dock(Message::Inner(m))
|
||||
})),
|
||||
sections.insert(configuration::<Page>(self)),
|
||||
sections.insert(reset_button::<Page, _>(|m| {
|
||||
crate::pages::Message::Dock(Message::Inner(m))
|
||||
})),
|
||||
]
|
||||
} else {
|
||||
vec![sections.insert(add_panel::<Page, _>(|m| {
|
||||
crate::pages::Message::Dock(Message::Inner(m))
|
||||
}))]
|
||||
})
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("dock", "preferences-dock-symbolic")
|
||||
.title(fl!("dock"))
|
||||
.description(fl!("dock", "desc"))
|
||||
}
|
||||
}
|
||||
80
cosmic-settings/src/pages/desktop/mod.rs
Normal file
80
cosmic-settings/src/pages/desktop/mod.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod appearance;
|
||||
pub mod dock;
|
||||
pub mod notifications;
|
||||
pub mod options;
|
||||
pub mod panel;
|
||||
pub mod wallpaper;
|
||||
pub mod workspaces;
|
||||
|
||||
use cosmic::{config::CosmicTk, cosmic_config::CosmicConfigEntry};
|
||||
use cosmic_settings_page as page;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct Page {
|
||||
pub cosmic_config: Option<cosmic::cosmic_config::Config>,
|
||||
pub cosmic_tk: CosmicTk,
|
||||
}
|
||||
|
||||
impl Default for Page {
|
||||
fn default() -> Self {
|
||||
let (cosmic_tk, cosmic_config) = CosmicTk::config().map_or_else(
|
||||
|why| {
|
||||
tracing::error!(?why, "failed to read CosmicTk config");
|
||||
(CosmicTk::default(), None)
|
||||
},
|
||||
|config| match CosmicTk::get_entry(&config) {
|
||||
Ok(tk) => (tk, Some(config)),
|
||||
Err((_errors, tk)) => (tk, Some(config)),
|
||||
},
|
||||
);
|
||||
|
||||
Self {
|
||||
cosmic_config,
|
||||
cosmic_tk,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("desktop", "video-display-symbolic").title(fl!("desktop"))
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {
|
||||
fn sub_pages(page: page::Insert<crate::pages::Message>) -> page::Insert<crate::pages::Message> {
|
||||
page.sub_page::<options::Page>()
|
||||
.sub_page::<wallpaper::Page>()
|
||||
.sub_page::<appearance::Page>()
|
||||
.sub_page::<workspaces::Page>()
|
||||
.sub_page::<notifications::Page>()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Message {
|
||||
ShowMinimizeButton(bool),
|
||||
ShowMaximizeButton(bool),
|
||||
}
|
||||
|
||||
impl Page {
|
||||
pub fn update(&mut self, message: Message) {
|
||||
match message {
|
||||
Message::ShowMaximizeButton(value) => {
|
||||
if let Some(config) = self.cosmic_config.as_mut() {
|
||||
let _res = self.cosmic_tk.set_show_maximize(config, value);
|
||||
}
|
||||
}
|
||||
|
||||
Message::ShowMinimizeButton(value) => {
|
||||
if let Some(config) = self.cosmic_config.as_mut() {
|
||||
let _res = self.cosmic_tk.set_show_minimize(config, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
cosmic-settings/src/pages/desktop/notifications.rs
Normal file
26
cosmic-settings/src/pages/desktop/notifications.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic_settings_page::Section;
|
||||
use cosmic_settings_page::{self as page, section};
|
||||
use slotmap::SlotMap;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![sections.insert(Section::default())])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("notifications", "preferences-system-notifications-symbolic")
|
||||
.title(fl!("notifications"))
|
||||
.description(fl!("notifications", "desc"))
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
163
cosmic-settings/src/pages/desktop/options.rs
Normal file
163
cosmic-settings/src/pages/desktop/options.rs
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use super::Message;
|
||||
use cosmic::{
|
||||
iced::Length,
|
||||
theme,
|
||||
widget::{button, container, horizontal_space, icon, row, settings, toggler},
|
||||
Apply, Element,
|
||||
};
|
||||
|
||||
use cosmic_settings_page::Section;
|
||||
use cosmic_settings_page::{self as page, section};
|
||||
use slotmap::SlotMap;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![
|
||||
sections.insert(super_key_action()),
|
||||
sections.insert(window_controls()),
|
||||
sections.insert(panel_dock_links()),
|
||||
])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("desktop-panel-options", "video-display-symbolic")
|
||||
.title(fl!("desktop-panel-options"))
|
||||
.description(fl!("desktop-panel-options", "desc"))
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {
|
||||
fn sub_pages(page: page::Insert<crate::pages::Message>) -> page::Insert<crate::pages::Message> {
|
||||
page.sub_page::<super::panel::Page>()
|
||||
.sub_page::<super::dock::Page>()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn super_key_action() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("super-key-action"))
|
||||
.descriptions(vec![
|
||||
fl!("super-key-action", "launcher").into(),
|
||||
fl!("super-key-action", "workspaces").into(),
|
||||
fl!("super-key-action", "applications").into(),
|
||||
])
|
||||
.view::<Page>(|_binder, _page, section| {
|
||||
let descriptions = §ion.descriptions;
|
||||
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(
|
||||
&*descriptions[0],
|
||||
horizontal_space(Length::Fill),
|
||||
))
|
||||
.add(settings::item(
|
||||
&*descriptions[1],
|
||||
horizontal_space(Length::Fill),
|
||||
))
|
||||
.add(settings::item(
|
||||
&*descriptions[2],
|
||||
horizontal_space(Length::Fill),
|
||||
))
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn window_controls() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("window-controls"))
|
||||
.descriptions(vec![
|
||||
fl!("window-controls", "minimize").into(),
|
||||
fl!("window-controls", "maximize").into(),
|
||||
])
|
||||
.view::<Page>(|binder, _page, section| {
|
||||
let desktop = binder
|
||||
.page::<super::Page>()
|
||||
.expect("desktop page not found");
|
||||
let descriptions = §ion.descriptions;
|
||||
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(
|
||||
&*descriptions[0],
|
||||
toggler(
|
||||
None,
|
||||
desktop.cosmic_tk.show_minimize,
|
||||
Message::ShowMinimizeButton,
|
||||
),
|
||||
))
|
||||
.add(settings::item(
|
||||
&*descriptions[1],
|
||||
toggler(
|
||||
None,
|
||||
desktop.cosmic_tk.show_maximize,
|
||||
Message::ShowMaximizeButton,
|
||||
),
|
||||
))
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Desktop)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn panel_dock_links() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("desktop-panels-and-applets"))
|
||||
.view::<Page>(|binder, _page, section| {
|
||||
// TODO probably a way of getting the entity and its info
|
||||
let mut settings = settings::view_section(§ion.title);
|
||||
settings = if let Some((panel_entity, panel_info)) =
|
||||
binder.info.iter().find(|(_, v)| v.id == "panel")
|
||||
{
|
||||
let control = row::with_children(vec![
|
||||
horizontal_space(Length::Fill).into(),
|
||||
icon::from_name("go-next-symbolic").size(16).into(),
|
||||
]);
|
||||
|
||||
settings.add(
|
||||
settings::item::builder(panel_info.title.clone())
|
||||
.description(panel_info.description.clone())
|
||||
.control(control)
|
||||
.spacing(16)
|
||||
.apply(container)
|
||||
.style(theme::Container::List)
|
||||
.apply(button)
|
||||
.style(theme::Button::Transparent)
|
||||
.on_press(crate::pages::Message::Page(panel_entity)),
|
||||
)
|
||||
} else {
|
||||
settings
|
||||
};
|
||||
|
||||
settings = if let Some((dock_entity, dock_info)) =
|
||||
binder.info.iter().find(|(_, v)| v.id == "dock")
|
||||
{
|
||||
let control = row::with_children(vec![
|
||||
horizontal_space(Length::Fill).into(),
|
||||
icon::from_name("go-next-symbolic").size(16).into(),
|
||||
]);
|
||||
|
||||
settings.add(
|
||||
settings::item::builder(dock_info.title.clone())
|
||||
.description(dock_info.description.clone())
|
||||
.control(control)
|
||||
.spacing(16)
|
||||
.apply(container)
|
||||
.style(theme::Container::List)
|
||||
.apply(button)
|
||||
.style(theme::Button::Transparent)
|
||||
.on_press(crate::pages::Message::Page(dock_entity)),
|
||||
)
|
||||
} else {
|
||||
settings
|
||||
};
|
||||
|
||||
Element::from(settings)
|
||||
})
|
||||
}
|
||||
1572
cosmic-settings/src/pages/desktop/panel/applets_inner.rs
Normal file
1572
cosmic-settings/src/pages/desktop/panel/applets_inner.rs
Normal file
File diff suppressed because it is too large
Load diff
512
cosmic-settings/src/pages/desktop/panel/inner.rs
Normal file
512
cosmic-settings/src/pages/desktop/panel/inner.rs
Normal file
|
|
@ -0,0 +1,512 @@
|
|||
use cosmic::{
|
||||
cctk::sctk::reexports::client::{backend::ObjectId, protocol::wl_output::WlOutput, Proxy},
|
||||
cosmic_config::{self, CosmicConfigEntry},
|
||||
iced::Length,
|
||||
theme,
|
||||
widget::{
|
||||
button, container, dropdown, horizontal_space, icon, row, settings, slider, text, toggler,
|
||||
},
|
||||
Element,
|
||||
};
|
||||
|
||||
use cosmic::Apply;
|
||||
use cosmic_panel_config::{
|
||||
AutoHide, CosmicPanelBackground, CosmicPanelConfig, CosmicPanelContainerConfig,
|
||||
CosmicPanelOuput, PanelAnchor, PanelSize,
|
||||
};
|
||||
use cosmic_settings_page::{self as page, Section};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct PageInner {
|
||||
pub(crate) config_helper: Option<cosmic_config::Config>,
|
||||
pub(crate) panel_config: Option<CosmicPanelConfig>,
|
||||
pub outputs: Vec<String>,
|
||||
pub anchors: Vec<String>,
|
||||
pub backgrounds: Vec<String>,
|
||||
pub(crate) container_config: Option<CosmicPanelContainerConfig>,
|
||||
// TODO move these into panel config
|
||||
pub(crate) outputs_map: HashMap<ObjectId, (String, WlOutput)>,
|
||||
pub(crate) system_default: Option<CosmicPanelConfig>,
|
||||
pub(crate) system_container: Option<CosmicPanelContainerConfig>,
|
||||
}
|
||||
|
||||
impl Default for PageInner {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
config_helper: Option::default(),
|
||||
panel_config: Option::default(),
|
||||
outputs: vec![fl!("all")],
|
||||
anchors: vec![
|
||||
Anchor(PanelAnchor::Left).to_string(),
|
||||
Anchor(PanelAnchor::Right).to_string(),
|
||||
Anchor(PanelAnchor::Top).to_string(),
|
||||
Anchor(PanelAnchor::Bottom).to_string(),
|
||||
],
|
||||
backgrounds: vec![
|
||||
Appearance::Match.to_string(),
|
||||
Appearance::Light.to_string(),
|
||||
Appearance::Dark.to_string(),
|
||||
],
|
||||
container_config: Option::default(),
|
||||
outputs_map: HashMap::default(),
|
||||
system_default: None,
|
||||
system_container: cosmic::cosmic_config::Config::system(
|
||||
cosmic_panel_config::NAME,
|
||||
CosmicPanelConfig::VERSION,
|
||||
)
|
||||
.map(
|
||||
|c| match CosmicPanelContainerConfig::load_from_config(&c, true) {
|
||||
Ok(c) => c,
|
||||
Err((errs, c)) => {
|
||||
for err in errs {
|
||||
tracing::error!(?err, "Error when loading Panel container config.");
|
||||
}
|
||||
c
|
||||
}
|
||||
},
|
||||
)
|
||||
.ok(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PanelPage {
|
||||
fn inner(&self) -> &PageInner;
|
||||
|
||||
fn inner_mut(&mut self) -> &mut PageInner;
|
||||
|
||||
fn autohide_label(&self) -> String;
|
||||
|
||||
fn gap_label(&self) -> String;
|
||||
|
||||
fn extend_label(&self) -> String;
|
||||
|
||||
fn configure_applets_label(&self) -> String;
|
||||
|
||||
fn applets_page_id(&self) -> &'static str;
|
||||
}
|
||||
|
||||
pub(crate) fn behavior_and_position<
|
||||
P: page::Page<crate::pages::Message> + PanelPage,
|
||||
T: Fn(Message) -> crate::pages::Message + Copy + 'static,
|
||||
>(
|
||||
p: &P,
|
||||
msg_map: T,
|
||||
) -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("panel-behavior-and-position"))
|
||||
.descriptions(vec![
|
||||
p.autohide_label().into(),
|
||||
fl!("panel-behavior-and-position", "position").into(),
|
||||
fl!("panel-behavior-and-position", "display").into(),
|
||||
])
|
||||
.view::<P>(move |_binder, page, section| {
|
||||
let descriptions = §ion.descriptions;
|
||||
let page = page.inner();
|
||||
let Some(panel_config) = page.panel_config.as_ref() else {
|
||||
return Element::from(text(fl!("unknown")));
|
||||
};
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(
|
||||
&*descriptions[0],
|
||||
toggler(None, panel_config.autohide.is_some(), |value| {
|
||||
Message::AutoHidePanel(value)
|
||||
}),
|
||||
))
|
||||
.add(settings::item(
|
||||
&*descriptions[1],
|
||||
dropdown(
|
||||
page.anchors.as_slice(),
|
||||
Some(panel_config.anchor as usize),
|
||||
Message::PanelAnchor,
|
||||
),
|
||||
))
|
||||
.add(settings::item(
|
||||
&*descriptions[2],
|
||||
dropdown(
|
||||
page.outputs.as_slice(),
|
||||
match &panel_config.output {
|
||||
CosmicPanelOuput::All => Some(0),
|
||||
CosmicPanelOuput::Active => None,
|
||||
CosmicPanelOuput::Name(n) => page.outputs.iter().position(|o| o == n),
|
||||
},
|
||||
Message::Output,
|
||||
),
|
||||
))
|
||||
.apply(Element::from)
|
||||
.map(msg_map)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn style<
|
||||
P: page::Page<crate::pages::Message> + PanelPage,
|
||||
T: Fn(Message) -> crate::pages::Message + Copy + 'static,
|
||||
>(
|
||||
p: &P,
|
||||
msg_map: T,
|
||||
) -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("panel-style"))
|
||||
.descriptions(vec![
|
||||
p.gap_label().into(),
|
||||
p.extend_label().into(),
|
||||
fl!("panel-style", "appearance").into(),
|
||||
fl!("panel-style", "size").into(),
|
||||
fl!("panel-style", "background-opacity").into(),
|
||||
])
|
||||
.view::<P>(move |_binder, page, section| {
|
||||
let descriptions = §ion.descriptions;
|
||||
let inner = page.inner();
|
||||
let Some(panel_config) = inner.panel_config.as_ref() else {
|
||||
return Element::from(text(fl!("unknown")));
|
||||
};
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(
|
||||
&*descriptions[0],
|
||||
toggler(None, panel_config.anchor_gap, |value| {
|
||||
Message::AnchorGap(value)
|
||||
}),
|
||||
))
|
||||
.add(settings::item(
|
||||
&*descriptions[1],
|
||||
toggler(None, panel_config.expand_to_edges, |value| {
|
||||
Message::ExtendToEdge(value)
|
||||
}),
|
||||
))
|
||||
.add(settings::item(
|
||||
&*descriptions[2],
|
||||
dropdown(
|
||||
inner.backgrounds.as_slice(),
|
||||
match panel_config.background {
|
||||
CosmicPanelBackground::ThemeDefault => Some(0),
|
||||
CosmicPanelBackground::Light => Some(1),
|
||||
CosmicPanelBackground::Dark => Some(2),
|
||||
CosmicPanelBackground::Color(_) => None,
|
||||
},
|
||||
Message::Appearance,
|
||||
),
|
||||
))
|
||||
.add(settings::item(
|
||||
&*descriptions[3],
|
||||
// TODO custom discrete slider variant
|
||||
row::with_children(vec![
|
||||
text(fl!("small")).into(),
|
||||
slider(
|
||||
0..=4,
|
||||
match panel_config.size {
|
||||
PanelSize::XS => 0,
|
||||
PanelSize::S => 1,
|
||||
PanelSize::M => 2,
|
||||
PanelSize::L => 3,
|
||||
PanelSize::XL => 4,
|
||||
},
|
||||
|v| {
|
||||
if v == 0 {
|
||||
Message::PanelSize(PanelSize::XS)
|
||||
} else if v == 1 {
|
||||
Message::PanelSize(PanelSize::S)
|
||||
} else if v == 2 {
|
||||
Message::PanelSize(PanelSize::M)
|
||||
} else if v == 3 {
|
||||
Message::PanelSize(PanelSize::L)
|
||||
} else {
|
||||
Message::PanelSize(PanelSize::XL)
|
||||
}
|
||||
},
|
||||
)
|
||||
.into(),
|
||||
text(fl!("large")).into(),
|
||||
])
|
||||
.spacing(12),
|
||||
))
|
||||
.add(settings::item(
|
||||
&*descriptions[4],
|
||||
row::with_children(vec![
|
||||
text(fl!("number", HashMap::from_iter(vec![("number", 0)]))).into(),
|
||||
slider(0..=100, (panel_config.opacity * 100.0) as i32, |v| {
|
||||
Message::Opacity(v as f32 / 100.0)
|
||||
})
|
||||
.breakpoints(&[50])
|
||||
.into(),
|
||||
text(fl!("number", HashMap::from_iter(vec![("number", 100)]))).into(),
|
||||
])
|
||||
.spacing(12),
|
||||
))
|
||||
.apply(Element::from)
|
||||
.map(msg_map)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn configuration<P: page::Page<crate::pages::Message> + PanelPage>(
|
||||
p: &P,
|
||||
) -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("panel-applets"))
|
||||
.descriptions(vec![p.configure_applets_label().into()])
|
||||
.view::<P>(move |binder, page, section| {
|
||||
let mut settings = settings::view_section(§ion.title);
|
||||
let descriptions = §ion.descriptions;
|
||||
settings = if let Some((panel_applets_entity, _panel_applets_info)) = binder
|
||||
.info
|
||||
.iter()
|
||||
.find(|(_, v)| v.id == page.applets_page_id())
|
||||
{
|
||||
let control = row::with_children(vec![
|
||||
horizontal_space(Length::Fill).into(),
|
||||
icon::from_name("go-next-symbolic").size(16).into(),
|
||||
]);
|
||||
|
||||
settings.add(
|
||||
settings::item::builder(&*descriptions[0])
|
||||
.control(control)
|
||||
.spacing(16)
|
||||
.apply(container)
|
||||
.style(theme::Container::List)
|
||||
.apply(button)
|
||||
.style(theme::Button::Transparent)
|
||||
.on_press(crate::pages::Message::Page(panel_applets_entity)),
|
||||
)
|
||||
} else {
|
||||
settings
|
||||
};
|
||||
|
||||
Element::from(settings)
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
pub(crate) fn add_panel<
|
||||
P: page::Page<crate::pages::Message> + PanelPage,
|
||||
T: Fn(Message) -> crate::pages::Message + Copy + 'static,
|
||||
>(
|
||||
msg_map: T,
|
||||
) -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("panel-missing"))
|
||||
.descriptions(vec![fl!("reset-to-default").into()])
|
||||
.view::<P>(move |_binder, _page, section| {
|
||||
let descriptions = §ion.descriptions;
|
||||
cosmic::iced::widget::row![button(text(&*descriptions[0])).on_press(Message::FullReset)]
|
||||
.apply(Element::from)
|
||||
.map(msg_map)
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn reset_button<
|
||||
P: page::Page<crate::pages::Message> + PanelPage,
|
||||
T: Fn(Message) -> crate::pages::Message + Copy + 'static,
|
||||
>(
|
||||
msg_map: T,
|
||||
) -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.descriptions(vec![fl!("reset-to-default").into()])
|
||||
.view::<P>(move |_binder, page, section| {
|
||||
let descriptions = §ion.descriptions;
|
||||
let inner = page.inner();
|
||||
if inner.system_default == inner.panel_config {
|
||||
horizontal_space(1).apply(Element::from)
|
||||
} else {
|
||||
cosmic::iced::widget::row![
|
||||
button(text(&*descriptions[0])).on_press(Message::ResetPanel)
|
||||
]
|
||||
.apply(Element::from)
|
||||
}
|
||||
.map(msg_map)
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub struct Anchor(PanelAnchor);
|
||||
|
||||
impl ToString for Anchor {
|
||||
fn to_string(&self) -> String {
|
||||
match self.0 {
|
||||
PanelAnchor::Top => fl!("panel-top"),
|
||||
PanelAnchor::Bottom => fl!("panel-bottom"),
|
||||
PanelAnchor::Left => fl!("panel-left"),
|
||||
PanelAnchor::Right => fl!("panel-right"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Appearance {
|
||||
Match,
|
||||
Light,
|
||||
Dark,
|
||||
}
|
||||
|
||||
impl ToString for Appearance {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
Appearance::Match => fl!("panel-appearance", "match"),
|
||||
Appearance::Light => fl!("panel-appearance", "light"),
|
||||
Appearance::Dark => fl!("panel-appearance", "dark"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<CosmicPanelBackground> for Appearance {
|
||||
type Error = ();
|
||||
fn try_from(value: CosmicPanelBackground) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
CosmicPanelBackground::ThemeDefault => Ok(Appearance::Match),
|
||||
CosmicPanelBackground::Light => Ok(Appearance::Light),
|
||||
CosmicPanelBackground::Dark => Ok(Appearance::Dark),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Appearance> for CosmicPanelBackground {
|
||||
fn from(appearance: Appearance) -> Self {
|
||||
match appearance {
|
||||
Appearance::Match => CosmicPanelBackground::ThemeDefault,
|
||||
Appearance::Light => CosmicPanelBackground::Light,
|
||||
Appearance::Dark => CosmicPanelBackground::Dark,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
// panel messages
|
||||
AutoHidePanel(bool),
|
||||
PanelAnchor(usize),
|
||||
Output(usize),
|
||||
AnchorGap(bool),
|
||||
PanelSize(PanelSize),
|
||||
Appearance(usize),
|
||||
ExtendToEdge(bool),
|
||||
Opacity(f32),
|
||||
OutputAdded(String, WlOutput),
|
||||
OutputRemoved(WlOutput),
|
||||
PanelConfig(CosmicPanelConfig),
|
||||
ResetPanel,
|
||||
FullReset,
|
||||
}
|
||||
|
||||
impl PageInner {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn update(&mut self, message: Message) {
|
||||
let Some(helper) = self.config_helper.as_ref() else {
|
||||
return;
|
||||
};
|
||||
match &message {
|
||||
Message::ResetPanel => {
|
||||
if let Some((default, config)) = self
|
||||
.system_default
|
||||
.as_ref()
|
||||
.zip(self.config_helper.as_ref())
|
||||
{
|
||||
self.panel_config = self.system_default.clone();
|
||||
if let Err(err) = default.write_entry(config) {
|
||||
tracing::error!(?err, "Error resetting panel config.");
|
||||
}
|
||||
} else {
|
||||
tracing::error!("Panel config default is missing.");
|
||||
}
|
||||
}
|
||||
Message::FullReset => {
|
||||
if let Some(container) = self.system_container.as_ref() {
|
||||
if let Err(err) = container.write_entries() {
|
||||
tracing::error!(?err, "Error fully resetting the panel config.");
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
let Some(panel_config) = self.panel_config.as_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
match message {
|
||||
Message::AutoHidePanel(enabled) => {
|
||||
if enabled {
|
||||
panel_config.exclusive_zone = false;
|
||||
panel_config.autohide = Some(AutoHide {
|
||||
wait_time: 1000,
|
||||
transition_time: 200,
|
||||
handle_size: 4,
|
||||
});
|
||||
} else {
|
||||
panel_config.exclusive_zone = true;
|
||||
panel_config.autohide = None;
|
||||
}
|
||||
}
|
||||
Message::PanelAnchor(i) => {
|
||||
if let Some(anchor) = [
|
||||
PanelAnchor::Left,
|
||||
PanelAnchor::Right,
|
||||
PanelAnchor::Top,
|
||||
PanelAnchor::Bottom,
|
||||
]
|
||||
.iter()
|
||||
.find(|a| a.to_string() == self.anchors[i])
|
||||
{
|
||||
panel_config.anchor = *anchor;
|
||||
}
|
||||
}
|
||||
Message::Output(i) => {
|
||||
if i == 0 {
|
||||
panel_config.output = CosmicPanelOuput::All;
|
||||
} else {
|
||||
panel_config.output = CosmicPanelOuput::Name(self.outputs[i].clone());
|
||||
}
|
||||
}
|
||||
Message::AnchorGap(enabled) => {
|
||||
panel_config.anchor_gap = enabled;
|
||||
|
||||
if enabled {
|
||||
panel_config.margin = 4;
|
||||
} else {
|
||||
panel_config.margin = 0;
|
||||
}
|
||||
}
|
||||
Message::PanelSize(size) => {
|
||||
panel_config.size = size;
|
||||
}
|
||||
Message::Appearance(a) => {
|
||||
if let Some(b) = [Appearance::Match, Appearance::Light, Appearance::Dark]
|
||||
.iter()
|
||||
.find(|b| b.to_string() == self.backgrounds[a])
|
||||
{
|
||||
panel_config.background = (*b).into();
|
||||
}
|
||||
}
|
||||
Message::ExtendToEdge(enabled) => {
|
||||
panel_config.expand_to_edges = enabled;
|
||||
}
|
||||
Message::Opacity(opacity) => {
|
||||
panel_config.opacity = opacity;
|
||||
}
|
||||
Message::OutputAdded(name, output) => {
|
||||
self.outputs.push(name.clone());
|
||||
self.outputs_map.insert(output.id(), (name, output));
|
||||
return;
|
||||
}
|
||||
Message::OutputRemoved(output) => {
|
||||
if let Some((name, _)) = self.outputs_map.remove(&output.id()) {
|
||||
if let Some(pos) = self.outputs.iter().position(|o| o == &name) {
|
||||
self.outputs.remove(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::PanelConfig(c) => {
|
||||
self.panel_config = Some(c);
|
||||
return;
|
||||
}
|
||||
Message::ResetPanel | Message::FullReset => {}
|
||||
}
|
||||
|
||||
if panel_config.anchor_gap || !panel_config.expand_to_edges {
|
||||
panel_config.border_radius = 8;
|
||||
} else {
|
||||
panel_config.border_radius = 0;
|
||||
}
|
||||
|
||||
_ = panel_config.write_entry(helper);
|
||||
}
|
||||
}
|
||||
134
cosmic-settings/src/pages/desktop/panel/mod.rs
Normal file
134
cosmic-settings/src/pages/desktop/panel/mod.rs
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use cosmic::cosmic_config::CosmicConfigEntry;
|
||||
use cosmic_panel_config::{CosmicPanelConfig, CosmicPanelContainerConfig};
|
||||
use cosmic_settings_page::{self as page, section, Section};
|
||||
use slotmap::SlotMap;
|
||||
|
||||
use crate::pages::desktop::panel::inner::{
|
||||
add_panel, behavior_and_position, configuration, reset_button, style,
|
||||
};
|
||||
|
||||
use self::inner::{PageInner, PanelPage};
|
||||
|
||||
pub mod applets_inner;
|
||||
pub mod inner;
|
||||
|
||||
pub struct Page {
|
||||
inner: PageInner,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Message(pub inner::Message);
|
||||
|
||||
impl Page {
|
||||
pub fn update(&mut self, message: Message) {
|
||||
self.inner.update(message.0);
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {
|
||||
fn sub_pages(page: page::Insert<crate::pages::Message>) -> page::Insert<crate::pages::Message> {
|
||||
page.sub_page::<applets_inner::Page>()
|
||||
}
|
||||
}
|
||||
|
||||
impl PanelPage for Page {
|
||||
fn inner(&self) -> &PageInner {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
fn inner_mut(&mut self) -> &mut PageInner {
|
||||
&mut self.inner
|
||||
}
|
||||
|
||||
fn autohide_label(&self) -> String {
|
||||
fl!("panel-behavior-and-position", "autohide")
|
||||
}
|
||||
|
||||
fn gap_label(&self) -> String {
|
||||
fl!("panel-style", "anchor-gap")
|
||||
}
|
||||
|
||||
fn extend_label(&self) -> String {
|
||||
fl!("panel-style", "extend")
|
||||
}
|
||||
|
||||
fn configure_applets_label(&self) -> String {
|
||||
fl!("panel-applets", "desc")
|
||||
}
|
||||
|
||||
fn applets_page_id(&self) -> &'static str {
|
||||
"panel_applets"
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Page {
|
||||
fn default() -> Self {
|
||||
let config_helper = CosmicPanelConfig::cosmic_config("Panel").ok();
|
||||
let panel_config = config_helper.as_ref().and_then(|config_helper| {
|
||||
let panel_config = CosmicPanelConfig::get_entry(config_helper).ok()?;
|
||||
// If the config is not present, it will be created with the default values and the name will not match
|
||||
(panel_config.name == "Panel").then_some(panel_config)
|
||||
});
|
||||
let system_default = cosmic::cosmic_config::Config::system(
|
||||
&format!("{}.Panel", cosmic_panel_config::NAME),
|
||||
CosmicPanelConfig::VERSION,
|
||||
)
|
||||
.map(|c| match CosmicPanelConfig::get_entry(&c) {
|
||||
Ok(c) => c,
|
||||
Err((errs, c)) => {
|
||||
for err in errs {
|
||||
tracing::error!(?err, "Failed to load Panel system config.");
|
||||
}
|
||||
c
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
let container_config = CosmicPanelContainerConfig::load().ok();
|
||||
Self {
|
||||
inner: PageInner {
|
||||
config_helper,
|
||||
panel_config,
|
||||
container_config,
|
||||
outputs_map: HashMap::new(),
|
||||
system_default,
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO cleanup
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(if self.inner.panel_config.is_some() {
|
||||
vec![
|
||||
sections.insert(behavior_and_position::<Page, _>(self, |m| {
|
||||
crate::pages::Message::Panel(Message(m))
|
||||
})),
|
||||
sections.insert(style::<Page, _>(self, |m| {
|
||||
crate::pages::Message::Panel(Message(m))
|
||||
})),
|
||||
sections.insert(configuration::<Page>(self)),
|
||||
sections.insert(reset_button::<Page, _>(|m| {
|
||||
crate::pages::Message::Panel(Message(m))
|
||||
})),
|
||||
]
|
||||
} else {
|
||||
vec![sections.insert(add_panel::<Page, _>(|m| {
|
||||
crate::pages::Message::Panel(Message(m))
|
||||
}))]
|
||||
})
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("panel", "preferences-dock-symbolic")
|
||||
.title(fl!("panel"))
|
||||
.description(fl!("panel", "desc"))
|
||||
}
|
||||
}
|
||||
263
cosmic-settings/src/pages/desktop/wallpaper/config.rs
Normal file
263
cosmic-settings/src/pages/desktop/wallpaper/config.rs
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::cosmic_config::{self, ConfigGet, ConfigSet};
|
||||
use cosmic_bg_config::Source;
|
||||
use cosmic_settings_wallpaper as wallpaper;
|
||||
use std::collections::VecDeque;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const NAME: &str = "com.system76.CosmicSettings.Wallpaper";
|
||||
const VERSION: u64 = 1;
|
||||
const CURRENT_FOLDER: &str = "current-folder";
|
||||
const CUSTOM_COLORS: &str = "custom-colors";
|
||||
const CUSTOM_IMAGES: &str = "custom-images";
|
||||
const RECENT_FOLDERS: &str = "recent-folders";
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Config {
|
||||
context: Option<cosmic_config::Config>,
|
||||
state: Option<cosmic_config::Config>,
|
||||
pub(super) current_folder: Option<PathBuf>,
|
||||
custom_colors: Vec<wallpaper::Color>,
|
||||
custom_images: Vec<PathBuf>,
|
||||
recent_folders: VecDeque<PathBuf>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new() -> Self {
|
||||
let mut config = Self::default();
|
||||
|
||||
let context = match cosmic_config::Config::new(NAME, VERSION) {
|
||||
Ok(context) => context,
|
||||
Err(why) => {
|
||||
tracing::warn!(?why, "failed to get config");
|
||||
return Self::default();
|
||||
}
|
||||
};
|
||||
|
||||
let state = match cosmic_config::Config::new_state("com.system76.CosmicBackground", VERSION)
|
||||
{
|
||||
Ok(state) => state,
|
||||
Err(why) => {
|
||||
tracing::warn!(?why, "failed to get state");
|
||||
return Self::default();
|
||||
}
|
||||
};
|
||||
|
||||
// Get the active background folder from cosmic-config.
|
||||
if let Ok(path) = context.get::<Option<PathBuf>>(CURRENT_FOLDER) {
|
||||
// Set current folder if it exists.
|
||||
config.current_folder = path.filter(|path| path.exists());
|
||||
}
|
||||
|
||||
// Get custom colors stored in cosmic-config.
|
||||
if let Ok(colors) = context.get::<Vec<wallpaper::Color>>(CUSTOM_COLORS) {
|
||||
config.custom_colors = colors;
|
||||
}
|
||||
|
||||
// Get custom background images stored in cosmic-config.
|
||||
if let Ok(images) = context.get::<Vec<PathBuf>>(CUSTOM_IMAGES) {
|
||||
// Update config if images are missing.
|
||||
let mut update_config = false;
|
||||
|
||||
for image in images {
|
||||
if image.exists() {
|
||||
config.custom_images.push(image);
|
||||
} else {
|
||||
update_config = true;
|
||||
}
|
||||
}
|
||||
|
||||
if update_config {
|
||||
let _res = config.update_custom_images();
|
||||
}
|
||||
}
|
||||
|
||||
// Get recently-added background folders from cosmic-config.
|
||||
if let Ok(folders) = context.get::<VecDeque<PathBuf>>(RECENT_FOLDERS) {
|
||||
// Update config if folders are missing
|
||||
let mut update_config = false;
|
||||
|
||||
for folder in folders {
|
||||
if folder.exists() {
|
||||
config.recent_folders.push_back(folder);
|
||||
} else {
|
||||
update_config = true;
|
||||
}
|
||||
}
|
||||
|
||||
if update_config {
|
||||
let _res = config.update_recent_folders();
|
||||
}
|
||||
}
|
||||
|
||||
config.context = Some(context);
|
||||
config.state = Some(state);
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn current_folder(&self) -> &Path {
|
||||
self.current_folder
|
||||
.as_deref()
|
||||
.unwrap_or(Self::default_folder())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn default_folder() -> &'static Path {
|
||||
Path::new("/usr/share/backgrounds/")
|
||||
}
|
||||
|
||||
/// Sets the current background folder
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the on-disk configuration could not be updated.
|
||||
pub fn set_current_folder(
|
||||
&mut self,
|
||||
folder: Option<PathBuf>,
|
||||
) -> Result<(), cosmic_config::Error> {
|
||||
let result = self.update(CURRENT_FOLDER, &folder);
|
||||
self.current_folder = folder;
|
||||
result
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn custom_colors(&self) -> &[wallpaper::Color] {
|
||||
&self.custom_colors
|
||||
}
|
||||
|
||||
/// Adds a custom color
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the on-disk configuration could not be updated.
|
||||
pub fn add_custom_color(
|
||||
&mut self,
|
||||
color: wallpaper::Color,
|
||||
) -> Result<(), cosmic_config::Error> {
|
||||
if !self.custom_colors.contains(&color) {
|
||||
self.custom_colors.push(color);
|
||||
return self.update_custom_colors();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes custom background colors.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the on-disk configuration could not be updated.
|
||||
pub fn remove_custom_color(
|
||||
&mut self,
|
||||
color: &wallpaper::Color,
|
||||
) -> Result<(), cosmic_config::Error> {
|
||||
if let Some(position) = self.custom_colors.iter().position(|c| c == color) {
|
||||
self.custom_colors.remove(position);
|
||||
return self.update_custom_colors();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn current_image(&self, output: &str) -> Option<Source> {
|
||||
let mut wallpapers = self
|
||||
.state
|
||||
.as_ref()?
|
||||
.get::<Vec<(String, Source)>>("wallpapers")
|
||||
.ok()?
|
||||
.into_iter();
|
||||
|
||||
let wallpaper = if output == "all" {
|
||||
wallpapers.next()
|
||||
} else {
|
||||
wallpapers.find(|(name, _path)| name == output)
|
||||
};
|
||||
|
||||
wallpaper.map(|(_name, path)| path)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn custom_images(&self) -> &[PathBuf] {
|
||||
&self.custom_images
|
||||
}
|
||||
|
||||
/// Adds a custom background image
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the on-disk configuration could not be updated.
|
||||
pub fn add_custom_image(&mut self, image: PathBuf) -> Result<(), cosmic_config::Error> {
|
||||
if !self.custom_images.contains(&image) {
|
||||
self.custom_images.push(image);
|
||||
return self.update_custom_images();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes custom background images.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the on-disk configuration could not be updated.
|
||||
pub fn remove_custom_image(&mut self, image: &Path) -> Result<(), cosmic_config::Error> {
|
||||
if let Some(position) = self.custom_images.iter().position(|p| p == image) {
|
||||
self.custom_images.remove(position);
|
||||
return self.update_custom_images();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn recent_folders(&self) -> &VecDeque<PathBuf> {
|
||||
&self.recent_folders
|
||||
}
|
||||
|
||||
/// Adds a folder to the recent folders list
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the on-disk configuration could not be updated.
|
||||
pub fn add_recent_folder(&mut self, folder: PathBuf) -> Result<(), cosmic_config::Error> {
|
||||
while self.recent_folders.len() > 4 {
|
||||
self.recent_folders.pop_front();
|
||||
}
|
||||
|
||||
if !self.recent_folders.contains(&folder) {
|
||||
self.recent_folders.push_back(folder);
|
||||
return self.update_recent_folders();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update<V: serde::Serialize>(
|
||||
&self,
|
||||
key: &str,
|
||||
value: &V,
|
||||
) -> Result<(), cosmic_config::Error> {
|
||||
if let Some(context) = self.context.as_ref() {
|
||||
context.set(key, value)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_custom_colors(&self) -> Result<(), cosmic_config::Error> {
|
||||
self.update(CUSTOM_COLORS, &self.custom_colors)
|
||||
}
|
||||
|
||||
fn update_custom_images(&self) -> Result<(), cosmic_config::Error> {
|
||||
self.update(CUSTOM_IMAGES, &self.custom_images)
|
||||
}
|
||||
|
||||
fn update_recent_folders(&self) -> Result<(), cosmic_config::Error> {
|
||||
self.update(RECENT_FOLDERS, &self.recent_folders)
|
||||
}
|
||||
}
|
||||
1346
cosmic-settings/src/pages/desktop/wallpaper/mod.rs
Normal file
1346
cosmic-settings/src/pages/desktop/wallpaper/mod.rs
Normal file
File diff suppressed because it is too large
Load diff
182
cosmic-settings/src/pages/desktop/wallpaper/widgets.rs
Normal file
182
cosmic-settings/src/pages/desktop/wallpaper/widgets.rs
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use super::Message;
|
||||
use cosmic::iced::Radius;
|
||||
use cosmic::iced_core::Border;
|
||||
use cosmic::iced_core::{self, gradient::Linear, Background, Color, Degrees, Length};
|
||||
use cosmic::iced_runtime::core::image::Handle as ImageHandle;
|
||||
use cosmic::prelude::*;
|
||||
use cosmic::widget::{button, container, space};
|
||||
use cosmic::{iced, Element};
|
||||
use cosmic_settings_wallpaper as wallpaper;
|
||||
use slotmap::DefaultKey;
|
||||
|
||||
const COLOR_WIDTH: u16 = 70;
|
||||
const COLUMN_SPACING: u16 = 12;
|
||||
const ROW_SPACING: u16 = 16;
|
||||
|
||||
/// A button for selecting a color or gradient.
|
||||
#[must_use]
|
||||
pub fn color_button(
|
||||
color: wallpaper::Color,
|
||||
removable: bool,
|
||||
selected: bool,
|
||||
) -> Element<'static, Message> {
|
||||
let content = color_image(color.clone(), COLOR_WIDTH, COLOR_WIDTH, None);
|
||||
let on_remove = if removable {
|
||||
Some(Message::ColorRemove(color.clone()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
button::custom_image_button(content, on_remove)
|
||||
.padding(0)
|
||||
.selected(selected)
|
||||
.style(button::Style::Image)
|
||||
.on_press(Message::ColorSelect(color))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// A sized container that's filled with a color or gradient.
|
||||
#[must_use]
|
||||
pub fn color_image<'a, M: 'a>(
|
||||
color: wallpaper::Color,
|
||||
width: u16,
|
||||
height: u16,
|
||||
border_radius: Option<f32>,
|
||||
) -> Element<'a, M> {
|
||||
container(space::Space::new(width, height))
|
||||
.style(cosmic::theme::Container::custom(move |theme| {
|
||||
container::Appearance {
|
||||
icon_color: None,
|
||||
text_color: None,
|
||||
background: Some(match &color {
|
||||
wallpaper::Color::Single([r, g, b]) => {
|
||||
Background::Color(Color::from_rgb(*r, *g, *b))
|
||||
}
|
||||
|
||||
wallpaper::Color::Gradient(wallpaper::Gradient { colors, radius }) => {
|
||||
let stop_increment = 1.0 / (colors.len() - 1) as f32;
|
||||
let mut stop = 0.0;
|
||||
|
||||
let mut linear = Linear::new(Degrees(*radius));
|
||||
|
||||
for &[r, g, b] in &**colors {
|
||||
linear = linear.add_stop(stop, iced::Color::from_rgb(r, g, b));
|
||||
stop += stop_increment;
|
||||
}
|
||||
|
||||
Background::Gradient(iced_core::Gradient::Linear(linear))
|
||||
}
|
||||
}),
|
||||
|
||||
border: Border {
|
||||
radius: border_radius.map_or_else(
|
||||
|| Radius::from(theme.cosmic().corner_radii.radius_s),
|
||||
|br| br.into(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
shadow: Default::default(),
|
||||
}
|
||||
}))
|
||||
.padding(0)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Color selection list
|
||||
#[must_use]
|
||||
pub fn color_select_options(
|
||||
context: &super::Context,
|
||||
selected: Option<&wallpaper::Color>,
|
||||
) -> Element<'static, Message> {
|
||||
let mut vec = Vec::with_capacity(wallpaper::DEFAULT_COLORS.len());
|
||||
|
||||
// Place removable custom colors first
|
||||
for color in context.custom_colors.iter().rev() {
|
||||
vec.push(color_button(
|
||||
color.clone(),
|
||||
true,
|
||||
selected.map_or(false, |selection| selection == color),
|
||||
));
|
||||
}
|
||||
|
||||
// Then non-removable default colors
|
||||
for color in wallpaper::DEFAULT_COLORS {
|
||||
vec.push(color_button(
|
||||
color.clone(),
|
||||
false,
|
||||
selected.map_or(false, |selection| selection == color),
|
||||
));
|
||||
}
|
||||
|
||||
flex_select_row(vec)
|
||||
}
|
||||
|
||||
/// Background selection list
|
||||
#[must_use]
|
||||
pub fn wallpaper_select_options(
|
||||
page: &super::Page,
|
||||
selected: Option<DefaultKey>,
|
||||
show_custom_images: bool,
|
||||
) -> Element<Message> {
|
||||
let mut vec = Vec::with_capacity(page.selection.selection_handles.len());
|
||||
|
||||
if show_custom_images {
|
||||
// Place removable custom images first
|
||||
for id in page.selection.custom_images.iter().rev() {
|
||||
let handle = &page.selection.selection_handles[*id];
|
||||
|
||||
vec.push(wallpaper_button(
|
||||
handle,
|
||||
*id,
|
||||
true,
|
||||
selected.map_or(false, |selection| id == &selection),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Then place non-removable images from the current folder
|
||||
for (id, handle) in &page.selection.selection_handles {
|
||||
if page.selection.is_custom.contains_key(id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
vec.push(wallpaper_button(
|
||||
handle,
|
||||
id,
|
||||
false,
|
||||
selected.map_or(false, |selection| id == selection),
|
||||
));
|
||||
}
|
||||
|
||||
flex_select_row(vec)
|
||||
}
|
||||
|
||||
fn flex_select_row(elements: Vec<Element<Message>>) -> Element<Message> {
|
||||
cosmic::widget::flex_row(elements)
|
||||
.column_spacing(COLUMN_SPACING)
|
||||
.row_spacing(ROW_SPACING)
|
||||
.apply(container)
|
||||
.width(Length::Fill)
|
||||
.center_x()
|
||||
.into()
|
||||
}
|
||||
|
||||
fn wallpaper_button(
|
||||
handle: &ImageHandle,
|
||||
id: DefaultKey,
|
||||
removable: bool,
|
||||
selected: bool,
|
||||
) -> Element<Message> {
|
||||
cosmic::widget::button::image(handle.clone())
|
||||
.selected(selected)
|
||||
.on_press(Message::Select(id))
|
||||
.on_remove_maybe(if removable {
|
||||
Some(Message::ImageRemove(id))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.into()
|
||||
}
|
||||
196
cosmic-settings/src/pages/desktop/workspaces.rs
Normal file
196
cosmic-settings/src/pages/desktop/workspaces.rs
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
// TODO make settings work
|
||||
|
||||
use cosmic::widget::settings;
|
||||
use cosmic::{
|
||||
cosmic_config::{self, ConfigGet, ConfigSet},
|
||||
widget::radio,
|
||||
Apply, Element,
|
||||
};
|
||||
use cosmic_comp_config::workspace::{WorkspaceConfig, WorkspaceLayout, WorkspaceMode};
|
||||
use cosmic_settings_page::Section;
|
||||
use cosmic_settings_page::{self as page, section};
|
||||
use slotmap::SlotMap;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
SetWorkspaceMode(WorkspaceMode),
|
||||
OrientationButtonSelected(cosmic::widget::segmented_button::Entity),
|
||||
SetShowName(bool),
|
||||
SetShowNumber(bool),
|
||||
}
|
||||
|
||||
pub struct Page {
|
||||
config: cosmic_config::Config,
|
||||
comp_config: cosmic_config::Config,
|
||||
comp_workspace_config: WorkspaceConfig,
|
||||
show_workspace_name: bool,
|
||||
show_workspace_number: bool,
|
||||
orientation_model: cosmic::widget::segmented_button::SingleSelectModel,
|
||||
}
|
||||
|
||||
impl Default for Page {
|
||||
fn default() -> Self {
|
||||
let comp_config = cosmic_config::Config::new("com.system76.CosmicComp", 1).unwrap();
|
||||
let comp_workspace_config = comp_config.get("workspaces").unwrap_or_else(|err| {
|
||||
error!(?err, "Failed to read config 'workspaces'");
|
||||
WorkspaceConfig::default()
|
||||
});
|
||||
let mut orientation_model = cosmic::widget::segmented_button::SingleSelectModel::builder()
|
||||
.insert(|b| b.text(fl!("workspaces-orientation", "vertical")))
|
||||
.insert(|b| b.text(fl!("workspaces-orientation", "horizontal")))
|
||||
.build();
|
||||
orientation_model.activate_position(0);
|
||||
let config = cosmic_config::Config::new("com.system76.CosmicWorkspaces", 1).unwrap();
|
||||
let show_workspace_name = config.get("show_workspace_name").unwrap_or_else(|err| {
|
||||
error!(?err, "Failed to read config 'show_workspace_name'");
|
||||
false
|
||||
});
|
||||
let show_workspace_number = config.get("show_workspace_number").unwrap_or_else(|err| {
|
||||
error!(?err, "Failed to read config 'show_workspace_number'");
|
||||
false
|
||||
});
|
||||
Self {
|
||||
config,
|
||||
comp_config,
|
||||
comp_workspace_config,
|
||||
show_workspace_name,
|
||||
show_workspace_number,
|
||||
orientation_model,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![
|
||||
sections.insert(multi_behavior()),
|
||||
sections.insert(overview_thumbnails()),
|
||||
sections.insert(workspace_orientation()),
|
||||
])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("workspaces", "preferences-workspaces-symbolic")
|
||||
.title(fl!("workspaces"))
|
||||
.description(fl!("workspaces", "desc"))
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
||||
impl Page {
|
||||
fn save_comp_config(&self) {
|
||||
if let Err(err) = self
|
||||
.comp_config
|
||||
.set("workspaces", &self.comp_workspace_config)
|
||||
{
|
||||
error!(?err, "Failed to set config 'workspaces'");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, message: Message) {
|
||||
match message {
|
||||
Message::SetWorkspaceMode(value) => {
|
||||
self.comp_workspace_config.workspace_mode = value;
|
||||
self.save_comp_config();
|
||||
}
|
||||
Message::OrientationButtonSelected(entity) => {
|
||||
self.orientation_model.activate(entity);
|
||||
let horizontal_entity = self.orientation_model.entity_at(1).unwrap();
|
||||
let layout = if self.orientation_model.active() == horizontal_entity {
|
||||
WorkspaceLayout::Horizontal
|
||||
} else {
|
||||
WorkspaceLayout::Vertical
|
||||
};
|
||||
self.comp_workspace_config.workspace_layout = layout;
|
||||
self.save_comp_config();
|
||||
}
|
||||
Message::SetShowName(value) => {
|
||||
self.show_workspace_name = value;
|
||||
if let Err(err) = self.config.set("show_workspace_name", value) {
|
||||
error!(?err, "Failed to set config 'show_workspace_name'");
|
||||
}
|
||||
}
|
||||
Message::SetShowNumber(value) => {
|
||||
self.show_workspace_number = value;
|
||||
if let Err(err) = self.config.set("show_workspace_number", value) {
|
||||
error!(?err, "Failed to set config 'show_workspace_number'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn multi_behavior() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("workspaces-multi-behavior"))
|
||||
.descriptions(vec![
|
||||
fl!("workspaces-multi-behavior", "span").into(),
|
||||
fl!("workspaces-multi-behavior", "separate").into(),
|
||||
])
|
||||
.view::<Page>(|_binder, page, section| {
|
||||
let descriptions = §ion.descriptions;
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item_row(vec![radio(
|
||||
&*descriptions[0],
|
||||
WorkspaceMode::Global,
|
||||
Some(page.comp_workspace_config.workspace_mode),
|
||||
Message::SetWorkspaceMode,
|
||||
)
|
||||
.into()]))
|
||||
.add(settings::item_row(vec![radio(
|
||||
&*descriptions[1],
|
||||
WorkspaceMode::OutputBound,
|
||||
Some(page.comp_workspace_config.workspace_mode),
|
||||
Message::SetWorkspaceMode,
|
||||
)
|
||||
.into()]))
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::DesktopWorkspaces)
|
||||
})
|
||||
}
|
||||
|
||||
fn overview_thumbnails() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("workspaces-overview-thumbnails"))
|
||||
.descriptions(vec![
|
||||
fl!("workspaces-overview-thumbnails", "show-number").into(),
|
||||
fl!("workspaces-overview-thumbnails", "show-name").into(),
|
||||
])
|
||||
.view::<Page>(|_binder, page, section| {
|
||||
let descriptions = §ion.descriptions;
|
||||
settings::view_section(§ion.title)
|
||||
.add(
|
||||
settings::item::builder(&*descriptions[0])
|
||||
.toggler(page.show_workspace_number, Message::SetShowNumber),
|
||||
)
|
||||
.add(
|
||||
settings::item::builder(&*descriptions[1])
|
||||
.toggler(page.show_workspace_name, Message::SetShowName),
|
||||
)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::DesktopWorkspaces)
|
||||
})
|
||||
}
|
||||
|
||||
fn workspace_orientation() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("workspaces-orientation"))
|
||||
.descriptions(vec![])
|
||||
.view::<Page>(|_binder, page, section| {
|
||||
settings::view_section(§ion.title)
|
||||
.add(
|
||||
cosmic::widget::segmented_control::horizontal(&page.orientation_model)
|
||||
.on_activate(Message::OrientationButtonSelected),
|
||||
)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::DesktopWorkspaces)
|
||||
})
|
||||
}
|
||||
598
cosmic-settings/src/pages/display/arrangement.rs
Normal file
598
cosmic-settings/src/pages/display/arrangement.rs
Normal file
|
|
@ -0,0 +1,598 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use cosmic::iced_core::renderer::Quad;
|
||||
use cosmic::iced_core::widget::{tree, Tree};
|
||||
use cosmic::iced_core::{
|
||||
self as core, Border, Clipboard, Element, Layout, Length, Rectangle, Renderer as IcedRenderer,
|
||||
Shell, Size, Widget,
|
||||
};
|
||||
use cosmic::iced_core::{alignment, event, text};
|
||||
use cosmic::iced_core::{layout, mouse, renderer, touch, Point};
|
||||
use cosmic::widget::segmented_button::{self, SingleSelectModel};
|
||||
use cosmic::Renderer;
|
||||
use cosmic_randr_shell::{self as randr, OutputKey};
|
||||
use randr::Transform;
|
||||
|
||||
const UNIT_PIXELS: f32 = 12.0;
|
||||
|
||||
pub type OnPlacementFunc<Message> = Box<dyn Fn(OutputKey, i32, i32) -> Message>;
|
||||
pub type OnSelectFunc<Message> = Box<dyn Fn(segmented_button::Entity) -> Message>;
|
||||
|
||||
#[must_use]
|
||||
#[derive(derive_setters::Setters)]
|
||||
pub struct Arrangement<'a, Message> {
|
||||
#[setters(skip)]
|
||||
list: &'a randr::List,
|
||||
#[setters(skip)]
|
||||
tab_model: &'a SingleSelectModel,
|
||||
#[setters(skip)]
|
||||
on_placement: Option<OnPlacementFunc<Message>>,
|
||||
#[setters(skip)]
|
||||
on_select: Option<OnSelectFunc<Message>>,
|
||||
width: Length,
|
||||
height: Length,
|
||||
}
|
||||
|
||||
impl<'a, Message> Arrangement<'a, Message> {
|
||||
pub fn new(list: &'a randr::List, tab_model: &'a SingleSelectModel) -> Self {
|
||||
Self {
|
||||
list,
|
||||
tab_model,
|
||||
on_placement: None,
|
||||
on_select: None,
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_placement(
|
||||
mut self,
|
||||
on_placement: impl Fn(OutputKey, i32, i32) -> Message + 'static,
|
||||
) -> Self {
|
||||
self.on_placement = Some(Box::new(on_placement));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_select(
|
||||
mut self,
|
||||
on_select: impl Fn(segmented_button::Entity) -> Message + 'static,
|
||||
) -> Self {
|
||||
self.on_select = Some(Box::new(on_select));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: Clone> Widget<Message, cosmic::Theme, Renderer> for Arrangement<'a, Message> {
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::default())
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<Length> {
|
||||
Size {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
_renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
// Determine the max display dimensions, and the total display area utilized.
|
||||
let mut max_dimensions = (0, 0);
|
||||
let mut display_area = (0, 0);
|
||||
|
||||
for output in self.list.outputs.values() {
|
||||
if !output.enabled {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(mode_key) = output.current else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(mode) = self.list.modes.get(mode_key) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let (mut width, mut height) = if output.transform.map_or(true, is_landscape) {
|
||||
(mode.size.0, mode.size.1)
|
||||
} else {
|
||||
(mode.size.1, mode.size.0)
|
||||
};
|
||||
|
||||
// Scale dimensions of the display with the output scale.
|
||||
width = (width as f64 / output.scale) as u32;
|
||||
height = (height as f64 / output.scale) as u32;
|
||||
|
||||
max_dimensions.0 = max_dimensions.0.max(width);
|
||||
max_dimensions.1 = max_dimensions.1.max(height);
|
||||
|
||||
display_area.0 = display_area.0.max(width as i32 + output.position.0);
|
||||
display_area.1 = display_area.1.max(height as i32 + output.position.1);
|
||||
}
|
||||
|
||||
let width = (max_dimensions.0 as i32 * 2 + display_area.0) as f32 / UNIT_PIXELS;
|
||||
let height = (max_dimensions.1 as i32 * 2 + display_area.1) as f32 / UNIT_PIXELS;
|
||||
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
state.max_dimensions = (
|
||||
max_dimensions.0 as f32 * 1.25 / UNIT_PIXELS,
|
||||
max_dimensions.1 as f32 * 1.25 / UNIT_PIXELS,
|
||||
);
|
||||
|
||||
let limits = limits
|
||||
.width(Length::Fixed(width))
|
||||
.height(Length::Fixed(height));
|
||||
|
||||
let size = limits.resolve(width, height, Size::ZERO);
|
||||
|
||||
layout::Node::new(size)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: cosmic::iced_core::Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
_viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
match event {
|
||||
core::Event::Mouse(mouse::Event::CursorMoved { .. })
|
||||
| core::Event::Touch(touch::Event::FingerMoved { .. }) => {
|
||||
if let Some(position) = cursor.position() {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
if let Some((output_key, region)) = state.dragging.as_mut() {
|
||||
update_dragged_region(
|
||||
self.tab_model,
|
||||
self.list,
|
||||
&bounds,
|
||||
*output_key,
|
||||
region,
|
||||
state.max_dimensions,
|
||||
(position.x - state.offset.0, position.y - state.offset.1),
|
||||
);
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
core::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| core::Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
if let Some(position) = cursor.position() {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
if let Some((output_key, output_region)) = display_region_hovers(
|
||||
self.tab_model,
|
||||
self.list,
|
||||
&bounds,
|
||||
state.max_dimensions,
|
||||
position,
|
||||
) {
|
||||
state.drag_from = position;
|
||||
state.offset = (position.x - output_region.x, position.y - output_region.y);
|
||||
state.dragging = Some((output_key, output_region));
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
core::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||
| core::Event::Touch(touch::Event::FingerLifted { .. }) => {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
if let Some((output_key, region)) = state.dragging.take() {
|
||||
if let Some(position) = cursor.position() {
|
||||
if position.distance(state.drag_from) < 4.0 {
|
||||
if let Some(ref on_select) = self.on_select {
|
||||
for id in self.tab_model.iter() {
|
||||
if let Some(&key) = self.tab_model.data::<OutputKey>(id) {
|
||||
if key == output_key {
|
||||
shell.publish(on_select(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref on_placement) = self.on_placement {
|
||||
shell.publish(on_placement(
|
||||
output_key,
|
||||
((region.x - state.max_dimensions.0 - bounds.x) * UNIT_PIXELS) as i32,
|
||||
((region.y - state.max_dimensions.1 - bounds.y) * UNIT_PIXELS) as i32,
|
||||
));
|
||||
}
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
|
||||
_ => (),
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
let bounds = layout.bounds();
|
||||
|
||||
for (_output_key, region) in
|
||||
display_regions(self.tab_model, self.list, &bounds, state.max_dimensions)
|
||||
{
|
||||
if cursor.is_over(region) {
|
||||
return mouse::Interaction::Grab;
|
||||
}
|
||||
}
|
||||
|
||||
mouse::Interaction::Idle
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
_theme: &cosmic::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
|
||||
let bounds = layout.bounds();
|
||||
let theme = cosmic::theme::active();
|
||||
let cosmic_theme = theme.cosmic();
|
||||
|
||||
let border_color = cosmic_theme.palette.neutral_7;
|
||||
|
||||
let active_key = self.tab_model.active_data::<OutputKey>();
|
||||
|
||||
for (id, (output_key, mut region)) in
|
||||
display_regions(self.tab_model, self.list, &bounds, state.max_dimensions).enumerate()
|
||||
{
|
||||
// If the output is being dragged, show its dragged position instead.
|
||||
if let Some((dragged_key, dragged_region)) = state.dragging {
|
||||
if dragged_key == output_key {
|
||||
region = dragged_region;
|
||||
}
|
||||
}
|
||||
|
||||
let (background, border_color) = if Some(&output_key) == active_key {
|
||||
let mut border_color = border_color;
|
||||
border_color.alpha = 0.4;
|
||||
|
||||
(cosmic_theme.accent_color(), border_color)
|
||||
} else {
|
||||
(cosmic_theme.palette.neutral_4, border_color)
|
||||
};
|
||||
|
||||
renderer.fill_quad(
|
||||
Quad {
|
||||
bounds: region,
|
||||
border: Border {
|
||||
color: border_color.into(),
|
||||
radius: 4.0.into(),
|
||||
width: 3.0,
|
||||
},
|
||||
shadow: Default::default(),
|
||||
},
|
||||
core::Background::Color(background.into()),
|
||||
);
|
||||
|
||||
let id_bounds = Rectangle {
|
||||
x: region.x + (region.width / 2.0 - 36.0),
|
||||
y: region.y + (region.height / 2.0 - 23.0),
|
||||
width: 72.0,
|
||||
height: 46.0,
|
||||
};
|
||||
|
||||
renderer.fill_quad(
|
||||
Quad {
|
||||
bounds: id_bounds,
|
||||
border: Border {
|
||||
radius: 30.0.into(),
|
||||
..Default::default()
|
||||
},
|
||||
shadow: Default::default(),
|
||||
},
|
||||
core::Background::Color(cosmic_theme.palette.neutral_1.into()),
|
||||
);
|
||||
|
||||
core::text::Renderer::fill_text(
|
||||
renderer,
|
||||
core::Text {
|
||||
content: itoa::Buffer::new().format(id),
|
||||
size: core::Pixels(24.0),
|
||||
line_height: core::text::LineHeight::Relative(1.2),
|
||||
font: cosmic::font::FONT_BOLD,
|
||||
bounds: id_bounds.size(),
|
||||
horizontal_alignment: alignment::Horizontal::Center,
|
||||
vertical_alignment: alignment::Vertical::Center,
|
||||
shaping: text::Shaping::Basic,
|
||||
},
|
||||
core::Point {
|
||||
x: id_bounds.center_x(),
|
||||
y: id_bounds.center_y(),
|
||||
},
|
||||
cosmic_theme.palette.neutral_10.into(),
|
||||
*viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: 'static + Clone> From<Arrangement<'a, Message>> for cosmic::Element<'a, Message> {
|
||||
fn from(display_positioner: Arrangement<'a, Message>) -> Self {
|
||||
Element::new(display_positioner)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
drag_from: Point,
|
||||
dragging: Option<(OutputKey, Rectangle)>,
|
||||
offset: (f32, f32),
|
||||
max_dimensions: (f32, f32),
|
||||
}
|
||||
|
||||
/// Iteratively calculate display regions for each display output in the list.
|
||||
fn display_regions<'a>(
|
||||
model: &'a SingleSelectModel,
|
||||
list: &'a randr::List,
|
||||
bounds: &'a Rectangle,
|
||||
max_dimensions: (f32, f32),
|
||||
) -> impl Iterator<Item = (OutputKey, Rectangle)> + 'a {
|
||||
model
|
||||
.iter()
|
||||
.filter_map(move |id| model.data::<OutputKey>(id))
|
||||
.filter_map(move |&key| {
|
||||
let Some(output) = list.outputs.get(key) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if !output.enabled {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(mode_key) = output.current else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let Some(mode) = list.modes.get(mode_key) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let (mut width, mut height) = (
|
||||
(mode.size.0 as f32 / output.scale as f32) / UNIT_PIXELS,
|
||||
(mode.size.1 as f32 / output.scale as f32) / UNIT_PIXELS,
|
||||
);
|
||||
|
||||
(width, height) = if output.transform.map_or(true, is_landscape) {
|
||||
(width, height)
|
||||
} else {
|
||||
(height, width)
|
||||
};
|
||||
|
||||
Some((
|
||||
key,
|
||||
Rectangle {
|
||||
width,
|
||||
height,
|
||||
x: max_dimensions.0 + bounds.x + (output.position.0 as f32) / UNIT_PIXELS,
|
||||
y: max_dimensions.1 + bounds.y + (output.position.1 as f32) / UNIT_PIXELS,
|
||||
},
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn display_region_hovers(
|
||||
model: &SingleSelectModel,
|
||||
list: &randr::List,
|
||||
bounds: &Rectangle,
|
||||
max_dimensions: (f32, f32),
|
||||
point: Point,
|
||||
) -> Option<(OutputKey, Rectangle)> {
|
||||
for (output_key, region) in display_regions(model, list, bounds, max_dimensions) {
|
||||
if region.contains(point) {
|
||||
return Some((output_key, region));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Updates a display's region, preventing coordinates from overlapping with existing displays.
|
||||
fn update_dragged_region(
|
||||
model: &SingleSelectModel,
|
||||
list: &randr::List,
|
||||
bounds: &Rectangle,
|
||||
output: OutputKey,
|
||||
region: &mut Rectangle,
|
||||
max_dimensions: (f32, f32),
|
||||
(x, y): (f32, f32),
|
||||
) {
|
||||
let mut dragged_region = Rectangle { x, y, ..*region };
|
||||
|
||||
let mut nearest = f32::MAX;
|
||||
let mut nearest_region = Rectangle::default();
|
||||
let mut nearest_side = NearestSide::East;
|
||||
|
||||
// Find the nearest adjacent display to the dragged display.
|
||||
for (other_output, other_region) in display_regions(model, list, bounds, max_dimensions) {
|
||||
if other_output == output {
|
||||
continue;
|
||||
}
|
||||
|
||||
let center = dragged_region.center();
|
||||
|
||||
let eastward = distance(east_point(&other_region), center) * 1.25;
|
||||
let westward = distance(west_point(&other_region), center) * 1.25;
|
||||
let northward = distance(north_point(&other_region), center);
|
||||
let southward = distance(south_point(&other_region), center);
|
||||
|
||||
let mut nearer = false;
|
||||
|
||||
if nearest > eastward {
|
||||
(nearest, nearest_side, nearer) = (eastward, NearestSide::East, true);
|
||||
}
|
||||
|
||||
if nearest > westward {
|
||||
(nearest, nearest_side, nearer) = (westward, NearestSide::West, true);
|
||||
}
|
||||
|
||||
if nearest > northward {
|
||||
(nearest, nearest_side, nearer) = (northward, NearestSide::North, true);
|
||||
}
|
||||
|
||||
if nearest > southward {
|
||||
(nearest, nearest_side, nearer) = (southward, NearestSide::South, true);
|
||||
}
|
||||
|
||||
if nearer {
|
||||
nearest_region = other_region;
|
||||
}
|
||||
}
|
||||
|
||||
// Attach dragged display to nearest adjacent display.
|
||||
match nearest_side {
|
||||
NearestSide::East => {
|
||||
dragged_region.x = nearest_region.x - dragged_region.width;
|
||||
dragged_region.y = dragged_region
|
||||
.y
|
||||
.max(nearest_region.y - dragged_region.height + 8.0)
|
||||
.min(nearest_region.y + nearest_region.height - 8.0);
|
||||
}
|
||||
|
||||
NearestSide::North => {
|
||||
dragged_region.y = nearest_region.y - dragged_region.height;
|
||||
dragged_region.x = dragged_region
|
||||
.x
|
||||
.max(nearest_region.x - dragged_region.width + 8.0)
|
||||
.min(nearest_region.x + nearest_region.width - 8.0);
|
||||
}
|
||||
|
||||
NearestSide::West => {
|
||||
dragged_region.x = nearest_region.x + nearest_region.width;
|
||||
dragged_region.y = dragged_region
|
||||
.y
|
||||
.max(nearest_region.y - dragged_region.height + 8.0)
|
||||
.min(nearest_region.y + nearest_region.height - 8.0);
|
||||
}
|
||||
|
||||
NearestSide::South => {
|
||||
dragged_region.y = nearest_region.y + nearest_region.height;
|
||||
dragged_region.x = dragged_region
|
||||
.x
|
||||
.max(nearest_region.x - dragged_region.width + 8.0)
|
||||
.min(nearest_region.x + nearest_region.width - 8.0);
|
||||
}
|
||||
}
|
||||
|
||||
// Snap-align on x-axis when alignment is near.
|
||||
if (dragged_region.x - nearest_region.x).abs() <= 8.0 {
|
||||
dragged_region.x = nearest_region.x;
|
||||
}
|
||||
|
||||
// Snap-align on x-axis when alignment is near bottom edge.
|
||||
if ((dragged_region.x + dragged_region.width) - (nearest_region.x + nearest_region.width)).abs()
|
||||
<= 8.0
|
||||
{
|
||||
dragged_region.x = nearest_region.x + nearest_region.width - dragged_region.width;
|
||||
}
|
||||
|
||||
// Snap-align on y-axis when alignment is near.
|
||||
if (dragged_region.y - nearest_region.y).abs() <= 8.0 {
|
||||
dragged_region.y = nearest_region.y;
|
||||
}
|
||||
|
||||
// Snap-align on y-axis when alignment is near bottom edge.
|
||||
if ((dragged_region.y + dragged_region.height) - (nearest_region.y + nearest_region.height))
|
||||
.abs()
|
||||
<= 8.0
|
||||
{
|
||||
dragged_region.y = nearest_region.y + nearest_region.height - dragged_region.height;
|
||||
}
|
||||
|
||||
// Prevent display from overlapping with other displays.
|
||||
for (other_output, other_region) in display_regions(model, list, bounds, max_dimensions) {
|
||||
if other_output == output {
|
||||
continue;
|
||||
}
|
||||
|
||||
if other_region.intersects(&dragged_region) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
*region = dragged_region;
|
||||
}
|
||||
|
||||
fn is_landscape(transform: Transform) -> bool {
|
||||
matches!(
|
||||
transform,
|
||||
Transform::Normal | Transform::Rotate180 | Transform::Flipped | Transform::Flipped180
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum NearestSide {
|
||||
East,
|
||||
North,
|
||||
South,
|
||||
West,
|
||||
}
|
||||
|
||||
fn distance(a: Point, b: Point) -> f32 {
|
||||
((b.x - a.x).powf(2.0) + (b.y - a.y).powf(2.0)).sqrt()
|
||||
}
|
||||
|
||||
fn east_point(r: &Rectangle) -> Point {
|
||||
Point {
|
||||
x: r.x,
|
||||
y: r.center_y(),
|
||||
}
|
||||
}
|
||||
|
||||
fn north_point(r: &Rectangle) -> Point {
|
||||
Point {
|
||||
x: r.center_x(),
|
||||
y: r.y,
|
||||
}
|
||||
}
|
||||
|
||||
fn west_point(r: &Rectangle) -> Point {
|
||||
Point {
|
||||
x: r.x + r.width,
|
||||
y: r.center_y(),
|
||||
}
|
||||
}
|
||||
|
||||
fn south_point(r: &Rectangle) -> Point {
|
||||
Point {
|
||||
x: r.center_x(),
|
||||
y: r.y + r.height,
|
||||
}
|
||||
}
|
||||
245
cosmic-settings/src/pages/display/graphics.rs
Normal file
245
cosmic-settings/src/pages/display/graphics.rs
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use super::{Message, NightLight};
|
||||
use crate::pages;
|
||||
use cosmic::iced_core::{Alignment, Length, Padding};
|
||||
use cosmic::prelude::CollectionWidget;
|
||||
use cosmic::widget::{button, column, icon, list_column, row, toggler};
|
||||
use cosmic::{Apply, Command, Element};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub const INTEGRATED: &str = "integrated";
|
||||
pub const NVIDIA: &str = "nvidia";
|
||||
pub const HYBRID: &str = "hybrid";
|
||||
pub const COMPUTE: &str = "compute";
|
||||
|
||||
pub async fn fetch() -> Option<Arc<std::io::Result<Mode>>> {
|
||||
let switchable = tokio::process::Command::new("system76-power")
|
||||
.args(["graphics", "switchable"])
|
||||
.output()
|
||||
.await
|
||||
.map(|output| {
|
||||
std::str::from_utf8(&output.stdout).map_or(false, |text| text.trim() == "switchable")
|
||||
});
|
||||
|
||||
match switchable {
|
||||
Ok(false) => None,
|
||||
|
||||
Ok(true) => Some(Arc::new(
|
||||
tokio::process::Command::new("system76-power")
|
||||
.arg("graphics")
|
||||
.output()
|
||||
.await
|
||||
.and_then(|output| {
|
||||
if let Ok(mut mode) = std::str::from_utf8(&output.stdout) {
|
||||
mode = mode.trim();
|
||||
if mode == COMPUTE {
|
||||
Ok(Mode::Compute)
|
||||
} else if mode == HYBRID {
|
||||
Ok(Mode::Hybrid)
|
||||
} else if mode == INTEGRATED {
|
||||
Ok(Mode::Integrated)
|
||||
} else if mode == NVIDIA {
|
||||
Ok(Mode::Nvidia)
|
||||
} else {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"unknown graphics mode",
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"system76-power output was not UTF-8",
|
||||
))
|
||||
}
|
||||
}),
|
||||
)),
|
||||
|
||||
Err(why) => Some(Arc::new(Err(why))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(
|
||||
mode: &'static str,
|
||||
description: &'static str,
|
||||
button: Option<(&'static str, Message)>,
|
||||
) -> Element<'static, Message> {
|
||||
let theme = cosmic::theme::active();
|
||||
let theme = theme.cosmic();
|
||||
let has_checkmark = button.is_none();
|
||||
|
||||
let content = column::with_capacity(3)
|
||||
.padding(Padding::from([theme.space_xxs(), theme.space_l()]))
|
||||
.push(cosmic::widget::text::body(mode))
|
||||
.push(cosmic::widget::text::caption(description))
|
||||
.push(cosmic::widget::Space::new(Length::Fill, 12))
|
||||
.push_maybe(button.map(|(text, message)| {
|
||||
button::text(text)
|
||||
.style(cosmic::theme::Button::Link)
|
||||
.trailing_icon(icon::from_name("go-next-symbolic").size(16))
|
||||
.padding(0)
|
||||
.on_press(message)
|
||||
}));
|
||||
|
||||
if has_checkmark {
|
||||
row::with_capacity(2)
|
||||
.align_items(Alignment::Center)
|
||||
.push(content)
|
||||
.push(icon::from_name("object-select-symbolic").size(24))
|
||||
.apply(Element::from)
|
||||
.apply(cosmic::widget::list::container)
|
||||
.into()
|
||||
} else {
|
||||
cosmic::widget::list::container(content).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Switchable graphics mode
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Mode {
|
||||
Compute,
|
||||
Hybrid,
|
||||
Integrated,
|
||||
Nvidia,
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
#[must_use]
|
||||
pub fn argument_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Compute => COMPUTE,
|
||||
Self::Hybrid => HYBRID,
|
||||
Self::Integrated => INTEGRATED,
|
||||
Self::Nvidia => NVIDIA,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn localized_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Compute => &super::text::GRAPHICS_MODE_COMPUTE,
|
||||
Self::Hybrid => &super::text::GRAPHICS_MODE_HYBRID,
|
||||
Self::Integrated => &super::text::GRAPHICS_MODE_INTEGRATED,
|
||||
Self::Nvidia => &super::text::GRAPHICS_MODE_NVIDIA,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Page {
|
||||
pub fn graphics_mode_view(&self) -> Element<pages::Message> {
|
||||
let mut container = list_column();
|
||||
|
||||
if let Some(graphics_mode) = self.config.graphics_mode {
|
||||
// Displays the active graphics mode, and a button for configuring it.
|
||||
container = container.add(cosmic::widget::settings::item(
|
||||
&*super::text::GRAPHICS_MODE,
|
||||
row()
|
||||
.align_items(Alignment::Center)
|
||||
.push(cosmic::widget::text::body(graphics_mode.localized_str()))
|
||||
.push(
|
||||
button::icon(icon::from_name("go-next-symbolic"))
|
||||
.extra_small()
|
||||
.on_press(Message::GraphicsModeContext),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// Displays the night light status, and a button for configuring it.
|
||||
container = container.add(
|
||||
cosmic::widget::settings::item::builder(&*super::text::NIGHT_LIGHT)
|
||||
.description(&*super::text::NIGHT_LIGHT_DESCRIPTION)
|
||||
.control(
|
||||
row()
|
||||
.align_items(Alignment::Center)
|
||||
.push(toggler(None, self.config.night_light_enabled, |enable| {
|
||||
Message::NightLight(NightLight::Toggle(enable))
|
||||
}))
|
||||
.push(
|
||||
button::icon(icon::from_name("go-next-symbolic"))
|
||||
.extra_small()
|
||||
.on_press(Message::NightLightContext),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
container
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Displays)
|
||||
}
|
||||
|
||||
pub fn graphics_mode_context_view(&self) -> Element<pages::Message> {
|
||||
let theme = cosmic::theme::active();
|
||||
let theme = theme.cosmic();
|
||||
|
||||
column::with_capacity(4)
|
||||
.spacing(theme.space_xs())
|
||||
.push(view(
|
||||
&super::text::GRAPHICS_MODE_INTEGRATED,
|
||||
&super::text::GRAPHICS_MODE_INTEGRATED_DESC,
|
||||
if let Some(Mode::Integrated) = self.config.graphics_mode {
|
||||
None
|
||||
} else {
|
||||
Some((
|
||||
&super::text::GRAPHICS_MODE_INTEGRATED_ENABLE,
|
||||
Message::GraphicsMode(Mode::Integrated),
|
||||
))
|
||||
},
|
||||
))
|
||||
.push(view(
|
||||
&super::text::GRAPHICS_MODE_NVIDIA,
|
||||
&super::text::GRAPHICS_MODE_NVIDIA_DESC,
|
||||
if let Some(Mode::Nvidia) = self.config.graphics_mode {
|
||||
None
|
||||
} else {
|
||||
Some((
|
||||
&super::text::GRAPHICS_MODE_NVIDIA_ENABLE,
|
||||
Message::GraphicsMode(Mode::Nvidia),
|
||||
))
|
||||
},
|
||||
))
|
||||
.push(view(
|
||||
&super::text::GRAPHICS_MODE_HYBRID,
|
||||
&super::text::GRAPHICS_MODE_HYBRID_DESC,
|
||||
if let Some(Mode::Hybrid) = self.config.graphics_mode {
|
||||
None
|
||||
} else {
|
||||
Some((
|
||||
&super::text::GRAPHICS_MODE_HYBRID_ENABLE,
|
||||
Message::GraphicsMode(Mode::Hybrid),
|
||||
))
|
||||
},
|
||||
))
|
||||
.push(view(
|
||||
&super::text::GRAPHICS_MODE_COMPUTE,
|
||||
&super::text::GRAPHICS_MODE_COMPUTE_DESC,
|
||||
if let Some(Mode::Compute) = self.config.graphics_mode {
|
||||
None
|
||||
} else {
|
||||
Some((
|
||||
&super::text::GRAPHICS_MODE_COMPUTE_ENABLE,
|
||||
Message::GraphicsMode(Mode::Compute),
|
||||
))
|
||||
},
|
||||
))
|
||||
.apply(Element::from)
|
||||
.map(pages::Message::Displays)
|
||||
}
|
||||
|
||||
/// Change the graphics mode.
|
||||
pub fn set_graphics_mode(&mut self, mode: Mode) -> Command<crate::app::Message> {
|
||||
self.config.graphics_mode = Some(mode);
|
||||
|
||||
// Runs `system76-power graphics {{mode}}`
|
||||
cosmic::command::future(async move {
|
||||
let result = tokio::process::Command::new("system76-power")
|
||||
.args(["graphics", mode.argument_str()])
|
||||
.status()
|
||||
.await;
|
||||
let page_message =
|
||||
crate::pages::Message::Displays(Message::GraphicsModeResult(Arc::new(result)));
|
||||
crate::app::Message::PageMessage(page_message)
|
||||
})
|
||||
}
|
||||
}
|
||||
805
cosmic-settings/src/pages/display/mod.rs
Normal file
805
cosmic-settings/src/pages/display/mod.rs
Normal file
|
|
@ -0,0 +1,805 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod arrangement;
|
||||
pub mod graphics;
|
||||
pub mod text;
|
||||
|
||||
use crate::{app, pages};
|
||||
use arrangement::Arrangement;
|
||||
use cosmic::iced::{Alignment, Length};
|
||||
use cosmic::iced_widget::scrollable::{Direction, Properties, RelativeOffset};
|
||||
use cosmic::widget::{
|
||||
column, container, dropdown, list_column, segmented_button, tab_bar, toggler,
|
||||
};
|
||||
use cosmic::{command, Apply, Command, Element};
|
||||
use cosmic_randr_shell::{List, Output, OutputKey, Transform};
|
||||
use cosmic_settings_page::{self as page, section, Section};
|
||||
use slotmap::{Key, SlotMap};
|
||||
use std::collections::BTreeMap;
|
||||
use std::{process::ExitStatus, sync::Arc};
|
||||
|
||||
/// Display color depth options
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ColorDepth(usize);
|
||||
|
||||
/// Identifies the content to display in the context drawer
|
||||
pub enum ContextDrawer {
|
||||
GraphicsMode,
|
||||
NightLight,
|
||||
}
|
||||
|
||||
/// Display mirroring options
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Mirroring {
|
||||
Disable,
|
||||
ProjectToAll,
|
||||
Project(OutputKey),
|
||||
Mirror(OutputKey),
|
||||
}
|
||||
|
||||
/// Night light preferences
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum NightLight {
|
||||
/// Toggles night light's automatic scheduling.
|
||||
AutoSchedule(bool),
|
||||
/// Sets the night light schedule.
|
||||
ManualSchedule,
|
||||
/// Changes the preferred night light temperature.
|
||||
Temperature(f32),
|
||||
/// Toggles night light mode
|
||||
Toggle(bool),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
/// Change placement of display
|
||||
Position(OutputKey, i32, i32),
|
||||
/// Changes the active display being configured.
|
||||
Display(segmented_button::Entity),
|
||||
/// Set the color depth of a display.
|
||||
ColorDepth(ColorDepth),
|
||||
/// Set the color profile of a display.
|
||||
ColorProfile(usize),
|
||||
/// Toggles display on or off.
|
||||
DisplayToggle(bool),
|
||||
/// Changes the hybrid graphics mode.
|
||||
GraphicsMode(graphics::Mode),
|
||||
/// Shows the graphics mode context drawer.
|
||||
GraphicsModeContext,
|
||||
/// Status of an applied graphics mode change
|
||||
GraphicsModeResult(Arc<std::io::Result<ExitStatus>>),
|
||||
/// Configures mirroring status of a display.
|
||||
Mirroring(Mirroring),
|
||||
/// Handle night light preferences.
|
||||
NightLight(NightLight),
|
||||
/// Show the night light mode context drawer.
|
||||
NightLightContext,
|
||||
/// Set the orientation of a display.
|
||||
Orientation(Transform),
|
||||
/// Status of an applied display change.
|
||||
RandrResult(Arc<std::io::Result<ExitStatus>>),
|
||||
/// Set the refresh rate of a display.
|
||||
RefreshRate(usize),
|
||||
/// Set the resolution of a display.
|
||||
Resolution(usize),
|
||||
/// Set the preferred scale for a display.
|
||||
Scale(usize),
|
||||
/// Refreshes display outputs.
|
||||
Update {
|
||||
/// The current graphics mode
|
||||
graphics: Option<Arc<std::io::Result<graphics::Mode>>>,
|
||||
|
||||
/// Available outputs from cosmic-randr.
|
||||
randr: Arc<Result<List, cosmic_randr_shell::Error>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<Message> for app::Message {
|
||||
fn from(message: Message) -> Self {
|
||||
let page_message = crate::pages::Message::Displays(message);
|
||||
app::Message::PageMessage(page_message)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Randr {
|
||||
Position(i32, i32),
|
||||
RefreshRate(u32),
|
||||
Resolution(u32, u32),
|
||||
Scale(u32),
|
||||
Transform(Transform),
|
||||
Toggle(bool),
|
||||
}
|
||||
|
||||
/// The page struct for the display settings page.
|
||||
pub struct Page {
|
||||
list: List,
|
||||
display_tabs: segmented_button::SingleSelectModel,
|
||||
active_display: OutputKey,
|
||||
config: Config,
|
||||
cache: ViewCache,
|
||||
context: Option<ContextDrawer>,
|
||||
display_arrangement_scrollable: cosmic::widget::Id,
|
||||
}
|
||||
|
||||
impl Default for Page {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
list: List::default(),
|
||||
display_tabs: segmented_button::SingleSelectModel::default(),
|
||||
active_display: OutputKey::default(),
|
||||
config: Config::default(),
|
||||
cache: ViewCache::default(),
|
||||
context: None,
|
||||
display_arrangement_scrollable: cosmic::widget::Id::unique(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Config {
|
||||
/// Whether night light is enabled.
|
||||
night_light_enabled: bool,
|
||||
graphics_mode: Option<graphics::Mode>,
|
||||
refresh_rate: Option<u32>,
|
||||
resolution: Option<(u32, u32)>,
|
||||
scale: u32,
|
||||
}
|
||||
|
||||
/// Cached view content for widgets.
|
||||
#[derive(Default)]
|
||||
struct ViewCache {
|
||||
modes: BTreeMap<(u32, u32), Vec<u32>>,
|
||||
orientations: [&'static str; 4],
|
||||
refresh_rates: Vec<String>,
|
||||
resolutions: Vec<String>,
|
||||
orientation_selected: Option<usize>,
|
||||
refresh_rate_selected: Option<usize>,
|
||||
resolution_selected: Option<usize>,
|
||||
scale_selected: Option<usize>,
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![
|
||||
// Graphics switching and light mode
|
||||
sections.insert(
|
||||
Section::default()
|
||||
.descriptions(vec![
|
||||
text::GRAPHICS_MODE.as_str().into(),
|
||||
text::GRAPHICS_MODE_COMPUTE_DESC.as_str().into(),
|
||||
text::GRAPHICS_MODE_HYBRID_DESC.as_str().into(),
|
||||
text::GRAPHICS_MODE_INTEGRATED_DESC.as_str().into(),
|
||||
text::GRAPHICS_MODE_NVIDIA_DESC.as_str().into(),
|
||||
text::NIGHT_LIGHT.as_str().into(),
|
||||
text::NIGHT_LIGHT_AUTO.as_str().into(),
|
||||
text::NIGHT_LIGHT_DESCRIPTION.as_str().into(),
|
||||
])
|
||||
.view::<Page>(|_binder, page, _section| page.graphics_mode_view()),
|
||||
),
|
||||
// Display arrangement
|
||||
sections.insert(
|
||||
Section::default()
|
||||
.title(&*text::DISPLAY_ARRANGEMENT)
|
||||
.descriptions(vec![
|
||||
text::DISPLAY_ARRANGEMENT.as_str().into(),
|
||||
text::DISPLAY_ARRANGEMENT_DESC.as_str().into(),
|
||||
])
|
||||
// Show section when there is more than 1 display
|
||||
.show_while::<Page>(|page| page.list.outputs.len() > 1)
|
||||
.view::<Page>(|_binder, page, _section| page.display_arrangement_view()),
|
||||
),
|
||||
// Display configuration
|
||||
sections.insert(
|
||||
Section::default()
|
||||
.descriptions([
|
||||
text::DISPLAY.as_str().into(),
|
||||
text::DISPLAY_REFRESH_RATE.as_str().into(),
|
||||
text::DISPLAY_SCALE.as_str().into(),
|
||||
text::ORIENTATION.as_str().into(),
|
||||
text::ORIENTATION_STANDARD.as_str().into(),
|
||||
text::ORIENTATION_ROTATE_90.as_str().into(),
|
||||
])
|
||||
.view::<Page>(|_binder, page, _section| page.display_view()),
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("display", "preferences-desktop-display-symbolic")
|
||||
.title(fl!("display"))
|
||||
.description(fl!("display", "desc"))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "test"))]
|
||||
fn reload(&mut self, _page: page::Entity) -> Command<crate::pages::Message> {
|
||||
command::future(reload())
|
||||
}
|
||||
|
||||
#[cfg(feature = "test")]
|
||||
fn reload(&mut self, _page: page::Entity) -> Command<crate::pages::Message> {
|
||||
command::future(async move {
|
||||
let mut randr = List::default();
|
||||
|
||||
let test_mode = randr.modes.insert(cosmic_randr_shell::Mode {
|
||||
size: (1920, 1080),
|
||||
refresh_rate: 144_000,
|
||||
preferred: true,
|
||||
});
|
||||
|
||||
randr.outputs.insert(cosmic_randr_shell::Output {
|
||||
name: "Dummy-1".into(),
|
||||
enabled: true,
|
||||
make: None,
|
||||
model: "Test 1".into(),
|
||||
physical: (1, 1),
|
||||
position: (0, 0),
|
||||
scale: 1.0,
|
||||
transform: Some(Transform::Normal),
|
||||
modes: vec![test_mode],
|
||||
current: Some(test_mode),
|
||||
});
|
||||
|
||||
randr.outputs.insert(cosmic_randr_shell::Output {
|
||||
name: "Dummy-2".into(),
|
||||
enabled: true,
|
||||
make: None,
|
||||
model: "Test 1".into(),
|
||||
physical: (1, 1),
|
||||
position: (1920, 0),
|
||||
scale: 1.0,
|
||||
transform: Some(Transform::Normal),
|
||||
modes: vec![test_mode],
|
||||
current: Some(test_mode),
|
||||
});
|
||||
|
||||
crate::pages::Message::Displays(Message::Update {
|
||||
graphics: graphics::fetch().await,
|
||||
randr: Arc::new(Ok(randr)),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn context_drawer(&self) -> Option<Element<pages::Message>> {
|
||||
Some(match self.context {
|
||||
Some(ContextDrawer::GraphicsMode) => self.graphics_mode_context_view(),
|
||||
|
||||
Some(ContextDrawer::NightLight) => self.night_light_context_view(),
|
||||
|
||||
None => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Page {
|
||||
pub fn update(&mut self, message: Message) -> Command<app::Message> {
|
||||
match message {
|
||||
Message::RandrResult(result) => {
|
||||
if let Some(Err(why)) = Arc::into_inner(result) {
|
||||
tracing::error!(?why, "cosmic-randr error");
|
||||
}
|
||||
|
||||
return cosmic::command::future(async {
|
||||
crate::Message::PageMessage(reload().await)
|
||||
});
|
||||
}
|
||||
|
||||
Message::Display(display) => self.set_display(display),
|
||||
|
||||
Message::ColorDepth(color_depth) => return self.set_color_depth(color_depth),
|
||||
|
||||
Message::ColorProfile(profile) => return self.set_color_profile(profile),
|
||||
|
||||
Message::DisplayToggle(enable) => return self.toggle_display(enable),
|
||||
|
||||
Message::GraphicsMode(mode) => return self.set_graphics_mode(mode),
|
||||
|
||||
Message::GraphicsModeContext => {
|
||||
self.context = Some(ContextDrawer::GraphicsMode);
|
||||
return cosmic::command::message(app::Message::OpenContextDrawer(
|
||||
text::GRAPHICS_MODE.clone().into(),
|
||||
));
|
||||
}
|
||||
|
||||
Message::GraphicsModeResult(result) => {
|
||||
if let Some(Err(why)) = Arc::into_inner(result) {
|
||||
tracing::error!(?why, "system76-power error");
|
||||
}
|
||||
}
|
||||
|
||||
Message::Mirroring(mirroring) => match mirroring {
|
||||
Mirroring::Disable => (),
|
||||
Mirroring::Mirror(target_display) => (),
|
||||
Mirroring::Project(target_display) => (),
|
||||
Mirroring::ProjectToAll => (),
|
||||
},
|
||||
|
||||
Message::NightLight(night_light) => {}
|
||||
|
||||
Message::NightLightContext => {
|
||||
self.context = Some(ContextDrawer::NightLight);
|
||||
return cosmic::command::message(app::Message::OpenContextDrawer(
|
||||
text::NIGHT_LIGHT.clone().into(),
|
||||
));
|
||||
}
|
||||
|
||||
Message::Orientation(orientation) => return self.set_orientation(orientation),
|
||||
|
||||
Message::Position(display, x, y) => return self.set_position(display, x, y),
|
||||
|
||||
Message::RefreshRate(rate) => return self.set_refresh_rate(rate),
|
||||
|
||||
Message::Resolution(option) => return self.set_resolution(option),
|
||||
|
||||
Message::Scale(scale) => return self.set_scale(scale),
|
||||
|
||||
Message::Update { graphics, randr } => {
|
||||
match graphics.and_then(Arc::into_inner) {
|
||||
Some(Ok(mode)) => {
|
||||
self.config.graphics_mode = Some(mode);
|
||||
}
|
||||
|
||||
Some(Err(why)) => {
|
||||
tracing::error!(?why, "error fetching graphics switching mode");
|
||||
}
|
||||
|
||||
None => (),
|
||||
}
|
||||
|
||||
match Arc::into_inner(randr) {
|
||||
Some(Ok(outputs)) => self.update_displays(outputs),
|
||||
|
||||
Some(Err(why)) => {
|
||||
tracing::error!(?why, "error fetching displays");
|
||||
}
|
||||
|
||||
None => (),
|
||||
}
|
||||
|
||||
self.cache.orientations = [
|
||||
text::ORIENTATION_STANDARD.as_str(),
|
||||
text::ORIENTATION_ROTATE_90.as_str(),
|
||||
text::ORIENTATION_ROTATE_180.as_str(),
|
||||
text::ORIENTATION_ROTATE_270.as_str(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
cosmic::iced::widget::scrollable::snap_to(
|
||||
self.display_arrangement_scrollable.clone(),
|
||||
RelativeOffset { x: 0.5, y: 0.5 },
|
||||
)
|
||||
}
|
||||
|
||||
/// View for the display arrangement section.
|
||||
pub fn display_arrangement_view(&self) -> Element<pages::Message> {
|
||||
let theme = cosmic::theme::active();
|
||||
|
||||
column()
|
||||
.padding(cosmic::iced::Padding::from([
|
||||
theme.cosmic().space_s(),
|
||||
theme.cosmic().space_m(),
|
||||
]))
|
||||
.spacing(theme.cosmic().space_xs())
|
||||
.push(cosmic::widget::text::body(&*text::DISPLAY_ARRANGEMENT_DESC))
|
||||
.push({
|
||||
Arrangement::new(&self.list, &self.display_tabs)
|
||||
.on_select(|id| pages::Message::Displays(Message::Display(id)))
|
||||
.on_placement(|id, x, y| pages::Message::Displays(Message::Position(id, x, y)))
|
||||
.apply(cosmic::widget::scrollable)
|
||||
.id(self.display_arrangement_scrollable.clone())
|
||||
.width(Length::Shrink)
|
||||
.direction(Direction::Horizontal(Properties::new()))
|
||||
.apply(container)
|
||||
.center_x()
|
||||
.width(Length::Fill)
|
||||
})
|
||||
.apply(cosmic::widget::list::container)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// View for the display configuration section.
|
||||
pub fn display_view(&self) -> Element<pages::Message> {
|
||||
let theme = cosmic::theme::active();
|
||||
|
||||
let Some(&active_id) = self.display_tabs.active_data::<OutputKey>() else {
|
||||
return column().into();
|
||||
};
|
||||
|
||||
let active_output = &self.list.outputs[active_id];
|
||||
|
||||
let display_options = list_column()
|
||||
.add(cosmic::widget::settings::item(
|
||||
&*text::DISPLAY_RESOLUTION,
|
||||
dropdown(
|
||||
&self.cache.resolutions,
|
||||
self.cache.resolution_selected,
|
||||
Message::Resolution,
|
||||
),
|
||||
))
|
||||
.add(cosmic::widget::settings::item(
|
||||
&*text::DISPLAY_REFRESH_RATE,
|
||||
dropdown(
|
||||
&self.cache.refresh_rates,
|
||||
self.cache.refresh_rate_selected,
|
||||
Message::RefreshRate,
|
||||
),
|
||||
))
|
||||
.add(cosmic::widget::settings::item(
|
||||
&*text::DISPLAY_SCALE,
|
||||
dropdown(
|
||||
&["50%", "75%", "100%", "125%", "150%", "175%", "200%"],
|
||||
self.cache.scale_selected,
|
||||
Message::Scale,
|
||||
),
|
||||
))
|
||||
.add(cosmic::widget::settings::item(
|
||||
&*text::ORIENTATION,
|
||||
dropdown(
|
||||
&self.cache.orientations,
|
||||
self.cache.orientation_selected,
|
||||
|id| {
|
||||
Message::Orientation(match id {
|
||||
0 => Transform::Normal,
|
||||
1 => Transform::Rotate90,
|
||||
2 => Transform::Rotate180,
|
||||
_ => Transform::Rotate270,
|
||||
})
|
||||
},
|
||||
),
|
||||
));
|
||||
|
||||
let mut content = column().spacing(theme.cosmic().space_m());
|
||||
|
||||
if self.list.outputs.len() > 1 {
|
||||
let display_switcher = tab_bar::horizontal(&self.display_tabs)
|
||||
.button_alignment(Alignment::Center)
|
||||
.on_activate(Message::Display);
|
||||
|
||||
let display_enable = list_column().add(cosmic::widget::settings::item(
|
||||
&*text::DISPLAY_ENABLE,
|
||||
toggler(None, active_output.enabled, Message::DisplayToggle),
|
||||
));
|
||||
|
||||
content = content.push(display_switcher).push(display_enable);
|
||||
}
|
||||
|
||||
content
|
||||
.push(cosmic::widget::text::heading(&*text::DISPLAY_OPTIONS))
|
||||
.push(display_options)
|
||||
.apply(Element::from)
|
||||
.map(pages::Message::Displays)
|
||||
}
|
||||
|
||||
/// Displays the night light context drawer.
|
||||
pub fn night_light_context_view(&self) -> Element<pages::Message> {
|
||||
column().into()
|
||||
}
|
||||
|
||||
/// Reloads the display list, and all information relevant to the active display.
|
||||
pub fn update_displays(&mut self, list: List) {
|
||||
self.active_display = OutputKey::null();
|
||||
self.display_tabs.clear();
|
||||
self.list = list;
|
||||
|
||||
let sorted_outputs = self
|
||||
.list
|
||||
.outputs
|
||||
.iter()
|
||||
.map(|(key, output)| (&*output.name, key))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
for (name, id) in sorted_outputs {
|
||||
let Some(output) = self.list.outputs.get(id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
self.display_tabs
|
||||
.insert()
|
||||
.text(crate::utils::display_name(&output.name, output.physical))
|
||||
.data::<OutputKey>(id);
|
||||
}
|
||||
|
||||
self.display_tabs.activate_position(0);
|
||||
|
||||
// Retrieve data for the first, activated display.
|
||||
self.set_display(self.display_tabs.active());
|
||||
}
|
||||
|
||||
/// Changes the color depth of the active display.
|
||||
pub fn set_color_depth(&mut self, depth: ColorDepth) -> Command<app::Message> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Changes the color profile of the active display.
|
||||
pub fn set_color_profile(&mut self, profile: usize) -> Command<app::Message> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Changes the active display, and regenerates available options for it.
|
||||
pub fn set_display(&mut self, display: segmented_button::Entity) {
|
||||
let Some(&output_id) = self.display_tabs.data::<OutputKey>(display) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(output) = self.list.outputs.get_mut(output_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.display_tabs.activate(display);
|
||||
self.active_display = output_id;
|
||||
self.config.refresh_rate = None;
|
||||
self.config.resolution = None;
|
||||
self.config.scale = (output.scale * 100.0) as u32;
|
||||
|
||||
self.cache.modes.clear();
|
||||
self.cache.refresh_rates.clear();
|
||||
self.cache.resolutions.clear();
|
||||
self.cache.orientation_selected = match output.transform {
|
||||
Some(Transform::Normal) => Some(0),
|
||||
Some(Transform::Rotate90) => Some(1),
|
||||
Some(Transform::Rotate180) => Some(2),
|
||||
Some(Transform::Rotate270) => Some(3),
|
||||
_ => None,
|
||||
};
|
||||
self.cache.resolution_selected = None;
|
||||
self.cache.refresh_rate_selected = None;
|
||||
|
||||
self.cache.scale_selected = Some(if self.config.scale < 75 {
|
||||
0
|
||||
} else if self.config.scale < 100 {
|
||||
1
|
||||
} else if self.config.scale < 125 {
|
||||
2
|
||||
} else if self.config.scale < 150 {
|
||||
3
|
||||
} else if self.config.scale < 175 {
|
||||
4
|
||||
} else if self.config.scale < 200 {
|
||||
5
|
||||
} else {
|
||||
6
|
||||
});
|
||||
|
||||
if let Some(current_mode_id) = output.current {
|
||||
for (mode_id, mode) in output
|
||||
.modes
|
||||
.iter()
|
||||
.filter_map(|&id| self.list.modes.get(id).map(|m| (id, m)))
|
||||
{
|
||||
let refresh_rates = self.cache.modes.entry(mode.size).or_default();
|
||||
|
||||
refresh_rates.push(mode.refresh_rate);
|
||||
|
||||
if current_mode_id == mode_id {
|
||||
self.cache.refresh_rate_selected = Some(refresh_rates.len() - 1);
|
||||
self.cache.resolution_selected = Some(self.cache.modes.len() - 1);
|
||||
self.config.resolution = Some(mode.size);
|
||||
self.config.refresh_rate = Some(mode.refresh_rate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (&resolution, rates) in self.cache.modes.iter().rev() {
|
||||
self.cache
|
||||
.resolutions
|
||||
.push(format!("{}x{}", resolution.0, resolution.1));
|
||||
if Some(resolution) == self.config.resolution {
|
||||
cache_rates(&mut self.cache.refresh_rates, rates);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Change display orientation.
|
||||
pub fn set_orientation(&mut self, transform: Transform) -> Command<app::Message> {
|
||||
let Some(output) = self.list.outputs.get(self.active_display) else {
|
||||
return Command::none();
|
||||
};
|
||||
|
||||
self.cache.orientation_selected = match transform {
|
||||
Transform::Normal => Some(0),
|
||||
Transform::Rotate90 => Some(1),
|
||||
Transform::Rotate180 => Some(2),
|
||||
_ => Some(3),
|
||||
};
|
||||
|
||||
self.exec_randr(output, Randr::Transform(transform))
|
||||
}
|
||||
|
||||
/// Changes the position of the display.
|
||||
pub fn set_position(&mut self, display: OutputKey, x: i32, y: i32) -> Command<app::Message> {
|
||||
let Some(output) = self.list.outputs.get_mut(display) else {
|
||||
return Command::none();
|
||||
};
|
||||
|
||||
output.position = (x, y);
|
||||
|
||||
if cfg!(feature = "test") {
|
||||
tracing::debug!("set position {x},{y}");
|
||||
return Command::none();
|
||||
}
|
||||
|
||||
let output = &self.list.outputs[display];
|
||||
self.exec_randr(output, Randr::Position(x, y))
|
||||
}
|
||||
|
||||
/// Changes the refresh rate of the active display.
|
||||
pub fn set_refresh_rate(&mut self, option: usize) -> Command<app::Message> {
|
||||
let Some(output) = self.list.outputs.get(self.active_display) else {
|
||||
return Command::none();
|
||||
};
|
||||
|
||||
if let Some(ref resolution) = self.config.resolution {
|
||||
if let Some(rates) = self.cache.modes.get(resolution) {
|
||||
if let Some(&rate) = rates.get(option) {
|
||||
self.cache.refresh_rate_selected = Some(option);
|
||||
self.config.refresh_rate = Some(rate);
|
||||
return self.exec_randr(output, Randr::RefreshRate(rate));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
/// Change the resolution of the active display.
|
||||
pub fn set_resolution(&mut self, option: usize) -> Command<app::Message> {
|
||||
let Some(output) = self.list.outputs.get(self.active_display) else {
|
||||
return Command::none();
|
||||
};
|
||||
|
||||
let Some((&resolution, rates)) = self.cache.modes.iter().rev().nth(option) else {
|
||||
return Command::none();
|
||||
};
|
||||
|
||||
self.cache.refresh_rates.clear();
|
||||
cache_rates(&mut self.cache.refresh_rates, rates);
|
||||
|
||||
let Some(&rate) = rates.first() else {
|
||||
return Command::none();
|
||||
};
|
||||
|
||||
self.config.refresh_rate = Some(rate);
|
||||
self.config.resolution = Some(resolution);
|
||||
self.cache.refresh_rate_selected = Some(0);
|
||||
self.cache.resolution_selected = Some(option);
|
||||
self.exec_randr(output, Randr::Resolution(resolution.0, resolution.1))
|
||||
}
|
||||
|
||||
/// Set the scale of the active display.
|
||||
pub fn set_scale(&mut self, option: usize) -> Command<app::Message> {
|
||||
let Some(output) = self.list.outputs.get(self.active_display) else {
|
||||
return Command::none();
|
||||
};
|
||||
|
||||
let scale = (option * 25 + 50) as u32;
|
||||
|
||||
self.cache.scale_selected = Some(option);
|
||||
self.config.scale = scale;
|
||||
self.exec_randr(output, Randr::Scale(scale))
|
||||
}
|
||||
|
||||
/// Enables or disables the active display.
|
||||
pub fn toggle_display(&mut self, enable: bool) -> Command<app::Message> {
|
||||
let Some(output) = self.list.outputs.get_mut(self.active_display) else {
|
||||
return Command::none();
|
||||
};
|
||||
|
||||
output.enabled = enable;
|
||||
|
||||
let output = &self.list.outputs[self.active_display];
|
||||
self.exec_randr(output, Randr::Toggle(output.enabled))
|
||||
}
|
||||
|
||||
/// Applies a display configuration via `cosmic-randr`.
|
||||
fn exec_randr(&self, output: &Output, request: Randr) -> Command<app::Message> {
|
||||
let Some(current) = output.current.and_then(|id| self.list.modes.get(id)) else {
|
||||
return Command::none();
|
||||
};
|
||||
|
||||
let name = &*output.name;
|
||||
|
||||
let mut command = tokio::process::Command::new("cosmic-randr");
|
||||
|
||||
match request {
|
||||
Randr::Position(x, y) => {
|
||||
command
|
||||
.arg("mode")
|
||||
.arg("--pos-x")
|
||||
.arg(itoa::Buffer::new().format(x))
|
||||
.arg("--pos-y")
|
||||
.arg(itoa::Buffer::new().format(y))
|
||||
.arg(name)
|
||||
.arg(itoa::Buffer::new().format(current.size.0))
|
||||
.arg(itoa::Buffer::new().format(current.size.1));
|
||||
}
|
||||
|
||||
Randr::RefreshRate(rate) => {
|
||||
command
|
||||
.arg("mode")
|
||||
.arg("--refresh")
|
||||
.arg(
|
||||
&[
|
||||
itoa::Buffer::new().format(rate / 1000),
|
||||
".",
|
||||
itoa::Buffer::new().format(rate % 1000),
|
||||
]
|
||||
.concat(),
|
||||
)
|
||||
.arg(name)
|
||||
.arg(itoa::Buffer::new().format(current.size.0))
|
||||
.arg(itoa::Buffer::new().format(current.size.1));
|
||||
}
|
||||
|
||||
Randr::Resolution(width, height) => {
|
||||
command
|
||||
.arg("mode")
|
||||
.arg(name)
|
||||
.arg(itoa::Buffer::new().format(width))
|
||||
.arg(itoa::Buffer::new().format(height));
|
||||
}
|
||||
|
||||
Randr::Scale(scale) => {
|
||||
command
|
||||
.arg("mode")
|
||||
.arg("--scale")
|
||||
.arg(
|
||||
&[
|
||||
itoa::Buffer::new().format(scale / 100),
|
||||
".",
|
||||
itoa::Buffer::new().format(scale % 100),
|
||||
]
|
||||
.concat(),
|
||||
)
|
||||
.arg(name)
|
||||
.arg(itoa::Buffer::new().format(current.size.0))
|
||||
.arg(itoa::Buffer::new().format(current.size.1));
|
||||
}
|
||||
|
||||
Randr::Toggle(enable) => {
|
||||
command
|
||||
.arg(if enable { "enable" } else { "disable" })
|
||||
.arg(name);
|
||||
}
|
||||
|
||||
Randr::Transform(transform) => {
|
||||
command
|
||||
.arg("mode")
|
||||
.arg("--transform")
|
||||
.arg(&*format!("{transform}"))
|
||||
.arg(name)
|
||||
.arg(itoa::Buffer::new().format(current.size.0))
|
||||
.arg(itoa::Buffer::new().format(current.size.1));
|
||||
}
|
||||
}
|
||||
|
||||
cosmic::command::future(async move {
|
||||
tracing::debug!(?command, "executing");
|
||||
app::Message::from(Message::RandrResult(Arc::new(command.status().await)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn cache_rates(cached_rates: &mut Vec<String>, rates: &[u32]) {
|
||||
*cached_rates = rates
|
||||
.iter()
|
||||
.map(|&rate| format!("{:>3}.{:02} Hz", rate / 1000, rate % 1000))
|
||||
.collect();
|
||||
}
|
||||
|
||||
pub async fn reload() -> crate::pages::Message {
|
||||
let graphics_fut = graphics::fetch();
|
||||
let randr_fut = cosmic_randr_shell::list();
|
||||
let (graphics, randr) = futures::future::zip(graphics_fut, randr_fut).await;
|
||||
|
||||
crate::pages::Message::Displays(Message::Update {
|
||||
graphics,
|
||||
randr: Arc::new(randr),
|
||||
})
|
||||
}
|
||||
65
cosmic-settings/src/pages/display/text.rs
Normal file
65
cosmic-settings/src/pages/display/text.rs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
crate::cache_dynamic_lazy! {
|
||||
pub static COLOR: String = fl!("color");
|
||||
pub static COLOR_DEPTH: String = fl!("color", "depth");
|
||||
pub static COLOR_PROFILE: String = fl!("color", "profile");
|
||||
pub static COLOR_PROFILES: String = fl!("color", "sidebar");
|
||||
pub static COLOR_TEMPERATURE: String = fl!("color", "temperature");
|
||||
|
||||
pub static DISPLAY: String = fl!("display");
|
||||
pub static DISPLAY_ARRANGEMENT: String = fl!("display", "arrangement");
|
||||
pub static DISPLAY_ARRANGEMENT_DESC: String = fl!("display", "arrangement-desc");
|
||||
pub static DISPLAY_ENABLE: String = fl!("display", "enable");
|
||||
pub static DISPLAY_EXTERNAL: String = fl!("display", "external");
|
||||
pub static DISPLAY_LAPTOP: String = fl!("display", "laptop");
|
||||
pub static DISPLAY_OPTIONS: String = fl!("display", "options");
|
||||
pub static DISPLAY_REFRESH_RATE: String = fl!("display", "refresh-rate");
|
||||
pub static DISPLAY_RESOLUTION: String = fl!("display", "resolution");
|
||||
pub static DISPLAY_SCALE: String = fl!("display", "scale");
|
||||
|
||||
pub static GRAPHICS_MODE: String = fl!("graphics-mode");
|
||||
pub static GRAPHICS_MODE_COMPUTE: String =
|
||||
fl!("graphics-mode", "mode", mode = super::graphics::COMPUTE);
|
||||
pub static GRAPHICS_MODE_COMPUTE_ENABLE: String =
|
||||
fl!("graphics-mode", "enable", mode = super::graphics::COMPUTE);
|
||||
pub static GRAPHICS_MODE_COMPUTE_DESC: String =
|
||||
fl!("graphics-mode", "desc", mode = super::graphics::COMPUTE);
|
||||
pub static GRAPHICS_MODE_HYBRID: String =
|
||||
fl!("graphics-mode", "mode", mode = super::graphics::HYBRID);
|
||||
pub static GRAPHICS_MODE_HYBRID_ENABLE: String =
|
||||
fl!("graphics-mode", "enable", mode = super::graphics::HYBRID);
|
||||
pub static GRAPHICS_MODE_HYBRID_DESC: String =
|
||||
fl!("graphics-mode", "desc", mode = super::graphics::HYBRID);
|
||||
pub static GRAPHICS_MODE_INTEGRATED: String =
|
||||
fl!("graphics-mode", "mode", mode = super::graphics::INTEGRATED);
|
||||
pub static GRAPHICS_MODE_INTEGRATED_ENABLE: String = fl!(
|
||||
"graphics-mode",
|
||||
"enable",
|
||||
mode = super::graphics::INTEGRATED
|
||||
);
|
||||
pub static GRAPHICS_MODE_INTEGRATED_DESC: String =
|
||||
fl!("graphics-mode", "desc", mode = super::graphics::INTEGRATED);
|
||||
pub static GRAPHICS_MODE_NVIDIA: String =
|
||||
fl!("graphics-mode", "mode", mode = super::graphics::NVIDIA);
|
||||
pub static GRAPHICS_MODE_NVIDIA_ENABLE: String =
|
||||
fl!("graphics-mode", "enable", mode = super::graphics::NVIDIA);
|
||||
pub static GRAPHICS_MODE_NVIDIA_DESC: String =
|
||||
fl!("graphics-mode", "desc", mode = super::graphics::NVIDIA);
|
||||
|
||||
pub static MIRRORING: String = fl!("mirroring");
|
||||
|
||||
pub static NIGHT_LIGHT: String = fl!("night-light");
|
||||
pub static NIGHT_LIGHT_AUTO: String = fl!("night-light", "auto");
|
||||
pub static NIGHT_LIGHT_DESCRIPTION: String = fl!("night-light", "desc");
|
||||
|
||||
pub static ORIENTATION: String = fl!("orientation");
|
||||
pub static ORIENTATION_STANDARD: String = fl!("orientation", "standard");
|
||||
pub static ORIENTATION_ROTATE_90: String = fl!("orientation", "rotate-90");
|
||||
pub static ORIENTATION_ROTATE_180: String = fl!("orientation", "rotate-180");
|
||||
pub static ORIENTATION_ROTATE_270: String = fl!("orientation", "rotate-270");
|
||||
|
||||
pub static SCHEDULING: String = fl!("scheduling");
|
||||
pub static SCHEDULING_MANUAL: String = fl!("scheduling", "manual");
|
||||
}
|
||||
327
cosmic-settings/src/pages/input/keyboard/mod.rs
Normal file
327
cosmic-settings/src/pages/input/keyboard/mod.rs
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
use cosmic::{
|
||||
iced::{
|
||||
self,
|
||||
widget::{self, horizontal_space},
|
||||
window, Length,
|
||||
},
|
||||
iced_core::Border,
|
||||
iced_style, theme,
|
||||
widget::{button, container, icon, radio, settings},
|
||||
Apply,
|
||||
};
|
||||
use cosmic_settings_page::{self as page, section, Section};
|
||||
use once_cell::sync::Lazy;
|
||||
use slotmap::SlotMap;
|
||||
|
||||
use super::Message;
|
||||
|
||||
pub static ADD_INPUT_SOURCE_DIALOGUE_ID: Lazy<window::Id> = Lazy::new(window::Id::unique);
|
||||
pub static SPECIAL_CHARACTER_DIALOGUE_ID: Lazy<window::Id> = Lazy::new(window::Id::unique);
|
||||
|
||||
static COMPOSE_OPTIONS: &[(&str, &str)] = &[
|
||||
// ("Left Alt", "compose:lalt"), XXX?
|
||||
("Right Alt", "compose:ralt"),
|
||||
("Left Super", "compose:lwin"),
|
||||
("Right Super", "compose:rwin"),
|
||||
("Menu key", "compose:menu"),
|
||||
("Right Ctrl", "compose:rctrl"),
|
||||
("Caps Lock", "compose:caps"),
|
||||
("Scroll Lock", "compose:sclk"),
|
||||
("Print Screen", "compose:prsc"),
|
||||
];
|
||||
|
||||
static ALTERNATE_CHARACTER_OPTIONS: &[(&str, &str)] = &[
|
||||
("Left Alt", "lv3:lalt_switch"),
|
||||
("Right Alt", "lv3:alt_switch"),
|
||||
("Left Super", "lv3:lwin_switch"),
|
||||
("Right Super", "lv3:win_switch"),
|
||||
("Menu key", "lv3:menu_switch"),
|
||||
// ("Right Ctrl", "lv3:"), XXX
|
||||
("Caps Lock", "lv3:caps_switch"),
|
||||
// ("Scroll Lock", "lv3:"), XXX
|
||||
// ("Print Screen", "lv3"), XXX
|
||||
];
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum SpecialKey {
|
||||
AlternateCharacters,
|
||||
Compose,
|
||||
}
|
||||
|
||||
impl SpecialKey {
|
||||
pub fn title(self) -> String {
|
||||
match self {
|
||||
Self::Compose => "Compose".to_string(),
|
||||
Self::AlternateCharacters => "Alternate Characters".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prefix(self) -> &'static str {
|
||||
match self {
|
||||
Self::Compose => "compose:",
|
||||
Self::AlternateCharacters => "lv3:",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn popover_menu_row(label: String) -> cosmic::Element<'static, Message> {
|
||||
widget::text(label)
|
||||
.apply(widget::container)
|
||||
.style(cosmic::theme::Container::custom(|theme| {
|
||||
iced_style::container::Appearance {
|
||||
background: None,
|
||||
..container::StyleSheet::appearance(theme, &cosmic::theme::Container::List)
|
||||
}
|
||||
}))
|
||||
.apply(button)
|
||||
.style(theme::Button::Transparent)
|
||||
.into()
|
||||
}
|
||||
|
||||
// TODO for on press, would need to clone ID for each row?
|
||||
fn popover_menu() -> cosmic::Element<'static, Message> {
|
||||
// XXX translate
|
||||
widget::column![
|
||||
popover_menu_row(fl!("keyboard-sources", "move-up")),
|
||||
popover_menu_row(fl!("keyboard-sources", "move-down")),
|
||||
//cosmic::widget::divider::horizontal::light(),
|
||||
cosmic::widget::divider::horizontal::light(),
|
||||
popover_menu_row(fl!("keyboard-sources", "settings")),
|
||||
popover_menu_row(fl!("keyboard-sources", "view-layout")),
|
||||
popover_menu_row(fl!("keyboard-sources", "remove")),
|
||||
]
|
||||
.width(Length::Shrink)
|
||||
.height(Length::Shrink)
|
||||
.apply(cosmic::widget::container)
|
||||
.style(cosmic::theme::Container::custom(|theme| {
|
||||
let cosmic = theme.cosmic();
|
||||
container::Appearance {
|
||||
icon_color: Some(theme.cosmic().background.on.into()),
|
||||
text_color: Some(theme.cosmic().background.on.into()),
|
||||
background: Some(iced::Color::from(theme.cosmic().background.base).into()),
|
||||
border: Border {
|
||||
radius: cosmic.corner_radii.radius_m.into(),
|
||||
..Default::default()
|
||||
},
|
||||
shadow: Default::default(),
|
||||
}
|
||||
}))
|
||||
.into()
|
||||
}
|
||||
|
||||
fn popover_button(input_source: &InputSource, expanded: bool) -> cosmic::Element<'static, Message> {
|
||||
let on_press = Message::ExpandInputSourcePopover(if expanded {
|
||||
None
|
||||
} else {
|
||||
Some(input_source.id.clone())
|
||||
});
|
||||
|
||||
let button = button::icon(icon::from_name("open-menu-symbolic"))
|
||||
.extra_small()
|
||||
.padding(0)
|
||||
.on_press(on_press);
|
||||
|
||||
if expanded {
|
||||
cosmic::widget::popover(button).popup(popover_menu()).into()
|
||||
} else {
|
||||
button.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn input_source<'a>(
|
||||
input_source: &'a InputSource,
|
||||
expanded_source_popover: Option<&'a str>,
|
||||
) -> cosmic::Element<'a, Message> {
|
||||
let expanded = expanded_source_popover == Some(input_source.id.as_str());
|
||||
settings::item(&input_source.label, popover_button(input_source, expanded)).into()
|
||||
}
|
||||
|
||||
pub mod shortcuts;
|
||||
|
||||
pub struct InputSource {
|
||||
id: String,
|
||||
// TODO Translate?
|
||||
label: String,
|
||||
}
|
||||
|
||||
impl super::Page {
|
||||
pub fn add_input_source_view(&self) -> cosmic::Element<'static, crate::app::Message> {
|
||||
widget::column![].into()
|
||||
}
|
||||
|
||||
pub fn special_character_key_view(&self) -> cosmic::Element<'_, crate::app::Message> {
|
||||
let Some(special_key) = self.special_character_dialog else {
|
||||
return widget::text("").into();
|
||||
};
|
||||
|
||||
let options = match special_key {
|
||||
SpecialKey::Compose => COMPOSE_OPTIONS,
|
||||
SpecialKey::AlternateCharacters => ALTERNATE_CHARACTER_OPTIONS,
|
||||
};
|
||||
let prefix = special_key.prefix();
|
||||
let current = self
|
||||
.xkb
|
||||
.options
|
||||
.iter()
|
||||
.flat_map(|x| x.split(','))
|
||||
.find(|x| x.starts_with(prefix));
|
||||
|
||||
// TODO description, layout default
|
||||
|
||||
let mut list = cosmic::widget::list_column();
|
||||
list = list.add(special_char_radio_row("None", None, current));
|
||||
for (desc, id) in options {
|
||||
list = list.add(special_char_radio_row(desc, Some(id), current));
|
||||
}
|
||||
widget::column![
|
||||
cosmic::widget::header_bar()
|
||||
.title(special_key.title())
|
||||
.on_close(Message::CloseSpecialCharacterDialog),
|
||||
cosmic::widget::container(
|
||||
cosmic::widget::scrollable(cosmic::widget::container(list).padding(24))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
)
|
||||
.style(theme::Container::Background)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
]
|
||||
.apply(cosmic::Element::from)
|
||||
.map(crate::pages::Message::Input)
|
||||
.map(crate::app::Message::PageMessage)
|
||||
}
|
||||
}
|
||||
|
||||
fn special_char_radio_row<'a>(
|
||||
desc: &'a str,
|
||||
value: Option<&'static str>,
|
||||
current_value: Option<&'a str>,
|
||||
) -> cosmic::Element<'a, Message> {
|
||||
settings::item_row(vec![radio(desc, value, Some(current_value), |_| {
|
||||
Message::SpecialCharacterSelect(value)
|
||||
})
|
||||
.into()])
|
||||
.into()
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Page;
|
||||
|
||||
// XXX
|
||||
pub fn default_input_sources() -> Vec<InputSource> {
|
||||
vec![InputSource {
|
||||
id: "us".to_string(),
|
||||
label: "English (US)".to_string(),
|
||||
}]
|
||||
}
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![
|
||||
sections.insert(input_sources()),
|
||||
sections.insert(special_character_entry()),
|
||||
sections.insert(keyboard_shortcuts()),
|
||||
])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("keyboard", "input-keyboard-symbolic")
|
||||
.title(fl!("keyboard"))
|
||||
.description(fl!("keyboard", "desc"))
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {
|
||||
fn sub_pages(page: page::Insert<crate::pages::Message>) -> page::Insert<crate::pages::Message> {
|
||||
page.sub_page::<shortcuts::Page>()
|
||||
}
|
||||
}
|
||||
|
||||
fn input_sources() -> Section<crate::pages::Message> {
|
||||
// TODO desc
|
||||
Section::default()
|
||||
.title(fl!("keyboard-sources"))
|
||||
.view::<Page>(|binder, _page, section| {
|
||||
let input = binder.page::<super::Page>().expect("input page not found");
|
||||
|
||||
// TODO Need something more custom, with drag and drop
|
||||
let mut section = settings::view_section(§ion.title);
|
||||
|
||||
let expanded_source = input.expanded_source_popover.as_deref();
|
||||
for source in &input.sources {
|
||||
section = section.add(input_source(source, expanded_source));
|
||||
}
|
||||
|
||||
section
|
||||
.apply(cosmic::Element::from)
|
||||
.map(crate::pages::Message::Input)
|
||||
})
|
||||
}
|
||||
|
||||
fn special_character_entry() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("keyboard-special-char"))
|
||||
.descriptions(vec![
|
||||
fl!("keyboard-special-char", "alternate").into(),
|
||||
fl!("keyboard-special-char", "compose").into(),
|
||||
])
|
||||
.view::<Page>(|_binder, _page, section| {
|
||||
let descriptions = §ion.descriptions;
|
||||
|
||||
// TODO dialogs
|
||||
settings::view_section(§ion.title)
|
||||
.add(go_next_item(
|
||||
&*descriptions[0],
|
||||
Message::OpenSpecialCharacterDialog(SpecialKey::AlternateCharacters),
|
||||
))
|
||||
.add(go_next_item(
|
||||
&*descriptions[1],
|
||||
Message::OpenSpecialCharacterDialog(SpecialKey::Compose),
|
||||
))
|
||||
.apply(cosmic::Element::from)
|
||||
.map(crate::pages::Message::Input)
|
||||
})
|
||||
}
|
||||
|
||||
fn keyboard_shortcuts() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("keyboard-shortcuts"))
|
||||
.descriptions(vec![fl!("keyboard-shortcuts", "desc").into()])
|
||||
.view::<Page>(|binder, _page, section| {
|
||||
let descriptions = §ion.descriptions;
|
||||
|
||||
let mut section = settings::view_section(§ion.title);
|
||||
if let Some((shortcuts_entity, _)) = binder
|
||||
.info
|
||||
.iter()
|
||||
.find(|(_, v)| v.id == "keyboard-shortcuts")
|
||||
{
|
||||
section = section.add(go_next_item(
|
||||
&*descriptions[0],
|
||||
crate::pages::Message::Page(shortcuts_entity),
|
||||
));
|
||||
}
|
||||
section.apply(cosmic::Element::from)
|
||||
})
|
||||
}
|
||||
|
||||
fn go_next_control<Msg: Clone + 'static>() -> cosmic::Element<'static, Msg> {
|
||||
widget::row!(
|
||||
horizontal_space(Length::Fill),
|
||||
icon::from_name("go-next-symbolic").size(16).icon(),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn go_next_item<Msg: Clone + 'static>(description: &str, msg: Msg) -> cosmic::Element<'_, Msg> {
|
||||
settings::item(description, go_next_control())
|
||||
.apply(widget::container)
|
||||
.style(cosmic::theme::Container::List)
|
||||
.apply(button)
|
||||
.style(theme::Button::Transparent)
|
||||
.on_press(msg)
|
||||
.into()
|
||||
}
|
||||
44
cosmic-settings/src/pages/input/keyboard/shortcuts.rs
Normal file
44
cosmic-settings/src/pages/input/keyboard/shortcuts.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
use cosmic::widget::{column, settings};
|
||||
use cosmic::{Apply, Element};
|
||||
use cosmic_settings_page::Section;
|
||||
use cosmic_settings_page::{self as page, section};
|
||||
use slotmap::SlotMap;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Page;
|
||||
|
||||
//crate::app::Message::Page
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![sections.insert(shortcuts())])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("keyboard-shortcuts", "input-keyboard-symbolic")
|
||||
.title(fl!("keyboard-shortcuts"))
|
||||
.description(fl!("keyboard-shortcuts", "desc"))
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
||||
fn shortcuts() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.descriptions(vec![])
|
||||
.view::<Page>(|_binder, _page, section| {
|
||||
// TODO need something more custom
|
||||
/*
|
||||
settings::view_section(§ion.title)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Input)
|
||||
*/
|
||||
column()
|
||||
.push(settings::view_section(§ion.title))
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Input)
|
||||
})
|
||||
}
|
||||
276
cosmic-settings/src/pages/input/mod.rs
Normal file
276
cosmic-settings/src/pages/input/mod.rs
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
use crate::app;
|
||||
use cosmic::{
|
||||
cosmic_config::{self, ConfigGet, ConfigSet},
|
||||
iced::{self, wayland::actions::window::SctkWindowSettings, window},
|
||||
iced_sctk::commands,
|
||||
iced_widget::core::layout,
|
||||
};
|
||||
use cosmic_comp_config::{
|
||||
input::{
|
||||
AccelConfig, AccelProfile, ClickMethod, InputConfig, ScrollConfig, ScrollMethod,
|
||||
TapButtonMap, TapConfig,
|
||||
},
|
||||
XkbConfig,
|
||||
};
|
||||
use cosmic_settings_page as page;
|
||||
use itertools::Itertools;
|
||||
use tracing::error;
|
||||
|
||||
pub mod keyboard;
|
||||
pub mod mouse;
|
||||
pub mod touchpad;
|
||||
|
||||
crate::cache_dynamic_lazy! {
|
||||
static ACCELERATION_DESC: String = fl!("acceleration-desc");
|
||||
static DISABLE_WHILE_TYPING: String = fl!("disable-while-typing");
|
||||
static PRIMARY_BUTTON: String = fl!("primary-button");
|
||||
static SCROLLING_EDGE: String = fl!("scrolling", "edge");
|
||||
static SCROLLING_NATURAL_DESC: String = fl!("scrolling", "natural-desc");
|
||||
static SCROLLING_NATURAL: String = fl!("scrolling", "natural");
|
||||
static SCROLLING_SPEED: String = fl!("scrolling", "speed");
|
||||
static SCROLLING_TWO_FINGER: String = fl!("scrolling", "two-finger");
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
CloseSpecialCharacterDialog,
|
||||
// seperate close message, to make sure another isn't closed?
|
||||
DisableWhileTyping(bool, bool),
|
||||
ExpandInputSourcePopover(Option<String>),
|
||||
OpenSpecialCharacterDialog(keyboard::SpecialKey),
|
||||
PrimaryButtonSelected(cosmic::widget::segmented_button::Entity, bool),
|
||||
SetAcceleration(bool, bool),
|
||||
SetMouseSpeed(f64, bool),
|
||||
SetNaturalScroll(bool, bool),
|
||||
SetSecondaryClickBehavior(Option<ClickMethod>, bool),
|
||||
SetScrollFactor(f64, bool),
|
||||
SetScrollMethod(Option<ScrollMethod>, bool),
|
||||
SpecialCharacterSelect(Option<&'static str>),
|
||||
TapToClick(bool),
|
||||
}
|
||||
|
||||
pub struct Page {
|
||||
config: cosmic_config::Config,
|
||||
input_default: InputConfig,
|
||||
#[allow(dead_code)]
|
||||
input_touchpad: InputConfig,
|
||||
|
||||
// Mouse
|
||||
primary_button: cosmic::widget::segmented_button::SingleSelectModel,
|
||||
|
||||
// Touchpad
|
||||
touchpad_primary_button: cosmic::widget::segmented_button::SingleSelectModel,
|
||||
|
||||
// Keyboard
|
||||
expanded_source_popover: Option<String>,
|
||||
sources: Vec<keyboard::InputSource>,
|
||||
special_character_dialog: Option<keyboard::SpecialKey>,
|
||||
xkb: XkbConfig,
|
||||
}
|
||||
|
||||
fn get_config<T: Default + serde::de::DeserializeOwned>(
|
||||
config: &cosmic_config::Config,
|
||||
key: &str,
|
||||
) -> T {
|
||||
config.get(key).unwrap_or_else(|why| {
|
||||
error!(?why, "Failed to read config '{}'", key);
|
||||
T::default()
|
||||
})
|
||||
}
|
||||
|
||||
impl Default for Page {
|
||||
fn default() -> Self {
|
||||
let config = cosmic_config::Config::new("com.system76.CosmicComp", 1).unwrap();
|
||||
let input_default: InputConfig = get_config(&config, "input_default");
|
||||
let input_touchpad: InputConfig = get_config(&config, "input_touchpad");
|
||||
let xkb = get_config(&config, "xkb_config");
|
||||
|
||||
let mut primary_button = mouse::default_primary_button();
|
||||
let idx = input_default.left_handed.unwrap_or(false) as u16;
|
||||
primary_button.activate_position(idx);
|
||||
|
||||
let mut touchpad_primary_button = mouse::default_primary_button();
|
||||
let idx = input_touchpad.left_handed.unwrap_or(false) as u16;
|
||||
touchpad_primary_button.activate_position(idx);
|
||||
|
||||
Self {
|
||||
config,
|
||||
input_default,
|
||||
input_touchpad,
|
||||
|
||||
// Mouse
|
||||
primary_button,
|
||||
|
||||
// Touchpad
|
||||
touchpad_primary_button,
|
||||
|
||||
// Keyboard
|
||||
expanded_source_popover: None,
|
||||
sources: keyboard::default_input_sources(),
|
||||
special_character_dialog: None,
|
||||
xkb,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Page {
|
||||
fn update_input<F: Fn(&mut InputConfig)>(&mut self, touchpad: bool, f: F) {
|
||||
let (name, input_config) = if touchpad {
|
||||
("input_touchpad", &mut self.input_touchpad)
|
||||
} else {
|
||||
("input_default", &mut self.input_default)
|
||||
};
|
||||
f(input_config);
|
||||
if let Err(err) = self.config.set(name, input_config) {
|
||||
error!(?err, "Failed to set config '{}'", name);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn update(&mut self, message: Message) -> iced::Command<app::Message> {
|
||||
match message {
|
||||
Message::SetAcceleration(value, touchpad) => {
|
||||
let profile = if value {
|
||||
AccelProfile::Adaptive
|
||||
} else {
|
||||
AccelProfile::Flat
|
||||
};
|
||||
|
||||
self.update_input(touchpad, |x| {
|
||||
x.acceleration.get_or_insert(AccelConfig::default()).profile = Some(profile);
|
||||
});
|
||||
}
|
||||
|
||||
Message::SetMouseSpeed(value, touchpad) => self.update_input(touchpad, |x| {
|
||||
x.acceleration.get_or_insert(AccelConfig::default()).speed = value;
|
||||
}),
|
||||
|
||||
Message::DisableWhileTyping(disabled, touchpad) => {
|
||||
self.update_input(touchpad, |conf| {
|
||||
conf.disable_while_typing = Some(disabled);
|
||||
});
|
||||
}
|
||||
|
||||
Message::SetNaturalScroll(enabled, touchpad) => self.update_input(touchpad, |x| {
|
||||
x.scroll_config
|
||||
.get_or_insert(ScrollConfig::default())
|
||||
.natural_scroll = Some(enabled);
|
||||
}),
|
||||
|
||||
Message::SetSecondaryClickBehavior(click_method, touchpad) => {
|
||||
self.update_input(touchpad, |x| {
|
||||
x.click_method = click_method;
|
||||
})
|
||||
}
|
||||
|
||||
Message::SetScrollFactor(value, touchpad) => self.update_input(touchpad, |x| {
|
||||
x.scroll_config
|
||||
.get_or_insert(ScrollConfig::default())
|
||||
.scroll_factor = Some(value);
|
||||
}),
|
||||
|
||||
Message::SetScrollMethod(method, touchpad) => {
|
||||
self.update_input(touchpad, |conf| {
|
||||
conf.scroll_config
|
||||
.get_or_insert(ScrollConfig::default())
|
||||
.method = method;
|
||||
});
|
||||
}
|
||||
|
||||
Message::PrimaryButtonSelected(entity, touchpad) => {
|
||||
let select_model = if touchpad {
|
||||
&mut self.touchpad_primary_button
|
||||
} else {
|
||||
&mut self.primary_button
|
||||
};
|
||||
select_model.activate(entity);
|
||||
|
||||
let Some(left_entity) = select_model.entity_at(1) else {
|
||||
return cosmic::Command::none();
|
||||
};
|
||||
|
||||
let left_handed = select_model.active() == left_entity;
|
||||
self.update_input(touchpad, |x| x.left_handed = Some(left_handed));
|
||||
}
|
||||
|
||||
Message::ExpandInputSourcePopover(value) => {
|
||||
self.expanded_source_popover = value;
|
||||
}
|
||||
|
||||
Message::OpenSpecialCharacterDialog(special_key) => {
|
||||
self.special_character_dialog = Some(special_key);
|
||||
let window_settings = SctkWindowSettings {
|
||||
window_id: *keyboard::SPECIAL_CHARACTER_DIALOGUE_ID,
|
||||
app_id: Some("com.system76.CosmicSettings".to_string()),
|
||||
title: Some(special_key.title()),
|
||||
parent: Some(window::Id::MAIN),
|
||||
autosize: false,
|
||||
size_limits: layout::Limits::NONE
|
||||
.min_width(300.0)
|
||||
.max_width(800.0)
|
||||
.min_height(200.0)
|
||||
.max_height(1080.0),
|
||||
size: (512, 420),
|
||||
resizable: None,
|
||||
client_decorations: true,
|
||||
transparent: true,
|
||||
..Default::default()
|
||||
};
|
||||
return commands::window::get_window(window_settings);
|
||||
}
|
||||
|
||||
Message::CloseSpecialCharacterDialog => {
|
||||
self.special_character_dialog = None;
|
||||
return commands::window::close_window(*keyboard::SPECIAL_CHARACTER_DIALOGUE_ID);
|
||||
}
|
||||
|
||||
Message::SpecialCharacterSelect(id) => {
|
||||
if let Some(special_key) = self.special_character_dialog {
|
||||
let options = self.xkb.options.as_deref().unwrap_or("");
|
||||
let prefix = special_key.prefix();
|
||||
let new_options = options
|
||||
.split(',')
|
||||
.filter(|x| !x.starts_with(prefix))
|
||||
.chain(id)
|
||||
.join(",");
|
||||
self.xkb.options = Some(new_options).filter(|x| !x.is_empty());
|
||||
if let Err(err) = self.config.set("xkb_config", &self.xkb) {
|
||||
error!(?err, "Failed to set config 'xkb_config'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Message::TapToClick(enabled) => {
|
||||
self.update_input(true, |conf| {
|
||||
conf.tap_config
|
||||
.get_or_insert(TapConfig {
|
||||
enabled: true,
|
||||
button_map: Some(TapButtonMap::LeftRightMiddle),
|
||||
drag: true,
|
||||
drag_lock: false,
|
||||
})
|
||||
.enabled = enabled;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cosmic::Command::none()
|
||||
}
|
||||
}
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn info(&self) -> page::Info {
|
||||
// XXX icon?
|
||||
page::Info::new("input-devices", "input-keyboard-symbolic")
|
||||
.title(fl!("input-devices"))
|
||||
.description(fl!("input-devices", "desc"))
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {
|
||||
fn sub_pages(page: page::Insert<crate::pages::Message>) -> page::Insert<crate::pages::Message> {
|
||||
page.sub_page::<keyboard::Page>()
|
||||
.sub_page::<mouse::Page>()
|
||||
.sub_page::<touchpad::Page>()
|
||||
}
|
||||
}
|
||||
154
cosmic-settings/src/pages/input/mouse.rs
Normal file
154
cosmic-settings/src/pages/input/mouse.rs
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
use cosmic::iced::Alignment;
|
||||
use cosmic::widget::{self, row, settings, text};
|
||||
use cosmic::{Apply, Element};
|
||||
use cosmic_comp_config::input::AccelProfile;
|
||||
use cosmic_settings_page::Section;
|
||||
use cosmic_settings_page::{self as page, section};
|
||||
use slotmap::SlotMap;
|
||||
|
||||
use super::Message;
|
||||
|
||||
crate::cache_dynamic_lazy! {
|
||||
static MOUSE_ACCELERATION: String = fl!("mouse", "acceleration");
|
||||
static MOUSE_SPEED: String = fl!("mouse", "speed");
|
||||
}
|
||||
|
||||
pub fn default_primary_button() -> cosmic::widget::segmented_button::SingleSelectModel {
|
||||
let mut model = cosmic::widget::segmented_button::SingleSelectModel::builder()
|
||||
.insert(|b| b.text(fl!("primary-button", "left")))
|
||||
.insert(|b| b.text(fl!("primary-button", "right")))
|
||||
.build();
|
||||
model.activate_position(0);
|
||||
model
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![sections.insert(mouse()), sections.insert(scrolling())])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("mouse", "input-mouse-symbolic")
|
||||
.title(fl!("mouse"))
|
||||
.description(fl!("mouse", "desc"))
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
||||
fn mouse() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.descriptions(vec![
|
||||
super::PRIMARY_BUTTON.as_str().into(),
|
||||
MOUSE_SPEED.as_str().into(),
|
||||
MOUSE_ACCELERATION.as_str().into(),
|
||||
super::ACCELERATION_DESC.as_str().into(),
|
||||
])
|
||||
.view::<Page>(|binder, _page, section| {
|
||||
let input = binder.page::<super::Page>().expect("input page not found");
|
||||
let theme = cosmic::theme::active();
|
||||
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(
|
||||
&*super::PRIMARY_BUTTON,
|
||||
cosmic::widget::segmented_control::horizontal(&input.primary_button)
|
||||
.minimum_button_width(0)
|
||||
.on_activate(|x| Message::PrimaryButtonSelected(x, false)),
|
||||
))
|
||||
.add(settings::item::builder(&*MOUSE_SPEED).control({
|
||||
let value = (input
|
||||
.input_default
|
||||
.acceleration
|
||||
.as_ref()
|
||||
.map_or(0.0, |x| x.speed)
|
||||
+ 1.0)
|
||||
* 50.0;
|
||||
|
||||
let slider = widget::slider(10.0..=80.0, value, |value| {
|
||||
Message::SetMouseSpeed((value / 50.0) - 1.0, false)
|
||||
})
|
||||
.width(250.0)
|
||||
.breakpoints(&[45.0]);
|
||||
|
||||
row::with_capacity(2)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(theme.cosmic().space_s())
|
||||
.push(text(format!("{:.0}", value.round())))
|
||||
.push(slider)
|
||||
}))
|
||||
.add(
|
||||
settings::item::builder(&*MOUSE_ACCELERATION)
|
||||
.description(&*super::ACCELERATION_DESC)
|
||||
.toggler(
|
||||
input
|
||||
.input_default
|
||||
.acceleration
|
||||
.as_ref()
|
||||
.map_or(true, |x| x.profile == Some(AccelProfile::Adaptive)),
|
||||
|x| Message::SetAcceleration(x, false),
|
||||
),
|
||||
)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Input)
|
||||
})
|
||||
}
|
||||
|
||||
fn scrolling() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("scrolling"))
|
||||
.descriptions(vec![
|
||||
super::SCROLLING_SPEED.as_str().into(),
|
||||
super::SCROLLING_NATURAL.as_str().into(),
|
||||
super::SCROLLING_NATURAL_DESC.as_str().into(),
|
||||
])
|
||||
.view::<Page>(|binder, _page, section| {
|
||||
let input = binder.page::<super::Page>().expect("input page not found");
|
||||
let theme = cosmic::theme::active();
|
||||
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(&*super::SCROLLING_SPEED, {
|
||||
let value = input
|
||||
.input_default
|
||||
.scroll_config
|
||||
.as_ref()
|
||||
.and_then(|x| x.scroll_factor)
|
||||
.unwrap_or(1.)
|
||||
.log(2.)
|
||||
* 10.0
|
||||
+ 50.0;
|
||||
|
||||
let slider = widget::slider(1.0..=100.0, value, |value| {
|
||||
Message::SetScrollFactor(2f64.powf((value - 50.0) / 10.0), false)
|
||||
})
|
||||
.width(250.0)
|
||||
.breakpoints(&[50.0]);
|
||||
|
||||
row::with_capacity(2)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(theme.cosmic().space_s())
|
||||
.push(text(format!("{:.0}", value.round())))
|
||||
.push(slider)
|
||||
}))
|
||||
.add(
|
||||
settings::item::builder(&*super::SCROLLING_NATURAL)
|
||||
.description(&*super::SCROLLING_NATURAL_DESC)
|
||||
.toggler(
|
||||
input
|
||||
.input_default
|
||||
.scroll_config
|
||||
.as_ref()
|
||||
.and_then(|x| x.natural_scroll)
|
||||
.unwrap_or(false),
|
||||
|x| Message::SetNaturalScroll(x, false),
|
||||
),
|
||||
)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Input)
|
||||
})
|
||||
}
|
||||
301
cosmic-settings/src/pages/input/touchpad.rs
Normal file
301
cosmic-settings/src/pages/input/touchpad.rs
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
use cosmic::iced::Alignment;
|
||||
use cosmic::widget::{self, row, settings, text};
|
||||
use cosmic::{Apply, Element};
|
||||
use cosmic_comp_config::input::{AccelProfile, ClickMethod, ScrollMethod};
|
||||
use cosmic_settings_page::Section;
|
||||
use cosmic_settings_page::{self as page, section};
|
||||
use slotmap::SlotMap;
|
||||
|
||||
use super::Message;
|
||||
|
||||
crate::cache_dynamic_lazy! {
|
||||
static CLICK_BEHAVIOR_CLICK_FINGER: String = fl!("click-behavior", "click-finger");
|
||||
static CLICK_BEHAVIOR_BUTTON_AREAS: String = fl!("click-behavior", "button-areas");
|
||||
|
||||
static TAP_TO_CLICK: String = fl!("tap-to-click");
|
||||
static TAP_TO_CLICK_DESC: String = fl!("tap-to-click", "desc");
|
||||
|
||||
static TOUCHPAD_ACCELERAION: String = fl!("touchpad", "acceleration");
|
||||
static TOUCHPAD_SPEED: String = fl!("touchpad", "speed");
|
||||
|
||||
static OPEN_APPLICATION_LIBRARY: String = fl!("open-application-library");
|
||||
static OPEN_WORKSPACES_VIEW: String = fl!("open-workspaces-view");
|
||||
static SWIPING_FOUR_FINGER_DOWN: String = fl!("swiping", "four-finger-down");
|
||||
static SWIPING_FOUR_FINGER_LEFT: String = fl!("swiping", "four-finger-left");
|
||||
static SWIPING_FOUR_FINGER_RIGHT: String = fl!("swiping", "four-finger-right");
|
||||
static SWIPING_FOUR_FINGER_UP: String = fl!("swiping", "four-finger-up");
|
||||
static SWIPING_THREE_FINGER_ANY: String = fl!("swiping", "three-finger-any");
|
||||
static SWITCH_BETWEEN_WINDOWS: String = fl!("switch-between-windows");
|
||||
static SWITCH_TO_NEXT_WORKSPACE: String = fl!("switch-to-next-workspace");
|
||||
static SWITCH_TO_PREV_WORKSPACE: String = fl!("switch-to-prev-workspace");
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![
|
||||
sections.insert(touchpad()),
|
||||
sections.insert(click_behavior()),
|
||||
sections.insert(scrolling()),
|
||||
sections.insert(swiping()),
|
||||
])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("touchpad", "input-touchpad-symbolic")
|
||||
.title(fl!("touchpad"))
|
||||
.description(fl!("touchpad", "desc"))
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
||||
fn touchpad() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.descriptions(vec![
|
||||
super::PRIMARY_BUTTON.as_str().into(),
|
||||
TOUCHPAD_SPEED.as_str().into(),
|
||||
TOUCHPAD_ACCELERAION.as_str().into(),
|
||||
super::ACCELERATION_DESC.as_str().into(),
|
||||
super::DISABLE_WHILE_TYPING.as_str().into(),
|
||||
])
|
||||
.view::<Page>(|binder, _page, section| {
|
||||
let input = binder.page::<super::Page>().expect("input page not found");
|
||||
let theme = cosmic::theme::active();
|
||||
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(
|
||||
&*super::PRIMARY_BUTTON,
|
||||
cosmic::widget::segmented_control::horizontal(&input.touchpad_primary_button)
|
||||
.minimum_button_width(0)
|
||||
.on_activate(|x| Message::PrimaryButtonSelected(x, true)),
|
||||
))
|
||||
.add(settings::item::builder(&*TOUCHPAD_SPEED).control({
|
||||
let value = (input
|
||||
.input_touchpad
|
||||
.acceleration
|
||||
.as_ref()
|
||||
.map_or(0.0, |x| x.speed)
|
||||
+ 1.0)
|
||||
* 50.0;
|
||||
|
||||
let slider = widget::slider(10.0..=80.0, value, |value| {
|
||||
Message::SetMouseSpeed((value / 50.0) - 1.0, true)
|
||||
})
|
||||
.width(250.0)
|
||||
.breakpoints(&[45.0]);
|
||||
|
||||
row::with_capacity(2)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(theme.cosmic().space_s())
|
||||
.push(text(format!("{:.0}", value.round())))
|
||||
.push(slider)
|
||||
}))
|
||||
.add(
|
||||
settings::item::builder(&*TOUCHPAD_ACCELERAION)
|
||||
.description(&*super::ACCELERATION_DESC)
|
||||
.toggler(
|
||||
input
|
||||
.input_touchpad
|
||||
.acceleration
|
||||
.as_ref()
|
||||
.map_or(true, |x| x.profile == Some(AccelProfile::Adaptive)),
|
||||
|x| Message::SetAcceleration(x, true),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
settings::item::builder(&*super::DISABLE_WHILE_TYPING).toggler(
|
||||
input.input_touchpad.disable_while_typing.unwrap_or(false),
|
||||
|enabled| Message::DisableWhileTyping(enabled, true),
|
||||
),
|
||||
)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Input)
|
||||
})
|
||||
}
|
||||
|
||||
fn click_behavior() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("click-behavior"))
|
||||
.descriptions(vec![
|
||||
CLICK_BEHAVIOR_CLICK_FINGER.as_str().into(),
|
||||
CLICK_BEHAVIOR_BUTTON_AREAS.as_str().into(),
|
||||
TAP_TO_CLICK.as_str().into(),
|
||||
TAP_TO_CLICK_DESC.as_str().into(),
|
||||
])
|
||||
.view::<Page>(|binder, _page, section| {
|
||||
let page = binder
|
||||
.page::<super::Page>()
|
||||
.expect("input devices page not found");
|
||||
|
||||
settings::view_section(&*section.title)
|
||||
// Secondary click via two fingers, and middle-click via three fingers
|
||||
.add(
|
||||
settings::item::builder(&*CLICK_BEHAVIOR_CLICK_FINGER).toggler(
|
||||
page.input_touchpad
|
||||
.click_method
|
||||
.as_ref()
|
||||
.map_or(false, |x| matches!(x, ClickMethod::Clickfinger)),
|
||||
|enabled| {
|
||||
Message::SetSecondaryClickBehavior(
|
||||
enabled.then_some(ClickMethod::Clickfinger),
|
||||
true,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
// Secondary and middle-click via button areas.
|
||||
.add(
|
||||
settings::item::builder(&*CLICK_BEHAVIOR_BUTTON_AREAS).toggler(
|
||||
page.input_touchpad
|
||||
.click_method
|
||||
.as_ref()
|
||||
.map_or(false, |x| matches!(x, ClickMethod::ButtonAreas)),
|
||||
|enabled| {
|
||||
Message::SetSecondaryClickBehavior(
|
||||
enabled.then_some(ClickMethod::ButtonAreas),
|
||||
true,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
.add(
|
||||
settings::item::builder(&*TAP_TO_CLICK).toggler(
|
||||
page.input_touchpad
|
||||
.tap_config
|
||||
.as_ref()
|
||||
.map_or(false, |x| x.enabled),
|
||||
Message::TapToClick,
|
||||
),
|
||||
)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Input)
|
||||
})
|
||||
}
|
||||
|
||||
fn scrolling() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("scrolling"))
|
||||
.descriptions(vec![
|
||||
super::SCROLLING_TWO_FINGER.as_str().into(),
|
||||
super::SCROLLING_EDGE.as_str().into(),
|
||||
super::SCROLLING_SPEED.as_str().into(),
|
||||
super::SCROLLING_NATURAL.as_str().into(),
|
||||
super::SCROLLING_NATURAL_DESC.as_str().into(),
|
||||
])
|
||||
.view::<Page>(|binder, _page, section| {
|
||||
let page = binder
|
||||
.page::<super::Page>()
|
||||
.expect("input devices page not found");
|
||||
let theme = cosmic::theme::active();
|
||||
|
||||
settings::view_section(§ion.title)
|
||||
// Two-finger scrolling toggle
|
||||
.add(
|
||||
settings::item::builder(&*super::SCROLLING_TWO_FINGER).toggler(
|
||||
page.input_touchpad
|
||||
.scroll_config
|
||||
.as_ref()
|
||||
.map_or(false, |x| matches!(x.method, Some(ScrollMethod::TwoFinger))),
|
||||
|enabled| {
|
||||
Message::SetScrollMethod(
|
||||
enabled.then_some(ScrollMethod::TwoFinger),
|
||||
true,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
// Edge scrolling toggle
|
||||
.add(
|
||||
settings::item::builder(&*super::SCROLLING_EDGE).toggler(
|
||||
page.input_touchpad
|
||||
.scroll_config
|
||||
.as_ref()
|
||||
.map_or(false, |x| matches!(x.method, Some(ScrollMethod::Edge))),
|
||||
|enabled| {
|
||||
Message::SetScrollMethod(enabled.then_some(ScrollMethod::Edge), true)
|
||||
},
|
||||
),
|
||||
)
|
||||
// Scroll speed slider
|
||||
.add(settings::item(&*super::SCROLLING_SPEED, {
|
||||
let value = page
|
||||
.input_touchpad
|
||||
.scroll_config
|
||||
.as_ref()
|
||||
.and_then(|x| x.scroll_factor)
|
||||
.unwrap_or(1.)
|
||||
.log(2.)
|
||||
* 10.0
|
||||
+ 50.0;
|
||||
|
||||
let slider = widget::slider(1.0..=100.0, value, |value| {
|
||||
Message::SetScrollFactor(2f64.powf((value - 50.0) / 10.0), true)
|
||||
})
|
||||
.width(250.0)
|
||||
.breakpoints(&[50.0]);
|
||||
|
||||
row::with_capacity(2)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(theme.cosmic().space_s())
|
||||
.push(text(format!("{:.0}", value.round())))
|
||||
.push(slider)
|
||||
}))
|
||||
// Natural scrolling toggle
|
||||
.add(
|
||||
settings::item::builder(&*super::SCROLLING_NATURAL)
|
||||
.description(&*super::SCROLLING_NATURAL_DESC)
|
||||
.toggler(
|
||||
page.input_touchpad
|
||||
.scroll_config
|
||||
.as_ref()
|
||||
.map_or(false, |conf| conf.natural_scroll.unwrap_or(false)),
|
||||
|enabled| Message::SetNaturalScroll(enabled, true),
|
||||
),
|
||||
)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Input)
|
||||
})
|
||||
}
|
||||
|
||||
fn swiping() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("swiping"))
|
||||
.descriptions(vec![
|
||||
SWIPING_FOUR_FINGER_DOWN.as_str().into(),
|
||||
SWIPING_FOUR_FINGER_LEFT.as_str().into(),
|
||||
SWIPING_FOUR_FINGER_RIGHT.as_str().into(),
|
||||
SWIPING_FOUR_FINGER_UP.as_str().into(),
|
||||
SWIPING_THREE_FINGER_ANY.as_str().into(),
|
||||
])
|
||||
.view::<Page>(|_binder, _page, section| {
|
||||
settings::view_section(&*section.title)
|
||||
.add(
|
||||
settings::item::builder(&*SWIPING_THREE_FINGER_ANY)
|
||||
.control(text(&*SWITCH_BETWEEN_WINDOWS)),
|
||||
)
|
||||
.add(
|
||||
settings::item::builder(&*SWIPING_FOUR_FINGER_UP)
|
||||
.control(text(&*SWITCH_TO_PREV_WORKSPACE)),
|
||||
)
|
||||
.add(
|
||||
settings::item::builder(&*SWIPING_FOUR_FINGER_DOWN)
|
||||
.control(text(&*SWITCH_TO_NEXT_WORKSPACE)),
|
||||
)
|
||||
.add(
|
||||
settings::item::builder(&*SWIPING_FOUR_FINGER_LEFT)
|
||||
.control(text(&*OPEN_WORKSPACES_VIEW)),
|
||||
)
|
||||
.add(
|
||||
settings::item::builder(&*SWIPING_FOUR_FINGER_RIGHT)
|
||||
.control(text(&*OPEN_APPLICATION_LIBRARY)),
|
||||
)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Input)
|
||||
})
|
||||
}
|
||||
36
cosmic-settings/src/pages/mod.rs
Normal file
36
cosmic-settings/src/pages/mod.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic_settings_page::Entity;
|
||||
|
||||
pub mod desktop;
|
||||
pub mod display;
|
||||
pub mod input;
|
||||
pub mod networking;
|
||||
pub mod sound;
|
||||
pub mod system;
|
||||
pub mod time;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
About(system::about::Message),
|
||||
Appearance(desktop::appearance::Message),
|
||||
DateAndTime(time::date::Message),
|
||||
Desktop(desktop::Message),
|
||||
DesktopWallpaper(desktop::wallpaper::Message),
|
||||
DesktopWorkspaces(desktop::workspaces::Message),
|
||||
Displays(display::Message),
|
||||
Dock(desktop::dock::Message),
|
||||
DockApplet(desktop::dock::applets::Message),
|
||||
External { id: String, message: Vec<u8> },
|
||||
Input(input::Message),
|
||||
Page(Entity),
|
||||
Panel(desktop::panel::Message),
|
||||
PanelApplet(desktop::panel::applets_inner::Message),
|
||||
}
|
||||
|
||||
impl From<Message> for crate::Message {
|
||||
fn from(message: Message) -> Self {
|
||||
crate::Message::PageMessage(message)
|
||||
}
|
||||
}
|
||||
10
cosmic-settings/src/pages/networking/accounts.rs
Normal file
10
cosmic-settings/src/pages/networking/accounts.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic_settings_page as page;
|
||||
|
||||
pub fn info() -> page::Info {
|
||||
page::Info::new("online-accounts", "goa-panel-symbolic")
|
||||
.title(fl!("online-accounts"))
|
||||
.description(fl!("online-accounts", "desc"))
|
||||
}
|
||||
5
cosmic-settings/src/pages/networking/mod.rs
Normal file
5
cosmic-settings/src/pages/networking/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod accounts;
|
||||
pub mod wired;
|
||||
10
cosmic-settings/src/pages/networking/wired.rs
Normal file
10
cosmic-settings/src/pages/networking/wired.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic_settings_page as page;
|
||||
|
||||
pub fn info() -> page::Info {
|
||||
page::Info::new("wired", "network-workgroup-symbolic")
|
||||
.title(fl!("wired"))
|
||||
.description(fl!("wired", "desc"))
|
||||
}
|
||||
111
cosmic-settings/src/pages/sound.rs
Normal file
111
cosmic-settings/src/pages/sound.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::widget::{settings, text};
|
||||
use cosmic_settings_page::{self as page, section, Section};
|
||||
use slotmap::SlotMap;
|
||||
|
||||
// crate::cache_dynamic_lazy! {
|
||||
// pub static SOUND_ALERTS_VOLUME: String = fl!("sound-alerts", "volume");
|
||||
// pub static SOUND_ALERTS_SOUND: String = fl!("sound-alerts", "sound");
|
||||
|
||||
// pub static SOUND_APPLICATIONS_DESC: String = fl!("sound-applications", "desc");
|
||||
|
||||
// pub static SOUND_INPUT_VOLUME: String = fl!("sound-input", "volume");
|
||||
// pub static SOUND_INPUT_DEVICE: String = fl!("sound-input", "device");
|
||||
// pub static SOUND_INPUT_LEVEL: String = fl!("sound-input", "level");
|
||||
|
||||
// pub static SOUND_OUTPUT_VOLUME: String = fl!("sound-output", "volume");
|
||||
// pub static SOUND_OUTPUT_DEVICE: String = fl!("sound-output", "device");
|
||||
// pub static SOUND_OUTPUT_LEVEL: String = fl!("sound-output", "level");
|
||||
// pub static SOUND_OUTPUT_CONFIG: String = fl!("sound-output", "config");
|
||||
// pub static SOUND_OUTPUT_BALANCE: String = fl!("sound-output", "balance");
|
||||
// }
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![
|
||||
sections.insert(output()),
|
||||
sections.insert(input()),
|
||||
sections.insert(alerts()),
|
||||
sections.insert(applications()),
|
||||
])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("sound", "multimedia-volume-control-symbolic")
|
||||
.title(fl!("sound"))
|
||||
.description(fl!("sound", "desc"))
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
||||
fn alerts() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("sound-alerts"))
|
||||
.descriptions(vec![
|
||||
fl!("sound-alerts", "volume").into(),
|
||||
fl!("sound-alerts", "sound").into(),
|
||||
])
|
||||
.view::<Page>(|_binder, _page, section| {
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(&*section.descriptions[0], text("TODO")))
|
||||
.add(settings::item(&*section.descriptions[1], text("TODO")))
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
fn applications() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("sound-applications"))
|
||||
.descriptions(vec![fl!("sound-applications", "desc").into()])
|
||||
.view::<Page>(|_binder, _page, section| {
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(&*section.descriptions[0], text("TODO")))
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
fn input() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("sound-input"))
|
||||
.descriptions(vec![
|
||||
fl!("sound-input", "volume").into(),
|
||||
fl!("sound-input", "device").into(),
|
||||
fl!("sound-input", "level").into(),
|
||||
])
|
||||
.view::<Page>(|_binder, _page, section| {
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(&*section.descriptions[0], text("TODO")))
|
||||
.add(settings::item(&*section.descriptions[1], text("TODO")))
|
||||
.add(settings::item(&*section.descriptions[2], text("TODO")))
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
fn output() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("sound-output"))
|
||||
.descriptions(vec![
|
||||
fl!("sound-output", "volume").into(),
|
||||
fl!("sound-output", "device").into(),
|
||||
fl!("sound-output", "level").into(),
|
||||
fl!("sound-output", "config").into(),
|
||||
fl!("sound-output", "balance").into(),
|
||||
])
|
||||
.view::<Page>(|_binder, _page, section| {
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(&*section.descriptions[0], text("TODO")))
|
||||
.add(settings::item(&*section.descriptions[1], text("TODO")))
|
||||
.add(settings::item(&*section.descriptions[2], text("TODO")))
|
||||
.add(settings::item(&*section.descriptions[3], text("TODO")))
|
||||
.into()
|
||||
})
|
||||
}
|
||||
140
cosmic-settings/src/pages/system/about.rs
Normal file
140
cosmic-settings/src/pages/system/about.rs
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic_settings_page::{self as page, section, Section};
|
||||
|
||||
use cosmic::widget::{list_column, settings, text};
|
||||
use cosmic::{command, Command};
|
||||
use cosmic_settings_system::about::Info;
|
||||
use slotmap::SlotMap;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
Info(Box<Info>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Page {
|
||||
info: Info,
|
||||
// support_page: page::Entity,
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![
|
||||
sections.insert(device()),
|
||||
sections.insert(hardware()),
|
||||
sections.insert(os()),
|
||||
sections.insert(related()),
|
||||
])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("about", "help-about-symbolic")
|
||||
.title(fl!("about"))
|
||||
.description(fl!("about", "desc"))
|
||||
}
|
||||
|
||||
fn reload(&mut self, _page: page::Entity) -> Command<crate::pages::Message> {
|
||||
command::future(async move {
|
||||
crate::pages::Message::About(Message::Info(Box::new(Info::load())))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Page {
|
||||
pub fn update(&mut self, message: Message) {
|
||||
match message {
|
||||
Message::Info(info) => self.info = *info,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn device() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.descriptions(vec![
|
||||
fl!("about-device").into(),
|
||||
fl!("about-device", "desc").into(),
|
||||
])
|
||||
.view::<Page>(|_binder, page, section| {
|
||||
let desc = §ion.descriptions;
|
||||
let device_name = settings::item::builder(&*desc[0])
|
||||
.description(&*desc[1])
|
||||
.control(text(&page.info.device_name));
|
||||
|
||||
list_column().add(device_name).into()
|
||||
})
|
||||
}
|
||||
|
||||
fn hardware() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("about-hardware"))
|
||||
.descriptions(vec![
|
||||
fl!("about-hardware", "model").into(),
|
||||
fl!("about-hardware", "memory").into(),
|
||||
fl!("about-hardware", "processor").into(),
|
||||
fl!("about-hardware", "graphics").into(),
|
||||
fl!("about-hardware", "disk-capacity").into(),
|
||||
])
|
||||
.view::<Page>(|_binder, page, section| {
|
||||
let desc = §ion.descriptions;
|
||||
|
||||
let mut sections = settings::view_section(§ion.title)
|
||||
.add(settings::item(&*desc[0], text(&page.info.hardware_model)))
|
||||
.add(settings::item(&*desc[1], text(&page.info.memory)))
|
||||
.add(settings::item(&*desc[2], text(&page.info.processor)));
|
||||
|
||||
for card in &page.info.graphics {
|
||||
sections = sections.add(settings::item(&*desc[3], text(card.as_str())));
|
||||
}
|
||||
|
||||
sections
|
||||
.add(settings::item(&*desc[4], text(&page.info.disk_capacity)))
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
fn os() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("about-os"))
|
||||
.descriptions(vec![
|
||||
fl!("about-os", "os").into(),
|
||||
fl!("about-os", "os-architecture").into(),
|
||||
fl!("about-os", "desktop-environment").into(),
|
||||
fl!("about-os", "windowing-system").into(),
|
||||
])
|
||||
.view::<Page>(|_binder, page, section| {
|
||||
let desc = §ion.descriptions;
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(&*desc[0], text(&page.info.operating_system)))
|
||||
.add(settings::item(&*desc[1], text(&page.info.os_architecture)))
|
||||
.add(settings::item(
|
||||
&*desc[2],
|
||||
text(&page.info.desktop_environment),
|
||||
))
|
||||
.add(settings::item(&*desc[3], text(&page.info.windowing_system)))
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
fn related() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("about-related"))
|
||||
.descriptions(vec![fl!("about-related", "support").into()])
|
||||
.view::<Page>(|_binder, _page, section| {
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(&*section.descriptions[0], text("TODO")))
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
// fn page(app: &crate::SettingsApp) -> &Page {
|
||||
// app.pages
|
||||
// .resource::<Page>()
|
||||
// .expect("missing system->about page")
|
||||
// }
|
||||
26
cosmic-settings/src/pages/system/firmware.rs
Normal file
26
cosmic-settings/src/pages/system/firmware.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic_settings_page::Section;
|
||||
use cosmic_settings_page::{self as page, section};
|
||||
use slotmap::SlotMap;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![sections.insert(Section::default())])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("firmware", "firmware-manager-symbolic")
|
||||
.title(fl!("firmware"))
|
||||
.description(fl!("firmware", "desc"))
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
25
cosmic-settings/src/pages/system/mod.rs
Normal file
25
cosmic-settings/src/pages/system/mod.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod about;
|
||||
pub mod firmware;
|
||||
pub mod users;
|
||||
|
||||
use cosmic_settings_page as page;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("system", "system-users-symbolic").title(fl!("system"))
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {
|
||||
fn sub_pages(page: page::Insert<crate::pages::Message>) -> page::Insert<crate::pages::Message> {
|
||||
page.sub_page::<users::Page>()
|
||||
.sub_page::<about::Page>()
|
||||
.sub_page::<firmware::Page>()
|
||||
}
|
||||
}
|
||||
26
cosmic-settings/src/pages/system/users.rs
Normal file
26
cosmic-settings/src/pages/system/users.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic_settings_page::Section;
|
||||
use cosmic_settings_page::{self as page, section};
|
||||
use slotmap::SlotMap;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![sections.insert(Section::default())])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("users", "system-users-symbolic")
|
||||
.title(fl!("users"))
|
||||
.description(fl!("users", "desc"))
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
131
cosmic-settings/src/pages/time/date.rs
Normal file
131
cosmic-settings/src/pages/time/date.rs
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::{
|
||||
iced::{widget::horizontal_space, Length},
|
||||
widget::settings,
|
||||
Apply,
|
||||
};
|
||||
use cosmic_settings_page::Section;
|
||||
use cosmic_settings_page::{self as page, section};
|
||||
// use icu::calendar::{DateTime, Gregorian};
|
||||
|
||||
use slotmap::SlotMap;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Page {
|
||||
auto: bool,
|
||||
auto_timezone: bool,
|
||||
military_time: bool,
|
||||
// info: Option<cosmic_settings_time::Info>,
|
||||
}
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![
|
||||
sections.insert(date()),
|
||||
sections.insert(timezone()),
|
||||
sections.insert(format()),
|
||||
])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("time-date", "preferences-system-time-symbolic")
|
||||
.title(fl!("time-date"))
|
||||
.description(fl!("time-date", "desc"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Page {
|
||||
pub fn update(&mut self, message: Message) {
|
||||
match message {
|
||||
Message::Automatic(enable) => self.auto = enable,
|
||||
Message::AutomaticTimezone(enable) => self.auto_timezone = enable,
|
||||
Message::MilitaryTime(enable) => self.military_time = enable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Message {
|
||||
Automatic(bool),
|
||||
AutomaticTimezone(bool),
|
||||
MilitaryTime(bool),
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
||||
fn date() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("time-date"))
|
||||
.descriptions(vec![
|
||||
fl!("time-date", "auto").into(),
|
||||
fl!("time-date").into(),
|
||||
])
|
||||
.view::<Page>(|_binder, page, section| {
|
||||
settings::view_section(§ion.title)
|
||||
.add(
|
||||
settings::item::builder(&*section.descriptions[0])
|
||||
.toggler(page.auto, Message::Automatic),
|
||||
)
|
||||
.add(settings::item(
|
||||
&*section.descriptions[1],
|
||||
horizontal_space(Length::Fill),
|
||||
))
|
||||
.apply(cosmic::Element::from)
|
||||
.map(crate::pages::Message::DateAndTime)
|
||||
})
|
||||
}
|
||||
|
||||
fn format() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("time-format"))
|
||||
.descriptions(vec![
|
||||
fl!("time-format", "twenty-four").into(),
|
||||
fl!("time-format", "first").into(),
|
||||
])
|
||||
.view::<Page>(|_binder, page, section| {
|
||||
settings::view_section(§ion.title)
|
||||
// 24-hour toggle
|
||||
.add(
|
||||
settings::item::builder(&*section.descriptions[0])
|
||||
.toggler(page.military_time, Message::MilitaryTime),
|
||||
)
|
||||
// First day of week
|
||||
.add(settings::item(
|
||||
&*section.descriptions[1],
|
||||
horizontal_space(Length::Fill),
|
||||
))
|
||||
.apply(cosmic::Element::from)
|
||||
.map(crate::pages::Message::DateAndTime)
|
||||
})
|
||||
}
|
||||
|
||||
fn timezone() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("time-zone"))
|
||||
.descriptions(vec![
|
||||
fl!("time-zone", "auto").into(),
|
||||
fl!("time-zone", "auto-info").into(),
|
||||
fl!("time-zone").into(),
|
||||
])
|
||||
.view::<Page>(|_binder, page, section| {
|
||||
settings::view_section(§ion.title)
|
||||
// Automatic timezone toggle
|
||||
.add(
|
||||
settings::item::builder(&*section.descriptions[0])
|
||||
.description(&*section.descriptions[1])
|
||||
.toggler(page.auto_timezone, Message::AutomaticTimezone),
|
||||
)
|
||||
// Time zone select
|
||||
.add(
|
||||
settings::item::builder(&*section.descriptions[2])
|
||||
.control(horizontal_space(Length::Fill)),
|
||||
)
|
||||
.apply(cosmic::Element::from)
|
||||
.map(crate::pages::Message::DateAndTime)
|
||||
})
|
||||
}
|
||||
24
cosmic-settings/src/pages/time/mod.rs
Normal file
24
cosmic-settings/src/pages/time/mod.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic_settings_page as page;
|
||||
|
||||
pub mod date;
|
||||
pub mod region;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("time", "preferences-system-time-symbolic")
|
||||
.title(fl!("time"))
|
||||
.description(fl!("time", "desc"))
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {
|
||||
fn sub_pages(page: page::Insert<crate::pages::Message>) -> page::Insert<crate::pages::Message> {
|
||||
page.sub_page::<date::Page>().sub_page::<region::Page>()
|
||||
}
|
||||
}
|
||||
26
cosmic-settings/src/pages/time/region.rs
Normal file
26
cosmic-settings/src/pages/time/region.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic_settings_page::Section;
|
||||
use cosmic_settings_page::{self as page, section};
|
||||
use slotmap::SlotMap;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![sections.insert(Section::default())])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("time-region", "preferences-desktop-locale-symbolic")
|
||||
.title(fl!("time-region"))
|
||||
.description(fl!("time-region", "desc"))
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
96
cosmic-settings/src/subscription/desktop_files.rs
Normal file
96
cosmic-settings/src/subscription/desktop_files.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
use cosmic::{
|
||||
iced::subscription,
|
||||
iced_futures::futures::{self, SinkExt},
|
||||
};
|
||||
use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use std::fmt::Debug;
|
||||
use std::hash::Hash;
|
||||
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum State {
|
||||
Ready,
|
||||
Waiting {
|
||||
watcher: RecommendedWatcher,
|
||||
rx: UnboundedReceiver<notify::Result<Event>>,
|
||||
},
|
||||
Finished,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum DesktopFileEvent {
|
||||
Changed,
|
||||
}
|
||||
|
||||
pub fn desktop_files<I: 'static + Hash + Copy + Send + Sync + Debug>(
|
||||
id: I,
|
||||
) -> cosmic::iced::Subscription<DesktopFileEvent> {
|
||||
subscription::channel(id, 50, move |mut output| async move {
|
||||
let mut state = State::Ready;
|
||||
|
||||
loop {
|
||||
state = start_watching(state, &mut output).await;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn start_watching(
|
||||
state: State,
|
||||
output: &mut futures::channel::mpsc::Sender<DesktopFileEvent>,
|
||||
) -> State {
|
||||
match state {
|
||||
State::Ready => {
|
||||
let paths = freedesktop_desktop_entry::default_paths();
|
||||
// TODO log errors
|
||||
if let Ok((mut watcher, rx)) = async_watcher() {
|
||||
for path in paths {
|
||||
let _ = watcher.watch(path.as_ref(), RecursiveMode::Recursive);
|
||||
}
|
||||
|
||||
_ = output.send(DesktopFileEvent::Changed).await;
|
||||
State::Waiting { watcher, rx }
|
||||
} else {
|
||||
State::Finished
|
||||
}
|
||||
}
|
||||
State::Waiting { watcher, rx } => {
|
||||
if let Some(rx) = async_watch(rx).await {
|
||||
_ = output.send(DesktopFileEvent::Changed).await;
|
||||
State::Waiting { watcher, rx }
|
||||
} else {
|
||||
State::Finished
|
||||
}
|
||||
}
|
||||
State::Finished => cosmic::iced::futures::future::pending().await,
|
||||
}
|
||||
}
|
||||
|
||||
fn async_watcher() -> notify::Result<(RecommendedWatcher, UnboundedReceiver<notify::Result<Event>>)>
|
||||
{
|
||||
let (tx, rx) = unbounded_channel();
|
||||
|
||||
// Automatically select the best implementation for your platform.
|
||||
// You can also access each implementation directly e.g. INotifyWatcher.
|
||||
let watcher = RecommendedWatcher::new(
|
||||
move |res| {
|
||||
_ = tx.send(res);
|
||||
},
|
||||
Config::default(),
|
||||
)?;
|
||||
|
||||
Ok((watcher, rx))
|
||||
}
|
||||
|
||||
async fn async_watch(
|
||||
mut rx: UnboundedReceiver<notify::Result<Event>>,
|
||||
) -> Option<UnboundedReceiver<notify::Result<Event>>> {
|
||||
// TODO log errors
|
||||
if let Some(res) = rx.recv().await {
|
||||
match res {
|
||||
Ok(_) => return Some(rx),
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
2
cosmic-settings/src/subscription/mod.rs
Normal file
2
cosmic-settings/src/subscription/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
mod desktop_files;
|
||||
pub use desktop_files::*;
|
||||
40
cosmic-settings/src/theme.rs
Normal file
40
cosmic-settings/src/theme.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::{iced_core::Border, theme};
|
||||
|
||||
#[must_use]
|
||||
pub fn display_container_frame() -> cosmic::theme::Container {
|
||||
theme::Container::custom(|theme| {
|
||||
let cosmic = theme.cosmic();
|
||||
cosmic::widget::container::Appearance {
|
||||
icon_color: None,
|
||||
text_color: None,
|
||||
background: Some(cosmic::iced::Background::Color(cosmic::iced::Color::WHITE)),
|
||||
border: Border {
|
||||
color: cosmic::iced::Color::WHITE,
|
||||
radius: cosmic.corner_radii.radius_xs.into(),
|
||||
width: 3.0,
|
||||
},
|
||||
shadow: Default::default(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn display_container_screen() -> cosmic::theme::Container {
|
||||
theme::Container::custom(|theme| {
|
||||
let cosmic = theme.cosmic();
|
||||
cosmic::widget::container::Appearance {
|
||||
icon_color: None,
|
||||
text_color: None,
|
||||
background: Some(cosmic::iced::Background::Color(cosmic::iced::Color::BLACK)),
|
||||
border: Border {
|
||||
color: cosmic::iced::Color::BLACK,
|
||||
radius: cosmic.corner_radii.radius_0.into(),
|
||||
width: 0.0,
|
||||
},
|
||||
shadow: Default::default(),
|
||||
}
|
||||
})
|
||||
}
|
||||
17
cosmic-settings/src/utils.rs
Normal file
17
cosmic-settings/src/utils.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/// Normalize the labeling of displays across settings pages.
|
||||
pub fn display_name(name: &str, physical: (u32, u32)) -> String {
|
||||
let inches = ((physical.0.pow(2) + physical.1.pow(2)) as f32).sqrt() * 0.039_370_1;
|
||||
let inches_string = format!("{inches:.1}\"");
|
||||
|
||||
match name {
|
||||
"eDP-1" | "LVDS1" => {
|
||||
fl!("display", "laptop", size = inches_string.as_str())
|
||||
}
|
||||
output => fl!(
|
||||
"display",
|
||||
"external",
|
||||
size = inches_string.as_str(),
|
||||
output = output
|
||||
),
|
||||
}
|
||||
}
|
||||
123
cosmic-settings/src/widget/mod.rs
Normal file
123
cosmic-settings/src/widget/mod.rs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::iced::Length;
|
||||
use cosmic::widget::{
|
||||
button, column, container, divider, horizontal_space, icon, list, row, settings, text,
|
||||
vertical_space,
|
||||
};
|
||||
use cosmic::{theme, Apply, Element};
|
||||
use cosmic_settings_page as page;
|
||||
|
||||
#[must_use]
|
||||
pub fn search_header<Message>(
|
||||
pages: &page::Binder<Message>,
|
||||
page: page::Entity,
|
||||
) -> cosmic::Element<crate::Message> {
|
||||
let page_meta = &pages.info[page];
|
||||
|
||||
let mut column_children = Vec::with_capacity(4);
|
||||
|
||||
if let Some(parent) = page_meta.parent {
|
||||
let parent_meta = &pages.info[parent];
|
||||
|
||||
column_children.push(
|
||||
text(parent_meta.title.as_str())
|
||||
.size(14)
|
||||
.apply(container)
|
||||
.padding([0, 0, 0, 6])
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
column_children.push(
|
||||
crate::widget::search_page_link(&page_meta.title)
|
||||
.on_press(crate::Message::Page(page))
|
||||
.into(),
|
||||
);
|
||||
|
||||
column_children.push(vertical_space(Length::Fixed(8.)).into());
|
||||
column_children.push(divider::horizontal::heavy().into());
|
||||
|
||||
column::with_children(column_children).into()
|
||||
}
|
||||
|
||||
pub fn search_page_link<Message: 'static>(title: &str) -> button::TextButton<Message> {
|
||||
button::text(title).style(button::Style::Link)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn page_title<Message: 'static>(page: &page::Info) -> Element<Message> {
|
||||
row::with_capacity(2)
|
||||
.push(text::title3(page.title.as_str()))
|
||||
.push(horizontal_space(Length::Fill))
|
||||
.into()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn unimplemented_page<Message: 'static>() -> Element<'static, Message> {
|
||||
settings::view_section("")
|
||||
.add(text("We haven't created that panel yet, and/or it is using a similar idea as current Pop! designs."))
|
||||
.into()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn display_container<'a, Message: 'a>(widget: Element<'a, Message>) -> Element<'a, Message> {
|
||||
let display = container(widget)
|
||||
.style(crate::theme::display_container_screen())
|
||||
.apply(container)
|
||||
.padding(4)
|
||||
.style(crate::theme::display_container_frame());
|
||||
|
||||
row::with_capacity(3)
|
||||
.push(horizontal_space(Length::Fill))
|
||||
.push(display)
|
||||
.push(horizontal_space(Length::Fill))
|
||||
.padding([0, 0, 8, 0])
|
||||
.into()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn page_list_item<'a, Message: 'static + Clone>(
|
||||
title: &'a str,
|
||||
description: &'a str,
|
||||
icon: &'a str,
|
||||
message: Message,
|
||||
) -> Element<'a, Message> {
|
||||
cosmic::widget::settings::item::builder(title)
|
||||
.description(description)
|
||||
.icon(icon::from_name(icon).size(16))
|
||||
.control(icon::from_name("go-next-symbolic").size(16))
|
||||
.spacing(16)
|
||||
.apply(container)
|
||||
.padding([20, 24])
|
||||
.style(theme::Container::List)
|
||||
.apply(button)
|
||||
.style(theme::Button::Transparent)
|
||||
.on_press(message)
|
||||
.into()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn sub_page_header<'a, Message: 'static + Clone>(
|
||||
sub_page: &'a str,
|
||||
parent_page: &'a str,
|
||||
on_press: Message,
|
||||
) -> Element<'a, Message> {
|
||||
let previous_button = button::icon(icon::from_name("go-previous-symbolic"))
|
||||
.extra_small()
|
||||
.padding(0)
|
||||
.label(parent_page)
|
||||
.spacing(4)
|
||||
.style(button::Style::Link)
|
||||
.on_press(on_press);
|
||||
|
||||
let sub_page_header = row::with_capacity(2).push(text::title3(sub_page));
|
||||
|
||||
column::with_capacity(2)
|
||||
.push(previous_button)
|
||||
.push(sub_page_header)
|
||||
.spacing(6)
|
||||
.width(Length::Shrink)
|
||||
.into()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue