feat: dock settings pages

This commit is contained in:
Ashley Wulber 2023-09-11 17:15:59 -04:00 committed by Ashley Wulber
parent 1c6ed73165
commit 87de348d86
11 changed files with 901 additions and 553 deletions

View file

@ -28,15 +28,18 @@ use cosmic::{
}, },
Element, ElementExt, Element, ElementExt,
}; };
use page::Page;
use crate::{ use crate::{
config::Config, config::Config,
pages::{ pages::{
desktop::{ desktop::{
self, self,
dock::{self, applets::ADD_DOCK_APPLET_DIALOGUE_ID},
panel::{ panel::{
self, self,
applets::{self, APPLET_DND_ICON_ID}, applets_inner::{self, AppletsPage, APPLET_DND_ICON_ID},
inner as _panel,
}, },
}, },
input::{self, keyboard}, input::{self, keyboard},
@ -125,8 +128,8 @@ impl Application for SettingsApp {
// app.insert_page::<bluetooth::Page>(); // app.insert_page::<bluetooth::Page>();
let desktop_id = app.insert_page::<desktop::Page>().id(); let desktop_id = app.insert_page::<desktop::Page>().id();
// app.insert_page::<panel::Page>(); app.insert_page::<panel::Page>();
// app.insert_page::<applets::Page>(); app.insert_page::<dock::Page>();
// app.insert_page::<input::Page>(); // app.insert_page::<input::Page>();
@ -175,13 +178,13 @@ impl Application for SettingsApp {
wayland::OutputEvent::Created(Some(info)), wayland::OutputEvent::Created(Some(info)),
o, o,
))) if info.name.is_some() => Some(Message::PageMessage(crate::pages::Message::Panel( ))) if info.name.is_some() => Some(Message::PageMessage(crate::pages::Message::Panel(
panel::Message::OutputAdded(info.name.unwrap(), o), panel::Message(_panel::Message::OutputAdded(info.name.unwrap(), o)),
))), ))),
iced::Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Output( iced::Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Output(
wayland::OutputEvent::Removed, wayland::OutputEvent::Removed,
o, o,
))) => Some(Message::PageMessage(crate::pages::Message::Panel( ))) => Some(Message::PageMessage(crate::pages::Message::Panel(
panel::Message::OutputRemoved(o), panel::Message(_panel::Message::OutputRemoved(o)),
))), ))),
_ => None, _ => None,
}); });
@ -224,7 +227,9 @@ impl Application for SettingsApp {
return self.search.focus(); return self.search.focus();
} }
}, },
Message::Page(page) => return self.activate_page(page), Message::Page(page) => {
return self.activate_page(page);
}
Message::Drag => return start_drag_window(window::Id(0)), Message::Drag => return start_drag_window(window::Id(0)),
Message::Close => { Message::Close => {
process::exit(0); process::exit(0);
@ -257,7 +262,6 @@ impl Application for SettingsApp {
Message::Search(search::Message::Clear) => { Message::Search(search::Message::Clear) => {
self.search_clear(); self.search_clear();
} }
Message::None | Message::Search(_) => {}
Message::PageMessage(message) => match message { Message::PageMessage(message) => match message {
crate::pages::Message::About(message) => { crate::pages::Message::About(message) => {
page::update!(self.pages, message, system::about::Page); page::update!(self.pages, message, system::about::Page);
@ -285,8 +289,16 @@ impl Application for SettingsApp {
crate::pages::Message::Panel(message) => { crate::pages::Message::Panel(message) => {
page::update!(self.pages, message, panel::Page); page::update!(self.pages, message, panel::Page);
} }
crate::pages::Message::Applet(message) => { crate::pages::Message::PanelApplet(message) => {
if let Some(page) = self.pages.page_mut::<applets::Page>() { if let Some(page) = self.pages.page_mut::<applets_inner::Page>() {
return page.update(message, applets_inner::ADD_PANEL_APPLET_DIALOGUE_ID);
}
}
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); return page.update(message);
} }
} }
@ -295,29 +307,54 @@ impl Application for SettingsApp {
self.sharp_corners = matches!(state, WindowState::Activated); self.sharp_corners = matches!(state, WindowState::Activated);
} }
Message::PanelConfig(config) if config.name.to_lowercase().contains("panel") => { Message::PanelConfig(config) if config.name.to_lowercase().contains("panel") => {
if let Some(page) = self.pages.page_mut::<panel::Page>() { page::update!(
page.update(panel::Message::PanelConfig(config.clone())); self.pages,
} panel::Message(_panel::Message::PanelConfig(config.clone())),
if let Some(page) = self.pages.page_mut::<applets::Page>() { panel::Page
_ = page.update(applets::Message::PanelConfig(config)); );
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,
);
} }
} }
Message::PanelConfig(_) => {} // ignore other config changes for now, 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 => { Message::DesktopInfo => {
if let Some(page) = self.pages.page_mut::<applets::Page>() { let info_list: Vec<_> = freedesktop_desktop_entry::Iter::new(
// collect the potential applets freedesktop_desktop_entry::default_paths(),
ret = page.update(applets::Message::Applets( )
freedesktop_desktop_entry::Iter::new( .filter_map(|p| applets_inner::Applet::try_from(Cow::from(p)).ok())
freedesktop_desktop_entry::default_paths(), .collect();
)
.filter_map(|p| applets::Applet::try_from(Cow::from(p)).ok()) page::update!(
.collect(), 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,
);
} }
} }
Message::ThemeChanged(theme) => { Message::ThemeChanged(theme) => {
self.theme = theme; self.theme = theme;
} }
Message::PanelConfig(_) | Message::None | Message::Search(_) => {} // Ignored
} }
ret ret
} }
@ -325,14 +362,21 @@ impl Application for SettingsApp {
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
fn view(&self, id: window::Id) -> Element<Message> { fn view(&self, id: window::Id) -> Element<Message> {
if let Some(Some(page)) = if let Some(Some(page)) =
(id == APPLET_DND_ICON_ID).then(|| self.pages.page::<applets::Page>()) (id == APPLET_DND_ICON_ID).then(|| self.pages.page::<applets_inner::Page>())
{ {
return page.dnd_icon(); return page.dnd_icon();
} }
if let Some(Some(page)) = if let Some(Some(page)) = (id == applets_inner::ADD_PANEL_APPLET_DIALOGUE_ID)
(id == applets::ADD_APPLET_DIALOGUE_ID).then(|| self.pages.page::<applets::Page>()) .then(|| self.pages.page::<applets_inner::Page>())
{ {
return page.add_applet_view(); 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)) = if let Some(Some(page)) =
(id == keyboard::ADD_INPUT_SOURCE_DIALOGUE_ID).then(|| self.pages.page::<input::Page>()) (id == keyboard::ADD_INPUT_SOURCE_DIALOGUE_ID).then(|| self.pages.page::<input::Page>())
@ -439,10 +483,14 @@ impl Application for SettingsApp {
fn close_requested(&self, id: window::Id) -> Self::Message { fn close_requested(&self, id: window::Id) -> Self::Message {
if id == window::Id(0) { if id == window::Id(0) {
Message::Close Message::Close
} else if id == applets::ADD_APPLET_DIALOGUE_ID { } else if id == applets_inner::ADD_PANEL_APPLET_DIALOGUE_ID {
Message::PageMessage(crate::pages::Message::Applet( Message::PageMessage(crate::pages::Message::PanelApplet(
applets::Message::ClosedAppletDialogue, applets_inner::Message::ClosedAppletDialogue,
)) ))
} else if id == ADD_DOCK_APPLET_DIALOGUE_ID {
Message::PageMessage(crate::pages::Message::DockApplet(dock::applets::Message(
applets_inner::Message::ClosedAppletDialogue,
)))
} else { } else {
Message::None Message::None
} }

View file

@ -1,26 +0,0 @@
// 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("dock", "preferences-pop-desktop-dock-symbolic")
.title(fl!("dock"))
.description(fl!("dock", "desc"))
}
}
impl page::AutoBind<crate::pages::Message> for Page {}

View file

@ -0,0 +1,82 @@
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 slotmap::SlotMap;
use std::borrow::Cow;
use crate::{
app,
pages::{
self,
desktop::panel::applets_inner::{self, lists, AppletsPage, ReorderWidgetState},
},
};
pub const ADD_DOCK_APPLET_DIALOGUE_ID: window::Id = window::Id(1002);
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_dialogue: 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-pop-desktop-dock-symbolic")
// .title(fl!("applets"))
}
}
impl page::AutoBind<crate::pages::Message> for Page {}

View file

@ -0,0 +1,166 @@
use std::collections::HashMap;
use apply::Apply;
use cosmic::{
cosmic_config::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 crate::pages::desktop::panel::inner::{add_panel, behavior_and_position, configuration, 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;
};
if enabled {
container_config.config_list.push(panel_config.clone());
} else {
container_config
.config_list
.retain(|c| c.name.as_str() != "Dock");
}
_ = container_config.write_entries();
}
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 container_config = CosmicPanelContainerConfig::load().ok();
Self {
inner: PageInner {
config_helper,
panel_config,
container_config,
outputs: HashMap::new(),
},
}
}
}
pub(crate) fn enable() -> Section<crate::pages::Message> {
Section::default()
.descriptions(vec![fl!("dock")])
.view::<Page>(|_binder, page, section| {
let descriptions = &section.descriptions;
let Some(container_config) = page.inner.container_config.as_ref() else {
return Element::from(text(fl!("unknown")));
};
settings::view_section(&section.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)),
]
} 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-pop-desktop-dock-symbolic")
.title(fl!("dock"))
.description(fl!("dock", "desc"))
}
}

