Add panel-wide styles and the Morph hub (#26)
Panel appearance now applies to the whole panel, with seven styles: Floating, Capsules, Notch, Edge to edge, Fused islands, Adaptive and Morph. - Share composition, geometry, and motion through the renderer-independent `ferese-panel` crate, including native materials, input regions, and Settings previews. - Add the Morph hub with media, calendar, notifications, controls, workspace previews, hover feedback, and attached mode. Keep recording and low-battery status visible and respect window clearance. - Cache workspace preview geometry, draw thumbnails directly, centralize Morph transitions, and keep rendering settings local to each shell instance. - Put Record Screen in the Morph header and show the display control only when an external monitor is connected. Update panel documentation, the desktop screenshot, and native check style names. Local validation included 660 compositor tests, 191 shell tests, isolated preview-cache checks, GPU Morph captures at 1×/1.25×/1.5×, formatting, and Clippy with warnings denied. The final header change passed all 23 Morph tests. Release workflow jobs are skipped for this PR.
This commit is contained in:
1 parent
42c3b290dd
commit
e22f0ebef9
131 files changed
+12562
-1634
No files matched your search
@@ -6,6 +6,7 @@ license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
ferese-panel = { path = "../../crates/ferese-panel" }
|
||||
ferese-theme-client = { path = "../../crates/ferese-theme-client" }
|
||||
ferese-theme = { path = "../../crates/ferese-theme" }
|
||||
ferese-config = { path = "../../crates/ferese-config" }
|
||||
|
||||
@@ -94,6 +94,8 @@ enum InitialPage {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Message {
|
||||
ReadRenderScale(cosmic::iced::window::Id),
|
||||
RenderScale(f32),
|
||||
Connections(connections::Input),
|
||||
ConnectionsReady(Result<std::sync::Arc<connections::Client>, String>),
|
||||
ConnectionsRefreshed(Box<connections::Snapshot>),
|
||||
@@ -114,9 +116,9 @@ enum Message {
|
||||
Change(Edit),
|
||||
PanelClearSelection,
|
||||
PanelPage(panel_controls::PanelPage),
|
||||
PanelSelect(ferese_config::panel::ItemId),
|
||||
PanelGroupSelect(ferese_config::panel::GroupId),
|
||||
PanelPreviewResolved(ferese_config::panel::layout::Resolution),
|
||||
PanelSelect(ferese_panel::ItemId),
|
||||
PanelGroupSelect(ferese_panel::GroupId),
|
||||
PanelPreviewResolved(ferese_panel::layout::Resolution),
|
||||
PanelPreviewOverflow,
|
||||
PanelEdit(panel_edit::Action),
|
||||
Draft(String, String),
|
||||
@@ -159,6 +161,7 @@ struct App {
|
||||
display_selection: Option<String>,
|
||||
display_error: Option<String>,
|
||||
core: Core,
|
||||
render_scale: f64,
|
||||
path: PathBuf,
|
||||
current: Snapshot,
|
||||
draft: Snapshot,
|
||||
@@ -187,10 +190,10 @@ struct App {
|
||||
undo_revision: u64,
|
||||
auto_details: bool,
|
||||
advanced_theme: bool,
|
||||
panel_selection: Option<ferese_config::panel::ItemId>,
|
||||
panel_selection: Option<ferese_panel::ItemId>,
|
||||
panel_page: panel_controls::PanelPage,
|
||||
panel_group_selection: Option<ferese_config::panel::GroupId>,
|
||||
panel_preview_resolution: ferese_config::panel::layout::Resolution,
|
||||
panel_group_selection: Option<ferese_panel::GroupId>,
|
||||
panel_preview_resolution: ferese_panel::layout::Resolution,
|
||||
panel_preview_overflow: bool,
|
||||
theme_file_target: usize,
|
||||
wallpaper: wallpaper_controls::State,
|
||||
@@ -263,6 +266,7 @@ impl cosmic::Application for App {
|
||||
display_selection: None,
|
||||
display_error: None,
|
||||
core,
|
||||
render_scale: 1.,
|
||||
path,
|
||||
draft: current.clone(),
|
||||
current,
|
||||
@@ -326,6 +330,13 @@ impl cosmic::Application for App {
|
||||
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::ReadRenderScale(id) => {
|
||||
return cosmic::iced::window::scale_factor(id)
|
||||
.map(|scale| cosmic::Action::App(Message::RenderScale(scale)));
|
||||
}
|
||||
Message::RenderScale(scale) if scale.is_finite() && scale > 0. && scale <= 16. => {
|
||||
self.render_scale = f64::from(scale);
|
||||
}
|
||||
Message::Connections(input) => return self.connections_input(input),
|
||||
Message::ConnectionsReady(result) => return self.connections_ready(result),
|
||||
Message::ConnectionsRefreshed(snapshot) => self.connections_refreshed(*snapshot),
|
||||
@@ -889,6 +900,21 @@ impl cosmic::Application for App {
|
||||
|
||||
fn subscription(&self) -> cosmic::iced::Subscription<Message> {
|
||||
cosmic::iced::Subscription::batch([
|
||||
cosmic::iced::event::listen_with(|event, _, id| match event {
|
||||
cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => {
|
||||
Some(Message::ReadRenderScale(id))
|
||||
}
|
||||
cosmic::iced::Event::Window(cosmic::iced::window::Event::Rescaled(scale)) => {
|
||||
Some(Message::RenderScale(scale))
|
||||
}
|
||||
cosmic::iced::Event::PlatformSpecific(cosmic::iced::event::PlatformSpecific::Wayland(
|
||||
cosmic::iced::event::wayland::Event::Output(
|
||||
cosmic::iced::event::wayland::OutputEvent::InfoUpdate(_),
|
||||
_,
|
||||
),
|
||||
)) => Some(Message::ReadRenderScale(id)),
|
||||
_ => None,
|
||||
}),
|
||||
if self.sidebar_open {
|
||||
cosmic::iced::event::listen_with(|event, _, _| match event {
|
||||
cosmic::iced::Event::Keyboard(cosmic::iced::keyboard::Event::KeyPressed {
|
||||
@@ -1102,7 +1128,7 @@ mod tests {
|
||||
let _ = app.update(Message::Range(field.clone(), 23.));
|
||||
assert_eq!(app.draft.source, before);
|
||||
let _ = app.update(Message::Release(field));
|
||||
let radii: ferese_config::panel::CornerRadii =
|
||||
let radii: ferese_panel::CornerRadii =
|
||||
serde_json::from_value(app.draft.item("panels.0.corner_radius").unwrap().clone()).unwrap();
|
||||
assert_eq!(radii.0, [4., 8., 23., 16.]);
|
||||
assert!(app.ranges.is_empty());
|
||||
@@ -1111,7 +1137,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn pending_panel_moves_save_both_groups_together_and_keep_the_selected_instance() {
|
||||
use ferese_config::panel::{GroupId, ItemId};
|
||||
use ferese_panel::{GroupId, ItemId};
|
||||
let mut app = app();
|
||||
let mut initial = Snapshot::parse(String::new()).unwrap();
|
||||
initial.edit(&panel_controls::initialize(&initial).unwrap()).unwrap();
|
||||
@@ -1139,13 +1165,13 @@ mod tests {
|
||||
assert!(app.saving, "the complete queued draft is saved once");
|
||||
assert!(app.pending.is_empty());
|
||||
assert_eq!(app.draft.doc.value(), &expected);
|
||||
let panels: Vec<ferese_config::panel::Panel> =
|
||||
let panels: Vec<ferese_panel::Panel> =
|
||||
serde_json::from_value(app.draft.item("panels").unwrap().clone()).unwrap();
|
||||
ferese_config::panel::validate(&panels).unwrap();
|
||||
ferese_panel::validate(&panels).unwrap();
|
||||
assert_eq!(panels[0].start.groups[1].items[1].id, id);
|
||||
assert_eq!(
|
||||
panels[0].start.groups[1].items[1].overflow,
|
||||
ferese_config::panel::OverflowPolicy::Always
|
||||
ferese_panel::OverflowPolicy::Always
|
||||
);
|
||||
assert!(!panels[0].end.groups[0].items.iter().any(|item| item.id == id));
|
||||
}
|
||||
@@ -1181,7 +1207,7 @@ mod tests {
|
||||
};
|
||||
app.panel_page = panel_controls::PanelPage::Arrange;
|
||||
let collapsed = nodes(&app);
|
||||
let id = ferese_config::panel::ItemId("clock".into());
|
||||
let id = ferese_panel::ItemId("clock".into());
|
||||
let _ = app.update(Message::PanelSelect(id.clone()));
|
||||
let expanded = nodes(&app);
|
||||
assert!(expanded > collapsed);
|
||||
@@ -1193,7 +1219,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn pending_group_moves_and_style_changes_save_one_coherent_draft() {
|
||||
use ferese_config::panel::GroupId;
|
||||
use ferese_panel::GroupId;
|
||||
let mut app = app();
|
||||
let mut initial = Snapshot::parse(String::new()).unwrap();
|
||||
initial.edit(&panel_controls::initialize(&initial).unwrap()).unwrap();
|
||||
@@ -1225,7 +1251,7 @@ mod tests {
|
||||
fn selecting_a_generated_preview_item_does_not_save_the_composition() {
|
||||
let mut app = app();
|
||||
app.saving = true;
|
||||
let id = ferese_config::panel::ItemId("clock".into());
|
||||
let id = ferese_panel::ItemId("clock".into());
|
||||
let _ = app.update(Message::PanelSelect(id.clone()));
|
||||
assert_eq!(app.panel_selection, Some(id));
|
||||
assert!(app.draft.item("panels").is_none());
|
||||
@@ -1244,19 +1270,17 @@ mod tests {
|
||||
assert!(app.draft.item("panels").is_none());
|
||||
}
|
||||
app.saving = true;
|
||||
let _ = app.update(Message::PanelSelect(ferese_config::panel::ItemId("clock".into())));
|
||||
let _ = app.update(Message::PanelSelect(ferese_panel::ItemId("clock".into())));
|
||||
assert_eq!(app.panel_page, panel_controls::PanelPage::Arrange);
|
||||
assert!(app.panel_group_selection.is_none());
|
||||
let _ = app.update(Message::PanelGroupSelect(ferese_config::panel::GroupId(
|
||||
"status".into(),
|
||||
)));
|
||||
let _ = app.update(Message::PanelGroupSelect(ferese_panel::GroupId("status".into())));
|
||||
assert!(app.panel_selection.is_none());
|
||||
assert_eq!(app.panel_page, panel_controls::PanelPage::Arrange);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dropped_items_follow_queued_group_moves_and_are_saved_once() {
|
||||
use ferese_config::panel::{GroupId, ItemId};
|
||||
use ferese_panel::{GroupId, ItemId};
|
||||
let mut app = app();
|
||||
let mut initial = Snapshot::parse(String::new()).unwrap();
|
||||
initial.edit(&panel_controls::initialize(&initial).unwrap()).unwrap();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ferese_config::panel::{Edge, ItemKind, Panel};
|
||||
use ferese_panel::{Edge, ItemKind, Panel};
|
||||
|
||||
use crate::panel_edit::{Action, Destination, Zone};
|
||||
use crate::{App, Element, Message, schema::Page, store, widget};
|
||||
@@ -12,7 +12,7 @@ pub(super) enum PanelPage {
|
||||
|
||||
pub(super) fn initialize(_snapshot: &store::Snapshot) -> Result<store::Edit, String> {
|
||||
let panel = Panel::default();
|
||||
ferese_config::panel::validate(std::slice::from_ref(&panel))?;
|
||||
ferese_panel::validate(std::slice::from_ref(&panel))?;
|
||||
Ok(store::set(
|
||||
"panels",
|
||||
serde_json::to_value(vec![panel]).map_err(|error| error.to_string())?,
|
||||
@@ -26,10 +26,18 @@ impl std::fmt::Display for CatalogItem {
|
||||
formatter.write_str(self.0.label())
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
struct StyleChoice(ferese_panel::PanelStyle);
|
||||
impl std::fmt::Display for StyleChoice {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str(self.0.label())
|
||||
}
|
||||
}
|
||||
|
||||
const CATALOG: [CatalogItem; 12] = [
|
||||
CatalogItem(ItemKind::Overview),
|
||||
CatalogItem(ItemKind::Workspaces {
|
||||
style: ferese_config::panel::WorkspaceStyle::Dots,
|
||||
style: ferese_panel::WorkspaceStyle::Dots,
|
||||
}),
|
||||
CatalogItem(ItemKind::FocusedWindow),
|
||||
CatalogItem(ItemKind::Media),
|
||||
@@ -104,11 +112,6 @@ impl App {
|
||||
.class(crate::visuals::surface(palette.card, 14.)),
|
||||
);
|
||||
let preset = self.preview_panel().ok().and_then(|panel| panel.preset());
|
||||
let preset_key = match preset {
|
||||
Some(ferese_config::PanelPreset::Continuous) => "continuous",
|
||||
Some(ferese_config::PanelPreset::Islands) => "islands",
|
||||
None => "custom",
|
||||
};
|
||||
let position = widget::row([Edge::Top, Edge::Bottom].into_iter().map(|position| {
|
||||
let label = if position == Edge::Top { "Top" } else { "Bottom" };
|
||||
widget::button::custom(self.label(label, 12.))
|
||||
@@ -134,53 +137,20 @@ impl App {
|
||||
.padding(16)
|
||||
.class(crate::visuals::surface(palette.card, 14.)),
|
||||
);
|
||||
let appearance = widget::column([])
|
||||
let mut appearance = widget::column([])
|
||||
.spacing(16)
|
||||
.push(self.panel_heading("Appearance", 15.))
|
||||
.push(self.panel_appearance_row(
|
||||
"Background",
|
||||
"Apply a preset, then customize each group.",
|
||||
self.panel_surface_choices(
|
||||
preset_key,
|
||||
&[("continuous", "Continuous"), ("islands", "Islands")],
|
||||
move |key| {
|
||||
Message::PanelEdit(Action::ApplyPreset(if key == "islands" {
|
||||
ferese_config::PanelPreset::Islands
|
||||
} else {
|
||||
ferese_config::PanelPreset::Continuous
|
||||
}))
|
||||
},
|
||||
),
|
||||
))
|
||||
.push(
|
||||
self.panel_appearance_row(
|
||||
"Default group surface",
|
||||
"Groups can override this in their inspector.",
|
||||
self.panel_surface_choices(
|
||||
self.preview_panel()
|
||||
.map_or(ferese_config::panel::GroupSurface::None, |panel| panel.group_surface)
|
||||
.key(),
|
||||
&[("none", "None"), ("inset", "Inset"), ("island", "Island")],
|
||||
|key| {
|
||||
Message::PanelEdit(Action::SetDefaultSurface(match key {
|
||||
"inset" => ferese_config::panel::GroupSurface::Inset,
|
||||
"island" => ferese_config::panel::GroupSurface::Island,
|
||||
_ => ferese_config::panel::GroupSurface::None,
|
||||
}))
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.push(self.panel_style_choices(preset.unwrap_or_default()))
|
||||
.push(self.panel_divider())
|
||||
.push(self.panel_opacity_controls())
|
||||
.push(self.panel_divider())
|
||||
.push(
|
||||
self.panel_appearance_row(
|
||||
"Group borders",
|
||||
"Outline each group of items.",
|
||||
"Outline",
|
||||
"Add a fine line around the panel shape.",
|
||||
widget::container(
|
||||
ferese_theme::controls::switch(self.draft.boolean("panels.0.border", false), palette)
|
||||
.name("Group borders")
|
||||
.name("Outline")
|
||||
.on_press(Message::PanelEdit(Action::SetBorder(
|
||||
!self.draft.boolean("panels.0.border", false),
|
||||
))),
|
||||
@@ -190,7 +160,44 @@ impl App {
|
||||
),
|
||||
)
|
||||
.push(self.panel_divider())
|
||||
.push(self.panel_radius_controls());
|
||||
.push(if preset == Some(ferese_panel::PanelStyle::Flat) {
|
||||
widget::Space::new().height(0).into()
|
||||
} else {
|
||||
self.panel_radius_controls()
|
||||
});
|
||||
if preset == Some(ferese_panel::PanelStyle::Morph) {
|
||||
let enabled = self.draft.boolean("panels.0.morph.peek_on_hover", true);
|
||||
let attached = self.draft.boolean("panels.0.morph.attached", false);
|
||||
appearance = appearance
|
||||
.push(self.panel_divider())
|
||||
.push(
|
||||
self.panel_appearance_row(
|
||||
"Attach to screen edge",
|
||||
"Join the island to the screen edge with curved shoulders.",
|
||||
widget::container(
|
||||
ferese_theme::controls::switch(attached, palette)
|
||||
.name("Attach to screen edge")
|
||||
.on_press(Message::PanelEdit(Action::SetMorphAttached(!attached))),
|
||||
)
|
||||
.align_right(cosmic::iced::Length::Fill)
|
||||
.into(),
|
||||
),
|
||||
)
|
||||
.push(self.panel_divider())
|
||||
.push(
|
||||
self.panel_appearance_row(
|
||||
"Preview on hover",
|
||||
"Expand the island when you hover over it.",
|
||||
widget::container(
|
||||
ferese_theme::controls::switch(enabled, palette)
|
||||
.name("Preview on hover")
|
||||
.on_press(Message::PanelEdit(Action::SetPeekOnHover(!enabled))),
|
||||
)
|
||||
.align_right(cosmic::iced::Length::Fill)
|
||||
.into(),
|
||||
),
|
||||
);
|
||||
}
|
||||
rows = rows.push(
|
||||
widget::container(appearance)
|
||||
.id("panel-appearance-settings")
|
||||
@@ -198,21 +205,70 @@ impl App {
|
||||
.width(cosmic::iced::Length::Fill)
|
||||
.class(crate::visuals::surface(palette.card, 14.)),
|
||||
);
|
||||
let mut settings = widget::column([])
|
||||
.spacing(4)
|
||||
.push(widget::container(self.panel_heading("Size & spacing", 15.)).padding([4, 10]));
|
||||
let pins = self.preview_panel().is_ok_and(|panel| {
|
||||
panel.geometry != ferese_panel::GeometryIntent::default() || panel.corner_radius.is_some()
|
||||
});
|
||||
let mut settings = widget::column([]).spacing(4).push(
|
||||
widget::container(
|
||||
widget::row![
|
||||
self.panel_heading("Size & spacing", 15.)
|
||||
.width(cosmic::iced::Length::Fill),
|
||||
self.settings_button(
|
||||
"Reset to style",
|
||||
"M3 12a9 9 0 1 0 3-6 M3 3v6h6",
|
||||
pins.then_some(Message::PanelEdit(Action::ResetShape)),
|
||||
false
|
||||
)
|
||||
]
|
||||
.align_y(cosmic::iced::Alignment::Center),
|
||||
)
|
||||
.padding([4, 10]),
|
||||
);
|
||||
for field in crate::schema::fields(Page::Bar)
|
||||
.into_iter()
|
||||
.filter(|field| field.path.starts_with("panels.0.geometry."))
|
||||
.filter(|field| {
|
||||
if preset == Some(ferese_panel::PanelStyle::Morph)
|
||||
&& self.draft.boolean("panels.0.morph.attached", false)
|
||||
&& field.path.ends_with(".edge_margin")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
!matches!(
|
||||
preset.unwrap_or_default(),
|
||||
ferese_panel::PanelStyle::Flat | ferese_panel::PanelStyle::Notch
|
||||
) || !matches!(field.path.rsplit('.').next(), Some("edge_margin" | "side_margins"))
|
||||
})
|
||||
{
|
||||
let mut field = field;
|
||||
if preset == Some(ferese_panel::PanelStyle::Morph) {
|
||||
if matches!(field.path.rsplit('.').next(), Some("inner_padding" | "side_margins")) {
|
||||
continue;
|
||||
}
|
||||
if field.path.ends_with(".height") {
|
||||
field.label = "Collapsed height".into();
|
||||
}
|
||||
}
|
||||
if let crate::schema::Kind::Range { default, .. } = &mut field.kind {
|
||||
let geometry =
|
||||
serde_json::to_value(self.preview_panel().map(|panel| panel.geometry).unwrap_or_default())
|
||||
.unwrap();
|
||||
let geometry = serde_json::to_value(
|
||||
self.preview_panel()
|
||||
.map(|panel| panel.resolved_geometry())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.unwrap();
|
||||
*default = geometry[field.path.rsplit('.').next().unwrap()].as_f64().unwrap();
|
||||
}
|
||||
settings = settings.push(self.field(field));
|
||||
let pinned = self.draft.item(&field.path).is_some();
|
||||
let reset = pinned.then_some(Message::PanelEdit(Action::ClearGeometry(field.path.clone())));
|
||||
let reset_label = format!("Reset {}", field.label);
|
||||
settings = settings.push(
|
||||
widget::row![
|
||||
self.field(field),
|
||||
self.settings_icon_button(&reset_label, "M3 12a9 9 0 1 0 3-6 M3 3v6h6", reset)
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(cosmic::iced::Alignment::Center),
|
||||
);
|
||||
}
|
||||
rows = rows.push(
|
||||
widget::container(settings)
|
||||
@@ -241,10 +297,11 @@ impl App {
|
||||
return None;
|
||||
}
|
||||
let panel = self.preview_panel().ok()?;
|
||||
Some(
|
||||
self.panel_preview_resolution
|
||||
.side_margin(4096, display.logical_width?, panel.geometry.inner_padding),
|
||||
)
|
||||
Some(self.panel_preview_resolution.side_margin(
|
||||
4096,
|
||||
display.logical_width?,
|
||||
panel.resolved_geometry().inner_padding,
|
||||
))
|
||||
}
|
||||
|
||||
fn panel_appearance_row(
|
||||
@@ -331,13 +388,16 @@ impl App {
|
||||
fn panel_radius_controls(&self) -> Element<'static, Message> {
|
||||
use cosmic::iced::{Alignment, Length};
|
||||
let palette = crate::visuals::Palette::from_resolved(&self.resolved.presented);
|
||||
let shell_radius = self.resolved.presented.tokens.geometry.shell_radius as f32;
|
||||
let shell_radius = self
|
||||
.preview_panel()
|
||||
.map(|panel| panel.resolved_geometry().height * 0.5)
|
||||
.unwrap_or(18.);
|
||||
let custom = self
|
||||
.draft
|
||||
.item("panels.0.corner_radius")
|
||||
.and_then(|value| serde_json::from_value::<ferese_config::panel::CornerRadii>(value.clone()).ok());
|
||||
let radii = custom.unwrap_or_else(|| ferese_config::panel::CornerRadii::uniform(shell_radius));
|
||||
let modes = widget::row([("Shell default", false), ("Custom", true)].map(|(label, selected)| {
|
||||
.and_then(|value| serde_json::from_value::<ferese_panel::CornerRadii>(value.clone()).ok());
|
||||
let radii = custom.unwrap_or_else(|| ferese_panel::CornerRadii::uniform(shell_radius));
|
||||
let modes = widget::row([("Automatic", false), ("Custom", true)].map(|(label, selected)| {
|
||||
widget::button::custom(self.label(label, 12.).align_x(Alignment::Center))
|
||||
.name(format!("Corner radius: {label}"))
|
||||
.padding([8, 12])
|
||||
@@ -359,10 +419,10 @@ impl App {
|
||||
let mut corners = Vec::new();
|
||||
// Match the physical corners: top row first, then bottom-left / bottom-right.
|
||||
for (index, label, icon) in [
|
||||
(0, "Top left", "M5 19V9a4 4 0 0 1 4-4h10"),
|
||||
(1, "Top right", "M5 5h10a4 4 0 0 1 4 4v10"),
|
||||
(3, "Bottom left", "M5 5v10a4 4 0 0 0 4 4h10"),
|
||||
(2, "Bottom right", "M5 19h10a4 4 0 0 0 4-4V5"),
|
||||
(0, "Edge left", "M5 19V9a4 4 0 0 1 4-4h10"),
|
||||
(1, "Edge right", "M5 5h10a4 4 0 0 1 4 4v10"),
|
||||
(3, "Outer left", "M5 5v10a4 4 0 0 0 4 4h10"),
|
||||
(2, "Outer right", "M5 19h10a4 4 0 0 0 4-4V5"),
|
||||
] {
|
||||
let value = radii.0[index];
|
||||
let tint = if custom.is_some() { palette.text } else { palette.muted };
|
||||
@@ -406,10 +466,21 @@ impl App {
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if custom.is_none() {
|
||||
return self.panel_appearance_row(
|
||||
"Corner radius",
|
||||
"Corners follow the style and panel height.",
|
||||
modes.into(),
|
||||
);
|
||||
}
|
||||
let bottom_corners = corners.split_off(2);
|
||||
let mut content = widget::column([])
|
||||
let content = widget::column([])
|
||||
.spacing(12)
|
||||
.push(self.panel_appearance_row("Corner radius", "Set the rounding of each corner.", modes.into()))
|
||||
.push(self.panel_appearance_row(
|
||||
"Corner radius",
|
||||
"Pins stay in place when you change styles. Top and bottom panels mirror them.",
|
||||
modes.into(),
|
||||
))
|
||||
.push(
|
||||
widget::column([])
|
||||
.spacing(8)
|
||||
@@ -426,14 +497,7 @@ impl App {
|
||||
.width(Length::Fill),
|
||||
),
|
||||
);
|
||||
if self.preview_panel().is_ok_and(|panel| panel.geometry.edge_margin == 0) {
|
||||
let bottom = self.preview_panel().is_ok_and(|panel| panel.edge == Edge::Bottom);
|
||||
content = content.push(self.note(if bottom {
|
||||
"Bottom corners stay square while the panel touches the bottom edge."
|
||||
} else {
|
||||
"Top corners stay square while the panel touches the top edge."
|
||||
}));
|
||||
}
|
||||
|
||||
content.into()
|
||||
}
|
||||
|
||||
@@ -536,51 +600,45 @@ impl App {
|
||||
.into()
|
||||
}
|
||||
|
||||
fn panel_surface_choices(
|
||||
&self,
|
||||
selected: &str,
|
||||
options: &'static [(&'static str, &'static str)],
|
||||
message: impl Fn(&'static str) -> Message,
|
||||
) -> Element<'static, Message> {
|
||||
use cosmic::iced::{Alignment, Length};
|
||||
fn panel_style_choices(&self, selected: ferese_panel::PanelStyle) -> Element<'static, Message> {
|
||||
use cosmic::iced::Length;
|
||||
use ferese_panel::PanelStyle;
|
||||
let palette = crate::visuals::Palette::from_resolved(&self.resolved.presented);
|
||||
let options = widget::row(options.iter().map(|&(key, label)| {
|
||||
widget::button::custom(
|
||||
widget::row([])
|
||||
.spacing(8)
|
||||
.align_y(Alignment::Center)
|
||||
.push(crate::visuals::action_icon(
|
||||
match key {
|
||||
"continuous" => "M3 7h18v10H3z",
|
||||
"none" => "M4 4l16 16 M4 20L20 4",
|
||||
"inset" => "M3 5h18v14H3z M7 9h10v6H7z",
|
||||
_ => "M2 7h5v10H2z M10 7h4v10h-4z M17 7h5v10h-5z",
|
||||
},
|
||||
palette.muted,
|
||||
))
|
||||
.push(self.label(label, 12.)),
|
||||
let description = match selected {
|
||||
PanelStyle::Floating => "Separate floating groups",
|
||||
PanelStyle::Capsules => "Connected groups with rounded ends",
|
||||
PanelStyle::Notch => "Attached to the screen edge",
|
||||
PanelStyle::Flat => "A continuous edge-to-edge panel",
|
||||
PanelStyle::Fused => "Groups blend where they meet",
|
||||
PanelStyle::Adaptive => "Follows the active window state",
|
||||
PanelStyle::Morph => "A clock hub with expandable controls",
|
||||
_ => "",
|
||||
};
|
||||
self.panel_appearance_row(
|
||||
"Style",
|
||||
description,
|
||||
crate::visuals::panel_select(
|
||||
cosmic::iced::widget::pick_list(
|
||||
PanelStyle::AVAILABLE.map(StyleChoice).to_vec(),
|
||||
Some(StyleChoice(selected)),
|
||||
|style| Message::PanelEdit(Action::ApplyPreset(style.0)),
|
||||
)
|
||||
.font(self.font)
|
||||
.text_size(12)
|
||||
.padding([6, 10])
|
||||
.width(Length::Fill),
|
||||
palette,
|
||||
)
|
||||
.name(format!("Background: {label}"))
|
||||
.padding([8, 10])
|
||||
.width(Length::Fill)
|
||||
.class(crate::visuals::panel_button(palette, selected == key))
|
||||
.on_press(message(key))
|
||||
.into()
|
||||
}))
|
||||
.spacing(2);
|
||||
widget::container(options)
|
||||
.padding(3)
|
||||
.width(Length::Fill)
|
||||
.class(crate::visuals::surface(palette.sidebar, 9.))
|
||||
.into()
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
fn panel_editor(&self, panel: &Panel) -> Element<'static, Message> {
|
||||
use cosmic::iced::{Alignment, Length};
|
||||
let palette = crate::visuals::Palette::from_resolved(&self.resolved.presented);
|
||||
let mut zones = widget::column([]).spacing(18);
|
||||
if panel.group_count() == ferese_config::panel::MAX_GROUPS {
|
||||
zones = zones.push(self.label(format!("{}-group limit reached", ferese_config::panel::MAX_GROUPS), 12.));
|
||||
if panel.group_count() == ferese_panel::MAX_GROUPS {
|
||||
zones = zones.push(self.label(format!("{}-group limit reached", ferese_panel::MAX_GROUPS), 12.));
|
||||
}
|
||||
for placement in Zone::ALL {
|
||||
let zone = placement.definition(panel);
|
||||
@@ -592,7 +650,7 @@ impl App {
|
||||
self.settings_button(
|
||||
"Add group",
|
||||
"M12 5v14 M5 12h14",
|
||||
(panel.group_count() < ferese_config::panel::MAX_GROUPS)
|
||||
(panel.group_count() < ferese_panel::MAX_GROUPS)
|
||||
.then_some(Message::PanelEdit(Action::AddGroup(placement))),
|
||||
false,
|
||||
),
|
||||
@@ -787,8 +845,8 @@ mod tests {
|
||||
modes: Vec::new(),
|
||||
}];
|
||||
app.panel_preview_resolution.items.insert(
|
||||
ferese_config::panel::ItemId("clock".into()),
|
||||
ferese_config::panel::layout::Placement::Overflow,
|
||||
ferese_panel::ItemId("clock".into()),
|
||||
ferese_panel::layout::Placement::Overflow,
|
||||
);
|
||||
app.panel_preview_resolution.minimum_width = 600.;
|
||||
assert_eq!(app.panel_side_margin_limit(), Some(408));
|
||||
@@ -806,8 +864,8 @@ mod tests {
|
||||
snapshot.edit(&initialize(&snapshot).unwrap()).unwrap();
|
||||
let snapshot = store::Snapshot::parse(snapshot.source).unwrap();
|
||||
let panels: Vec<Panel> = serde_json::from_value(snapshot.item("panels").unwrap().clone()).unwrap();
|
||||
assert_eq!(panels[0].preset(), Some(ferese_config::PanelPreset::Continuous));
|
||||
assert_eq!(panels[0].geometry, ferese_config::panel::PanelGeometry::default());
|
||||
assert_eq!(panels[0].preset(), Some(ferese_panel::PanelStyle::Flat));
|
||||
assert_eq!(panels[0].resolved_geometry(), ferese_panel::PanelGeometry::default());
|
||||
assert!(panels[0].center.groups[0].items[0].visible);
|
||||
assert_eq!(snapshot.number("animations.speed", 1.), 0.8);
|
||||
assert!(snapshot.source.contains("// keep"));
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
//! Local item dragging. Pointer motion changes only the drop hint; release saves once.
|
||||
use cosmic::iced::advanced::{Clipboard, Layout, Shell, Widget, layout, mouse, renderer, widget};
|
||||
use cosmic::iced::{Border, Color, Event, Length, Point, Rectangle, Size, Vector, keyboard};
|
||||
use cosmic::{Element, Renderer, Theme};
|
||||
use ferese_config::panel::{GroupId, ItemId, Panel};
|
||||
use ferese_panel::{GroupId, ItemId, Panel};
|
||||
|
||||
use crate::Message;
|
||||
use crate::panel_edit::{Action, Destination, Zone};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ferese_config::panel::{GroupId, Item, ItemId, ItemKind, Panel};
|
||||
use ferese_panel::{GroupId, Item, ItemId, ItemKind, Panel};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::store::{Edit, Snapshot, set};
|
||||
@@ -26,14 +26,16 @@ impl std::fmt::Display for Destination {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) enum Action {
|
||||
ApplyPreset(ferese_config::PanelPreset),
|
||||
SetDefaultSurface(ferese_config::panel::GroupSurface),
|
||||
ClearGroupSurface(GroupId),
|
||||
SetEdge(ferese_config::panel::Edge),
|
||||
ApplyPreset(ferese_panel::PanelStyle),
|
||||
ResetShape,
|
||||
ClearGeometry(String),
|
||||
SetEdge(ferese_panel::Edge),
|
||||
SetGeometry(String, Value),
|
||||
SetRadius(String),
|
||||
SetCorner(usize, f32),
|
||||
SetBorder(bool),
|
||||
SetPeekOnHover(bool),
|
||||
SetMorphAttached(bool),
|
||||
SetOpacity(f32),
|
||||
ClearOpacity,
|
||||
ClearRadius,
|
||||
@@ -71,7 +73,7 @@ impl Zone {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn definition(self, panel: &Panel) -> &ferese_config::panel::Zone {
|
||||
pub fn definition(self, panel: &Panel) -> &ferese_panel::Zone {
|
||||
match self {
|
||||
Self::Start => &panel.start,
|
||||
Self::Center => &panel.center,
|
||||
@@ -168,30 +170,29 @@ pub(super) fn plan(snapshot: &Snapshot, action: Action) -> Result<Vec<Edit>, Str
|
||||
}
|
||||
let panels: Vec<Panel> = serde_json::from_value(snapshot.item("panels").ok_or("Customize items first.")?.clone())
|
||||
.map_err(|error| error.to_string())?;
|
||||
ferese_config::panel::validate(&panels)?;
|
||||
ferese_panel::validate(&panels)?;
|
||||
let panel = &panels[0];
|
||||
let earlier = matches!(&action, Action::Earlier(_));
|
||||
let earlier_group = matches!(&action, Action::EarlierGroup(_));
|
||||
let edits = match action {
|
||||
Action::ApplyPreset(preset) => {
|
||||
let mut edited = panel.clone();
|
||||
edited.apply_preset(preset);
|
||||
let mut edits = vec![
|
||||
set("panels.0.surface", serde_json::to_value(edited.surface).unwrap()),
|
||||
set(
|
||||
"panels.0.group_surface",
|
||||
serde_json::to_value(edited.group_surface).unwrap(),
|
||||
),
|
||||
];
|
||||
for group in Zone::ALL.into_iter().flat_map(|zone| &zone.definition(panel).groups) {
|
||||
if group.surface.is_some() {
|
||||
edits.push(Edit::Unset(format!("{}.surface", group_record_path(panel, &group.id)?)));
|
||||
}
|
||||
Action::ApplyPreset(style) => vec![set("panels.0.style", style.key())],
|
||||
Action::ResetShape => vec![
|
||||
Edit::Unset("panels.0.geometry".into()),
|
||||
Edit::Unset("panels.0.corner_radius".into()),
|
||||
],
|
||||
Action::ClearGeometry(path) => {
|
||||
if !matches!(
|
||||
path.as_str(),
|
||||
"panels.0.geometry.height"
|
||||
| "panels.0.geometry.edge_margin"
|
||||
| "panels.0.geometry.side_margins"
|
||||
| "panels.0.geometry.window_clearance"
|
||||
| "panels.0.geometry.inner_padding"
|
||||
) {
|
||||
return Err("Unknown panel geometry setting.".into());
|
||||
}
|
||||
edits
|
||||
vec![Edit::Unset(path)]
|
||||
}
|
||||
Action::SetDefaultSurface(surface) => vec![set("panels.0.group_surface", surface.key())],
|
||||
Action::ClearGroupSurface(id) => vec![Edit::Unset(format!("{}.surface", group_record_path(panel, &id)?))],
|
||||
Action::SetEdge(edge) => vec![set("panels.0.edge", edge.key())],
|
||||
Action::SetGeometry(field, value) => {
|
||||
if !matches!(
|
||||
@@ -204,6 +205,8 @@ pub(super) fn plan(snapshot: &Snapshot, action: Action) -> Result<Vec<Edit>, Str
|
||||
}
|
||||
Action::SetOpacity(value) => vec![set("panels.0.background_opacity", value)],
|
||||
Action::ClearOpacity => vec![Edit::Unset("panels.0.background_opacity".into())],
|
||||
Action::SetMorphAttached(enabled) => vec![set("panels.0.morph.attached", enabled)],
|
||||
Action::SetPeekOnHover(enabled) => vec![set("panels.0.morph.peek_on_hover", enabled)],
|
||||
Action::SetBorder(enabled) => vec![set("panels.0.border", enabled)],
|
||||
Action::SetCorner(index, value) => {
|
||||
let mut radii = panel.corner_radius.ok_or("Customize corners first.")?;
|
||||
@@ -212,7 +215,7 @@ pub(super) fn plan(snapshot: &Snapshot, action: Action) -> Result<Vec<Edit>, Str
|
||||
vec![set("panels.0.corner_radius", radii.to_string())]
|
||||
}
|
||||
Action::SetRadius(value) => {
|
||||
let radii: ferese_config::panel::CornerRadii = value.parse()?;
|
||||
let radii: ferese_panel::CornerRadii = value.parse()?;
|
||||
vec![set("panels.0.corner_radius", radii.to_string())]
|
||||
}
|
||||
Action::ClearRadius => vec![Edit::Unset("panels.0.corner_radius".into())],
|
||||
@@ -407,7 +410,7 @@ pub(super) fn plan(snapshot: &Snapshot, action: Action) -> Result<Vec<Edit>, Str
|
||||
}
|
||||
_ => (field, value),
|
||||
};
|
||||
if !matches!(field.as_str(), "spacing" | "padding" | "island_padding" | "surface") {
|
||||
if !matches!(field.as_str(), "spacing" | "padding" | "island_padding") {
|
||||
return Err("Unknown panel group setting.".into());
|
||||
}
|
||||
vec![set(&format!("{}.{field}", group_record_path(panel, &id)?), value)]
|
||||
@@ -434,9 +437,13 @@ mod tests {
|
||||
snapshot.edit(&edit).unwrap();
|
||||
}
|
||||
let panels: Vec<Panel> = serde_json::from_value(snapshot.item("panels").unwrap().clone()).unwrap();
|
||||
assert_eq!(panels[0].geometry.height, 28.);
|
||||
assert_eq!(panels[0].geometry.edge_margin, 0);
|
||||
assert_eq!(panels[0].geometry.side_margins, 18);
|
||||
assert_eq!(panels[0].resolved_geometry().height, 36.);
|
||||
assert_eq!(panels[0].resolved_geometry().edge_margin, 0);
|
||||
assert_eq!(panels[0].geometry.side_margins, Some(18));
|
||||
assert_eq!(panels[0].resolved_geometry().side_margins, 0);
|
||||
let mut floating = panels[0].clone();
|
||||
floating.apply_preset(ferese_panel::PanelStyle::Floating);
|
||||
assert_eq!(floating.resolved_geometry().side_margins, 18);
|
||||
assert!(snapshot.source.contains("// keep"));
|
||||
assert!(plan(&snapshot, Action::SetGeometry("height".into(), 12.into())).is_err());
|
||||
}
|
||||
@@ -444,7 +451,7 @@ mod tests {
|
||||
#[test]
|
||||
fn group_capacity_is_checked_before_saving_and_removal_reopens_a_slot() {
|
||||
let mut snapshot = Snapshot::parse("panel main {}".into()).unwrap();
|
||||
for _ in 0..ferese_config::panel::MAX_GROUPS {
|
||||
for _ in 0..ferese_panel::MAX_GROUPS {
|
||||
apply(&mut snapshot, Action::AddGroup(Zone::End));
|
||||
}
|
||||
let source = snapshot.source.clone();
|
||||
@@ -497,6 +504,42 @@ animations { speed 0.8; }
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn morph_attachment_restores_floating_margin_and_preserves_clearance() {
|
||||
let mut snapshot =
|
||||
Snapshot::parse("panel main { style morph; geometry { edge-margin 9; window-clearance 3; }; }".into())
|
||||
.unwrap();
|
||||
let before = serde_json::from_value::<Vec<Panel>>(snapshot.item("panels").unwrap().clone()).unwrap();
|
||||
apply(&mut snapshot, Action::SetMorphAttached(true));
|
||||
let attached = serde_json::from_value::<Vec<Panel>>(snapshot.item("panels").unwrap().clone()).unwrap();
|
||||
assert_eq!(attached[0].resolved_geometry().edge_margin, 0);
|
||||
assert_eq!(attached[0].resolved_geometry().window_clearance, 3);
|
||||
assert_eq!(attached[0].exclusive_zone(false), before[0].exclusive_zone(false));
|
||||
assert!(attached[0].shape_parameters().attached);
|
||||
apply(&mut snapshot, Action::SetMorphAttached(false));
|
||||
assert_eq!(
|
||||
serde_json::from_value::<Vec<Panel>>(snapshot.item("panels").unwrap().clone()).unwrap(),
|
||||
before
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_preference_round_trips_without_changing_shape_or_items() {
|
||||
let mut snapshot = Snapshot::parse("panel main { style morph; morph { idle-width 180; }; }".into()).unwrap();
|
||||
let before = serde_json::from_value::<Vec<Panel>>(snapshot.item("panels").unwrap().clone()).unwrap();
|
||||
apply(&mut snapshot, Action::SetPeekOnHover(false));
|
||||
let after = serde_json::from_value::<Vec<Panel>>(snapshot.item("panels").unwrap().clone()).unwrap();
|
||||
assert!(!after[0].morph.peek_on_hover);
|
||||
let mut expected = before.clone();
|
||||
expected[0].morph.peek_on_hover = false;
|
||||
assert_eq!(after, expected);
|
||||
apply(&mut snapshot, Action::SetPeekOnHover(true));
|
||||
assert_eq!(
|
||||
serde_json::from_value::<Vec<Panel>>(snapshot.item("panels").unwrap().clone()).unwrap(),
|
||||
before
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_group_style_and_opacity_edit_only_the_panel_and_can_restore_inheritance() {
|
||||
let mut snapshot = Snapshot::parse(String::new()).unwrap();
|
||||
@@ -541,7 +584,7 @@ animations { speed 0.8; }
|
||||
apply(&mut snapshot, Action::SetRadius("4px 8px 12px 16px".into()));
|
||||
apply(&mut snapshot, Action::SetCorner(0, 9.));
|
||||
apply(&mut snapshot, Action::SetCorner(3, 21.));
|
||||
let radii: ferese_config::panel::CornerRadii =
|
||||
let radii: ferese_panel::CornerRadii =
|
||||
serde_json::from_value(snapshot.item("panels.0.corner_radius").unwrap().clone()).unwrap();
|
||||
assert_eq!(radii.0, [9., 8., 12., 21.]);
|
||||
let previous = snapshot.source.clone();
|
||||
@@ -585,11 +628,11 @@ animations { speed 0.8; }
|
||||
#[test]
|
||||
fn changing_edge_materializes_preferences_and_round_trips_without_moving_items() {
|
||||
let mut snapshot = Snapshot::parse("".into()).unwrap();
|
||||
apply(&mut snapshot, Action::SetEdge(ferese_config::panel::Edge::Bottom));
|
||||
apply(&mut snapshot, Action::SetEdge(ferese_panel::Edge::Bottom));
|
||||
let before = snapshot.item("panels.0.start").unwrap().clone();
|
||||
assert_eq!(snapshot.string("panels.0.edge", ""), "bottom");
|
||||
assert!(snapshot.boolean("panels.0.end.groups.0.items.6.percentage", false));
|
||||
apply(&mut snapshot, Action::SetEdge(ferese_config::panel::Edge::Top));
|
||||
apply(&mut snapshot, Action::SetEdge(ferese_panel::Edge::Top));
|
||||
assert_eq!(snapshot.string("panels.0.edge", ""), "top");
|
||||
assert_eq!(snapshot.item("panels.0.start"), Some(&before));
|
||||
Snapshot::parse(snapshot.source).unwrap();
|
||||
@@ -809,7 +852,7 @@ animations { speed 0.8; }
|
||||
|
||||
#[test]
|
||||
fn empty_zones_can_gain_unique_groups_and_nonempty_groups_cannot_be_removed() {
|
||||
let mut snapshot = Snapshot::parse("panel \"main\" { surface none; group-surface island; }".into()).unwrap();
|
||||
let mut snapshot = Snapshot::parse("panel \"main\" { style floating; }".into()).unwrap();
|
||||
for _ in 0..3 {
|
||||
apply(&mut snapshot, Action::AddGroup(Zone::End));
|
||||
}
|
||||
@@ -934,7 +977,7 @@ animations { speed 0.8; }
|
||||
&mut snapshot,
|
||||
Action::Set(ItemId("battery".into()), "percentage".into(), false.into()),
|
||||
);
|
||||
for style in ferese_config::panel::WorkspaceStyle::ALL {
|
||||
for style in ferese_panel::WorkspaceStyle::ALL {
|
||||
apply(
|
||||
&mut snapshot,
|
||||
Action::Set(ItemId("workspaces".into()), "style".into(), style.key().into()),
|
||||
@@ -961,7 +1004,7 @@ animations { speed 0.8; }
|
||||
assert_eq!(
|
||||
panel.item(&ItemId("workspaces".into())).unwrap().kind,
|
||||
ItemKind::Workspaces {
|
||||
style: ferese_config::panel::WorkspaceStyle::AppIcons
|
||||
style: ferese_panel::WorkspaceStyle::AppIcons
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -982,38 +1025,43 @@ animations { speed 0.8; }
|
||||
assert!(panels[0].item(&ItemId("title".into())).unwrap().visible);
|
||||
}
|
||||
#[test]
|
||||
fn group_surface_edits_and_presets_preserve_composition_and_comments() {
|
||||
let mut snapshot = Snapshot::parse("// keep\npanel main { surface none; group-surface island; center { group title surface=none { item title kind=focused-window; }; }; end { group media { item media kind=media; }; }; }".into()).unwrap();
|
||||
let items = snapshot.item("panels.0.center.groups.0.items").unwrap().clone();
|
||||
let media = GroupId("media".into());
|
||||
fn resetting_one_geometry_pin_preserves_composition_and_other_pins() {
|
||||
let mut snapshot = Snapshot::parse("// keep\npanel main { style floating; geometry { height 32; side-margins 18; }; corner-radius \"8px 9px 10px 11px\"; end { group status { item clock kind=clock; }; }; }".into()).unwrap();
|
||||
let groups = snapshot.item("panels.0.end.groups").unwrap().clone();
|
||||
apply(
|
||||
&mut snapshot,
|
||||
Action::SetGroup(media.clone(), "surface".into(), "inset".into()),
|
||||
Action::ClearGeometry("panels.0.geometry.side_margins".into()),
|
||||
);
|
||||
assert_eq!(snapshot.string("panels.0.end.groups.0.surface", ""), "inset");
|
||||
apply(&mut snapshot, Action::ClearGroupSurface(media));
|
||||
assert!(snapshot.item("panels.0.end.groups.0.surface").is_none());
|
||||
apply(
|
||||
&mut snapshot,
|
||||
Action::ApplyPreset(ferese_config::PanelPreset::Continuous),
|
||||
);
|
||||
assert_eq!(snapshot.string("panels.0.surface", ""), "solid");
|
||||
assert!(snapshot.item("panels.0.center.groups.0.surface").is_none());
|
||||
assert_eq!(snapshot.item("panels.0.center.groups.0.items"), Some(&items));
|
||||
assert_eq!(snapshot.item("panels.0.geometry.side_margins"), None);
|
||||
assert_eq!(snapshot.item("panels.0.geometry.height"), Some(&serde_json::json!(32)));
|
||||
assert!(snapshot.item("panels.0.corner_radius").is_some());
|
||||
apply(&mut snapshot, Action::ResetShape);
|
||||
assert_eq!(snapshot.item("panels.0.geometry"), None);
|
||||
assert_eq!(snapshot.item("panels.0.corner_radius"), None);
|
||||
assert_eq!(snapshot.item("panels.0.end.groups"), Some(&groups));
|
||||
assert!(snapshot.source.contains("// keep"));
|
||||
apply(
|
||||
&mut snapshot,
|
||||
Action::SetGroup(GroupId("title".into()), "surface".into(), "none".into()),
|
||||
);
|
||||
apply(&mut snapshot, Action::ApplyPreset(ferese_config::PanelPreset::Islands));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panel_presets_preserve_group_layout_items_and_comments() {
|
||||
let mut snapshot = Snapshot::parse("// keep\npanel main { style floating; center { group title { padding 2 7; item title kind=focused-window; }; }; end { group media { item media kind=media; }; }; }".into()).unwrap();
|
||||
let groups = snapshot.item("panels.0.center.groups").unwrap().clone();
|
||||
apply(&mut snapshot, Action::ApplyPreset(ferese_panel::PanelStyle::Flat));
|
||||
assert_eq!(snapshot.string("panels.0.style", ""), "flat");
|
||||
assert_eq!(snapshot.item("panels.0.center.groups"), Some(&groups));
|
||||
assert!(snapshot.source.contains("// keep"));
|
||||
apply(&mut snapshot, Action::ApplyPreset(ferese_panel::PanelStyle::Floating));
|
||||
assert_eq!(snapshot.item("panels.0.center.groups"), Some(&groups));
|
||||
let panels: Vec<Panel> = serde_json::from_value(snapshot.item("panels").unwrap().clone()).unwrap();
|
||||
assert_eq!(panels[0].preset(), Some(ferese_config::PanelPreset::Islands));
|
||||
assert!(
|
||||
plan(
|
||||
&snapshot,
|
||||
Action::SetGroup(GroupId("title".into()), "surface".into(), "unsupported".into())
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(panels[0].preset(), Some(ferese_panel::PanelStyle::Floating));
|
||||
for surface in ["none", "inset", "island"] {
|
||||
assert!(
|
||||
plan(
|
||||
&snapshot,
|
||||
Action::SetGroup(GroupId("title".into()), "surface".into(), surface.into())
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::panel_edit::{Action, Destination, Zone};
|
||||
use crate::{App, Element, Field, Kind, Message, widget};
|
||||
use cosmic::iced::{Alignment, Length};
|
||||
use ferese_config::panel::{Group, Item, ItemKind, Panel, Representation};
|
||||
use ferese_panel::{Group, Item, ItemKind, Panel, Representation};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
struct Choice(&'static str, &'static str);
|
||||
@@ -146,7 +146,7 @@ impl App {
|
||||
.spacing(6)
|
||||
.push(self.label("Workspace style", 13.))
|
||||
.push(self.note("A sample of active, occupied and empty workspaces."));
|
||||
for style in ferese_config::panel::WorkspaceStyle::ALL {
|
||||
for style in ferese_panel::WorkspaceStyle::ALL {
|
||||
let sample = ferese_theme::workspaces::sample(style, palette, self.font, 22.);
|
||||
choices = choices.push(
|
||||
widget::button::custom(
|
||||
@@ -192,30 +192,6 @@ impl App {
|
||||
palette,
|
||||
)),
|
||||
);
|
||||
let surface_choices = [
|
||||
Choice("inherit", "Default"),
|
||||
Choice("none", "None"),
|
||||
Choice("inset", "Inset"),
|
||||
Choice("island", "Island"),
|
||||
];
|
||||
let selected_surface = group.surface.map_or(surface_choices[0], |surface| {
|
||||
*surface_choices.iter().find(|choice| choice.0 == surface.key()).unwrap()
|
||||
});
|
||||
let surface_id = group.id.clone();
|
||||
rows = rows.push(self.label("Surface", 13.)).push(crate::visuals::panel_select(
|
||||
cosmic::iced::widget::pick_list(surface_choices.to_vec(), Some(selected_surface), move |choice| {
|
||||
Message::PanelEdit(if choice.0 == "inherit" {
|
||||
Action::ClearGroupSurface(surface_id.clone())
|
||||
} else {
|
||||
Action::SetGroup(surface_id.clone(), "surface".into(), choice.0.into())
|
||||
})
|
||||
})
|
||||
.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 [
|
||||
@@ -229,7 +205,7 @@ impl App {
|
||||
),
|
||||
("island_padding", "Island padding", group.island_padding, 32.),
|
||||
] {
|
||||
if panel.resolved_surface(group) != ferese_config::panel::GroupSurface::Island && name == "island_padding" {
|
||||
if panel.group_surface() != ferese_panel::GroupSurface::Island && name == "island_padding" {
|
||||
continue;
|
||||
}
|
||||
let change = |value: f32| {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use cosmic::iced::{Alignment, Length};
|
||||
use cosmic::widget::{button, column, container, row};
|
||||
use ferese_config::panel::layout::{Placement, Resolution};
|
||||
use ferese_config::panel::{Availability, Group, Item, ItemId, ItemKind, Panel, Representation, Zone};
|
||||
use ferese_panel::layout::{Placement, Resolution};
|
||||
use ferese_panel::{Availability, Group, Item, ItemId, ItemKind, Panel, Representation, Zone};
|
||||
use ferese_theme::panel::{Sample, Spacing, frame};
|
||||
|
||||
use crate::{App, Element, Message, panel_controls, visuals};
|
||||
@@ -36,12 +36,12 @@ impl App {
|
||||
.push(
|
||||
container(self.panel_preview_frame(panel.clone()))
|
||||
.padding(cosmic::iced::Padding {
|
||||
top: if panel.edge == ferese_config::panel::Edge::Top {
|
||||
top: if panel.edge == ferese_panel::Edge::Top {
|
||||
10.
|
||||
} else {
|
||||
26.
|
||||
},
|
||||
bottom: if panel.edge == ferese_config::panel::Edge::Top {
|
||||
bottom: if panel.edge == ferese_panel::Edge::Top {
|
||||
26.
|
||||
} else {
|
||||
10.
|
||||
@@ -98,6 +98,47 @@ impl App {
|
||||
}
|
||||
|
||||
fn panel_preview_frame(&self, panel: Panel) -> Element<'_, Message> {
|
||||
if panel.style == ferese_panel::PanelStyle::Morph {
|
||||
let palette = visuals::Palette::from_resolved(&self.resolved.presented);
|
||||
let height = panel.resolved_geometry().height;
|
||||
let hub = container(self.label("● 10:09", 13.))
|
||||
.id("morph-preview-body")
|
||||
.center_x(panel.morph.idle_width as f32)
|
||||
.center_y(height);
|
||||
let hub = ferese_theme::panel_surface::frame(
|
||||
container(hub)
|
||||
.padding([0, if panel.morph.attached { 12 } else { 0 }])
|
||||
.into(),
|
||||
ferese_theme::panel_surface::Options {
|
||||
parameters: panel.shape_parameters(),
|
||||
bounds: Some(cosmic::iced::advanced::widget::Id::new("morph-preview-body")),
|
||||
groups: Vec::new(),
|
||||
scale: self.render_scale,
|
||||
high_contrast: palette.high_contrast,
|
||||
background: cosmic::iced::Color {
|
||||
a: if self.resolved.presented.accessibility.reduce_transparency {
|
||||
1.
|
||||
} else {
|
||||
panel.background_opacity.unwrap_or(palette.card.a)
|
||||
},
|
||||
..palette.card
|
||||
},
|
||||
border: if panel.border {
|
||||
visuals::color(
|
||||
&self.resolved.presented.tokens.colors.border,
|
||||
cosmic::iced::Color::TRANSPARENT,
|
||||
)
|
||||
} else {
|
||||
cosmic::iced::Color::TRANSPARENT
|
||||
},
|
||||
divider: cosmic::iced::Color::TRANSPARENT,
|
||||
native: false,
|
||||
},
|
||||
|_| {},
|
||||
None,
|
||||
);
|
||||
return container(hub).center_x(Length::Fill).height(height).into();
|
||||
}
|
||||
let mut samples = Vec::new();
|
||||
for zone in [&panel.center, &panel.start, &panel.end] {
|
||||
for item in zone
|
||||
@@ -128,24 +169,42 @@ impl App {
|
||||
view: self.preview_control(&overflow.items[0], Representation::Icon, None),
|
||||
});
|
||||
let overflow_width = 2. * f32::from(overflow.padding[1])
|
||||
+ if panel.resolved_surface(&overflow) == ferese_config::panel::GroupSurface::Island {
|
||||
+ if panel.group_surface() == ferese_panel::GroupSurface::Island {
|
||||
2. * overflow.island_padding
|
||||
} else {
|
||||
0.
|
||||
};
|
||||
let continuous = panel.surface == ferese_config::panel::PanelSurface::Solid;
|
||||
let corners = panel
|
||||
.resolved_radius(self.resolved.presented.tokens.geometry.shell_radius as f32)
|
||||
.at_edge(panel.edge, panel.geometry.edge_margin == 0)
|
||||
.0;
|
||||
let parameters = panel.shape_parameters();
|
||||
let groups = [
|
||||
(ferese_panel::shape::ZonePosition::Start, &panel.start),
|
||||
(ferese_panel::shape::ZonePosition::Center, &panel.center),
|
||||
(ferese_panel::shape::ZonePosition::End, &panel.end),
|
||||
]
|
||||
.into_iter()
|
||||
.flat_map(|(position, zone)| {
|
||||
zone.groups.iter().map(move |group| {
|
||||
(
|
||||
cosmic::iced::advanced::widget::Id::new(format!("preview-group:{}", group.id.0)),
|
||||
position,
|
||||
)
|
||||
})
|
||||
})
|
||||
.chain(std::iter::once((
|
||||
cosmic::iced::advanced::widget::Id::new("preview-group:_overflow"),
|
||||
ferese_panel::shape::ZonePosition::End,
|
||||
)))
|
||||
.collect();
|
||||
let opacity = self.resolved.presented.panel_opacity(panel.background_opacity);
|
||||
let height = panel.resolved_geometry().height;
|
||||
let padding = panel.resolved_geometry().inner_padding;
|
||||
let border = panel.border;
|
||||
let measured_panel = panel.clone();
|
||||
let content = frame(
|
||||
std::borrow::Cow::Owned(measured_panel),
|
||||
samples,
|
||||
Spacing {
|
||||
overflow_decoration: overflow_width,
|
||||
zone_gap: 32.,
|
||||
zone_gap: 8.,
|
||||
},
|
||||
self.panel_preview_resolution.clone(),
|
||||
move |resolution| {
|
||||
@@ -172,7 +231,7 @@ impl App {
|
||||
let has_end = panel.end.groups.iter().flat_map(|group| &group.items)
|
||||
.any(|item| matches!(resolution.items.get(&item.id), Some(Placement::Visible { width, .. }) if *width > 0.));
|
||||
end = row([])
|
||||
.spacing(if has_end { panel.end.spacing } else { 0. })
|
||||
.spacing(if has_end { panel.zone_spacing(&panel.end) } else { 0. })
|
||||
.align_y(Alignment::Center)
|
||||
.push(trigger)
|
||||
.push(end)
|
||||
@@ -193,29 +252,44 @@ impl App {
|
||||
Message::PanelPreviewResolved,
|
||||
);
|
||||
let palette = visuals::Palette::from_resolved(&self.resolved.presented);
|
||||
container(content)
|
||||
.width(Length::Fill)
|
||||
.height(48)
|
||||
.clip(true)
|
||||
.class(cosmic::theme::Container::custom(move |_| {
|
||||
let mut style = ferese_theme::controls::surface_appearance(
|
||||
cosmic::iced::Color {
|
||||
a: opacity,
|
||||
..palette.card
|
||||
},
|
||||
0.,
|
||||
);
|
||||
style.border.radius = corners.into();
|
||||
if !continuous {
|
||||
style.background = None;
|
||||
}
|
||||
style
|
||||
}))
|
||||
.into()
|
||||
ferese_theme::panel_surface::frame(
|
||||
container(content)
|
||||
.width(Length::Fill)
|
||||
.height(height)
|
||||
.padding([0., padding])
|
||||
.clip(true)
|
||||
.into(),
|
||||
ferese_theme::panel_surface::Options {
|
||||
parameters,
|
||||
bounds: None,
|
||||
groups,
|
||||
scale: self.render_scale,
|
||||
high_contrast: palette.high_contrast,
|
||||
background: cosmic::iced::Color {
|
||||
a: opacity,
|
||||
..palette.card
|
||||
},
|
||||
border: if border {
|
||||
visuals::color(
|
||||
&self.resolved.presented.tokens.colors.border,
|
||||
cosmic::iced::Color::TRANSPARENT,
|
||||
)
|
||||
} else {
|
||||
cosmic::iced::Color::TRANSPARENT
|
||||
},
|
||||
divider: visuals::color(
|
||||
&self.resolved.presented.tokens.colors.border,
|
||||
cosmic::iced::Color::TRANSPARENT,
|
||||
),
|
||||
native: false,
|
||||
},
|
||||
|_| {},
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
fn preview_zone(&self, panel: &Panel, zone: &Zone, resolution: &Resolution) -> Element<'static, Message> {
|
||||
let mut groups = row([]).spacing(zone.spacing).align_y(Alignment::Center);
|
||||
let mut groups = row([]).spacing(panel.zone_spacing(zone)).align_y(Alignment::Center);
|
||||
for group in &zone.groups {
|
||||
let mut controls = row([]).align_y(Alignment::Center);
|
||||
let mut count = 0;
|
||||
@@ -246,44 +320,22 @@ impl App {
|
||||
group: &Group,
|
||||
controls: Element<'static, Message>,
|
||||
) -> Element<'static, Message> {
|
||||
let palette = visuals::Palette::from_resolved(&self.resolved.presented);
|
||||
let surface = panel.resolved_surface(group);
|
||||
let radius = panel
|
||||
.resolved_radius(self.resolved.presented.tokens.geometry.shell_radius as f32)
|
||||
.at_edge(
|
||||
panel.edge,
|
||||
surface == ferese_config::panel::GroupSurface::Island && panel.geometry.edge_margin == 0,
|
||||
)
|
||||
.0;
|
||||
let opacity = self.resolved.presented.panel_opacity(panel.background_opacity);
|
||||
let border = panel.border;
|
||||
let content: Element<'static, Message> = container(controls)
|
||||
.padding(group.padding)
|
||||
.class(cosmic::theme::Container::custom(move |_| {
|
||||
ferese_theme::panel::group_border(palette.muted.scale_alpha(0.4), radius, border)
|
||||
}))
|
||||
.into();
|
||||
if surface == ferese_config::panel::GroupSurface::None {
|
||||
return content;
|
||||
}
|
||||
let parent_solid = panel.surface == ferese_config::panel::PanelSurface::Solid;
|
||||
container(content)
|
||||
.padding(if surface == ferese_config::panel::GroupSurface::Island {
|
||||
[2., group.island_padding]
|
||||
let horizontal = f32::from(group.padding[1])
|
||||
+ if panel.group_surface() == ferese_panel::GroupSurface::Island {
|
||||
group.island_padding
|
||||
} else {
|
||||
[0., 0.]
|
||||
0.
|
||||
};
|
||||
container(controls)
|
||||
.id(format!("preview-group:{}", group.id.0))
|
||||
.padding(cosmic::iced::Padding {
|
||||
top: group.padding[0].into(),
|
||||
bottom: group.padding[0].into(),
|
||||
left: horizontal,
|
||||
right: horizontal,
|
||||
})
|
||||
.class(cosmic::theme::Container::custom(move |_| {
|
||||
let mut style = ferese_theme::controls::surface_appearance(
|
||||
cosmic::iced::Color {
|
||||
a: if parent_solid { opacity * 0.08 } else { opacity },
|
||||
..if parent_solid { palette.text } else { palette.card }
|
||||
},
|
||||
0.,
|
||||
);
|
||||
style.border.radius = radius.into();
|
||||
style
|
||||
}))
|
||||
.height(panel.resolved_geometry().height)
|
||||
.align_y(Alignment::Center)
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -294,13 +346,14 @@ impl App {
|
||||
width: Option<f32>,
|
||||
) -> Element<'static, Message> {
|
||||
use ferese_theme::icons;
|
||||
let control_height = (self.draft.number("panels.0.geometry.height", 36.) as f32 - 4.).max(20.);
|
||||
let palette = visuals::Palette::from_resolved(&self.resolved.presented);
|
||||
if let ItemKind::Workspaces { style } = item.kind {
|
||||
let sample = ferese_theme::workspaces::sample(style, palette, self.font, 20.);
|
||||
let control = button::custom(container(sample).id(format!("preview-icon:{}", item.id.0)))
|
||||
.name("Edit Workspaces")
|
||||
.padding([4, 2])
|
||||
.height(28)
|
||||
.padding([0, 2])
|
||||
.height(control_height)
|
||||
.on_press(Message::PanelSelect(item.id.clone()))
|
||||
.class(preview_button(palette, self.panel_selection.as_ref() == Some(&item.id)));
|
||||
return container(control)
|
||||
@@ -353,8 +406,8 @@ impl App {
|
||||
};
|
||||
let control: Element<'static, Message> = button::custom(content)
|
||||
.name(format!("Edit {}", item.id.0))
|
||||
.padding([4, 7])
|
||||
.height(28)
|
||||
.padding([0, 7])
|
||||
.height(control_height)
|
||||
.on_press(action)
|
||||
.class(preview_button(
|
||||
palette,
|
||||
@@ -587,11 +640,8 @@ mod tests {
|
||||
),
|
||||
)
|
||||
.0;
|
||||
for background in [
|
||||
ferese_config::PanelPreset::Continuous,
|
||||
ferese_config::PanelPreset::Islands,
|
||||
] {
|
||||
let mut panel = Panel::from_preset(ferese_config::PanelPreset::Islands);
|
||||
for background in [ferese_panel::PanelStyle::Flat, ferese_panel::PanelStyle::Floating] {
|
||||
let mut panel = Panel::from_preset(ferese_panel::PanelStyle::Floating);
|
||||
panel.apply_preset(background);
|
||||
// Match a three-group layout with media in navigation and a larger status group.
|
||||
let media = panel.end.groups[0].items.remove(0);
|
||||
@@ -648,11 +698,7 @@ mod tests {
|
||||
} else {
|
||||
side.x - center.x - center.width
|
||||
};
|
||||
assert!(
|
||||
gap >= 31.9,
|
||||
"{background:?} at {width}: {} crowds title ({gap}px)",
|
||||
id.0
|
||||
);
|
||||
assert!(gap >= 7.9, "{background:?} at {width}: {} crowds title ({gap}px)", id.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -680,10 +726,7 @@ mod tests {
|
||||
),
|
||||
)
|
||||
.0;
|
||||
for background in [
|
||||
ferese_config::PanelPreset::Continuous,
|
||||
ferese_config::PanelPreset::Islands,
|
||||
] {
|
||||
for background in [ferese_panel::PanelStyle::Flat, ferese_panel::PanelStyle::Floating] {
|
||||
let panel = Panel::from_preset(background);
|
||||
let ids: Vec<_> = [&panel.start, &panel.center, &panel.end]
|
||||
.into_iter()
|
||||
|
||||
@@ -526,7 +526,7 @@ pub fn fields(page: Page) -> Vec<Field> {
|
||||
"Height",
|
||||
"Keeps icon and text sizes unchanged.",
|
||||
RangeSpec {
|
||||
default: 28.0,
|
||||
default: 36.0,
|
||||
min: 24.0,
|
||||
max: 48.0,
|
||||
step: 1.0,
|
||||
|
||||
@@ -348,7 +348,6 @@ fn gpu_panel_text_survives_preview_clipping_and_scrolling() {
|
||||
| "Shown on all displays"
|
||||
| "Position"
|
||||
| "Background"
|
||||
| "Default group surface"
|
||||
| "Background opacity"
|
||||
| "Group borders"
|
||||
| "Corner radius"
|
||||
@@ -363,6 +362,8 @@ fn gpu_panel_text_survives_preview_clipping_and_scrolling() {
|
||||
| "Add item"
|
||||
| "Overview"
|
||||
| "Workspaces"
|
||||
| "Morph"
|
||||
| "Collapsed height"
|
||||
| "Battery"
|
||||
| "Placement"
|
||||
| "Show item"
|
||||
@@ -409,8 +410,8 @@ fn gpu_panel_text_survives_preview_clipping_and_scrolling() {
|
||||
))
|
||||
.expect("GPU panel rendering requires a hardware or software WGPU adapter");
|
||||
for width in [740., 1100.] {
|
||||
for scale in [1., 1.75] {
|
||||
for selection in ["panels", "items", "battery", "status"] {
|
||||
for scale in [1., 1.25, 1.5, 1.75] {
|
||||
for selection in ["panels", "morph", "items", "battery", "status"] {
|
||||
let mut app = App::init(
|
||||
Core::default(),
|
||||
(
|
||||
@@ -421,14 +422,24 @@ fn gpu_panel_text_survives_preview_clipping_and_scrolling() {
|
||||
)
|
||||
.0;
|
||||
app.font = font;
|
||||
app.panel_page = if selection == "panels" {
|
||||
app.render_scale = f64::from(scale);
|
||||
if selection == "morph" {
|
||||
for edit in crate::panel_edit::plan(
|
||||
&app.draft,
|
||||
crate::panel_edit::Action::ApplyPreset(ferese_panel::PanelStyle::Morph),
|
||||
)
|
||||
.unwrap()
|
||||
{
|
||||
app.draft.edit(&edit).unwrap();
|
||||
}
|
||||
}
|
||||
app.panel_page = if matches!(selection, "panels" | "morph") {
|
||||
panel_controls::PanelPage::Panels
|
||||
} else {
|
||||
panel_controls::PanelPage::Arrange
|
||||
};
|
||||
app.panel_selection = (selection == "battery").then(|| ferese_config::panel::ItemId("battery".into()));
|
||||
app.panel_group_selection =
|
||||
(selection == "status").then(|| ferese_config::panel::GroupId("status".into()));
|
||||
app.panel_selection = (selection == "battery").then(|| ferese_panel::ItemId("battery".into()));
|
||||
app.panel_group_selection = (selection == "status").then(|| ferese_panel::GroupId("status".into()));
|
||||
let bounds = Rectangle::with_size(Size::new(width, 650.));
|
||||
let physical = Size::new((width * scale) as u32, (650. * scale) as u32);
|
||||
let theme = app.native_palette.native_theme();
|
||||
@@ -565,14 +576,15 @@ fn render_panel_review_states() {
|
||||
app.native_palette = visuals::Palette::from_resolved(&app.resolved.presented);
|
||||
for (name, tab, item, group) in [
|
||||
("panels", panel_controls::PanelPage::Panels, None, None),
|
||||
("mixed-surfaces", panel_controls::PanelPage::Panels, None, None),
|
||||
("floating-groups", panel_controls::PanelPage::Panels, None, None),
|
||||
(
|
||||
"surface-inspector",
|
||||
"group-spacing-inspector",
|
||||
panel_controls::PanelPage::Arrange,
|
||||
None,
|
||||
Some("title"),
|
||||
),
|
||||
("corners", panel_controls::PanelPage::Panels, None, None),
|
||||
("morph", panel_controls::PanelPage::Panels, None, None),
|
||||
("preview-hover", panel_controls::PanelPage::Panels, None, None),
|
||||
("navigation", panel_controls::PanelPage::Arrange, None, None),
|
||||
("shortcuts", panel_controls::PanelPage::Panels, None, None),
|
||||
@@ -603,15 +615,23 @@ fn render_panel_review_states() {
|
||||
_ => Page::Bar,
|
||||
};
|
||||
app.draft = Snapshot::parse(String::new()).unwrap();
|
||||
if matches!(name, "mixed-surfaces" | "surface-inspector") {
|
||||
let mut panel = ferese_config::panel::Panel::from_preset(ferese_config::PanelPreset::Islands);
|
||||
panel.center.groups[0].surface = Some(ferese_config::panel::GroupSurface::None);
|
||||
panel.end.groups[0].surface = Some(ferese_config::panel::GroupSurface::Inset);
|
||||
panel.geometry.edge_margin = 6;
|
||||
if matches!(name, "floating-groups" | "group-spacing-inspector") {
|
||||
let mut panel = ferese_panel::Panel::from_preset(ferese_panel::PanelStyle::Floating);
|
||||
panel.geometry.edge_margin = Some(6);
|
||||
app.draft
|
||||
.edit(&crate::store::set("panels", serde_json::to_value(vec![panel]).unwrap()))
|
||||
.unwrap();
|
||||
}
|
||||
if name == "morph" {
|
||||
for edit in crate::panel_edit::plan(
|
||||
&app.draft,
|
||||
crate::panel_edit::Action::ApplyPreset(ferese_panel::PanelStyle::Morph),
|
||||
)
|
||||
.unwrap()
|
||||
{
|
||||
app.draft.edit(&edit).unwrap();
|
||||
}
|
||||
}
|
||||
if name == "corners" {
|
||||
for edit in crate::panel_edit::plan(
|
||||
&app.draft,
|
||||
@@ -625,8 +645,8 @@ fn render_panel_review_states() {
|
||||
}
|
||||
app.panel_page = tab;
|
||||
app.sidebar_open = name == "navigation";
|
||||
app.panel_selection = item.map(|id| ferese_config::panel::ItemId(id.into()));
|
||||
app.panel_group_selection = group.map(|id| ferese_config::panel::GroupId(id.into()));
|
||||
app.panel_selection = item.map(|id| ferese_panel::ItemId(id.into()));
|
||||
app.panel_group_selection = group.map(|id| ferese_panel::GroupId(id.into()));
|
||||
for (width, height) in [(1100, 860), (740, 650)] {
|
||||
let bounds = Rectangle::with_size(Size::new(width as f32, height as f32));
|
||||
let theme = app.native_palette.native_theme();
|
||||
@@ -763,10 +783,8 @@ fn selecting_panel_items_preserves_editor_scroll_and_pane_bounds() {
|
||||
let mut initial = None;
|
||||
for selection in [
|
||||
None,
|
||||
Some(Message::PanelSelect(ferese_config::panel::ItemId("battery".into()))),
|
||||
Some(Message::PanelGroupSelect(ferese_config::panel::GroupId(
|
||||
"status".into(),
|
||||
))),
|
||||
Some(Message::PanelSelect(ferese_panel::ItemId("battery".into()))),
|
||||
Some(Message::PanelGroupSelect(ferese_panel::GroupId("status".into()))),
|
||||
Some(Message::PanelClearSelection),
|
||||
] {
|
||||
if let Some(selection) = selection {
|
||||
|
||||
@@ -22,9 +22,9 @@ impl Snapshot {
|
||||
}
|
||||
let doc = Document::parse(&source).map_err(|e| e.to_string())?;
|
||||
if let Some(value) = doc.get("panels") {
|
||||
let panels: Vec<ferese_config::panel::Panel> =
|
||||
let panels: Vec<ferese_panel::Panel> =
|
||||
serde_json::from_value(value.clone()).map_err(|error| error.to_string())?;
|
||||
ferese_config::panel::validate(&panels)?;
|
||||
ferese_panel::validate(&panels)?;
|
||||
}
|
||||
Ok(Self { source, doc })
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
//! Watch the parent directory so atomic editor saves keep working.
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
|
||||
Reference in new issue
Block a user