use cosmic::{ cctk::sctk::reexports::client::{backend::ObjectId, protocol::wl_output::WlOutput, Proxy}, cosmic_config::{self, CosmicConfigEntry}, iced::Length, iced_widget::slider, theme, widget::{ button, container, dropdown, horizontal_space, icon, list, row, 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}; use std::collections::HashMap; pub struct PageInner { pub(crate) config_helper: Option, pub(crate) panel_config: Option, pub outputs: Vec, pub anchors: Vec, pub backgrounds: Vec, pub(crate) container_config: Option, // TODO move these into panel config pub(crate) outputs_map: HashMap, } 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(), } } } 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 + PanelPage, T: Fn(Message) -> crate::pages::Message + Copy + 'static, >( p: &P, msg_map: T, ) -> Section { 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::

(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 + PanelPage, T: Fn(Message) -> crate::pages::Message + Copy + 'static, >( p: &P, msg_map: T, ) -> Section { 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::

(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) }) .into(), text(fl!("number", HashMap::from_iter(vec![("number", 100)]))).into(), ]) .spacing(12), )) .apply(Element::from) .map(msg_map) }) } pub(crate) fn configuration + PanelPage>( p: &P, ) -> Section { Section::default() .title(fl!("panel-applets")) .descriptions(vec![p.configure_applets_label().into()]) .view::

(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::custom(list::style)) .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 + PanelPage, T: Fn(Message) -> crate::pages::Message + Copy + 'static, >( msg_map: T, ) -> Section { Section::default() .title(fl!("panel-missing")) .descriptions(vec![ fl!("panel-missing", "desc").into(), fl!("panel-missing", "fix").into(), ]) .view::

(move |_binder, _page, section| { // _descriptions = §ion.descriptions; settings::view_section(§ion.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 for Appearance { type Error = (); fn try_from(value: CosmicPanelBackground) -> Result { match value { CosmicPanelBackground::ThemeDefault => Ok(Appearance::Match), CosmicPanelBackground::Light => Ok(Appearance::Light), CosmicPanelBackground::Dark => Ok(Appearance::Dark), _ => Err(()), } } } impl From 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), } impl PageInner { #[allow(clippy::too_many_lines)] pub fn update(&mut self, message: Message) { let helper = self.config_helper.as_ref().unwrap(); 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; } } 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); } }