use crate::panel_edit::{Action, Destination, Zone}; use crate::{App, Element, Field, Kind, Message, widget}; use cosmic::iced::{Alignment, Length}; use ferese_panel::{Group, Item, ItemKind, Panel, Representation}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct Choice(&'static str, &'static str); impl std::fmt::Display for Choice { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(self.1) } } impl App { fn inspector_heading(&self, title: &str, subtitle: &str) -> Element<'static, Message> { widget::column([]) .spacing(5) .push( widget::row([]) .align_y(Alignment::Center) .push( self.panel_heading(title.to_owned(), 16.) .width(Length::Fill), ) .push(self.settings_icon_button( "Deselect", "M6 6l12 12 M6 18L18 6", Some(Message::PanelClearSelection), )), ) .push(self.note(subtitle)) .push(self.panel_divider()) .into() } pub(super) fn panel_divider(&self) -> Element<'static, Message> { let p = crate::visuals::Palette::from_resolved(&self.resolved.presented); widget::container(widget::Space::new().height(1).width(Length::Fill)) .class(crate::visuals::surface( ferese_theme::mix(p.card, p.text, 0.09), 0., )) .into() } pub(super) fn item_inspector( &self, panel: &Panel, item: &Item, destination: Destination, path: String, ) -> Element<'static, Message> { let palette = crate::visuals::Palette::from_resolved(&self.resolved.presented); let mut rows = widget::column([]) .spacing(12) .push(self.inspector_heading(item.kind.label(), "Item settings")); let item_id = item.id.clone(); rows = rows.push( widget::column([]) .spacing(8) .push(self.label("Placement", 13.)) .push(crate::visuals::panel_select( cosmic::iced::widget::pick_list( crate::panel_edit::destinations(panel), Some(destination.clone()), move |destination| { Message::PanelEdit(Action::Move(item_id.clone(), destination)) }, ) .font(self.font) .text_size(13) .padding([8, 10]) .width(Length::Fill), palette, )), ); rows = rows.push(self.panel_field( item, Field::new( format!("{path}.visible"), "Show item", "", Kind::Toggle(true), ), )); rows = rows.push(self.panel_divider()); if matches!(item.kind, ItemKind::Workspaces { .. }) { rows = rows.push(self.workspace_style_picker(item)); rows = rows.push(self.panel_divider()); } rows = rows.push(self.panel_field( item, Field::new( format!("{path}.overflow"), "Overflow", "Choose when to use the overflow menu.", Kind::Choice { default: "auto", choices: &[ ("never", "Keep visible"), ("auto", "When needed"), ("always", "Always"), ], }, ), )); let representations = item.kind.representations(); if representations.len() > 1 { let choices: &'static [(&'static str, &'static str)] = match representations { [Representation::Wide, Representation::Icon] => { &[("wide", "Full"), ("icon", "Icon")] } _ => &[("wide", "Full"), ("compact", "Compact"), ("icon", "Icon")], }; rows = rows.push(self.panel_field( item, Field::new( format!("{path}.representation"), "Preferred size", "Adapts to available space.", Kind::Choice { default: "wide", choices, }, ), )); } if matches!(item.kind, ItemKind::Battery { .. }) { rows = rows.push(self.panel_field( item, Field::new( format!("{path}.percentage"), "Battery percentage", "", Kind::Toggle(true), ), )); } let group = Zone::ALL .into_iter() .flat_map(|zone| &zone.definition(panel).groups) .find(|group| group.id == destination.group) .expect("the inspector selection refers to an item or group in the current panel"); let index = group .items .iter() .position(|candidate| candidate.id == item.id) .expect("the inspector selection refers to an item or group in the current panel"); rows.push(self.panel_divider()) .push( self.inspector_actions( (index > 0).then(|| Message::PanelEdit(Action::Earlier(item.id.clone()))), (index + 1 < group.items.len()) .then(|| Message::PanelEdit(Action::Later(item.id.clone()))), "Remove item", Some(Message::PanelEdit(Action::Remove(item.id.clone()))), ), ) .into() } fn workspace_style_picker(&self, item: &Item) -> Element<'static, Message> { let palette = crate::visuals::Palette::from_resolved(&self.resolved.presented); let ItemKind::Workspaces { style: selected } = item.kind else { unreachable!() }; let mut choices = widget::column([]) .spacing(6) .push(self.label("Workspace style", 13.)) .push(self.note("A sample of active, occupied and empty workspaces.")); for style in ferese_panel::WorkspaceStyle::ALL { let sample = ferese_theme::workspaces::sample(style, palette, self.font, 22.); choices = choices.push( widget::button::custom( widget::row![self.label(style.label(), 12.).width(Length::Fill), sample,] .spacing(10) .align_y(Alignment::Center), ) .id(format!("workspace-style:{}", style.key()).into()) .name(format!("Workspace style: {}", style.label())) .padding([8, 10]) .width(Length::Fill) .class(crate::visuals::panel_button(palette, style == selected)) .on_press_maybe((style != selected).then(|| { Message::PanelEdit(Action::Set( item.id.clone(), "style".into(), style.key().into(), )) })), ); } choices.into() } pub(super) fn group_inspector( &self, panel: &Panel, group: &Group, placement: Zone, ) -> Element<'static, Message> { let palette = crate::visuals::Palette::from_resolved(&self.resolved.presented); let id = group.id.clone(); let mut rows = widget::column([]) .spacing(12) .push(self.inspector_heading( &crate::panel_controls::group_name(&group.id.0), "Group arrangement & spacing", )) .push( widget::column([]) .spacing(8) .push(self.label("Placement", 13.)) .push(crate::visuals::panel_select( cosmic::iced::widget::pick_list( Zone::ALL.to_vec(), Some(placement), move |zone| Message::PanelEdit(Action::MoveGroup(id.clone(), zone)), ) .font(self.font) .text_size(13) .padding([8, 10]) .width(Length::Fill), palette, )), ); rows = rows.push(self.panel_divider()); let mut spacing = widget::column([]).spacing(12); for (name, label, value, maximum) in [ ("spacing", "Item spacing", group.spacing, 64.), ( "padding_vertical", "Vertical padding", f32::from(group.padding[0]), 32., ), ( "padding_horizontal", "Horizontal padding", f32::from(group.padding[1]), 32., ), ( "island_padding", "Island padding", group.island_padding, 32., ), ] { if panel.group_surface() != ferese_panel::GroupSurface::Island && name == "island_padding" { continue; } let change = |value: f32| { let value = if name.starts_with("padding_") { serde_json::json!(value as u16) } else { serde_json::json!(value) }; Message::PanelEdit(Action::SetGroup(group.id.clone(), name.into(), value)) }; spacing = spacing.push( widget::row([]) .spacing(8) .align_y(Alignment::Center) .push(self.label(label, 13.).width(Length::Fill)) .push( widget::row([]) .spacing(4) .align_y(Alignment::Center) .push(self.settings_icon_button( &format!("Decrease {label}"), "M5 12h14", (value > 0.).then(|| change((value - 1.).max(0.))), )) .push( self.label(format!("{value} px"), 12.) .width(48) .align_x(Alignment::Center), ) .push(self.settings_icon_button( &format!("Increase {label}"), "M12 5v14 M5 12h14", (value < maximum).then(|| change((value + 1.).min(maximum))), )), ), ); } rows = rows.push(spacing).push(self.panel_divider()); let index = placement .definition(panel) .groups .iter() .position(|candidate| candidate.id == group.id) .expect("the inspector selection refers to an item or group in the current panel"); rows = rows.push( self.inspector_actions( (index > 0).then(|| Message::PanelEdit(Action::EarlierGroup(group.id.clone()))), (index + 1 < placement.definition(panel).groups.len()) .then(|| Message::PanelEdit(Action::LaterGroup(group.id.clone()))), "Remove group", group .items .is_empty() .then(|| Message::PanelEdit(Action::RemoveGroup(group.id.clone()))), ), ); if !group.items.is_empty() { rows = rows .push(self.note("Move or remove this group’s items before removing the group.")); } rows.into() } fn inspector_actions( &self, earlier: Option, later: Option, remove_label: &str, remove: Option, ) -> Element<'static, Message> { widget::row([]) .spacing(8) .push(self.settings_icon_button("Up", "M6 15l6-6 6 6", earlier)) .push(self.settings_icon_button("Down", "M6 9l6 6 6-6", later)) .push(self.settings_icon_button( remove_label, "M4 6h16 M9 6V3h6v3 M6 6l1 15h10l1-15 M10 10v7 M14 10v7", remove, )) .into() } fn panel_field(&self, item: &Item, field: Field) -> Element<'static, Message> { let p = crate::visuals::Palette::from_resolved(&self.resolved.presented); let id = item.id.clone(); let name = field .path .rsplit('.') .next() .expect("splitting a field path always yields its final component") .to_owned(); let values = serde_json::to_value(item).expect("serializable panel item"); match field.kind { Kind::Choice { default, choices } => { let value = values .get(&name) .and_then(serde_json::Value::as_str) .unwrap_or(default); if name == "overflow" { let selected = choices .iter() .find(|(key, _)| *key == value) .map(|&(key, label)| Choice(key, label)); return widget::column([]) .spacing(8) .push(self.label(field.label, 13.)) .push(crate::visuals::panel_select( cosmic::iced::widget::pick_list( choices .iter() .map(|&(key, label)| Choice(key, label)) .collect::>(), selected, move |choice| { Message::PanelEdit(Action::Set( id.clone(), name.clone(), choice.0.into(), )) }, ) .font(self.font) .text_size(13) .padding([8, 10]) .width(Length::Fill), p, )) .push(self.note(&field.description)) .into(); } let options = widget::row(choices.iter().map(|&(key, label)| { widget::button::custom(self.label(label, 12.)) .name(format!("{}: {label}", field.label)) .padding([8, 4]) .width(Length::Fill) .class(crate::visuals::panel_button(p, value == key)) .on_press(Message::PanelEdit(Action::Set( id.clone(), name.clone(), key.into(), ))) .into() })) .spacing(2) .width(Length::Fill); let mut rows = widget::column([]) .spacing(8) .push(self.label(field.label, 13.)) .push( widget::container(options) .padding(3) .class(crate::visuals::surface(p.sidebar, 9.)), ); if !field.description.is_empty() { rows = rows.push(self.note(&field.description)); } rows.into() } Kind::Toggle(default) => { let enabled = values .get(&name) .and_then(serde_json::Value::as_bool) .unwrap_or(default); widget::row([]) .spacing(12) .align_y(Alignment::Center) .push(self.label(field.label.clone(), 13.).width(Length::Fill)) .push( ferese_theme::controls::switch(enabled, p) .name(format!( "{}: {}", field.label, if enabled { "on" } else { "off" } )) .on_press(Message::PanelEdit(Action::Set(id, name, (!enabled).into()))), ) .into() } _ => unreachable!("panel inspector fields are choices or toggles"), } } }