diff --git a/compositor/src/config.rs b/compositor/src/config.rs index e6055b4..f024c5c 100644 --- a/compositor/src/config.rs +++ b/compositor/src/config.rs @@ -421,7 +421,9 @@ struct ThemeGeometryConfig { #[serde(default = "default_window_radius")] window_radius: f64, #[serde(default)] - top_bar_radius: f64, + top_bar_radius: Option, + #[serde(default)] + shell_radius: Option, } impl Default for ThemeGeometryConfig { @@ -430,7 +432,8 @@ impl Default for ThemeGeometryConfig { border_width: default_border_width(), focus_ring_width: default_focus_ring_width(), window_radius: default_window_radius(), - top_bar_radius: 0.0, + top_bar_radius: None, + shell_radius: None, } } } @@ -1072,8 +1075,13 @@ impl Config { unit_theme_value(self.theme.shadow.soft.opacity, "shadow.soft.opacity")?; let material_radius = nonnegative_theme_value( - self.appearance.corner_radius.unwrap_or(14.0), - "appearance.corner_radius", + self.theme + .geometry + .shell_radius + .or(self.appearance.corner_radius) + .or(self.theme.geometry.top_bar_radius) + .unwrap_or(14.0), + "geometry.shell_radius", )?; Ok(ThemeSettings { border_width, @@ -1113,10 +1121,7 @@ impl Config { )? .min(32.0), material_radius, - panel_radius: nonnegative_theme_value( - self.theme.geometry.top_bar_radius, - "geometry.top_bar_radius", - )?, + panel_radius: material_radius, }) } @@ -1988,6 +1993,24 @@ mod tests { assert!(invalid.gap_config().is_err()); } + #[test] + fn shell_radius_is_independent_of_windows_and_overrides_legacy_keys() { + for radius in [0.0, 18.0] { + let config = parse(&format!( + "appearance {{\n corner-radius 9\n}}\ntheme {{\n geometry {{\n shell-radius {radius}\n top-bar-radius 5\n window-radius 23\n }}\n}}" + )); + let theme = config.theme_settings().unwrap(); + assert_eq!(theme.material_radius, radius); + assert_eq!(theme.panel_radius, radius); + assert_eq!(theme.window_radius, 23.0); + } + assert!( + parse("theme {\n geometry {\n shell-radius -1\n }\n}") + .theme_settings() + .is_err() + ); + } + #[test] fn parses_and_validates_theme_window_tokens() { let configured = parse( @@ -2024,7 +2047,7 @@ mod tests { material_style: MaterialStyle::Translucent, backdrop_blur: 12.0, material_radius: 14.0, - panel_radius: 0.0, + panel_radius: 14.0, } ); assert!(invalid_color.theme_settings().is_err()); diff --git a/docs/configuration.md b/docs/configuration.md index 616996c..e718a7d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -126,7 +126,7 @@ these six visually distinct. | Section / key | Type | Default | Meaning | | --- | --- | --- | --- | -| `appearance.corner_radius` | number ≥ 0 | `14` | Shell material/card radius | +| `appearance.corner_radius` | number ≥ 0 | unset | Legacy fallback for shell radius | | `appearance.inactive_dim.enabled` | boolean | `false` | Dim unfocused windows | | `appearance.inactive_dim.amount` | number 0–1 | `0.15` | Darkening strength | | `appearance.inactive_dim.duration_ms` | number ≥ 0 | `150` | Dimming and focus-ring transition; 0 snaps | @@ -149,12 +149,13 @@ these six visually distinct. | --- | --- | --- | --- | | `border_width` | number ≥ 0 | `1` | Window border thickness | | `focus_ring_width` | number ≥ 0 | `2` | Focused border thickness | -| `window_radius` | number ≥ 0 | `14` | Managed-window corners | +| `window_radius` | number ≥ 0 | `14` | Managed-window corners, independent of the shell | +| `shell_radius` | number ≥ 0 | `14` | All shell surfaces, cards, widgets and interaction backgrounds; 0 makes them square | | `top_bar_height` | number > 0 | `28` | Menu-bar height | | `top_bar_margin_top` | integer ≥ 0 | `0` | Space above bar | | `top_bar_window_gap` | integer ≥ 0 | `0` | Clearance below bar | | `top_bar_margin_horizontal` | integer ≥ 0 | `0` | Bar side margins | -| `top_bar_radius` | number ≥ 0 | `0` | Bar corners | +| `top_bar_radius` | number ≥ 0 | unset | Legacy fallback for shell radius | | `panel_padding` | number ≥ 0 | `12` | Bar inner padding | | `control_gap` | number ≥ 0 | `12` | Right-side control spacing | @@ -387,3 +388,10 @@ Settings → Notifications controls popups, Do Not Disturb, and the default time A single-output session starts with workspace 1. Numbered workspace shortcuts create workspaces on demand; switching forward past the last workspace also creates the next one. Additional displays receive their own workspace. + +Shell rounding is controlled in Settings → Appearance → Shell corner radius. +Small controls cap the radius to fit their size. Window rounding remains under +Settings → Windows. `theme.geometry.shell_radius` takes precedence over the old +`appearance.corner_radius` and then `theme.geometry.top_bar_radius` keys; when +none are set, the shell uses 14 px. Legacy clock/note radius fields no longer +override shell rounding. diff --git a/shell/ferese-settings/src/main.rs b/shell/ferese-settings/src/main.rs index 7928c4b..f62fcc4 100644 --- a/shell/ferese-settings/src/main.rs +++ b/shell/ferese-settings/src/main.rs @@ -1109,10 +1109,20 @@ impl App { column([]) .spacing(6) .push( - widget::dropdown(families, selected, move |index| { - let family = if index == 0 { "" } else { &families[index] }; - Message::SelectFont(selection_path.clone(), family.to_owned()) - }) + cosmic::iced::widget::pick_list( + families, + selected.map(|index| families[index].clone()), + move |family: String| { + let value = if family == "Default font" { + String::new() + } else { + family + }; + Message::SelectFont(selection_path.clone(), value) + }, + ) + .font(self.font) + .text_size(12) .width(225), ) .push( diff --git a/shell/ferese-settings/src/schema.rs b/shell/ferese-settings/src/schema.rs index e5e45aa..9db4bc1 100644 --- a/shell/ferese-settings/src/schema.rs +++ b/shell/ferese-settings/src/schema.rs @@ -376,17 +376,6 @@ fn clock_fields() -> Vec { " px", false, ), - range( - "desktop_widgets.clock.radius", - "Background radius", - "Used only when a background is set.", - 16.0, - 0.0, - 128.0, - 1.0, - " px", - false, - ), choice( "desktop_widgets.clock.alignment", "Text alignment", @@ -541,17 +530,6 @@ pub fn note_fields(index: usize) -> Vec { " px", false, ), - range( - format!("{prefix}.radius"), - "Corner radius", - "Logical pixels; centered axes ignore margins.", - 16.0, - 0.0, - 128.0, - 1.0, - " px", - false, - ), Field::new( format!("{prefix}.alignment"), "Text alignment", @@ -567,6 +545,17 @@ pub fn note_fields(index: usize) -> Vec { pub fn fields(page: Page) -> Vec { match page { Page::Appearance => vec![ + range( + "theme.geometry.shell_radius", + "Shell corner radius", + "Rounds the bar, menus, notifications, desktop widgets and hover backgrounds. Window corners are separate.", + 14.0, + 0.0, + 32.0, + 1.0, + " px", + false, + ), text( "theme.colors.accent", "Accent color", @@ -690,17 +679,6 @@ pub fn fields(page: Page) -> Vec { " px", true, ), - range( - "theme.geometry.top_bar_radius", - "Corner radius", - "The shape of the floating bar.", - 0.0, - 0.0, - 24.0, - 1.0, - " px", - false, - ), range( "theme.surface.bar.opacity", "Opacity", diff --git a/shell/ferese-settings/src/visuals.rs b/shell/ferese-settings/src/visuals.rs index 20b4a96..b19108c 100644 --- a/shell/ferese-settings/src/visuals.rs +++ b/shell/ferese-settings/src/visuals.rs @@ -438,7 +438,7 @@ pub fn preview(snapshot: &Snapshot) -> Element<'static, Message> { let bar_y = snapshot.number("theme.geometry.top_bar_margin_top", 0.) * 0.5 + 12.; let bar_margin = snapshot.number("theme.geometry.top_bar_margin_horizontal", 0.) * 0.5 + 14.; let bar_height = snapshot.number("theme.geometry.top_bar_height", 30.) * 0.65; - let bar_radius = snapshot.number("theme.geometry.top_bar_radius", 0.) * 0.65; + let bar_radius = snapshot.number("theme.geometry.shell_radius", 14.) * 0.65; let opacity = snapshot.number("theme.surface.bar.opacity", 0.78); let right = 340. + gap / 2.; let left_width = 288. - gap / 2.; diff --git a/shell/ferese-shell/src/config.rs b/shell/ferese-shell/src/config.rs index dd3c061..8930a57 100644 --- a/shell/ferese-shell/src/config.rs +++ b/shell/ferese-shell/src/config.rs @@ -76,7 +76,7 @@ impl Default for ShellTheme { bar_margin_top: 0, bar_window_gap: 0, bar_margin_horizontal: 0, - bar_radius: 0.0, + bar_radius: 14.0, material_radius: 14.0, panel_padding: 12.0, control_gap: 12.0, @@ -253,8 +253,10 @@ struct ThemeGeometryConfig { top_bar_window_gap: i32, #[serde(default = "default_bar_margin_horizontal")] top_bar_margin_horizontal: i32, - #[serde(default = "default_bar_radius")] - top_bar_radius: f32, + #[serde(default)] + shell_radius: Option, + #[serde(default)] + top_bar_radius: Option, #[serde(default = "default_panel_padding")] panel_padding: f32, #[serde(default = "default_control_gap")] @@ -268,7 +270,8 @@ impl Default for ThemeGeometryConfig { top_bar_margin_top: default_bar_margin_top(), top_bar_window_gap: default_bar_window_gap(), top_bar_margin_horizontal: default_bar_margin_horizontal(), - top_bar_radius: default_bar_radius(), + shell_radius: None, + top_bar_radius: None, panel_padding: default_panel_padding(), control_gap: default_control_gap(), } @@ -330,8 +333,17 @@ pub(crate) fn parse_source(source: &str) -> Result ShellTheme { bar_margin_top: theme.geometry.top_bar_margin_top.max(0), bar_window_gap: theme.geometry.top_bar_window_gap.max(0), bar_margin_horizontal: theme.geometry.top_bar_margin_horizontal.max(0), - bar_radius: nonnegative_or(theme.geometry.top_bar_radius, defaults.bar_radius), + bar_radius: nonnegative_or( + theme + .geometry + .shell_radius + .or(theme.geometry.top_bar_radius) + .unwrap_or(14.0), + defaults.bar_radius, + ), panel_padding: nonnegative_or(theme.geometry.panel_padding, defaults.panel_padding), control_gap: nonnegative_or(theme.geometry.control_gap, defaults.control_gap), shadow_offset_y: finite_or(theme.shadow.soft.offset_y, defaults.shadow_offset_y), @@ -476,10 +495,6 @@ const fn default_bar_margin_horizontal() -> i32 { 0 } -const fn default_bar_radius() -> f32 { - 0.0 -} - const fn default_panel_padding() -> f32 { 12.0 } @@ -572,6 +587,21 @@ theme { assert_eq!(config.theme.background.mode, WallpaperMode::Fit); } + #[test] + fn shell_radius_unifies_surfaces_and_preserves_legacy_fallbacks() { + for radius in [0.0, 18.0] { + let config = parse_source(&format!("appearance {{\n corner-radius 9\n}}\ntheme {{\n geometry {{\n shell-radius {radius}\n top-bar-radius 5\n window-radius 23\n }}\n}}")).unwrap(); + assert_eq!(config.theme.material_radius, radius); + assert_eq!(config.theme.bar_radius, radius); + } + let legacy = parse_source("theme {\n geometry {\n top-bar-radius 7\n }\n}").unwrap(); + assert_eq!(legacy.theme.material_radius, 7.0); + assert_eq!(legacy.theme.bar_radius, 7.0); + let legacy = parse_source("appearance {\n corner-radius 6\n}").unwrap(); + assert_eq!(legacy.theme.material_radius, 6.0); + assert_eq!(legacy.theme.bar_radius, 6.0); + } + #[test] fn defaults_to_the_ferese_visual_profile() { let config: FereseConfig = ferese_config::from_str("").unwrap(); @@ -586,7 +616,7 @@ theme { assert_eq!(theme.bar_margin_top, 0); assert_eq!(theme.bar_height, 28.0); assert_eq!(theme.bar_margin_horizontal, 0); - assert_eq!(theme.bar_radius, 0.0); + assert_eq!(theme.bar_radius, 14.0); assert_eq!(theme.bar_background, ShellTheme::default().bar_background); assert_eq!(theme.for_bar().text_primary, theme.bar_text_primary); assert_ne!(theme.for_bar().text_primary, theme.text_primary); diff --git a/shell/ferese-shell/src/main.rs b/shell/ferese-shell/src/main.rs index 4225ca8..434923b 100644 --- a/shell/ferese-shell/src/main.rs +++ b/shell/ferese-shell/src/main.rs @@ -154,7 +154,7 @@ fn main() -> cosmic::iced::Result { "../../../assets/fonts/Cantarell-ExtraBold.otf" ))); let mut config = config::load(); - motion::configure(config.animations); + motion::configure(config.animations, config.theme.material_radius); let compositor_wallpaper = std::env::var_os("FERESE_COMPOSITOR_WALLPAPER").is_some(); if compositor_wallpaper { // The compositor owns one GPU image, shared across output renderers; @@ -771,7 +771,7 @@ impl FereseShell { .write() .unwrap() = configured_font(config.font_family.as_deref()); } - motion::configure(config.animations); + motion::configure(config.animations, config.theme.material_radius); if self.config.animations != config.animations && let Some(menu) = &mut self.menu { @@ -1365,15 +1365,20 @@ impl FereseShell { .note_editor .as_ref() .is_some_and(|editor| editor.id == id); - body = body.push(row([]).push(header).push( - button::text(if editing { "Done" } else { "Edit" }).on_press(cosmic::Action::App( - if editing { - Message::FinishNoteEdit - } else { - Message::BeginNoteEdit(id.to_owned()) - }, + body = body.push( + row([]).push(header).push(motion::button( + button::custom(text(if editing { "Done" } else { "Edit" })) + .padding([4, 8]) + .on_press(cosmic::Action::App(if editing { + Message::FinishNoteEdit + } else { + Message::BeginNoteEdit(id.to_owned()) + })), + foreground, + false, + 1.0, )), - )); + ); } else if !note.title.is_empty() { body = body.push( cosmic::widget::text(note.title.clone()) @@ -1393,6 +1398,12 @@ impl FereseShell { let id = id.to_owned(); body = body.push( cosmic::widget::TextEditor::new(&editor.content) + .style(|theme, status| { + use cosmic::iced::widget::text_editor::Catalog; + let mut style = theme.style(&::default(), status); + style.border.radius = motion::radius(f32::MAX).into(); + style + }) .height(Length::Fill) .font(font) .size(note.text_size) @@ -1417,7 +1428,7 @@ impl FereseShell { .class(theme::Text::Color(foreground)), ); } - let radius = note.radius; + let radius = self.config.theme.material_radius; container(body) .padding(note.padding) .width(Length::Fill) @@ -1646,7 +1657,7 @@ impl FereseShell { tint.a *= clock.opacity; Background::Color(tint) }); - let radius = clock.radius; + let radius = self.config.theme.material_radius; container(labels) .padding(clock.padding) .width(Length::Fill) @@ -2249,7 +2260,7 @@ fn workspace_selector_style( border: Border { color: color_with_opacity(shell_theme.accent, 0.55), width: if active_elsewhere { 1.0 } else { 0.0 }, - radius: 6.0.into(), + radius: shell_theme.material_radius.min(14.0).into(), }, ..Default::default() } @@ -2440,7 +2451,7 @@ fn bar_group_style(theme: ShellTheme) -> container::Style { border: Border { color: color_with_opacity(theme.text_muted, 0.12), width: 1.0, - radius: 7.0.into(), + radius: theme.material_radius.min(16.0).into(), }, ..Default::default() } diff --git a/shell/ferese-shell/src/motion.rs b/shell/ferese-shell/src/motion.rs index 0f75734..8a6b5d6 100644 --- a/shell/ferese-shell/src/motion.rs +++ b/shell/ferese-shell/src/motion.rs @@ -42,7 +42,16 @@ impl Settings { } } static SETTINGS: std::sync::OnceLock> = std::sync::OnceLock::new(); -pub(crate) fn configure(settings: Settings) { +static SHELL_RADIUS: std::sync::atomic::AtomicU32 = + std::sync::atomic::AtomicU32::new(14.0_f32.to_bits()); + +/// All shell shapes share one radius, capped to fit smaller controls. +pub(crate) fn radius(limit: f32) -> f32 { + f32::from_bits(SHELL_RADIUS.load(std::sync::atomic::Ordering::Relaxed)).min(limit) +} + +pub(crate) fn configure(settings: Settings, radius: f32) { + SHELL_RADIUS.store(radius.to_bits(), std::sync::atomic::Ordering::Relaxed); *SETTINGS .get_or_init(|| std::sync::RwLock::new(Settings::default())) .write() @@ -138,7 +147,7 @@ pub(crate) fn button<'a, M: Clone + 'a>( let paint = move |progress: f32, pressed: bool| cosmic::widget::button::Style { text_color: Some(foreground), icon_color: Some(foreground), - border_radius: 6.0.into(), + border_radius: radius(14.0).into(), background: Some(cosmic::iced::Background::Color(cosmic::iced::Color { a: if pressed { 0.20 * opacity diff --git a/shell/ferese-shell/src/notification_ui.rs b/shell/ferese-shell/src/notification_ui.rs index 014c363..201b1aa 100644 --- a/shell/ferese-shell/src/notification_ui.rs +++ b/shell/ferese-shell/src/notification_ui.rs @@ -98,7 +98,7 @@ fn card_button<'a>( text_color: Some(foreground), icon_color: Some(foreground), background: (active || round).then_some(Background::Color(hover)), - border_radius: (if round { 9.0 } else { 0.0 }).into(), + border_radius: (if round { motion::radius(11.0) } else { 0.0 }).into(), ..Default::default() }; button::custom(content) @@ -141,7 +141,7 @@ fn icon_control<'a>( .class(theme::Container::custom(move |_| container::Style { background: Some(Background::Color(background)), border: Border { - radius: 6.0.into(), + radius: motion::radius(12.0).into(), ..Default::default() }, text_color: Some(foreground), @@ -380,7 +380,7 @@ impl FereseShell { .class(theme::Container::custom(move |_| container::Style { background: Some(Background::Color(well)), border: Border { - radius: 8.0.into(), + radius: motion::radius(15.0).into(), ..Default::default() }, ..Default::default() @@ -397,7 +397,7 @@ impl FereseShell { .class(theme::Container::custom(move |_| container::Style { background: Some(Background::Color(accent)), border: Border { - radius: 8.0.into(), + radius: motion::radius(8.0).into(), ..Default::default() }, ..Default::default() @@ -747,7 +747,7 @@ impl FereseShell { .class(theme::Container::custom(move |_| container::Style { background: Some(Background::Color(well)), border: Border { - radius: 28.0.into(), + radius: motion::radius(28.0).into(), ..Default::default() }, ..Default::default() diff --git a/shell/ferese-shell/src/status_ui.rs b/shell/ferese-shell/src/status_ui.rs index 97d1128..5adfb43 100644 --- a/shell/ferese-shell/src/status_ui.rs +++ b/shell/ferese-shell/src/status_ui.rs @@ -277,7 +277,7 @@ impl FereseShell { theme::Container::custom(move |_| container::Style { background: Some(Background::Color(foreground)), border: Border { - radius: 2.0.into(), + radius: motion::radius(2.0).into(), ..Default::default() }, ..Default::default() @@ -345,7 +345,7 @@ impl FereseShell { 0.12 * p, ))), border: Border { - radius: 10.0.into(), + radius: motion::radius(20.0).into(), ..Default::default() }, ..Default::default() @@ -772,7 +772,7 @@ fn status_summary<'a>( ..badge_color })), border: Border { - radius: 9.0.into(), + radius: motion::radius(16.0).into(), ..Default::default() }, ..Default::default() @@ -823,7 +823,7 @@ fn level_meter( } })), border: Border { - radius: 2.5.into(), + radius: motion::radius(2.5).into(), ..Default::default() }, ..Default::default() @@ -854,7 +854,7 @@ fn control_card<'a>( ..foreground })), border: Border { - radius: 11.0.into(), + radius: motion::radius(22.0).into(), ..Default::default() }, ..Default::default() @@ -890,7 +890,7 @@ fn connection_control<'a>( opacity, )), border: Border { - radius: 10.0.into(), + radius: motion::radius(20.0).into(), ..Default::default() }, ..Default::default() @@ -1006,12 +1006,16 @@ fn slider_row( ), width: 4.0, border: Border { - radius: 2.0.into(), + radius: motion::radius(2.0).into(), ..Default::default() }, }, handle: Handle { - shape: HandleShape::Circle { radius: 5.0 }, + shape: HandleShape::Rectangle { + width: 10, + height: 10, + border_radius: motion::radius(5.0).into(), + }, background: foreground.into(), border_width: 0.0, border_color: Color::TRANSPARENT,