feat(ui): refine Graphite visuals and center navigation content

This commit is contained in:
aria committed 2026-10-28 04:09:10 +09:00
1 parent cc3b6f903f
commit 1e34ec47ad
14 files changed
+646 -121

No files matched your search

+3 -1
View File
@@ -567,5 +567,7 @@
"Application icons": "应用图标",
"Refresh applications": "刷新应用列表",
"Panel status icons": "面板状态图标",
"Install the matching Ferese compositor beside Settings.": "请将匹配版本的 Ferese 主程序与设置程序安装在同一目录。"
"Install the matching Ferese compositor beside Settings.": "请将匹配版本的 Ferese 主程序与设置程序安装在同一目录。",
"Quick access": "快速访问",
"Saving…": "正在保存…"
}
+2 -1
View File
@@ -1,13 +1,14 @@
use crate::theme::Appearance;
pub use ferese_theme_model::families::{Family, Palette};
pub const BUILTINS: [(&str, &str, &str, &str); 6] = [
pub const BUILTINS: [(&str, &str, &str, &str); 7] = [
(
"ferese-blue",
"Ferese Blue",
"ferese-blue-light",
"ferese-blue",
),
("graphite", "Graphite", "graphite-light", "graphite"),
(
"catppuccin",
"Catppuccin",
+37 -1
View File
@@ -19,7 +19,7 @@ pub struct Preset {
pub shadow_opacity: f64,
}
pub const PRESETS: [Preset; 12] = [
pub const PRESETS: [Preset; 14] = [
Preset {
appearance: Appearance::Dark,
id: "ferese-blue",
@@ -38,6 +38,24 @@ pub const PRESETS: [Preset; 12] = [
tint_strength: 0.5,
shadow_opacity: 0.2,
},
Preset {
appearance: Appearance::Dark,
id: "graphite",
name: "Graphite",
description: "Quiet graphite",
base: "#101113",
raised: "#1B1D21",
application_background: "#141518",
text: "#E7E9ED",
muted: "#9A9FA9",
accent: "#8EABD8",
border: "#FFFFFF16",
gradient_end: "#8EABD8",
opacity: 0.96,
blur_radius: 8.0,
tint_strength: 0.06,
shadow_opacity: 0.12,
},
Preset {
appearance: Appearance::Dark,
id: "catppuccin-mocha",
@@ -146,6 +164,24 @@ pub const PRESETS: [Preset; 12] = [
tint_strength: 1.0,
shadow_opacity: 0.1,
},
Preset {
appearance: Appearance::Light,
id: "graphite-light",
name: "Graphite",
description: "Quiet paper",
base: "#F7F8FA",
raised: "#FFFFFF",
application_background: "#F1F3F5",
text: "#23262C",
muted: "#626A76",
accent: "#3F6DAA",
border: "#00000018",
gradient_end: "#3F6DAA",
opacity: 0.98,
blur_radius: 4.0,
tint_strength: 0.02,
shadow_opacity: 0.08,
},
Preset {
appearance: Appearance::Light,
id: "catppuccin-latte",
+3 -3
View File
@@ -1358,8 +1358,8 @@ mod tests {
.iter()
.filter(|p| p.appearance == Appearance::Light)
.collect();
assert_eq!(dark.len(), 6);
assert_eq!(light.len(), 6);
assert_eq!(dark.len(), crate::families::BUILTINS.len());
assert_eq!(light.len(), crate::families::BUILTINS.len());
for (dark, light) in dark.iter().zip(light) {
assert_eq!(dark.name, light.name);
assert_ne!(dark.base, light.base);
@@ -1443,7 +1443,7 @@ mod tests {
);
assert_eq!(resolved.policy.light.family.as_deref(), Some("gruvbox"));
assert_eq!(resolved.policy.dark.family.as_deref(), Some("catppuccin"));
assert_eq!(resolved.families.len(), 6);
assert_eq!(resolved.families.len(), crate::families::BUILTINS.len());
}
}
let resolved = candidate(r#"theme { mode "light"; family "catppuccin"; split #true; light { family "gruvbox"; }; dark { family "everforest"; }; }"#).unwrap();
+21 -1
View File
@@ -107,6 +107,7 @@ fn main() -> Result<(), Box<dyn Error>> {
}
let mut sequences: Vec<Vec<u32>> = Vec::new();
let mut clicks = Vec::new();
let mut hovers = Vec::new();
let mut drag = None;
let mut extent = (1280, 800);
let mut args = std::env::args().skip(1);
@@ -119,6 +120,13 @@ fn main() -> Result<(), Box<dyn Error>> {
}
extent = (v[0], v[1]);
}
"--hover" => {
let v = parse_numbers(&args.next().ok_or("missing hover position")?)?;
if v.len() != 2 {
return Err("invalid hover".into());
}
hovers.push((v[0], v[1]));
}
"--click" => {
let v = parse_numbers(&args.next().ok_or("missing click position")?)?;
if v.len() != 2 {
@@ -247,12 +255,24 @@ fn main() -> Result<(), Box<dyn Error>> {
std::thread::sleep(Duration::from_millis(35));
}
}
if !clicks.is_empty() || drag.is_some() {
if !clicks.is_empty() || !hovers.is_empty() || drag.is_some() {
let pointer = state
.pointer_manager
.as_ref()
.ok_or("test pointer is not enabled on this compositor")?
.create_virtual_pointer(state.seat.as_ref(), &qh, ());
for (x, y) in hovers {
pointer.motion_absolute(
started.elapsed().as_millis() as u32,
x,
y,
extent.0,
extent.1,
);
pointer.frame();
queue.roundtrip(&mut state)?;
std::thread::sleep(Duration::from_millis(120));
}
for (x, y) in clicks {
pointer.motion_absolute(
started.elapsed().as_millis() as u32,
+126 -25
View File
@@ -116,14 +116,24 @@ pub fn settings_input(p: Palette) -> theme::TextInput {
let appearance = move |focused: bool, hovered: bool| cosmic::widget::text_input::Appearance {
shape: Some(BorderShape::Continuous),
outline: None,
background: mix(p.card, p.text, if hovered { 0.06 } else { 0.035 }).into(),
background: mix(p.background, p.card, if hovered { 0.55 } else { 0.35 }).into(),
border_radius: p.radius.min(7.).into(),
border_width: 1.,
border_offset: None,
border_color: if focused {
p.accent
} else {
mix(p.card, p.text, if hovered { 0.26 } else { 0.18 })
mix(
p.card,
p.text,
if p.high_contrast {
0.5
} else if hovered {
0.20
} else {
0.10
},
)
},
icon_color: Some(p.muted),
text_color: Some(p.text),
@@ -166,25 +176,52 @@ fn styled_button(
opacity: f32,
focus_visible: bool,
) -> theme::Button {
let style = move |hover: bool, focused: bool| {
let background = if selected {
mix(p.sidebar, p.accent, if hover { 0.24 } else { 0.17 })
} else if hover {
if navigation {
mix(p.card, crate::surface_shade(p.sidebar), 0.06)
} else {
mix(p.card, p.text, 0.08)
}
} else if navigation {
let style = move |hover: bool, focused: bool, pressed: bool| {
let resting = if navigation {
p.sidebar
} else {
mix(p.card, p.text, 0.035)
mix(p.background, p.card, 0.5)
};
let (background, on) = if selected {
crate::accent_pair(background, p.text)
let selected_strength = if navigation { 0.09 } else { 0.10 };
let background = if selected && navigation {
mix(
resting,
p.text,
if pressed {
0.16
} else if hover {
0.13
} else {
selected_strength
},
)
} else if selected {
mix(
resting,
p.accent,
if pressed {
0.20
} else if hover {
0.16
} else {
selected_strength
},
)
} else if hover {
mix(resting, p.text, if pressed { 0.09 } else { 0.055 })
} else {
(background, p.text)
resting
};
let mut on = crate::foreground(background, p.text);
if p.high_contrast && crate::contrast(background, on) < 7. {
on = if crate::contrast(background, Color::BLACK)
>= crate::contrast(background, Color::WHITE)
{
Color::BLACK
} else {
Color::WHITE
};
}
let mut background = background;
background.a *= if hover && opacity < 1.0 {
@@ -193,9 +230,15 @@ fn styled_button(
opacity
};
let fill = if selected {
let fill = if selected && !navigation {
p.selected_background(
if hover { 0.24 } else { 0.17 },
if pressed {
0.20
} else if hover {
0.16
} else {
0.10
},
background.a,
on,
background,
@@ -211,11 +254,23 @@ fn styled_button(
text_color: Some(on),
icon_color: Some(on),
border_radius: p.radius.min(9.).into(),
border_width: if selected || !navigation { 1. } else { 0. },
border_width: if !navigation || (selected && p.high_contrast) {
1.
} else {
0.
},
border_color: if selected {
p.accent
if p.high_contrast {
p.accent
} else {
mix(background, p.accent, 0.40)
}
} else if !navigation {
mix(p.card, p.text, 0.18)
mix(
background,
p.text,
if p.high_contrast { 0.50 } else { 0.10 },
)
} else {
Color::TRANSPARENT
},
@@ -227,10 +282,17 @@ fn styled_button(
};
theme::Button::Custom {
active: Box::new(move |focused, _| style(false, focused)),
hovered: Box::new(move |focused, _| style(true, focused)),
pressed: Box::new(move |focused, _| style(true, focused)),
disabled: Box::new(move |_| style(false, false)),
active: Box::new(move |focused, _| style(false, focused, false)),
hovered: Box::new(move |focused, _| style(true, focused, false)),
pressed: Box::new(move |focused, _| style(true, focused, true)),
disabled: Box::new(move |_| {
let mut disabled = style(false, false, false);
if !selected {
disabled.text_color = Some(p.muted);
disabled.icon_color = Some(p.muted);
}
disabled
}),
}
}
@@ -433,6 +495,45 @@ pub fn filled_button(fill: Color, foreground: Color, radius: f32, opacity: f32)
mod tests {
use super::*;
#[test]
fn navigation_selection_is_quiet_but_keyboard_focus_and_press_are_explicit() {
use cosmic::widget::button::Catalog;
for preset in ferese_config::presets::PRESETS {
let mut resolved = ferese_config::theme::default_theme();
resolved.appearance = preset.appearance;
resolved.tokens = ferese_config::theme::preset(preset.id, preset.appearance).unwrap();
for high_contrast in [false, true] {
resolved.accessibility.increase_contrast = high_contrast;
let palette = Palette::from_resolved(&resolved);
let native = palette.native_theme();
for selected in [false, true] {
let class = navigation_style(palette, selected);
let resting = native.active(false, selected, &class);
let focus = native.active(true, selected, &class);
assert_eq!(resting.outline_width, 0.);
assert_eq!(focus.outline_width, 2.);
assert_eq!(
resting.border_width,
if selected && high_contrast { 1. } else { 0. }
);
let Some(Background::Color(fill)) = resting.background else {
panic!("navigation should not be a gradient")
};
assert!(
crate::contrast(fill, resting.text_color.unwrap())
>= if high_contrast { 7. } else { 4.5 },
"{}",
preset.id
);
assert_ne!(
native.hovered(false, selected, &class).background,
native.pressed(false, selected, &class).background
);
}
}
}
}
#[test]
fn hiding_modal_focus_preserves_button_borders_and_materials() {
use cosmic::widget::button::Catalog;