Share native theme components and fix dialog backdrop blur
Centralize native palettes, presets, typography, controls, menus, calendar rendering, and icon assets in ferese-theme. Reuse the shared components across the shell, settings, lock screen, authentication prompt, and screen-sharing chooser. Attach themed dialogs to compositor-owned materials, correct backdrop capture and sampling across output transforms, smooth patterned backgrounds, and prevent clipped picker buttons with the configured shell font.
This commit is contained in:
1 parent
362bf79012
commit
b2363a18b0
85 files changed
+2163
-1591
No files matched your search
@@ -0,0 +1,189 @@
|
||||
use cosmic::iced::Color;
|
||||
use ferese_config::Document;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Palette {
|
||||
pub background: Color,
|
||||
pub sidebar: Color,
|
||||
pub card: Color,
|
||||
pub accent: Color,
|
||||
pub text: Color,
|
||||
pub muted: Color,
|
||||
pub error: Color,
|
||||
pub radius: f32,
|
||||
}
|
||||
|
||||
impl Palette {
|
||||
pub fn from_document(document: Option<&Document>) -> Self {
|
||||
let color = |key, fallback| {
|
||||
document
|
||||
.and_then(|document| document.get(key))
|
||||
.and_then(|value| value.as_str())
|
||||
.and_then(parse_color)
|
||||
.unwrap_or_else(|| parse_color(fallback).expect("valid default theme color"))
|
||||
};
|
||||
let base = color("theme.colors.surface_base", "#111821");
|
||||
Self {
|
||||
background: mix(base, surface_shade(base), 0.025),
|
||||
sidebar: base,
|
||||
card: mix(base, surface_shade(base), 0.055),
|
||||
accent: color("theme.colors.accent", "#3D7BE6"),
|
||||
text: color("theme.colors.text_primary", "#F4F7FB"),
|
||||
muted: color("theme.colors.text_muted", "#8793A2"),
|
||||
error: mix(base, Color::from_rgb8(210, 80, 80), 0.2),
|
||||
radius: document
|
||||
.and_then(|document| {
|
||||
document
|
||||
.get("theme.geometry.shell_radius")
|
||||
.or_else(|| document.get("appearance.corner_radius"))
|
||||
})
|
||||
.and_then(|value| value.as_f64())
|
||||
.filter(|value| value.is_finite())
|
||||
.unwrap_or(14.)
|
||||
.clamp(0., 64.) as f32,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn flat(mut self) -> Self {
|
||||
self.background = self.sidebar;
|
||||
self.card = self.sidebar;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn native_theme(self) -> cosmic::Theme {
|
||||
let rgba = |c: Color| cosmic::cosmic_theme::palette::Srgba::new(c.r, c.g, c.b, 1.);
|
||||
let builder = if crate::luminance(self.sidebar) > 0.5 {
|
||||
cosmic::cosmic_theme::ThemeBuilder::light()
|
||||
} else {
|
||||
cosmic::cosmic_theme::ThemeBuilder::dark()
|
||||
};
|
||||
let corners = cosmic::cosmic_theme::CornerRadii {
|
||||
radius_xs: [self.radius.min(4.); 4],
|
||||
radius_s: [self.radius.min(8.); 4],
|
||||
radius_m: [self.radius; 4],
|
||||
radius_l: [self.radius; 4],
|
||||
radius_xl: [self.radius; 4],
|
||||
radius_0: Default::default(),
|
||||
};
|
||||
let mut native = builder
|
||||
.corner_radii(corners)
|
||||
.bg_color(rgba(self.background))
|
||||
.primary_container_bg(rgba(self.card))
|
||||
.text_tint(rgba(self.text).color)
|
||||
.accent(crate::accent_color(self.accent, self.card))
|
||||
.build();
|
||||
crate::apply(&mut native, self.text);
|
||||
cosmic::Theme::custom(std::sync::Arc::new(native))
|
||||
}
|
||||
|
||||
pub fn application_style(self, background: Color) -> cosmic::iced::theme::Style {
|
||||
cosmic::iced::theme::Style {
|
||||
background_color: background,
|
||||
text_color: self.text,
|
||||
icon_color: self.text,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_color(value: &str) -> Option<Color> {
|
||||
let hex = value.strip_prefix('#')?;
|
||||
let packed = u32::from_str_radix(hex, 16).ok()?;
|
||||
let rgba = match hex.len() {
|
||||
6 => (packed << 8) | 255,
|
||||
8 => packed,
|
||||
_ => return None,
|
||||
};
|
||||
Some(Color::from_rgba8(
|
||||
(rgba >> 24) as u8,
|
||||
(rgba >> 16) as u8,
|
||||
(rgba >> 8) as u8,
|
||||
(rgba & 255) as f32 / 255.,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn mix(a: Color, b: Color, t: f32) -> Color {
|
||||
Color::from_rgb(
|
||||
a.r + (b.r - a.r) * t,
|
||||
a.g + (b.g - a.g) * t,
|
||||
a.b + (b.b - a.b) * t,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn surface_shade(base: Color) -> Color {
|
||||
if crate::luminance(base) > 0.5 {
|
||||
Color::BLACK
|
||||
} else {
|
||||
Color::WHITE
|
||||
}
|
||||
}
|
||||
|
||||
pub fn material_opacity(document: Option<&Document>) -> f32 {
|
||||
let style = document
|
||||
.and_then(|document| document.get("theme.material.style"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or("solid");
|
||||
if style != "translucent" {
|
||||
return 1.;
|
||||
}
|
||||
document
|
||||
.and_then(|document| document.get("theme.material.opacity"))
|
||||
.and_then(|value| value.as_f64())
|
||||
.filter(|value| value.is_finite())
|
||||
.unwrap_or(ferese_config::DEFAULT_MATERIAL_OPACITY)
|
||||
.clamp(0., 1.) as f32
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn every_preset_builds_an_opaque_control_palette_over_translucent_materials() {
|
||||
for preset in crate::PRESETS {
|
||||
let document = Document::parse(&format!(
|
||||
r##"theme {{
|
||||
colors {{
|
||||
surface-base "{}"
|
||||
accent "{}"
|
||||
text-primary "{}"
|
||||
text-muted "{}"
|
||||
}}
|
||||
material {{ style translucent; opacity 0.25; }}
|
||||
geometry {{ shell-radius 0; }}
|
||||
}}"##,
|
||||
preset.base, preset.accent, preset.text, preset.muted,
|
||||
))
|
||||
.unwrap();
|
||||
let palette = Palette::from_document(Some(&document));
|
||||
let native = palette.native_theme();
|
||||
assert_eq!(material_opacity(Some(&document)), 0.25);
|
||||
assert_eq!(native.cosmic().primary(false).base.alpha, 1.);
|
||||
assert_eq!(native.cosmic().corner_radii.radius_m, [0.; 4]);
|
||||
for component in [&native.cosmic().accent, &native.cosmic().accent_button] {
|
||||
for fill in [component.base, component.hover, component.pressed] {
|
||||
assert!(
|
||||
crate::contrast(fill.into(), component.on.into()) >= 4.5,
|
||||
"{}",
|
||||
preset.name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn solid_materials_ignore_opacity_and_translucent_materials_bound_it() {
|
||||
for (style, opacity, expected) in [
|
||||
("solid", 0.4, 1.),
|
||||
("translucent", 0.4, 0.4),
|
||||
("translucent", 2., 1.),
|
||||
("translucent", -1., 0.),
|
||||
] {
|
||||
let document = Document::parse(&format!(
|
||||
"theme {{ material {{ style {style}; opacity {opacity}; }} }}"
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(material_opacity(Some(&document)), expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user