Centralize accent foreground contrast across Ferese UI
This commit is contained in:
1 parent
299f60e671
commit
24e15f76cb
14 files changed
+309
-46
No files matched your search
Generated
+11
@@ -1709,6 +1709,7 @@ name = "ferese-lock"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ferese-config",
|
"ferese-config",
|
||||||
|
"ferese-theme",
|
||||||
"image",
|
"image",
|
||||||
"jiff",
|
"jiff",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -1737,6 +1738,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"ferese-config",
|
"ferese-config",
|
||||||
"ferese-ipc",
|
"ferese-ipc",
|
||||||
|
"ferese-theme",
|
||||||
"image",
|
"image",
|
||||||
"libcosmic",
|
"libcosmic",
|
||||||
"notify",
|
"notify",
|
||||||
@@ -1753,6 +1755,7 @@ dependencies = [
|
|||||||
"ferese-config",
|
"ferese-config",
|
||||||
"ferese-core",
|
"ferese-core",
|
||||||
"ferese-protocols",
|
"ferese-protocols",
|
||||||
|
"ferese-theme",
|
||||||
"iced_accessibility",
|
"iced_accessibility",
|
||||||
"jiff",
|
"jiff",
|
||||||
"libcosmic",
|
"libcosmic",
|
||||||
@@ -1764,6 +1767,13 @@ dependencies = [
|
|||||||
"zbus",
|
"zbus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ferese-theme"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"libcosmic",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "feresectl"
|
name = "feresectl"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -7371,6 +7381,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs",
|
"dirs",
|
||||||
"ferese-config",
|
"ferese-config",
|
||||||
|
"ferese-theme",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"gstreamer",
|
"gstreamer",
|
||||||
"jiff",
|
"jiff",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ members = [
|
|||||||
"crates/ferese-animation",
|
"crates/ferese-animation",
|
||||||
"crates/ferese-core",
|
"crates/ferese-core",
|
||||||
"crates/ferese-config",
|
"crates/ferese-config",
|
||||||
|
"crates/ferese-theme",
|
||||||
"crates/ferese-ipc",
|
"crates/ferese-ipc",
|
||||||
"crates/ferese-layout",
|
"crates/ferese-layout",
|
||||||
"crates/ferese-protocols",
|
"crates/ferese-protocols",
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "ferese-theme"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
rust-version = "1.95"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
libcosmic.workspace = true
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
//! Ferese owns foreground contrast; toolkit defaults must not choose it separately.
|
||||||
|
use cosmic::{
|
||||||
|
iced::{Background, Color},
|
||||||
|
theme,
|
||||||
|
widget::button,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn luminance(color: Color) -> f32 {
|
||||||
|
let linear = |v: f32| {
|
||||||
|
if v <= 0.04045 {
|
||||||
|
v / 12.92
|
||||||
|
} else {
|
||||||
|
((v + 0.055) / 1.055).powf(2.4)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
0.2126 * linear(color.r) + 0.7152 * linear(color.g) + 0.0722 * linear(color.b)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contrast(a: Color, b: Color) -> f32 {
|
||||||
|
let (a, b) = (luminance(a), luminance(b));
|
||||||
|
(a.max(b) + 0.05) / (a.min(b) + 0.05)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a control fill against its surface before selecting its foreground.
|
||||||
|
/// The returned fill is opaque, so wallpaper and translucent panels cannot change
|
||||||
|
/// the contrast of a small, filled control.
|
||||||
|
pub fn composite(fill: Color, surface: Color) -> Color {
|
||||||
|
let a = fill.a.clamp(0., 1.);
|
||||||
|
Color::from_rgb(
|
||||||
|
fill.r * a + surface.r * (1. - a),
|
||||||
|
fill.g * a + surface.g * (1. - a),
|
||||||
|
fill.b * a + surface.b * (1. - a),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Preserve the user's preferred color when it passes normal-text contrast.
|
||||||
|
/// Otherwise black or white always provides at least 4.5:1 on an opaque sRGB fill.
|
||||||
|
pub fn foreground(background: Color, preferred: Color) -> Color {
|
||||||
|
let preferred = composite(preferred, background);
|
||||||
|
if contrast(background, preferred) >= 4.5 {
|
||||||
|
return preferred;
|
||||||
|
}
|
||||||
|
if contrast(background, Color::BLACK) >= contrast(background, Color::WHITE) {
|
||||||
|
Color::BLACK
|
||||||
|
} else {
|
||||||
|
Color::WHITE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve configured accent transparency before passing an opaque color to a toolkit.
|
||||||
|
pub fn accent_color(accent: Color, surface: Color) -> cosmic::cosmic_theme::palette::Srgb {
|
||||||
|
rgba(composite(accent, surface)).color
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Supply the same semantic on-accent colors to native toolkit controls.
|
||||||
|
pub fn apply(native: &mut cosmic::cosmic_theme::Theme, preferred: Color) {
|
||||||
|
let surface: Color = native.primary(false).base.into();
|
||||||
|
for component in [&mut native.accent, &mut native.accent_button] {
|
||||||
|
let base = composite(component.base.into(), surface);
|
||||||
|
let on = foreground(base, preferred);
|
||||||
|
component.hover = rgba(readable_fill(
|
||||||
|
composite(component.hover.into(), surface),
|
||||||
|
base,
|
||||||
|
on,
|
||||||
|
));
|
||||||
|
component.pressed = rgba(readable_fill(
|
||||||
|
composite(component.pressed.into(), surface),
|
||||||
|
base,
|
||||||
|
on,
|
||||||
|
));
|
||||||
|
component.selected = rgba(composite(component.selected.into(), surface));
|
||||||
|
component.base = rgba(base);
|
||||||
|
component.on = rgba(on);
|
||||||
|
component.selected_text = rgba(foreground(
|
||||||
|
composite(component.selected.into(), surface),
|
||||||
|
on,
|
||||||
|
));
|
||||||
|
component.disabled = rgba(composite(Color { a: 0.5, ..base }, surface));
|
||||||
|
component.on_disabled = rgba(foreground(composite(Color { a: 0.5, ..base }, surface), on));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Native controls reuse one foreground for normal/hover/pressed. Keep their
|
||||||
|
// hover feedback, but pull an unreadable fill toward the readable normal fill.
|
||||||
|
fn readable_fill(candidate: Color, base: Color, on: Color) -> Color {
|
||||||
|
for step in 0..=20 {
|
||||||
|
let t = step as f32 / 20.;
|
||||||
|
let fill = Color::from_rgb(
|
||||||
|
candidate.r + (base.r - candidate.r) * t,
|
||||||
|
candidate.g + (base.g - candidate.g) * t,
|
||||||
|
candidate.b + (base.b - candidate.b) * t,
|
||||||
|
);
|
||||||
|
if contrast(fill, on) >= 4.5 {
|
||||||
|
return fill;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rgba(c: Color) -> cosmic::cosmic_theme::palette::Srgba {
|
||||||
|
cosmic::cosmic_theme::palette::Srgba::new(c.r, c.g, c.b, c.a)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum State {
|
||||||
|
Active,
|
||||||
|
Hovered,
|
||||||
|
Pressed,
|
||||||
|
Disabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style(theme: &cosmic::Theme, state: State, focused: bool) -> button::Style {
|
||||||
|
let native = theme.cosmic();
|
||||||
|
let component = &native.accent_button;
|
||||||
|
let fill: Color = match state {
|
||||||
|
State::Active => component.base.into(),
|
||||||
|
State::Hovered => component.hover.into(),
|
||||||
|
State::Pressed => component.pressed.into(),
|
||||||
|
State::Disabled => Color {
|
||||||
|
a: 0.5,
|
||||||
|
..component.base.into()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let background = composite(fill, native.primary(false).base.into());
|
||||||
|
let foreground = foreground(background, component.on.into());
|
||||||
|
button::Style {
|
||||||
|
background: Some(Background::Color(background)),
|
||||||
|
text_color: Some(foreground),
|
||||||
|
icon_color: Some(foreground),
|
||||||
|
border_radius: native.corner_radii.radius_xl.into(),
|
||||||
|
outline_width: if focused { 1. } else { 0. },
|
||||||
|
outline_color: foreground,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filled accent buttons and selected rows share this per-state contrast policy.
|
||||||
|
pub fn accent_button() -> theme::Button {
|
||||||
|
theme::Button::Custom {
|
||||||
|
active: Box::new(|focused, theme| style(theme, State::Active, focused)),
|
||||||
|
hovered: Box::new(|focused, theme| style(theme, State::Hovered, focused)),
|
||||||
|
pressed: Box::new(|focused, theme| style(theme, State::Pressed, focused)),
|
||||||
|
disabled: Box::new(|theme| style(theme, State::Disabled, false)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn reference_contrast_values_use_linear_srgb() {
|
||||||
|
assert!((contrast(Color::BLACK, Color::WHITE) - 21.).abs() < 0.00001);
|
||||||
|
assert!((contrast(Color::from_rgb8(128, 128, 128), Color::BLACK) - 5.31721).abs() < 0.0001);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn foreground_preserves_readable_theme_text_and_rejects_low_contrast() {
|
||||||
|
let preferred = Color::from_rgb8(240, 242, 246);
|
||||||
|
assert_eq!(foreground(Color::BLACK, preferred), preferred);
|
||||||
|
assert_eq!(foreground(Color::WHITE, preferred), Color::BLACK);
|
||||||
|
assert_eq!(
|
||||||
|
foreground(Color::from_rgb8(61, 123, 230), preferred),
|
||||||
|
Color::BLACK
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn all_accent_hues_and_opacities_have_readable_text() {
|
||||||
|
for r in (0..=255).step_by(17) {
|
||||||
|
for g in (0..=255).step_by(17) {
|
||||||
|
for b in (0..=255).step_by(17) {
|
||||||
|
for surface in [Color::BLACK, Color::WHITE] {
|
||||||
|
for alpha in [0., 0.2, 0.5, 1.] {
|
||||||
|
let fill = composite(Color::from_rgba8(r, g, b, alpha), surface);
|
||||||
|
let on = foreground(fill, Color::from_rgb8(120, 130, 140));
|
||||||
|
assert!(contrast(fill, on) >= 4.5, "{fill:?}: {on:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn native_button_states_keep_text_and_icons_readable() {
|
||||||
|
for accent in [
|
||||||
|
Color::BLACK,
|
||||||
|
Color::WHITE,
|
||||||
|
Color::from_rgb8(61, 123, 230),
|
||||||
|
Color::from_rgb8(255, 220, 0),
|
||||||
|
] {
|
||||||
|
for mut native in [
|
||||||
|
cosmic::cosmic_theme::ThemeBuilder::dark()
|
||||||
|
.accent(rgba(accent).color)
|
||||||
|
.build(),
|
||||||
|
cosmic::cosmic_theme::ThemeBuilder::light()
|
||||||
|
.accent(rgba(accent).color)
|
||||||
|
.build(),
|
||||||
|
] {
|
||||||
|
apply(&mut native, Color::from_rgb8(244, 247, 251));
|
||||||
|
for component in [&native.accent, &native.accent_button] {
|
||||||
|
for fill in [component.base, component.hover, component.pressed] {
|
||||||
|
assert!(contrast(fill.into(), component.on.into()) >= 4.5);
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
contrast(component.selected.into(), component.selected_text.into()) >= 4.5
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
contrast(component.disabled.into(), component.on_disabled.into()) >= 4.5
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let theme = cosmic::Theme::custom(std::sync::Arc::new(native));
|
||||||
|
for state in [
|
||||||
|
State::Active,
|
||||||
|
State::Hovered,
|
||||||
|
State::Pressed,
|
||||||
|
State::Disabled,
|
||||||
|
] {
|
||||||
|
let style = style(&theme, state, false);
|
||||||
|
let Some(Background::Color(fill)) = style.background else {
|
||||||
|
panic!("No fill")
|
||||||
|
};
|
||||||
|
assert_eq!(style.text_color, style.icon_color);
|
||||||
|
assert!(contrast(fill, style.text_color.unwrap()) >= 4.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -145,6 +145,15 @@ these six visually distinct.
|
|||||||
| `border` | `"#FFFFFF18"` | Unfocused window border |
|
| `border` | `"#FFFFFF18"` | Unfocused window border |
|
||||||
| `shadow` | `"#00000055"` | Window shadow color |
|
| `shadow` | `"#00000055"` | Window shadow color |
|
||||||
|
|
||||||
|
Ferese derives the text and icon color for filled accent controls from the
|
||||||
|
configured theme. It keeps the configured text color when it reaches a 4.5:1
|
||||||
|
contrast ratio; otherwise it chooses black or white. The same rule is shared by
|
||||||
|
native dialogs, settings selections, and notification badges. Hover, pressed,
|
||||||
|
and disabled accent-button states use the same policy. Small filled controls
|
||||||
|
resolve accent transparency against their theme surface so wallpaper cannot
|
||||||
|
make their labels unreadable. No separate per-application color setting is needed.
|
||||||
|
The target follows [normal-text contrast guidance](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html).
|
||||||
|
|
||||||
| `theme.geometry` key | Type | Default | Meaning |
|
| `theme.geometry` key | Type | Default | Meaning |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `border_width` | number ≥ 0 | `1` | Window border thickness |
|
| `border_width` | number ≥ 0 | `1` | Window border thickness |
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ name = "ferese-record"
|
|||||||
path = "src/record_main.rs"
|
path = "src/record_main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
ferese-theme = { path = "../crates/ferese-theme" }
|
||||||
gstreamer = "0.24"
|
gstreamer = "0.24"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
dirs = "6"
|
dirs = "6"
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ impl cosmic::Application for Picker {
|
|||||||
sources = sources.push(
|
sources = sources.push(
|
||||||
button::custom(entry)
|
button::custom(entry)
|
||||||
.class(if selected {
|
.class(if selected {
|
||||||
cosmic::theme::Button::Suggested
|
ferese_theme::accent_button()
|
||||||
} else {
|
} else {
|
||||||
cosmic::theme::Button::Standard
|
cosmic::theme::Button::Standard
|
||||||
})
|
})
|
||||||
@@ -215,7 +215,7 @@ impl cosmic::Application for Picker {
|
|||||||
.spacing(8)
|
.spacing(8)
|
||||||
.align_y(Alignment::Center),
|
.align_y(Alignment::Center),
|
||||||
)
|
)
|
||||||
.class(cosmic::theme::Button::Suggested)
|
.class(ferese_theme::accent_button())
|
||||||
.padding([6, 16]);
|
.padding([6, 16]);
|
||||||
if !self.selected.is_empty() {
|
if !self.selected.is_empty() {
|
||||||
share = share.on_press(Message::Share);
|
share = share.on_press(Message::Share);
|
||||||
@@ -288,15 +288,15 @@ fn appearance() -> (cosmic::Theme, cosmic::font::Font) {
|
|||||||
} else {
|
} else {
|
||||||
cosmic::cosmic_theme::ThemeBuilder::dark()
|
cosmic::cosmic_theme::ThemeBuilder::dark()
|
||||||
};
|
};
|
||||||
let theme = cosmic::Theme::custom(std::sync::Arc::new(
|
let mut native = builder
|
||||||
builder
|
|
||||||
.bg_color(background)
|
.bg_color(background)
|
||||||
.primary_container_bg(background)
|
.primary_container_bg(background)
|
||||||
.text_tint(foreground.color)
|
.text_tint(foreground.color)
|
||||||
.accent(accent.color)
|
.accent(ferese_theme::accent_color(accent.into(), background.into()))
|
||||||
.corner_radii(corners)
|
.corner_radii(corners)
|
||||||
.build(),
|
.build();
|
||||||
));
|
ferese_theme::apply(&mut native, foreground.into());
|
||||||
|
let theme = cosmic::Theme::custom(std::sync::Arc::new(native));
|
||||||
let family: &'static str =
|
let family: &'static str =
|
||||||
Box::leak(string("theme.typography.font_family", "Inter").into_boxed_str());
|
Box::leak(string("theme.typography.font_family", "Inter").into_boxed_str());
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ license.workspace = true
|
|||||||
rust-version = "1.95"
|
rust-version = "1.95"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
ferese-theme = { path = "../../crates/ferese-theme" }
|
||||||
ferese-config = { path = "../../crates/ferese-config" }
|
ferese-config = { path = "../../crates/ferese-config" }
|
||||||
libcosmic.workspace = true
|
libcosmic.workspace = true
|
||||||
jiff.workspace = true
|
jiff.workspace = true
|
||||||
|
|||||||
@@ -115,15 +115,15 @@ impl Appearance {
|
|||||||
corners.radius_m = [self.radius; 4];
|
corners.radius_m = [self.radius; 4];
|
||||||
corners.radius_l = [self.radius; 4];
|
corners.radius_l = [self.radius; 4];
|
||||||
corners.radius_xl = [self.radius; 4];
|
corners.radius_xl = [self.radius; 4];
|
||||||
cosmic::Theme::custom(std::sync::Arc::new(
|
let mut native = builder
|
||||||
builder
|
|
||||||
.corner_radii(corners)
|
.corner_radii(corners)
|
||||||
.bg_color(color(self.panel))
|
.bg_color(color(self.panel))
|
||||||
.primary_container_bg(color(self.panel))
|
.primary_container_bg(color(self.panel))
|
||||||
.text_tint(color(self.text).color)
|
.text_tint(color(self.text).color)
|
||||||
.accent(color(self.accent).color)
|
.accent(ferese_theme::accent_color(self.accent, self.panel))
|
||||||
.build(),
|
.build();
|
||||||
))
|
ferese_theme::apply(&mut native, self.text);
|
||||||
|
cosmic::Theme::custom(std::sync::Arc::new(native))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -362,9 +362,12 @@ impl Locker {
|
|||||||
.center_x(avatar_size)
|
.center_x(avatar_size)
|
||||||
.center_y(avatar_size)
|
.center_y(avatar_size)
|
||||||
.class(theme::Container::custom(move |_| {
|
.class(theme::Container::custom(move |_| {
|
||||||
|
let fill = ferese_theme::composite(a.accent.scale_alpha(0.18), a.panel);
|
||||||
|
let on = ferese_theme::foreground(fill, a.accent);
|
||||||
widget::container::Style {
|
widget::container::Style {
|
||||||
background: Some(iced::Background::Color(a.accent.scale_alpha(0.18))),
|
background: Some(iced::Background::Color(fill)),
|
||||||
text_color: Some(a.accent),
|
text_color: Some(on),
|
||||||
|
icon_color: Some(on),
|
||||||
border: iced::Border {
|
border: iced::Border {
|
||||||
radius: (avatar_size / 2.).into(),
|
radius: (avatar_size / 2.).into(),
|
||||||
width: 1.,
|
width: 1.,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ license.workspace = true
|
|||||||
rust-version = "1.93"
|
rust-version = "1.93"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
ferese-theme = { path = "../../crates/ferese-theme" }
|
||||||
ferese-config = { path = "../../crates/ferese-config" }
|
ferese-config = { path = "../../crates/ferese-config" }
|
||||||
libcosmic = { workspace = true, features = ["wgpu", "a11y", "xdg-portal"] }
|
libcosmic = { workspace = true, features = ["wgpu", "a11y", "xdg-portal"] }
|
||||||
ferese-ipc = { path = "../../crates/ferese-ipc" }
|
ferese-ipc = { path = "../../crates/ferese-ipc" }
|
||||||
|
|||||||
@@ -62,16 +62,7 @@ impl Palette {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn luminance(color: Color) -> f32 {
|
use ferese_theme::luminance;
|
||||||
let channel = |value: f32| {
|
|
||||||
if value <= 0.04045 {
|
|
||||||
value / 12.92
|
|
||||||
} else {
|
|
||||||
((value + 0.055) / 1.055).powf(2.4)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
0.2126 * channel(color.r) + 0.7152 * channel(color.g) + 0.0722 * channel(color.b)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn surface_shade(base: Color) -> Color {
|
fn surface_shade(base: Color) -> Color {
|
||||||
if luminance(base) > 0.5 {
|
if luminance(base) > 0.5 {
|
||||||
@@ -145,8 +136,8 @@ pub fn input_style(p: Palette) -> theme::TextInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn styled_button(p: Palette, selected: bool, navigation: bool) -> theme::Button {
|
fn styled_button(p: Palette, selected: bool, navigation: bool) -> theme::Button {
|
||||||
let style = move |hover: bool| button::Style {
|
let style = move |hover: bool| {
|
||||||
background: Some(Background::Color(if selected {
|
let background = if selected {
|
||||||
mix(p.sidebar, p.accent, if hover { 0.24 } else { 0.17 })
|
mix(p.sidebar, p.accent, if hover { 0.24 } else { 0.17 })
|
||||||
} else if hover {
|
} else if hover {
|
||||||
mix(p.card, surface_shade(p.sidebar), 0.06)
|
mix(p.card, surface_shade(p.sidebar), 0.06)
|
||||||
@@ -154,9 +145,16 @@ fn styled_button(p: Palette, selected: bool, navigation: bool) -> theme::Button
|
|||||||
p.sidebar
|
p.sidebar
|
||||||
} else {
|
} else {
|
||||||
p.card
|
p.card
|
||||||
})),
|
};
|
||||||
text_color: Some(p.text),
|
let on = if selected {
|
||||||
icon_color: None,
|
ferese_theme::foreground(background, p.text)
|
||||||
|
} else {
|
||||||
|
p.text
|
||||||
|
};
|
||||||
|
button::Style {
|
||||||
|
background: Some(Background::Color(background)),
|
||||||
|
text_color: Some(on),
|
||||||
|
icon_color: Some(on),
|
||||||
border_radius: 9.into(),
|
border_radius: 9.into(),
|
||||||
border_width: if selected { 1. } else { 0. },
|
border_width: if selected { 1. } else { 0. },
|
||||||
border_color: if selected {
|
border_color: if selected {
|
||||||
@@ -168,6 +166,7 @@ fn styled_button(p: Palette, selected: bool, navigation: bool) -> theme::Button
|
|||||||
outline_color: Color::TRANSPARENT,
|
outline_color: Color::TRANSPARENT,
|
||||||
overlay: None,
|
overlay: None,
|
||||||
shadow_offset: Vector::ZERO,
|
shadow_offset: Vector::ZERO,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
theme::Button::Custom {
|
theme::Button::Custom {
|
||||||
@@ -180,7 +179,10 @@ fn styled_button(p: Palette, selected: bool, navigation: bool) -> theme::Button
|
|||||||
|
|
||||||
pub fn native_theme(snapshot: Option<&Snapshot>) -> cosmic::Theme {
|
pub fn native_theme(snapshot: Option<&Snapshot>) -> cosmic::Theme {
|
||||||
let Some(snapshot) = snapshot else {
|
let Some(snapshot) = snapshot else {
|
||||||
return cosmic::Theme::custom(std::sync::Arc::new(cosmic::theme::COSMIC_DARK.clone()));
|
let mut native = cosmic::theme::COSMIC_DARK.clone();
|
||||||
|
let preferred = native.background(false).on.into();
|
||||||
|
ferese_theme::apply(&mut native, preferred);
|
||||||
|
return cosmic::Theme::custom(std::sync::Arc::new(native));
|
||||||
};
|
};
|
||||||
let palette = Palette::from(snapshot);
|
let palette = Palette::from(snapshot);
|
||||||
let builder = if luminance(palette.sidebar) > 0.5 {
|
let builder = if luminance(palette.sidebar) > 0.5 {
|
||||||
@@ -188,12 +190,13 @@ pub fn native_theme(snapshot: Option<&Snapshot>) -> cosmic::Theme {
|
|||||||
} else {
|
} else {
|
||||||
cosmic::cosmic_theme::ThemeBuilder::dark()
|
cosmic::cosmic_theme::ThemeBuilder::dark()
|
||||||
};
|
};
|
||||||
let theme = builder
|
let mut theme = builder
|
||||||
.bg_color(cosmic_color(palette.background))
|
.bg_color(cosmic_color(palette.background))
|
||||||
.primary_container_bg(cosmic_color(palette.card))
|
.primary_container_bg(cosmic_color(palette.card))
|
||||||
.text_tint(cosmic_color(palette.text).color)
|
.text_tint(cosmic_color(palette.text).color)
|
||||||
.accent(cosmic_color(palette.accent).color)
|
.accent(ferese_theme::accent_color(palette.accent, palette.card))
|
||||||
.build();
|
.build();
|
||||||
|
ferese_theme::apply(&mut theme, palette.text);
|
||||||
cosmic::Theme::custom(std::sync::Arc::new(theme))
|
cosmic::Theme::custom(std::sync::Arc::new(theme))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ license.workspace = true
|
|||||||
rust-version = "1.93"
|
rust-version = "1.93"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
ferese-theme = { path = "../../crates/ferese-theme" }
|
||||||
ferese-config = { path = "../../crates/ferese-config" }
|
ferese-config = { path = "../../crates/ferese-config" }
|
||||||
ferese-core = { path = "../../crates/ferese-core" }
|
ferese-core = { path = "../../crates/ferese-core" }
|
||||||
ferese-protocols = { path = "../../crates/ferese-protocols", default-features = false, features = ["client"] }
|
ferese-protocols = { path = "../../crates/ferese-protocols", default-features = false, features = ["client"] }
|
||||||
|
|||||||
@@ -65,18 +65,6 @@ fn blend(base: Color, foreground: Color, amount: f32) -> Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn contrast_text(background: Color, opacity: f32) -> Color {
|
|
||||||
let level = background.r * 0.2126 + background.g * 0.7152 + background.b * 0.0722;
|
|
||||||
Color {
|
|
||||||
a: opacity,
|
|
||||||
..if level > 0.6 {
|
|
||||||
Color::BLACK
|
|
||||||
} else {
|
|
||||||
Color::WHITE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn age_label(age: Duration) -> String {
|
fn age_label(age: Duration) -> String {
|
||||||
match age.as_secs() {
|
match age.as_secs() {
|
||||||
0..60 => "now".into(),
|
0..60 => "now".into(),
|
||||||
@@ -345,16 +333,25 @@ impl FereseShell {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
}));
|
}));
|
||||||
let icon: Element<'_, cosmic::Action<Message>> = if count > 1 {
|
let icon: Element<'_, cosmic::Action<Message>> = if count > 1 {
|
||||||
|
let badge_fill = ferese_theme::composite(
|
||||||
|
color(palette.accent),
|
||||||
|
color(if history {
|
||||||
|
palette.surface_base
|
||||||
|
} else {
|
||||||
|
palette.surface_popover
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let badge_text = ferese_theme::foreground(badge_fill, color(palette.text_primary));
|
||||||
let badge = container(
|
let badge = container(
|
||||||
text(count.to_string())
|
text(count.to_string())
|
||||||
.size(10)
|
.size(10)
|
||||||
.font(bold)
|
.font(bold)
|
||||||
.class(theme::Text::Color(contrast_text(accent, opacity))),
|
.class(theme::Text::Color(badge_text.scale_alpha(opacity))),
|
||||||
)
|
)
|
||||||
.center_x(16)
|
.center_x(16)
|
||||||
.center_y(16)
|
.center_y(16)
|
||||||
.class(theme::Container::custom(move |_| container::Style {
|
.class(theme::Container::custom(move |_| container::Style {
|
||||||
background: Some(Background::Color(accent)),
|
background: Some(Background::Color(badge_fill.scale_alpha(opacity))),
|
||||||
border: Border {
|
border: Border {
|
||||||
radius: motion::radius(8.0).into(),
|
radius: motion::radius(8.0).into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|||||||
Reference in new issue
Block a user