Unify shell corner radius and speed up Settings page switching

This commit is contained in:
Samuel Onoja committed 2026-09-27 09:41:43 +01:00
1 parent ef912daeba
commit e5e2b74a25
10 files changed
+164 -91

No files matched your search

+32 -9
View File
@@ -421,7 +421,9 @@ struct ThemeGeometryConfig {
#[serde(default = "default_window_radius")]
window_radius: f64,
#[serde(default)]
top_bar_radius: f64,
top_bar_radius: Option<f64>,
#[serde(default)]
shell_radius: Option<f64>,
}
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());
+11 -3
View File
@@ -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.
+14 -4
View File
@@ -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(
+11 -33
View File
@@ -376,17 +376,6 @@ fn clock_fields() -> Vec<Field> {
" 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<Field> {
" 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<Field> {
pub fn fields(page: Page) -> Vec<Field> {
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<Field> {
" 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",
+1 -1
View File
@@ -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.;
+42 -12
View File
@@ -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<f32>,
#[serde(default)]
top_bar_radius: Option<f32>,
#[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<ShellConfig, ferese_config::E
.validate()
.map_err(ferese_config::Error::from)?;
let mut theme = shell_theme(&config.theme);
theme.material_radius =
nonnegative_or(config.appearance.corner_radius.unwrap_or(14.0), 14.0);
theme.material_radius = nonnegative_or(
config
.theme
.geometry
.shell_radius
.or(config.appearance.corner_radius)
.or(config.theme.geometry.top_bar_radius)
.unwrap_or(14.0),
14.0,
);
theme.bar_radius = theme.material_radius;
Ok(ShellConfig {
notifications: config.notifications,
@@ -388,7 +400,14 @@ fn shell_theme(theme: &ThemeConfig) -> 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);
+25 -14
View File
@@ -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(&<cosmic::Theme as Catalog>::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()
}
+11 -2
View File
@@ -42,7 +42,16 @@ impl Settings {
}
}
static SETTINGS: std::sync::OnceLock<std::sync::RwLock<Settings>> = 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
+5 -5
View File
@@ -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()
+12 -8
View File
@@ -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,