View file

@ -1,15 +1,6 @@
use std::{
borrow::{Borrow, Cow},
fmt::Debug,
mem,
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
};
use apply::Apply; use apply::Apply;
use cosmic::{ use cosmic::{
cosmic_config::{self, Config, CosmicConfigEntry}, cosmic_config::{Config, CosmicConfigEntry},
iced::{ iced::{
alignment::{Horizontal, Vertical}, alignment::{Horizontal, Vertical},
event::{ event::{
@ -47,6 +38,14 @@ use cosmic::{
widget::{button, header_bar, icon, list_column}, widget::{button, header_bar, icon, list_column},
Element, Element,
}; };
use std::{
borrow::{Borrow, Cow},
fmt::Debug,
mem,
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
};
use crate::{app, pages}; use crate::{app, pages};
use cosmic_panel_config::CosmicPanelConfig; use cosmic_panel_config::CosmicPanelConfig;
@ -70,28 +69,31 @@ const SPACING: f32 = 8.0;
const DRAG_START_DISTANCE_SQUARED: f32 = 64.0; const DRAG_START_DISTANCE_SQUARED: f32 = 64.0;
pub const APPLET_DND_ICON_ID: window::Id = window::Id(1000); pub const APPLET_DND_ICON_ID: window::Id = window::Id(1000);
pub const ADD_APPLET_DIALOGUE_ID: window::Id = window::Id(1001); pub const ADD_PANEL_APPLET_DIALOGUE_ID: window::Id = window::Id(1001);
pub struct Page { pub struct Page {
available_entries: Vec<Applet<'static>>, pub(crate) available_entries: Vec<Applet<'static>>,
config_helper: Option<Config>, pub(crate) config_helper: Option<Config>,
current_config: Option<CosmicPanelConfig>, pub(crate) current_config: Option<CosmicPanelConfig>,
reorder_widget_state: ReorderWidgetState, pub(crate) reorder_widget_state: ReorderWidgetState,
search: String, pub(crate) search: String,
has_dialogue: bool, pub(crate) has_dialogue: bool,
} }
impl Default for Page { impl Default for Page {
fn default() -> Self { fn default() -> Self {
let config_helper = cosmic_config::Config::new("com.system76.CosmicPanel.Panel", 1).ok(); let config_helper = CosmicPanelConfig::cosmic_config("Panel").ok();
let current_config = config_helper.as_ref().and_then(|config_helper| { let current_config = config_helper.as_ref().and_then(|config_helper| {
// TODO error handling...
let panel_config = CosmicPanelConfig::get_entry(config_helper).ok()?; 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 // 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) (panel_config.name == "Panel").then_some(panel_config)
}); });
Self { Self {
available_entries: Vec::new(), available_entries: freedesktop_desktop_entry::Iter::new(
freedesktop_desktop_entry::default_paths(),
)
.filter_map(|p| Applet::try_from(Cow::from(p)).ok())
.collect(),
config_helper, config_helper,
current_config, current_config,
reorder_widget_state: ReorderWidgetState::default(), reorder_widget_state: ReorderWidgetState::default(),
@ -101,13 +103,31 @@ impl Default for Page {
} }
} }
pub trait AppletsPage {
fn inner(&self) -> &Page;
fn inner_mut(&mut self) -> &mut Page;
}
impl AppletsPage for Page {
fn inner(&self) -> &Page {
self
}
fn inner_mut(&mut self) -> &mut Page {
self
}
}
impl page::Page<crate::pages::Message> for Page { impl page::Page<crate::pages::Message> for Page {
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
fn content( fn content(
&self, &self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>, sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> { ) -> Option<page::Content> {
Some(vec![sections.insert(lists())]) Some(vec![
sections.insert(lists::<Page, _>(pages::Message::PanelApplet))
])
} }
fn info(&self) -> page::Info { fn info(&self) -> page::Info {
@ -141,7 +161,6 @@ pub enum Message {
DragAppletDialogue, DragAppletDialogue,
Save, Save,
Cancel, Cancel,
Ignore,
} }
impl Debug for Message { impl Debug for Message {
@ -154,7 +173,6 @@ impl Debug for Message {
Message::PanelConfig(_) => write!(f, "PanelConfig"), Message::PanelConfig(_) => write!(f, "PanelConfig"),
Message::StartDnd(_) => write!(f, "StartDnd"), Message::StartDnd(_) => write!(f, "StartDnd"),
Message::DnDCommand(_) => write!(f, "DnDCommand"), Message::DnDCommand(_) => write!(f, "DnDCommand"),
Message::Ignore => write!(f, "Ignore"),
Message::Save => write!(f, "ApplyReorder"), Message::Save => write!(f, "ApplyReorder"),
Message::RemoveStart(_) => write!(f, "RemoveStart"), Message::RemoveStart(_) => write!(f, "RemoveStart"),
Message::RemoveCenter(_) => write!(f, "RemoveCenter"), Message::RemoveCenter(_) => write!(f, "RemoveCenter"),
@ -195,7 +213,10 @@ impl Page {
#[must_use] #[must_use]
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
pub fn add_applet_view(&self) -> Element<app::Message> { pub fn add_applet_view<T: Fn(Message) -> crate::pages::Message + Copy + 'static>(
&self,
msg_map: T,
) -> Element<app::Message> {
let mut list_column = list_column(); let mut list_column = list_column();
let mut has_some = false; let mut has_some = false;
for info in self for info in self
@ -245,9 +266,9 @@ impl Page {
}) })
}) })
.padding(8.0) .padding(8.0)
.on_press(app::Message::PageMessage(pages::Message::Applet( .on_press(app::Message::PageMessage(msg_map(Message::AddApplet(
Message::AddApplet(info.clone()) info.clone()
))), )))),
] ]
.padding([0, 32, 0, 32]) .padding([0, 32, 0, 32])
.spacing(12) .spacing(12)
@ -264,10 +285,10 @@ impl Page {
column![ column![
header_bar() header_bar()
.title(fl!("add-applet")) .title(fl!("add-applet"))
.on_close(app::Message::PageMessage(pages::Message::Applet( .on_close(app::Message::PageMessage(msg_map(
Message::CloseAppletDialogue Message::CloseAppletDialogue
))) )))
.on_drag(app::Message::PageMessage(pages::Message::Applet( .on_drag(app::Message::PageMessage(msg_map(
Message::DragAppletDialogue Message::DragAppletDialogue
))), ))),
container( container(
@ -284,15 +305,11 @@ impl Page {
spacing: 12.0, spacing: 12.0,
side: Side::Left, side: Side::Left,
}) })
.on_input(|s| { .on_input(move |s| {
app::Message::PageMessage(pages::Message::Applet(Message::Search( app::Message::PageMessage(msg_map(Message::Search(s)))
s,
)))
}) })
.on_paste(|s| { .on_paste(move |s| {
app::Message::PageMessage(pages::Message::Applet(Message::Search( app::Message::PageMessage(msg_map(Message::Search(s)))
s,
)))
}) })
.width(Length::Fixed(312.0)), .width(Length::Fixed(312.0)),
list_column list_column
@ -312,7 +329,7 @@ impl Page {
} }
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
pub fn update(&mut self, message: Message) -> Command<app::Message> { pub fn update(&mut self, message: Message, window_id: window::Id) -> Command<app::Message> {
match message { match message {
Message::PanelConfig(c) => { Message::PanelConfig(c) => {
self.current_config = Some(c); self.current_config = Some(c);
@ -349,6 +366,7 @@ impl Page {
*list = end_list.into_iter().map(|a| a.id.into()).collect(); *list = end_list.into_iter().map(|a| a.id.into()).collect();
} }
Message::Applets(applets) => { Message::Applets(applets) => {
dbg!(&applets);
self.available_entries = applets; self.available_entries = applets;
} }
Message::StartDnd(state) => { Message::StartDnd(state) => {
@ -358,7 +376,6 @@ impl Page {
Message::DnDCommand(action) => { Message::DnDCommand(action) => {
return data_device_action(action()); return data_device_action(action());
} }
Message::Ignore => {}
Message::Save => { Message::Save => {
self.reorder_widget_state = ReorderWidgetState::default(); self.reorder_widget_state = ReorderWidgetState::default();
self.save(); self.save();
@ -429,12 +446,12 @@ impl Page {
list.push(applet.id.to_string()); list.push(applet.id.to_string());
self.save(); self.save();
return commands::window::close_window(ADD_APPLET_DIALOGUE_ID); return commands::window::close_window(window_id);
} }
Message::AddAppletDialogue => { Message::AddAppletDialogue => {
self.has_dialogue = true; self.has_dialogue = true;
let window_settings = SctkWindowSettings { let window_settings = SctkWindowSettings {
window_id: ADD_APPLET_DIALOGUE_ID, window_id,
app_id: Some("com.system76.CosmicSettings".to_string()), app_id: Some("com.system76.CosmicSettings".to_string()),
title: Some(fl!("add-applet")), title: Some(fl!("add-applet")),
parent: Some(window::Id(0)), parent: Some(window::Id(0)),
@ -456,10 +473,10 @@ impl Page {
} }
Message::CloseAppletDialogue => { Message::CloseAppletDialogue => {
self.has_dialogue = false; self.has_dialogue = false;
return commands::window::close_window(ADD_APPLET_DIALOGUE_ID); return commands::window::close_window(window_id);
} }
Message::DragAppletDialogue => { Message::DragAppletDialogue => {
return commands::window::start_drag_window(ADD_APPLET_DIALOGUE_ID); return commands::window::start_drag_window(window_id);
} }
}; };
Command::none() Command::none()
@ -467,13 +484,20 @@ impl Page {
} }
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
pub fn lists() -> Section<crate::pages::Message> { pub fn lists<
Section::default().view::<Page>(|_binder, page, _section| { P: page::Page<crate::pages::Message> + AppletsPage,
T: Fn(Message) -> crate::pages::Message + Copy + 'static,
>(
msg_map: T,
) -> Section<crate::pages::Message> {
Section::default().view::<P>(move |_binder, page, _section| {
let page = page.inner();
let Some(config) = page.current_config.as_ref() else { let Some(config) = page.current_config.as_ref() else {
return Element::from( return Element::from(
text(fl!("unknown")) text(fl!("unknown"))
); );
}; };
let button = cosmic::iced::widget::button(text(fl!("add-applet"))) let button = cosmic::iced::widget::button(text(fl!("add-applet")))
.style(theme::Button::Secondary) .style(theme::Button::Secondary)
.padding(8.0); .padding(8.0);
@ -510,7 +534,7 @@ pub fn lists() -> Section<crate::pages::Message> {
Message::ReorderStart, Message::ReorderStart,
Message::Save, Message::Save,
Message::Cancel, Message::Cancel,
page.reorder_widget_state.dragged_applet() page.reorder_widget_state.dragged_applet().as_ref()
) )
] ]
.spacing(8.0), .spacing(8.0),
@ -537,7 +561,7 @@ pub fn lists() -> Section<crate::pages::Message> {
Message::ReorderCenter, Message::ReorderCenter,
Message::Save, Message::Save,
Message::Cancel, Message::Cancel,
page.reorder_widget_state.dragged_applet() page.reorder_widget_state.dragged_applet().as_ref()
) )
] ]
.spacing(8.0), .spacing(8.0),
@ -565,7 +589,7 @@ pub fn lists() -> Section<crate::pages::Message> {
Message::ReorderEnd, Message::ReorderEnd,
Message::Save, Message::Save,
Message::Cancel, Message::Cancel,
page.reorder_widget_state.dragged_applet() page.reorder_widget_state.dragged_applet().as_ref()
) )
] ]
.spacing(8.0), .spacing(8.0),
@ -573,7 +597,7 @@ pub fn lists() -> Section<crate::pages::Message> {
.padding([0, 16, 0, 16]) .padding([0, 16, 0, 16])
.spacing(12.0) .spacing(12.0)
.apply(Element::from) .apply(Element::from)
.map(pages::Message::Applet) .map(msg_map)
}) })
} }
@ -587,6 +611,7 @@ pub struct Applet<'a> {
} }
impl Applet<'_> { impl Applet<'_> {
#[must_use]
pub fn matches(&self, query: &str) -> bool { pub fn matches(&self, query: &str) -> bool {
self.name.contains(query) || self.description.contains(query) || self.id.contains(query) self.name.contains(query) || self.description.contains(query) || self.id.contains(query)
} }
@ -667,9 +692,8 @@ impl<'a, Message: 'static + Clone> AppletReorderList<'a, Message> {
on_reorder: impl Fn(Vec<Applet<'static>>) -> Message + 'a, on_reorder: impl Fn(Vec<Applet<'static>>) -> Message + 'a,
on_apply_reorder: Message, on_apply_reorder: Message,
on_cancel: Message, on_cancel: Message,
active_dnd: Option<Applet<'a>>, // state: Option<&State>, active_dnd: Option<&Applet<'a>>,
) -> Self { ) -> Self {
// let dragged_path = state.and_then(|state| state.dragged_applet());
let applet_buttons = info let applet_buttons = info
.clone() .clone()
.into_iter() .into_iter()
@ -1057,7 +1081,6 @@ where
))); )));
let data = match &state.dragging_state { let data = match &state.dragging_state {
DraggingState::Dragging(a) => Some(a.clone()), DraggingState::Dragging(a) => Some(a.clone()),
_ => { _ => {
shell.publish((self.on_dnd_command_produced.as_ref())(Box::new( shell.publish((self.on_dnd_command_produced.as_ref())(Box::new(
move || { move || {
@ -1087,7 +1110,7 @@ where
shell.publish((self.on_reorder.as_ref())( shell.publish((self.on_reorder.as_ref())(
filtered filtered
.into_iter() .into_iter()
.map(pages::desktop::panel::applets::Applet::into_owned) .map(pages::desktop::panel::applets_inner::Applet::into_owned)
.collect(), .collect(),
)); ));
} }
@ -1225,7 +1248,7 @@ where
shell.publish((self.on_reorder.as_ref())( shell.publish((self.on_reorder.as_ref())(
reordered_list reordered_list
.into_iter() .into_iter()
.map(pages::desktop::panel::applets::Applet::into_owned) .map(pages::desktop::panel::applets_inner::Applet::into_owned)
.collect(), .collect(),
)); ));
} }
@ -1292,7 +1315,7 @@ where
shell.publish((self.on_reorder.as_ref())( shell.publish((self.on_reorder.as_ref())(
filtered filtered
.into_iter() .into_iter()
.map(pages::desktop::panel::applets::Applet::into_owned) .map(pages::desktop::panel::applets_inner::Applet::into_owned)
.collect(), .collect(),
)); ));
} }

View file

@ -0,0 +1,393 @@
use cosmic::{
cosmic_config::{self, CosmicConfigEntry},
iced::widget::{button, container, horizontal_space, pick_list, row},
iced::Length,
iced_widget::slider,
sctk::reexports::client::{backend::ObjectId, protocol::wl_output::WlOutput, Proxy},
theme,
widget::{icon, list, settings, text, toggler},
Element,
};
use apply::Apply;
use cosmic_panel_config::{
AutoHide, CosmicPanelBackground, CosmicPanelConfig, CosmicPanelContainerConfig,
CosmicPanelOuput, PanelAnchor, PanelSize,
};
use cosmic_settings_page::{self as page, section, Section};
use std::{borrow::Cow, collections::HashMap};
pub struct PageInner {
pub(crate) config_helper: Option<cosmic_config::Config>,
pub(crate) panel_config: Option<CosmicPanelConfig>,
pub(crate) container_config: Option<CosmicPanelContainerConfig>,
// TODO move these into panel config
pub(crate) outputs: HashMap<ObjectId, (String, WlOutput)>,
}
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(),
fl!("panel-behavior-and-position", "position"),
fl!("panel-behavior-and-position", "display"),
])
.view::<P>(move |_binder, page, section| {
let descriptions = &section.descriptions;
let page = page.inner();
let Some(panel_config) = page.panel_config.as_ref() else {
return Element::from(text(fl!("unknown")));
};
settings::view_section(&section.title)
.add(settings::item(
&descriptions[0],
toggler(None, panel_config.autohide.is_some(), |value| {
Message::AutoHidePanel(value)
}),
))
.add(settings::item(
&descriptions[1],
pick_list(
Cow::from(vec![
Anchor(PanelAnchor::Top),
Anchor(PanelAnchor::Left),
Anchor(PanelAnchor::Right),
Anchor(PanelAnchor::Bottom),
]),
Some(Anchor(panel_config.anchor)),
|a| Message::PanelAnchor(a.0),
),
))
.add(settings::item(
&descriptions[2],
pick_list(
Cow::from(
Some(fl!("all"))
.into_iter()
.chain(page.outputs.values().map(|(name, _)| name.clone()))
.collect::<Vec<_>>(),
),
match &panel_config.output {
CosmicPanelOuput::All => Some(fl!("all")),
CosmicPanelOuput::Active => None,
CosmicPanelOuput::Name(ref name) => Some(name.clone()),
},
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(),
p.extend_label(),
fl!("panel-style", "appearance"),
fl!("panel-style", "size"),
fl!("panel-style", "background-opacity"),
])
.view::<P>(move |_binder, page, section| {
let descriptions = &section.descriptions;
let inner = page.inner();
let Some(panel_config) = inner.panel_config.as_ref() else {
return Element::from(text(fl!("unknown")));
};
settings::view_section(&section.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],
pick_list(
Cow::from(vec![Appearance::Match, Appearance::Light, Appearance::Dark]),
panel_config.background.clone().try_into().ok(),
Message::Appearance,
),
))
.add(settings::item(
&descriptions[3],
// TODO custom discrete slider variant
row![
text(fl!("small")),
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)
}
},
),
text(fl!("large"))
]
.spacing(12),
))
.add(settings::item(
&descriptions[4],
row![
text(fl!("number", HashMap::from_iter(vec![("number", 0)]))),
slider(0..=100, (panel_config.opacity * 100.0) as i32, |v| {
Message::Opacity(v as f32 / 100.0)
},),
text(fl!("number", HashMap::from_iter(vec![("number", 100)]))),
]
.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()])
.view::<P>(move |binder, page, section| {
let mut settings = settings::view_section(&section.title);
let descriptions = &section.descriptions;
settings = if let Some((panel_applets_entity, _panel_applets_info)) = binder
.info
.iter()
.find(|(_, v)| v.id == page.applets_page_id())
{
settings.add(
settings::item::builder(&descriptions[0])
.control(row!(
horizontal_space(Length::Fill),
icon("go-next-symbolic", 20).style(theme::Svg::Symbolic)
))
.spacing(16)
.apply(container)
.style(theme::Container::custom(list::column::style))
.apply(button)
.padding(0)
.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!("panel-missing", "desc"),
fl!("panel-missing", "fix"),
])
.view::<P>(move |_binder, page, section| {
// _descriptions = &section.descriptions;
settings::view_section(&section.title)
.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(PanelAnchor),
Output(String),
AnchorGap(bool),
PanelSize(PanelSize),
Appearance(Appearance),
ExtendToEdge(bool),
Opacity(f32),
OutputAdded(String, WlOutput),
OutputRemoved(WlOutput),
PanelConfig(CosmicPanelConfig),
}
impl PageInner {
#[allow(clippy::too_many_lines)]
pub fn update(&mut self, message: Message) {
let helper = self.config_helper.as_ref().unwrap();
let mut panel_config = self.panel_config.as_mut().unwrap();
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(anchor) => {
panel_config.anchor = anchor;
}
Message::Output(name) => {
panel_config.output = match name {
s if s == fl!("all") => CosmicPanelOuput::All,
_ => CosmicPanelOuput::Name(name),
};
}
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) => {
panel_config.background = a.into();
}
Message::ExtendToEdge(enabled) => {
panel_config.expand_to_edges = enabled;
}
Message::Opacity(opacity) => {
panel_config.opacity = opacity;
}
Message::OutputAdded(name, output) => {
self.outputs.insert(output.id(), (name, output));
}
Message::OutputRemoved(output) => {
self.outputs.remove(&output.id());
}
Message::PanelConfig(c) => {
self.panel_config = Some(c);
return;
}
}
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);
}
}

View file

@ -1,64 +1,107 @@
use cosmic::{ use std::collections::HashMap;
cosmic_config::{self, CosmicConfigEntry},
iced::widget::{button, container, horizontal_space, pick_list, row},
iced::Length,
iced_widget::slider,
sctk::reexports::client::{backend::ObjectId, protocol::wl_output::WlOutput, Proxy},
theme,
widget::{icon, list, settings, text, toggler},
Element,
};
use apply::Apply; use cosmic::cosmic_config::CosmicConfigEntry;
use cosmic_panel_config::{ use cosmic_panel_config::{CosmicPanelConfig, CosmicPanelContainerConfig};
AutoHide, CosmicPanelBackground, CosmicPanelConfig, CosmicPanelOuput, PanelAnchor, PanelSize,
};
use cosmic_settings_page::{self as page, section, Section}; use cosmic_settings_page::{self as page, section, Section};
use slotmap::SlotMap; use slotmap::SlotMap;
use std::{borrow::Cow, collections::HashMap};
pub mod applets; use crate::pages::desktop::panel::inner::{add_panel, behavior_and_position, configuration, style};
use self::inner::{PageInner, PanelPage};
pub mod applets_inner;
pub mod inner;
pub struct Page { pub struct Page {
config_helper: Option<cosmic_config::Config>, inner: PageInner,
panel_config: Option<CosmicPanelConfig>, }
// TODO move these into panel config
pub outputs: HashMap<ObjectId, (String, WlOutput)>, #[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 { impl Default for Page {
fn default() -> Self { fn default() -> Self {
// TODO CosmicPanelConfig should return its own version let config_helper = CosmicPanelConfig::cosmic_config("Panel").ok();
let config_helper = cosmic_config::Config::new("com.system76.CosmicPanel.Panel", 1).ok();
let panel_config = config_helper.as_ref().and_then(|config_helper| { let panel_config = config_helper.as_ref().and_then(|config_helper| {
// TODO error handling...
let panel_config = CosmicPanelConfig::get_entry(config_helper).ok()?; 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 // 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) (panel_config.name == "Panel").then_some(panel_config)
}); });
let container_config = CosmicPanelContainerConfig::load().ok();
Self { Self {
config_helper, inner: PageInner {
panel_config, config_helper,
outputs: HashMap::new(), panel_config,
container_config,
outputs: HashMap::new(),
},
} }
} }
} }
// TODO cleanup
impl page::Page<crate::pages::Message> for Page { impl page::Page<crate::pages::Message> for Page {
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
fn content( fn content(
&self, &self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>, sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> { ) -> Option<page::Content> {
Some(if self.panel_config.is_some() { Some(if self.inner.panel_config.is_some() {
vec![ vec![
sections.insert(behavior_and_position()), sections.insert(behavior_and_position::<Page, _>(self, |m| {
sections.insert(style()), crate::pages::Message::Panel(Message(m))
sections.insert(configuration()), })),
sections.insert(style::<Page, _>(self, |m| {
crate::pages::Message::Panel(Message(m))
})),
sections.insert(configuration::<Page>(self)),
] ]
} else { } else {
vec![sections.insert(add_panel())] vec![sections.insert(add_panel::<Page, _>(|m| {
crate::pages::Message::Panel(Message(m))
}))]
}) })
} }
@ -68,392 +111,3 @@ impl page::Page<crate::pages::Message> for Page {
.description(fl!("panel", "desc")) .description(fl!("panel", "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::<applets::Page>()
}
}
pub fn behavior_and_position() -> Section<crate::pages::Message> {
Section::default()
.title(fl!("panel-behavior-and-position"))
.descriptions(vec![
fl!("panel-behavior-and-position", "autohide"),
fl!("panel-behavior-and-position", "position"),
fl!("panel-behavior-and-position", "display"),
])
.view::<Page>(|_binder, page, section| {
let descriptions = &section.descriptions;
let Some(panel_config) = page.panel_config.as_ref() else {
return Element::from(text(fl!("unknown")));
};
settings::view_section(&section.title)
.add(settings::item(
&descriptions[0],
toggler(None, panel_config.autohide.is_some(), |value| {
Message::AutoHidePanel(value)
}),
))
.add(settings::item(
&descriptions[1],
pick_list(
Cow::from(vec![
Anchor(PanelAnchor::Top),
Anchor(PanelAnchor::Left),
Anchor(PanelAnchor::Right),
Anchor(PanelAnchor::Bottom),
]),
Some(Anchor(panel_config.anchor)),
|a| Message::PanelAnchor(a.0),
),
))
.add(settings::item(
&descriptions[2],
pick_list(
Cow::from(
Some(fl!("all"))
.into_iter()
.chain(page.outputs.values().map(|(name, _)| name.clone()))
.collect::<Vec<_>>(),
),
match &panel_config.output {
CosmicPanelOuput::All => Some(fl!("all")),
CosmicPanelOuput::Active => None,
CosmicPanelOuput::Name(ref name) => Some(name.clone()),
},
Message::Output,
),
))
.apply(Element::from)
.map(crate::pages::Message::Panel)
})
}
pub fn style() -> Section<crate::pages::Message> {
Section::default()
.title(fl!("panel-style"))
.descriptions(vec![
fl!("panel-style", "anchor-gap"),
fl!("panel-style", "extend"),
fl!("panel-style", "appearance"),
fl!("panel-style", "size"),
fl!("panel-style", "background-opacity"),
])
.view::<Page>(|_binder, page, section| {
let descriptions = &section.descriptions;
let Some(panel_config) = page.panel_config.as_ref() else {
return Element::from(text(fl!("unknown")));
};
settings::view_section(&section.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],
pick_list(
Cow::from(vec![Appearance::Match, Appearance::Light, Appearance::Dark]),
panel_config.background.clone().try_into().ok(),
Message::Appearance,
),
))
.add(settings::item(
&descriptions[3],
// TODO custom discrete slider variant
row![
text(fl!("small")),
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)
}
},
),
text(fl!("large"))
]
.spacing(12),
))
.add(settings::item(
&descriptions[4],
row![
text(fl!("number", HashMap::from_iter(vec![("number", 0)]))),
slider(0..=100, (panel_config.opacity * 100.0) as i32, |v| {
Message::Opacity(v as f32 / 100.0)
},),
text(fl!("number", HashMap::from_iter(vec![("number", 100)]))),
]
.spacing(12),
))
.apply(Element::from)
.map(crate::pages::Message::Panel)
})
}
pub fn configuration() -> Section<crate::pages::Message> {
Section::default()
.title(fl!("panel-applets"))
.descriptions(vec![fl!("panel-applets", "desc")])
.view::<Page>(|binder, _page, section| {
let mut settings = settings::view_section(&section.title);
settings = if let Some((panel_applets_entity, _panel_applets_info)) =
binder.info.iter().find(|(_, v)| v.id == "panel_applets")
{
settings.add(
settings::item::builder(fl!("panel-applets", "desc"))
.control(row!(
horizontal_space(Length::Fill),
icon("go-next-symbolic", 20).style(theme::Svg::Symbolic)
))
.spacing(16)
.apply(container)
.style(theme::Container::custom(list::column::style))
.apply(button)
.padding(0)
.style(theme::Button::Transparent)
.on_press(crate::pages::Message::Page(panel_applets_entity)),
)
} else {
settings
};
Element::from(settings)
})
}
#[allow(clippy::module_name_repetitions)]
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(&section.title);
settings = if let Some((panel_entity, panel_info)) =
binder.info.iter().find(|(_, v)| v.id == "panel")
{
settings.add(
settings::item::builder(panel_info.title.clone())
.description(panel_info.description.clone())
.control(row!(
horizontal_space(Length::Fill),
icon("go-next-symbolic", 20).style(theme::Svg::Symbolic)
))
.spacing(16)
.apply(container)
.style(theme::Container::custom(list::column::style))
.apply(button)
.padding(0)
.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")
{
settings.add(
settings::item::builder(dock_info.title.clone())
.description(dock_info.description.clone())
.control(row!(
horizontal_space(Length::Fill),
icon("go-next-symbolic", 20).style(theme::Svg::Symbolic)
))
.spacing(16)
.apply(container)
.style(theme::Container::custom(list::column::style))
.apply(button)
.padding(0)
.style(theme::Button::Transparent)
.on_press(crate::pages::Message::Page(dock_entity)),
)
} else {
settings
};
Element::from(settings)
})
}
#[allow(clippy::module_name_repetitions)]
pub fn add_panel() -> Section<crate::pages::Message> {
Section::default()
.title(fl!("panel-missing"))
.descriptions(vec![
fl!("panel-missing", "desc"),
fl!("panel-missing", "fix"),
])
.view::<Page>(|_binder, _page, section| {
// _descriptions = &section.descriptions;
settings::view_section(&section.title)
.apply(Element::from)
.map(crate::pages::Message::Desktop)
})
}
#[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(PanelAnchor),
Output(String),
AnchorGap(bool),
PanelSize(PanelSize),
Appearance(Appearance),
ExtendToEdge(bool),
Opacity(f32),
Applets,
OutputAdded(String, WlOutput),
OutputRemoved(WlOutput),
PanelConfig(CosmicPanelConfig),
}
impl Page {
pub fn update(&mut self, message: Message) {
let helper = self.config_helper.as_ref().unwrap();
let mut panel_config = self.panel_config.as_mut().unwrap();
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(anchor) => {
panel_config.anchor = anchor;
}
Message::Output(name) => {
panel_config.output = match name {
s if s == fl!("all") => CosmicPanelOuput::All,
_ => CosmicPanelOuput::Name(name),
};
}
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) => {
panel_config.background = a.into();
}
Message::ExtendToEdge(enabled) => {
panel_config.expand_to_edges = enabled;
}
Message::Opacity(opacity) => {
panel_config.opacity = opacity;
}
Message::Applets => todo!(),
Message::OutputAdded(name, output) => {
self.outputs.insert(output.id(), (name, output));
}
Message::OutputRemoved(output) => {
self.outputs.remove(&output.id());
}
Message::PanelConfig(c) => {
self.panel_config = Some(c);
return;
}
}
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);
}
}

View file

@ -16,8 +16,10 @@ pub enum Message {
DateAndTime(time::date::Message), DateAndTime(time::date::Message),
Desktop(desktop::Message), Desktop(desktop::Message),
Panel(desktop::panel::Message), Panel(desktop::panel::Message),
Dock(desktop::dock::Message),
DesktopWallpaper(desktop::wallpaper::Message), DesktopWallpaper(desktop::wallpaper::Message),
Applet(desktop::panel::applets::Message), PanelApplet(desktop::panel::applets_inner::Message),
DockApplet(desktop::dock::applets::Message),
Input(input::Message), Input(input::Message),
External { id: String, message: Vec<u8> }, External { id: String, message: Vec<u8> },
Page(Entity), Page(Entity),

View file

@ -1,4 +1,7 @@
use cosmic::iced::subscription; use cosmic::{
iced::subscription,
iced_futures::futures::{self, SinkExt},
};
use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher}; use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
use std::fmt::Debug; use std::fmt::Debug;
use std::hash::Hash; use std::hash::Hash;
@ -21,19 +24,20 @@ pub enum DesktopFileEvent {
pub fn desktop_files<I: 'static + Hash + Copy + Send + Sync + Debug>( pub fn desktop_files<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I, id: I,
) -> cosmic::iced::Subscription<(I, DesktopFileEvent)> { ) -> cosmic::iced::Subscription<DesktopFileEvent> {
subscription::unfold(id, State::Ready, move |mut state| async move { subscription::channel(id, 50, move |mut output| async move {
let mut state = State::Ready;
loop { loop {
let (event, new_state) = start_watching(id, state).await; state = start_watching(state, &mut output).await;
state = new_state;
if let Some(event) = event {
return (event, state);
}
} }
}) })
} }
async fn start_watching<I: Copy>(id: I, state: State) -> (Option<(I, DesktopFileEvent)>, State) { async fn start_watching(
state: State,
output: &mut futures::channel::mpsc::Sender<DesktopFileEvent>,
) -> State {
match state { match state {
State::Ready => { State::Ready => {
let paths = freedesktop_desktop_entry::default_paths(); let paths = freedesktop_desktop_entry::default_paths();
@ -42,22 +46,19 @@ async fn start_watching<I: Copy>(id: I, state: State) -> (Option<(I, DesktopFile
for path in paths { for path in paths {
let _ = watcher.watch(path.as_ref(), RecursiveMode::Recursive); let _ = watcher.watch(path.as_ref(), RecursiveMode::Recursive);
} }
(
Some((id, DesktopFileEvent::Changed)), _ = output.send(DesktopFileEvent::Changed).await;
State::Waiting { watcher, rx }, State::Waiting { watcher, rx }
)
} else { } else {
(None, State::Finished) State::Finished
} }
} }
State::Waiting { watcher, rx } => { State::Waiting { watcher, rx } => {
if let Some(rx) = async_watch(rx).await { if let Some(rx) = async_watch(rx).await {
( _ = output.send(DesktopFileEvent::Changed).await;
Some((id, DesktopFileEvent::Changed)), State::Waiting { watcher, rx }
State::Waiting { watcher, rx },
)
} else { } else {
(None, State::Finished) State::Finished
} }
} }
State::Finished => cosmic::iced::futures::future::pending().await, State::Finished => cosmic::iced::futures::future::pending().await,

View file

@ -44,6 +44,7 @@
]; ];
buildInputs = with pkgs; [ buildInputs = with pkgs; [
systemdMinimal systemdMinimal
bashInteractive
libxkbcommon libxkbcommon
freetype freetype
fontconfig fontconfig

View file

@ -52,6 +52,7 @@ panel = Panel
panel-behavior-and-position = Behavior and Positions panel-behavior-and-position = Behavior and Positions
.autohide = Automatically hide panel .autohide = Automatically hide panel
.dock-autohide = Automatically hide dock
.position = Position on screen .position = Position on screen
.display = Show on display .display = Show on display
@ -67,7 +68,9 @@ panel-appearance = Appearance
panel-style = Style panel-style = Style
.anchor-gap = Gap between panel and screen edges .anchor-gap = Gap between panel and screen edges
.dock-anchor-gap = Gap between dock and screen edges
.extend = Extend panel to screen edges .extend = Extend panel to screen edges
.dock-extend = Extend dock to screen edges
.appearance = Appearance .appearance = Appearance
.size = Size .size = Size
.background-opacity = Background opacity .background-opacity = Background opacity
@ -76,6 +79,7 @@ small = Small
large = Large large = Large
panel-applets = Configuration panel-applets = Configuration
.dock-desc = Configure dock applets.
.desc = Configure panel applets. .desc = Configure panel applets.
panel-missing = Panel Configuration is Missing panel-missing = Panel Configuration is Missing