Add distinct theme presets and bundle the default Ferese wallpaper
This commit is contained in:
1 parent
9e90de4e74
commit
7e96a62e43
14 files changed
+462
-84
No files matched your search
@@ -100,7 +100,10 @@ raising the window; clicking raises it within its layer.
|
|||||||
|
|
||||||
## Make it yours
|
## Make it yours
|
||||||
|
|
||||||
Open **Control Center → Settings**, or run `ferese-settings`.
|
Open **Control Center → Settings**, or run `ferese-settings`. Appearance includes
|
||||||
|
six presets: Ferese Blue, Monochrome, Gruvbox, Dracula, Ayu Light, and Monokai.
|
||||||
|
The custom Ferese wallpaper is bundled as the default. Choose another image in
|
||||||
|
Settings → Wallpaper, or keep your existing wallpaper configuration.
|
||||||
Advanced options live in `~/.config/ferese/config.kdl` (or under
|
Advanced options live in `~/.config/ferese/config.kdl` (or under
|
||||||
`$XDG_CONFIG_HOME`). Changes reload automatically. The complete
|
`$XDG_CONFIG_HOME`). Changes reload automatically. The complete
|
||||||
[configuration reference](docs/configuration.md) covers every supported option;
|
[configuration reference](docs/configuration.md) covers every supported option;
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 196 KiB |
@@ -0,0 +1,62 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1920" height="1080" viewBox="0 0 1920 1080" xmlns:c2pa="http://c2pa.org/manifest"><metadata><c2pa:manifest>AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTo4MzkwZjY3Yi1lMWMzLTRkZjItYjc5Ni1kMThmOTcwMzJiOTgAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaHMKM0mGirl6EAsa915f3d8AAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDplYTUzOTY3Ni03MTE4LTQzMGItYTU3Ni02MDk2Zjk0ZmY0NTdscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNomXih8e+70OF/YLHlVgL9LgAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggnypnxLNZxd05KEbslyHWdZn0du7pvfZdyFM3FXgUiimkZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaEYNHg3OE8TdlNzzrWNuGkkAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCBmyCV0xk4L/Lqf9X1S9qWNTl5TCz9r70H5YR7X/R91+mRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBiaZmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjgzOTBmNjdiLWUxYzMtNGRmMi1iNzk2LWQxOGY5NzAzMmI5OC9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOmY4MGM0NTA5LTE1NmEtNDQxYi04ZDU4LTNjMzE5YTRiOWU4ZHJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCCfKmfEs1nF3TkoRuyXIdZ1mfR27um99l3IUzcVeBSKKaJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggBkna+FVWA7ttg4e5fN3SGCgCzCQlDA9sXSLaXIILGWiiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggaFremDqsMi+9hC8EXdAqcvUmalJPQpuJro7fci7a9hF0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALine truncated
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="bg" cx="50%" cy="42%" r="75%">
|
||||||
|
<stop offset="0%" stop-color="#12151c"/>
|
||||||
|
<stop offset="55%" stop-color="#0a0c11"/>
|
||||||
|
<stop offset="100%" stop-color="#050609"/>
|
||||||
|
</radialGradient>
|
||||||
|
|
||||||
|
<linearGradient id="glow" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#6ea3ff"/>
|
||||||
|
<stop offset="100%" stop-color="#3d7be6"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<linearGradient id="glass" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.06"/>
|
||||||
|
<stop offset="100%" stop-color="#ffffff" stop-opacity="0.015"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<filter id="soft" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feGaussianBlur stdDeviation="18"/>
|
||||||
|
</filter>
|
||||||
|
|
||||||
|
<filter id="softer" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feGaussianBlur stdDeviation="45"/>
|
||||||
|
</filter>
|
||||||
|
|
||||||
|
<filter id="markGlow" x="-100%" y="-100%" width="300%" height="300%">
|
||||||
|
<feGaussianBlur stdDeviation="14" result="blur"/>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="blur"/>
|
||||||
|
<feMergeNode in="SourceGraphic"/>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- base -->
|
||||||
|
<rect width="1920" height="1080" fill="url(#bg)"/>
|
||||||
|
|
||||||
|
<!-- distant blurred glow behind mark -->
|
||||||
|
<circle cx="960" cy="540" r="420" fill="url(#glow)" opacity="0.10" filter="url(#softer)"/>
|
||||||
|
|
||||||
|
<!-- faint background tiling grid, evoking Ferese's window layout -->
|
||||||
|
<g opacity="0.35">
|
||||||
|
<rect x="90" y="90" width="430" height="300" rx="18" fill="url(#glass)" stroke="#ffffff" stroke-opacity="0.05" filter="url(#soft)"/>
|
||||||
|
<rect x="90" y="420" width="430" height="220" rx="18" fill="url(#glass)" stroke="#ffffff" stroke-opacity="0.05" filter="url(#soft)"/>
|
||||||
|
<rect x="1400" y="120" width="420" height="230" rx="18" fill="url(#glass)" stroke="#ffffff" stroke-opacity="0.05" filter="url(#soft)"/>
|
||||||
|
<rect x="1400" y="380" width="420" height="330" rx="18" fill="url(#glass)" stroke="#ffffff" stroke-opacity="0.05" filter="url(#soft)"/>
|
||||||
|
<rect x="120" y="760" width="360" height="220" rx="18" fill="url(#glass)" stroke="#ffffff" stroke-opacity="0.04" filter="url(#soft)"/>
|
||||||
|
<rect x="1440" y="800" width="360" height="180" rx="18" fill="url(#glass)" stroke="#ffffff" stroke-opacity="0.04" filter="url(#soft)"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- centered Ferese mark, scaled from the 200x200 source -->
|
||||||
|
<g transform="translate(960,540) scale(1.9) translate(-100,-100)" filter="url(#markGlow)">
|
||||||
|
<rect x="20" y="20" width="50" height="160" rx="8" fill="#3d7be6"/>
|
||||||
|
<rect x="80" y="20" width="100" height="55" rx="8" fill="#3d7be6"/>
|
||||||
|
<rect x="80" y="95" width="70" height="45" rx="8" fill="#3d7be6"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- subtle vignette -->
|
||||||
|
<rect width="1920" height="1080" fill="url(#bg)" opacity="0" />
|
||||||
|
<rect x="0" y="0" width="1920" height="1080" fill="black" opacity="0.12" style="mix-blend-mode:multiply"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 11 KiB |
@@ -1674,7 +1674,7 @@ fn default_surface_base_color() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn default_accent_color() -> String {
|
fn default_accent_color() -> String {
|
||||||
"#5B8CFF".to_owned()
|
"#3D7BE6".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_shadow_color() -> String {
|
fn default_shadow_color() -> String {
|
||||||
|
|||||||
@@ -17,13 +17,27 @@ use smithay::{
|
|||||||
};
|
};
|
||||||
use std::{collections::HashMap, path::PathBuf, sync::mpsc};
|
use std::{collections::HashMap, path::PathBuf, sync::mpsc};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||||
pub(crate) struct WallpaperConfig {
|
pub(crate) struct WallpaperConfig {
|
||||||
|
#[serde(default = "default_wallpaper_path")]
|
||||||
pub path: Option<PathBuf>,
|
pub path: Option<PathBuf>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub mode: WallpaperMode,
|
pub mode: WallpaperMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_wallpaper_path() -> Option<PathBuf> {
|
||||||
|
Some(PathBuf::from(ferese_config::default_wallpaper()))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WallpaperConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
path: default_wallpaper_path(),
|
||||||
|
mode: WallpaperMode::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub(crate) enum WallpaperMode {
|
pub(crate) enum WallpaperMode {
|
||||||
@@ -261,7 +275,7 @@ mod tests {
|
|||||||
let (sender, receiver) = mpsc::channel();
|
let (sender, receiver) = mpsc::channel();
|
||||||
state.receiver = Some(receiver);
|
state.receiver = Some(receiver);
|
||||||
state.reload(WallpaperConfig {
|
state.reload(WallpaperConfig {
|
||||||
path: None,
|
path: state.config.path.clone(),
|
||||||
mode: WallpaperMode::Fit,
|
mode: WallpaperMode::Fit,
|
||||||
});
|
});
|
||||||
assert!(state.receiver.is_some());
|
assert!(state.receiver.is_some());
|
||||||
@@ -284,7 +298,7 @@ mod tests {
|
|||||||
let (sender, receiver) = mpsc::channel();
|
let (sender, receiver) = mpsc::channel();
|
||||||
state.receiver = Some(receiver);
|
state.receiver = Some(receiver);
|
||||||
state.reload(WallpaperConfig {
|
state.reload(WallpaperConfig {
|
||||||
path: None,
|
path: state.config.path.clone(),
|
||||||
mode: WallpaperMode::Fit,
|
mode: WallpaperMode::Fit,
|
||||||
});
|
});
|
||||||
state.reload(WallpaperConfig::default());
|
state.reload(WallpaperConfig::default());
|
||||||
@@ -314,7 +328,18 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn missing_wallpaper_preserves_external_shell_fallback() {
|
fn missing_wallpaper_preserves_external_shell_fallback() {
|
||||||
let state = WallpaperState::new(WallpaperConfig::default());
|
let state = WallpaperState::new(WallpaperConfig {
|
||||||
|
path: None,
|
||||||
|
mode: WallpaperMode::default(),
|
||||||
|
});
|
||||||
assert!(!state.owns_background());
|
assert!(!state.owns_background());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_wallpaper_is_bundled_and_decodable() {
|
||||||
|
let config: WallpaperConfig = ferese_config::from_str("").unwrap();
|
||||||
|
let path = config.path.unwrap();
|
||||||
|
assert_eq!(image::image_dimensions(&path).unwrap(), (1920, 1080));
|
||||||
|
assert!(WallpaperState::new(WallpaperConfig::default()).owns_background());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -27,6 +27,23 @@ pub fn config_path() -> Option<PathBuf> {
|
|||||||
.map(|p| p.join("ferese/config.kdl"))
|
.map(|p| p.join("ferese/config.kdl"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn default_wallpaper() -> &'static str {
|
||||||
|
static PATH: std::sync::OnceLock<String> = std::sync::OnceLock::new();
|
||||||
|
PATH.get_or_init(|| {
|
||||||
|
let path = std::env::current_exe()
|
||||||
|
.ok()
|
||||||
|
.and_then(|path| path.parent().map(|dir| dir.join("wallpapers/ferese.png")))
|
||||||
|
.filter(|path| path.is_file())
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../assets/wallpapers/ferese.png")
|
||||||
|
});
|
||||||
|
path.canonicalize()
|
||||||
|
.unwrap_or(path)
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn field(name: &str, parent: &str) -> String {
|
fn field(name: &str, parent: &str) -> String {
|
||||||
if parent == "commands" {
|
if parent == "commands" {
|
||||||
return name.to_owned();
|
return name.to_owned();
|
||||||
|
|||||||
+20
-3
@@ -105,6 +105,23 @@ Changing default width preserves manually resized columns.
|
|||||||
|
|
||||||
## Appearance
|
## Appearance
|
||||||
|
|
||||||
|
Settings → Appearance offers six presets: **Ferese Blue** (default),
|
||||||
|
**Monochrome**, **Gruvbox**, **Dracula**, **Ayu Light**, and **Monokai**.
|
||||||
|
Ferese Blue uses the README logo's `#3D7BE6`. Each preset coordinates surfaces,
|
||||||
|
text, menu-bar colors, borders and the focus ring. Ayu Light also switches native
|
||||||
|
Settings controls to light styling. Presets preserve wallpaper, typography,
|
||||||
|
geometry and opacity settings; colors remain individually editable.
|
||||||
|
|
||||||
|
The community palettes are desktop adaptations with text shades adjusted for
|
||||||
|
readability. Sources: [Gruvbox](https://github.com/morhetz/gruvbox),
|
||||||
|
[Dracula](https://github.com/dracula/visual-studio-code),
|
||||||
|
[Ayu](https://github.com/ayu-theme/ayu-colors), and
|
||||||
|
[classic Monokai](https://github.com/microsoft/vscode/blob/main/extensions/theme-monokai/themes/monokai-color-theme.json).
|
||||||
|
Marketplace install counts informed the shortlist; they are not a measurement
|
||||||
|
of desktop users. Monochrome and Gruvbox replace similar blue palettes to keep
|
||||||
|
these six visually distinct.
|
||||||
|
|
||||||
|
|
||||||
| Section / key | Type | Default | Meaning |
|
| Section / key | Type | Default | Meaning |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `appearance.corner_radius` | number ≥ 0 | `14` | Shell material/card radius |
|
| `appearance.corner_radius` | number ≥ 0 | `14` | Shell material/card radius |
|
||||||
@@ -112,7 +129,7 @@ Changing default width preserves manually resized columns.
|
|||||||
| `appearance.inactive_dim.amount` | number 0–1 | `0.15` | Darkening strength |
|
| `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 |
|
| `appearance.inactive_dim.duration_ms` | number ≥ 0 | `150` | Dimming and focus-ring transition; 0 snaps |
|
||||||
| `theme.typography.font_family` | string | system sans-serif | Shell, Settings and overview font |
|
| `theme.typography.font_family` | string | system sans-serif | Shell, Settings and overview font |
|
||||||
| `theme.background.path` | string | omitted | Wallpaper image path |
|
| `theme.background.path` | string | bundled Ferese wallpaper | Wallpaper image path; an existing selection overrides the default |
|
||||||
| `theme.background.mode` | `"fill"`, `"fit"` | `"fill"` | Crop or letterbox |
|
| `theme.background.mode` | `"fill"`, `"fit"` | `"fill"` | Crop or letterbox |
|
||||||
| `theme.material.style` | `"solid"`, `"translucent"` | `"solid"` | Shell background material |
|
| `theme.material.style` | `"solid"`, `"translucent"` | `"solid"` | Shell background material |
|
||||||
| `theme.material.blur_radius` | number ≥ 0 | `12` | Translucent backdrop blur, capped at 32; 0 disables |
|
| `theme.material.blur_radius` | number ≥ 0 | `12` | Translucent backdrop blur, capped at 32; 0 disables |
|
||||||
@@ -121,8 +138,8 @@ Changing default width preserves manually resized columns.
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `surface_base` | `"#111821"` | Material/card base color |
|
| `surface_base` | `"#111821"` | Material/card base color |
|
||||||
| `text_primary` | `"#F4F7FB"` | Main text |
|
| `text_primary` | `"#F4F7FB"` | Main text |
|
||||||
| `text_muted` | `"#7F8A98"` | Secondary text |
|
| `text_muted` | `"#8793A2"` | Secondary text |
|
||||||
| `accent` | `"#5B8CFF"` | Active controls and solid focus ring |
|
| `accent` | `"#3D7BE6"` | Active controls and solid focus ring |
|
||||||
| `border` | `"#FFFFFF18"` | Unfocused window border |
|
| `border` | `"#FFFFFF18"` | Unfocused window border |
|
||||||
| `shadow` | `"#00000055"` | Window shadow color |
|
| `shadow` | `"#00000055"` | Window shadow color |
|
||||||
|
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ layout {
|
|||||||
theme {
|
theme {
|
||||||
surface {
|
surface {
|
||||||
bar {
|
bar {
|
||||||
background "#1C202E"
|
background "#111821"
|
||||||
opacity 0.95
|
opacity 0.95
|
||||||
text-primary "#F0F3FA"
|
text-primary "#F4F7FB"
|
||||||
text-muted "#AAB4C7"
|
text-muted "#8793A2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ fi
|
|||||||
for name in ferese ferese-shell ferese-settings feresectl; do
|
for name in ferese ferese-shell ferese-settings feresectl; do
|
||||||
[[ -x $repo_dir/target/release/$name ]] || { echo "Missing release binary: $name" >&2; exit 1; }
|
[[ -x $repo_dir/target/release/$name ]] || { echo "Missing release binary: $name" >&2; exit 1; }
|
||||||
done
|
done
|
||||||
|
for name in ferese.png ferese.svg; do
|
||||||
|
[[ -f $repo_dir/assets/wallpapers/$name ]] || { echo "Missing wallpaper asset: $name" >&2; exit 1; }
|
||||||
|
done
|
||||||
desktop-file-validate "$repo_dir/packaging/ferese.desktop"
|
desktop-file-validate "$repo_dir/packaging/ferese.desktop"
|
||||||
desktop-file-validate "$repo_dir/packaging/dev.ferese.Settings.desktop"
|
desktop-file-validate "$repo_dir/packaging/dev.ferese.Settings.desktop"
|
||||||
|
|
||||||
@@ -56,6 +59,9 @@ install -m 0755 -- "$repo_dir/packaging/ferese-session" "$release_dir/ferese-ses
|
|||||||
install -m 0755 -- "$repo_dir/packaging/ferese-lock" "$release_dir/ferese-lock"
|
install -m 0755 -- "$repo_dir/packaging/ferese-lock" "$release_dir/ferese-lock"
|
||||||
install -m 0755 -- "$repo_dir/packaging/ferese-screenshot" "$release_dir/ferese-screenshot"
|
install -m 0755 -- "$repo_dir/packaging/ferese-screenshot" "$release_dir/ferese-screenshot"
|
||||||
install -m 0644 -- "$repo_dir/packaging/config.kdl" "$release_dir/config.example.kdl"
|
install -m 0644 -- "$repo_dir/packaging/config.kdl" "$release_dir/config.example.kdl"
|
||||||
|
install -d -m 0755 -- "$release_dir/wallpapers"
|
||||||
|
install -m 0644 -- "$repo_dir/assets/wallpapers/ferese.png" "$release_dir/wallpapers/ferese.png"
|
||||||
|
install -m 0644 -- "$repo_dir/assets/wallpapers/ferese.svg" "$release_dir/wallpapers/ferese.svg"
|
||||||
|
|
||||||
if [[ -L $install_root/current ]]; then
|
if [[ -L $install_root/current ]]; then
|
||||||
ln -sfn -- "$(readlink -- "$install_root/current")" "$install_root/previous"
|
ln -sfn -- "$(readlink -- "$install_root/current")" "$install_root/previous"
|
||||||
|
|||||||
@@ -416,7 +416,11 @@ impl cosmic::Application for App {
|
|||||||
Message::WallpaperPicked(Err(error)) => self.error = Some(error),
|
Message::WallpaperPicked(Err(error)) => self.error = Some(error),
|
||||||
Message::Thumbnail(path, result) => {
|
Message::Thumbnail(path, result) => {
|
||||||
self.thumbnail_loading = false;
|
self.thumbnail_loading = false;
|
||||||
if path == self.current.string("theme.background.path", "") {
|
if path
|
||||||
|
== self
|
||||||
|
.current
|
||||||
|
.string("theme.background.path", ferese_config::default_wallpaper())
|
||||||
|
{
|
||||||
self.thumbnail_path = path;
|
self.thumbnail_path = path;
|
||||||
match result {
|
match result {
|
||||||
Ok(handle) => {
|
Ok(handle) => {
|
||||||
@@ -591,27 +595,31 @@ impl cosmic::Application for App {
|
|||||||
body = body.push(visuals::preview(&self.draft));
|
body = body.push(visuals::preview(&self.draft));
|
||||||
}
|
}
|
||||||
if self.page == Page::Appearance {
|
if self.page == Page::Appearance {
|
||||||
let mut presets = row([]).spacing(10);
|
let mut presets = column([]).spacing(10);
|
||||||
for (index, title) in ["Amberwood", "Monochrome", "Catppuccin"]
|
for (row_index, choices) in visuals::PRESETS.chunks(3).enumerate() {
|
||||||
.into_iter()
|
let mut tiles = row([]).spacing(10);
|
||||||
.enumerate()
|
for (column_index, preset) in choices.iter().enumerate() {
|
||||||
{
|
let index = row_index * 3 + column_index;
|
||||||
presets = presets.push(
|
let tile = button::custom(
|
||||||
button::custom(
|
column([])
|
||||||
row([])
|
.spacing(6)
|
||||||
.spacing(10)
|
|
||||||
.align_y(Alignment::Center)
|
|
||||||
.push(visuals::swatches(index))
|
.push(visuals::swatches(index))
|
||||||
.push(self.label(title, 12.)),
|
.push(self.label(preset.name, 12.))
|
||||||
|
.push(self.label(preset.description, 10.)),
|
||||||
)
|
)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.padding(10)
|
.padding(10)
|
||||||
.class(visuals::button_style(
|
.class(visuals::button_style(
|
||||||
palette,
|
palette,
|
||||||
visuals::preset_selected(&self.draft, index),
|
visuals::preset_selected(&self.draft, index),
|
||||||
))
|
));
|
||||||
.on_press(Message::Preset(index)),
|
tiles = tiles.push(if self.saving {
|
||||||
);
|
tile
|
||||||
|
} else {
|
||||||
|
tile.on_press(Message::Preset(index))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
presets = presets.push(tiles);
|
||||||
}
|
}
|
||||||
body = body.push(presets);
|
body = body.push(presets);
|
||||||
}
|
}
|
||||||
@@ -970,7 +978,9 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn load_thumbnail(&mut self) -> Task<Message> {
|
fn load_thumbnail(&mut self) -> Task<Message> {
|
||||||
let path = self.current.string("theme.background.path", "");
|
let path = self
|
||||||
|
.current
|
||||||
|
.string("theme.background.path", ferese_config::default_wallpaper());
|
||||||
if self.page != Page::Wallpaper || self.thumbnail_loading || path == self.thumbnail_path {
|
if self.page != Page::Wallpaper || self.thumbnail_loading || path == self.thumbnail_path {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -558,7 +558,7 @@ pub fn fields(page: Page) -> Vec<Field> {
|
|||||||
"theme.colors.accent",
|
"theme.colors.accent",
|
||||||
"Accent color",
|
"Accent color",
|
||||||
"Focus rings and selected controls. Use a hex color.",
|
"Focus rings and selected controls. Use a hex color.",
|
||||||
"#5B8CFF",
|
"#3D7BE6",
|
||||||
),
|
),
|
||||||
text(
|
text(
|
||||||
"theme.colors.surface_base",
|
"theme.colors.surface_base",
|
||||||
@@ -607,7 +607,7 @@ pub fn fields(page: Page) -> Vec<Field> {
|
|||||||
"theme.background.path",
|
"theme.background.path",
|
||||||
"Image location",
|
"Image location",
|
||||||
"Choose an image above, or enter its full path.",
|
"Choose an image above, or enter its full path.",
|
||||||
"",
|
ferese_config::default_wallpaper(),
|
||||||
),
|
),
|
||||||
choice(
|
choice(
|
||||||
"theme.background.mode",
|
"theme.background.mode",
|
||||||
|
|||||||
@@ -414,7 +414,7 @@ window-rule app-id="mine" floating=#true
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for preset in 0..3 {
|
for preset in 0..crate::visuals::PRESETS.len() {
|
||||||
let mut snapshot = Snapshot::parse(String::new()).unwrap();
|
let mut snapshot = Snapshot::parse(String::new()).unwrap();
|
||||||
for edit in crate::visuals::preset(preset) {
|
for edit in crate::visuals::preset(preset) {
|
||||||
snapshot.edit(&edit).unwrap();
|
snapshot.edit(&edit).unwrap();
|
||||||
|
|||||||
@@ -42,19 +42,19 @@ impl Palette {
|
|||||||
Color::from_rgb8(15, 16, 20),
|
Color::from_rgb8(15, 16, 20),
|
||||||
);
|
);
|
||||||
Self {
|
Self {
|
||||||
background: mix(base, Color::WHITE, 0.025),
|
background: mix(base, surface_shade(base), 0.025),
|
||||||
sidebar: base,
|
sidebar: base,
|
||||||
card: mix(base, Color::WHITE, 0.055),
|
card: mix(base, surface_shade(base), 0.055),
|
||||||
accent: color(
|
accent: color(
|
||||||
&snapshot.string("theme.colors.accent", "#5B8CFF"),
|
&snapshot.string("theme.colors.accent", "#3D7BE6"),
|
||||||
Color::from_rgb8(229, 200, 144),
|
Color::from_rgb8(61, 123, 230),
|
||||||
),
|
),
|
||||||
text: color(
|
text: color(
|
||||||
&snapshot.string("theme.colors.text_primary", "#e2e0e6"),
|
&snapshot.string("theme.colors.text_primary", "#F4F7FB"),
|
||||||
Color::WHITE,
|
Color::WHITE,
|
||||||
),
|
),
|
||||||
muted: color(
|
muted: color(
|
||||||
&snapshot.string("theme.colors.text_muted", "#96959f"),
|
&snapshot.string("theme.colors.text_muted", "#8793A2"),
|
||||||
Color::from_rgb8(150, 149, 159),
|
Color::from_rgb8(150, 149, 159),
|
||||||
),
|
),
|
||||||
error: mix(base, Color::from_rgb8(210, 80, 80), 0.2),
|
error: mix(base, Color::from_rgb8(210, 80, 80), 0.2),
|
||||||
@@ -62,6 +62,29 @@ impl Palette {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn luminance(color: Color) -> f32 {
|
||||||
|
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 {
|
||||||
|
if luminance(base) > 0.5 {
|
||||||
|
Color::BLACK
|
||||||
|
} else {
|
||||||
|
Color::WHITE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cosmic_color(color: Color) -> cosmic::theme::CosmicColor {
|
||||||
|
cosmic::theme::CosmicColor::new(color.r, color.g, color.b, 1.)
|
||||||
|
}
|
||||||
|
|
||||||
fn mix(a: Color, b: Color, t: f32) -> Color {
|
fn mix(a: Color, b: Color, t: f32) -> Color {
|
||||||
Color::from_rgb(
|
Color::from_rgb(
|
||||||
a.r + (b.r - a.r) * t,
|
a.r + (b.r - a.r) * t,
|
||||||
@@ -126,17 +149,21 @@ fn styled_button(p: Palette, selected: bool, navigation: bool) -> theme::Button
|
|||||||
background: Some(Background::Color(if selected {
|
background: Some(Background::Color(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, Color::WHITE, 0.06)
|
mix(p.card, surface_shade(p.sidebar), 0.06)
|
||||||
} else if navigation {
|
} else if navigation {
|
||||||
p.sidebar
|
p.sidebar
|
||||||
} else {
|
} else {
|
||||||
p.card
|
p.card
|
||||||
})),
|
})),
|
||||||
text_color: Some(if selected { p.accent } else { p.text }),
|
text_color: Some(p.text),
|
||||||
icon_color: None,
|
icon_color: None,
|
||||||
border_radius: 9.into(),
|
border_radius: 9.into(),
|
||||||
border_width: 0.,
|
border_width: if selected { 1. } else { 0. },
|
||||||
border_color: Color::TRANSPARENT,
|
border_color: if selected {
|
||||||
|
p.accent
|
||||||
|
} else {
|
||||||
|
Color::TRANSPARENT
|
||||||
|
},
|
||||||
outline_width: 0.,
|
outline_width: 0.,
|
||||||
outline_color: Color::TRANSPARENT,
|
outline_color: Color::TRANSPARENT,
|
||||||
overlay: None,
|
overlay: None,
|
||||||
@@ -152,14 +179,21 @@ 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 mut theme = cosmic::theme::COSMIC_DARK.clone();
|
let Some(snapshot) = snapshot else {
|
||||||
if let Some(snapshot) = snapshot {
|
return cosmic::Theme::custom(std::sync::Arc::new(cosmic::theme::COSMIC_DARK.clone()));
|
||||||
let p = Palette::from(snapshot);
|
};
|
||||||
let accent = cosmic::theme::CosmicColor::new(p.accent.r, p.accent.g, p.accent.b, 1.);
|
let palette = Palette::from(snapshot);
|
||||||
theme.accent.base = accent;
|
let builder = if luminance(palette.sidebar) > 0.5 {
|
||||||
theme.accent_button.base = accent;
|
cosmic::cosmic_theme::ThemeBuilder::light()
|
||||||
theme.text_button.on = accent;
|
} else {
|
||||||
}
|
cosmic::cosmic_theme::ThemeBuilder::dark()
|
||||||
|
};
|
||||||
|
let theme = builder
|
||||||
|
.bg_color(cosmic_color(palette.background))
|
||||||
|
.primary_container_bg(cosmic_color(palette.card))
|
||||||
|
.text_tint(cosmic_color(palette.text).color)
|
||||||
|
.accent(cosmic_color(palette.accent).color)
|
||||||
|
.build();
|
||||||
cosmic::Theme::custom(std::sync::Arc::new(theme))
|
cosmic::Theme::custom(std::sync::Arc::new(theme))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +215,18 @@ pub fn configured_font(snapshot: &Snapshot) -> cosmic::font::Font {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn icon(page: Page, tint: Color) -> svg_icon::Icon {
|
pub fn icon(page: Page, tint: Color) -> svg_icon::Icon {
|
||||||
svg_icon::from_svg_bytes(format!(r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="{}" fill="none" stroke="{}" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>"##, page.icon(),hex(tint)).into_bytes()).symbolic(false).icon().size(19)
|
let svg = format!(
|
||||||
|
r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path d="{}" fill="none" stroke="{}" stroke-width="1.6"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>"##,
|
||||||
|
page.icon(),
|
||||||
|
hex(tint),
|
||||||
|
);
|
||||||
|
svg_icon::from_svg_bytes(svg.into_bytes())
|
||||||
|
.symbolic(false)
|
||||||
|
.icon()
|
||||||
|
.size(19)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn brand_icon() -> svg_icon::Icon {
|
pub fn brand_icon() -> svg_icon::Icon {
|
||||||
@@ -192,51 +237,149 @@ pub fn brand_icon() -> svg_icon::Icon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn action_icon(path: &str, tint: Color) -> svg_icon::Icon {
|
pub fn action_icon(path: &str, tint: Color) -> svg_icon::Icon {
|
||||||
svg_icon::from_svg_bytes(format!(r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="{path}" fill="none" stroke="{}" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>"##, hex(tint)).into_bytes())
|
let svg = format!(
|
||||||
|
r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path d="{path}" fill="none" stroke="{}" stroke-width="1.6"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>"##,
|
||||||
|
hex(tint),
|
||||||
|
);
|
||||||
|
svg_icon::from_svg_bytes(svg.into_bytes())
|
||||||
.symbolic(false)
|
.symbolic(false)
|
||||||
.icon()
|
.icon()
|
||||||
.size(16)
|
.size(16)
|
||||||
}
|
}
|
||||||
const PRESETS: [(&str, &str, &str, &str, &str); 3] = [
|
|
||||||
("#e5c890", "#0f1014", "#c9c7cd", "#9998a8", "#795e40"),
|
pub struct Preset {
|
||||||
("#e5e5e5", "#101012", "#ededf0", "#97979f", "#696973"),
|
pub name: &'static str,
|
||||||
("#cba6f7", "#1e1e2e", "#cdd6f4", "#a6adc8", "#89b4fa"),
|
pub description: &'static str,
|
||||||
|
accent: &'static str,
|
||||||
|
base: &'static str,
|
||||||
|
text: &'static str,
|
||||||
|
muted: &'static str,
|
||||||
|
border: &'static str,
|
||||||
|
gradient_end: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const PRESETS: [Preset; 6] = [
|
||||||
|
Preset {
|
||||||
|
name: "Ferese Blue",
|
||||||
|
description: "Default · logo blue",
|
||||||
|
accent: "#3D7BE6",
|
||||||
|
base: "#111821",
|
||||||
|
text: "#F4F7FB",
|
||||||
|
muted: "#8793A2",
|
||||||
|
border: "#FFFFFF18",
|
||||||
|
gradient_end: "#3D7BE6",
|
||||||
|
},
|
||||||
|
Preset {
|
||||||
|
name: "Monochrome",
|
||||||
|
description: "Dark · grayscale",
|
||||||
|
accent: "#E5E5E5",
|
||||||
|
base: "#101012",
|
||||||
|
text: "#EDEDF0",
|
||||||
|
muted: "#97979F",
|
||||||
|
border: "#FFFFFF18",
|
||||||
|
gradient_end: "#696973",
|
||||||
|
},
|
||||||
|
Preset {
|
||||||
|
name: "Gruvbox",
|
||||||
|
description: "Dark · warm orange",
|
||||||
|
accent: "#FE8019",
|
||||||
|
base: "#282828",
|
||||||
|
text: "#EBDBB2",
|
||||||
|
muted: "#BDAE93",
|
||||||
|
border: "#504945",
|
||||||
|
gradient_end: "#FABD2F",
|
||||||
|
},
|
||||||
|
Preset {
|
||||||
|
name: "Dracula",
|
||||||
|
description: "Dark · purple",
|
||||||
|
accent: "#BD93F9",
|
||||||
|
base: "#282A36",
|
||||||
|
text: "#F8F8F2",
|
||||||
|
muted: "#A4ADCD",
|
||||||
|
border: "#44475A",
|
||||||
|
gradient_end: "#FF79C6",
|
||||||
|
},
|
||||||
|
Preset {
|
||||||
|
name: "Ayu Light",
|
||||||
|
description: "Light · paper",
|
||||||
|
accent: "#8F5300",
|
||||||
|
base: "#F8F9FA",
|
||||||
|
text: "#5C6166",
|
||||||
|
muted: "#596574",
|
||||||
|
border: "#C8CDD3",
|
||||||
|
gradient_end: "#996000",
|
||||||
|
},
|
||||||
|
Preset {
|
||||||
|
name: "Monokai",
|
||||||
|
description: "Dark · warm green",
|
||||||
|
accent: "#A6E22E",
|
||||||
|
base: "#272822",
|
||||||
|
text: "#F8F8F2",
|
||||||
|
muted: "#B2B29F",
|
||||||
|
border: "#414339",
|
||||||
|
gradient_end: "#E6DB74",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn preset(index: usize) -> Vec<Edit> {
|
pub fn preset(index: usize) -> Vec<Edit> {
|
||||||
let (accent, base, text, muted, end) = PRESETS[index.min(2)];
|
let Some(preset) = PRESETS.get(index) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
vec![
|
vec![
|
||||||
set("theme.colors.accent", accent),
|
set("theme.colors.accent", preset.accent),
|
||||||
set("theme.colors.surface_base", base),
|
set("theme.colors.surface_base", preset.base),
|
||||||
set("theme.colors.text_primary", text),
|
set("theme.colors.text_primary", preset.text),
|
||||||
set("theme.colors.text_muted", muted),
|
set("theme.colors.text_muted", preset.muted),
|
||||||
set("theme.surface.bar.background", base),
|
set("theme.colors.border", preset.border),
|
||||||
set("theme.surface.bar.text_primary", text),
|
set("theme.colors.shadow", "#00000055"),
|
||||||
set("theme.surface.bar.text_muted", muted),
|
set("theme.surface.bar.background", preset.base),
|
||||||
set("theme.focus_ring.gradient.from", accent),
|
set("theme.surface.bar.text_primary", preset.text),
|
||||||
set("theme.focus_ring.gradient.to", end),
|
set("theme.surface.bar.text_muted", preset.muted),
|
||||||
|
set("theme.focus_ring.gradient.from", preset.accent),
|
||||||
|
set("theme.focus_ring.gradient.to", preset.gradient_end),
|
||||||
|
set("theme.focus_ring.gradient.angle", 0.0),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn preset_selected(snapshot: &Snapshot, index: usize) -> bool {
|
pub fn preset_selected(snapshot: &Snapshot, index: usize) -> bool {
|
||||||
let (accent, base, text, muted, _) = PRESETS[index];
|
let Some(preset) = PRESETS.get(index) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
[
|
[
|
||||||
("accent", accent),
|
("theme.colors.accent", preset.accent, "#3D7BE6"),
|
||||||
("surface_base", base),
|
("theme.colors.surface_base", preset.base, "#111821"),
|
||||||
("text_primary", text),
|
("theme.colors.text_primary", preset.text, "#F4F7FB"),
|
||||||
("text_muted", muted),
|
("theme.colors.text_muted", preset.muted, "#8793A2"),
|
||||||
|
("theme.colors.border", preset.border, "#FFFFFF18"),
|
||||||
|
("theme.colors.shadow", "#00000055", "#00000055"),
|
||||||
|
("theme.surface.bar.background", preset.base, "#1C202EF2"),
|
||||||
|
("theme.surface.bar.text_primary", preset.text, "#F0F3FA"),
|
||||||
|
("theme.surface.bar.text_muted", preset.muted, "#AAB4C7"),
|
||||||
|
("theme.focus_ring.gradient.from", preset.accent, "#3D7BE6"),
|
||||||
|
(
|
||||||
|
"theme.focus_ring.gradient.to",
|
||||||
|
preset.gradient_end,
|
||||||
|
"#3D7BE6",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.all(|(key, value)| {
|
.all(|(path, expected, fallback)| {
|
||||||
|
if index == 0 && snapshot.item(path).is_none() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
snapshot
|
snapshot
|
||||||
.string(&format!("theme.colors.{key}"), "")
|
.string(path, fallback)
|
||||||
.eq_ignore_ascii_case(value)
|
.eq_ignore_ascii_case(expected)
|
||||||
})
|
}) && snapshot.number("theme.focus_ring.gradient.angle", 0.) == 0.
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn swatches(index: usize) -> Element<'static, Message> {
|
pub fn swatches(index: usize) -> Element<'static, Message> {
|
||||||
let (accent, base, _, _, end) = PRESETS[index];
|
let preset = &PRESETS[index];
|
||||||
let mut row = widget::row([]).spacing(3);
|
let mut row = widget::row([]).spacing(3);
|
||||||
for value in [base, end, accent] {
|
for value in [preset.base, preset.gradient_end, preset.accent] {
|
||||||
row = row.push(
|
row = row.push(
|
||||||
container(widget::Space::new().width(12).height(22))
|
container(widget::Space::new().width(12).height(22))
|
||||||
.class(surface(color(value, Color::WHITE), 4.)),
|
.class(surface(color(value, Color::WHITE), 4.)),
|
||||||
@@ -244,8 +387,7 @@ pub fn swatches(index: usize) -> Element<'static, Message> {
|
|||||||
}
|
}
|
||||||
row.into()
|
row.into()
|
||||||
}
|
}
|
||||||
// A deliberately stylized preview, built as vector geometry at the actual
|
|
||||||
// output scale. No full-resolution wallpaper buffers or screenshot polling.
|
|
||||||
pub fn preview(snapshot: &Snapshot) -> Element<'static, Message> {
|
pub fn preview(snapshot: &Snapshot) -> Element<'static, Message> {
|
||||||
let p = Palette::from(snapshot);
|
let p = Palette::from(snapshot);
|
||||||
let base = hex(p.sidebar);
|
let base = hex(p.sidebar);
|
||||||
@@ -291,3 +433,82 @@ pub fn preview(snapshot: &Snapshot) -> Element<'static, Message> {
|
|||||||
.content_fit(cosmic::iced::ContentFit::Contain)
|
.content_fit(cosmic::iced::ContentFit::Contain)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn configured_preset(index: usize) -> Snapshot {
|
||||||
|
let mut snapshot = Snapshot::parse(String::new()).unwrap();
|
||||||
|
for edit in preset(index) {
|
||||||
|
snapshot.edit(&edit).unwrap();
|
||||||
|
}
|
||||||
|
snapshot
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contrast(a: Color, b: Color) -> f32 {
|
||||||
|
let a = luminance(a);
|
||||||
|
let b = luminance(b);
|
||||||
|
(a.max(b) + 0.05) / (a.min(b) + 0.05)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn presets_are_distinct_and_readable_on_settings_surfaces() {
|
||||||
|
for (index, preset) in PRESETS.iter().enumerate() {
|
||||||
|
let snapshot = configured_preset(index);
|
||||||
|
let palette = Palette::from(&snapshot);
|
||||||
|
for background in [palette.background, palette.sidebar, palette.card] {
|
||||||
|
assert!(
|
||||||
|
contrast(palette.text, background) >= 4.5,
|
||||||
|
"{} text",
|
||||||
|
preset.name
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
contrast(palette.muted, background) >= 4.5,
|
||||||
|
"{} muted text",
|
||||||
|
preset.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for other in PRESETS.iter().skip(index + 1) {
|
||||||
|
assert_ne!(preset.accent, other.accent);
|
||||||
|
assert_ne!(preset.base, other.base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn selection_tracks_all_preset_colors_and_gradient_changes() {
|
||||||
|
for index in 0..PRESETS.len() {
|
||||||
|
let mut snapshot = configured_preset(index);
|
||||||
|
assert!(preset_selected(&snapshot, index));
|
||||||
|
snapshot
|
||||||
|
.edit(&set("theme.surface.bar.text_primary", "#123456"))
|
||||||
|
.unwrap();
|
||||||
|
assert!(!preset_selected(&snapshot, index));
|
||||||
|
let mut snapshot = configured_preset(index);
|
||||||
|
snapshot
|
||||||
|
.edit(&set("theme.focus_ring.gradient.to", "#123456"))
|
||||||
|
.unwrap();
|
||||||
|
assert!(!preset_selected(&snapshot, index));
|
||||||
|
}
|
||||||
|
assert!(preset_selected(&Snapshot::parse(String::new()).unwrap(), 0));
|
||||||
|
assert!(preset(usize::MAX).is_empty());
|
||||||
|
assert!(!preset_selected(
|
||||||
|
&Snapshot::parse(String::new()).unwrap(),
|
||||||
|
usize::MAX
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_uses_logo_blue_and_native_controls_follow_lightness() {
|
||||||
|
let logo = include_str!("../../../packaging/icons/ferese.svg").to_ascii_uppercase();
|
||||||
|
assert!(logo.contains(PRESETS[0].accent));
|
||||||
|
for index in 0..PRESETS.len() {
|
||||||
|
let snapshot = configured_preset(index);
|
||||||
|
assert_eq!(
|
||||||
|
native_theme(Some(&snapshot)).cosmic().is_dark,
|
||||||
|
PRESETS[index].name != "Ayu Light"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,8 +67,8 @@ impl Default for ShellTheme {
|
|||||||
surface_base: [17, 24, 33, 255],
|
surface_base: [17, 24, 33, 255],
|
||||||
surface_popover: [17, 24, 33, 245],
|
surface_popover: [17, 24, 33, 245],
|
||||||
text_primary: [244, 247, 251, 255],
|
text_primary: [244, 247, 251, 255],
|
||||||
text_muted: [127, 138, 152, 255],
|
text_muted: [135, 147, 162, 255],
|
||||||
accent: [91, 140, 255, 255],
|
accent: [61, 123, 230, 255],
|
||||||
border: [255, 255, 255, 24],
|
border: [255, 255, 255, 24],
|
||||||
shadow: [0, 0, 0, 85],
|
shadow: [0, 0, 0, 85],
|
||||||
bar_height: 28.0,
|
bar_height: 28.0,
|
||||||
@@ -95,13 +95,27 @@ impl ShellTheme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub(crate) struct WallpaperConfig {
|
pub(crate) struct WallpaperConfig {
|
||||||
|
#[serde(default = "default_wallpaper_path")]
|
||||||
pub(crate) path: Option<PathBuf>,
|
pub(crate) path: Option<PathBuf>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) mode: WallpaperMode,
|
pub(crate) mode: WallpaperMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_wallpaper_path() -> Option<PathBuf> {
|
||||||
|
Some(PathBuf::from(ferese_config::default_wallpaper()))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WallpaperConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
path: default_wallpaper_path(),
|
||||||
|
mode: WallpaperMode::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub(crate) enum WallpaperMode {
|
pub(crate) enum WallpaperMode {
|
||||||
@@ -423,11 +437,11 @@ fn default_text_primary() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn default_text_muted() -> String {
|
fn default_text_muted() -> String {
|
||||||
"#7F8A98".to_owned()
|
"#8793A2".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_accent() -> String {
|
fn default_accent() -> String {
|
||||||
"#5B8CFF".to_owned()
|
"#3D7BE6".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_border() -> String {
|
fn default_border() -> String {
|
||||||
@@ -555,7 +569,10 @@ theme {
|
|||||||
let config: FereseConfig = ferese_config::from_str("").unwrap();
|
let config: FereseConfig = ferese_config::from_str("").unwrap();
|
||||||
|
|
||||||
assert_eq!(config.theme.typography.font_family, None);
|
assert_eq!(config.theme.typography.font_family, None);
|
||||||
assert_eq!(config.theme.background.path, None);
|
assert_eq!(
|
||||||
|
config.theme.background.path,
|
||||||
|
Some(PathBuf::from(ferese_config::default_wallpaper()))
|
||||||
|
);
|
||||||
assert_eq!(config.theme.background.mode, WallpaperMode::Fill);
|
assert_eq!(config.theme.background.mode, WallpaperMode::Fill);
|
||||||
let theme = shell_theme(&config.theme);
|
let theme = shell_theme(&config.theme);
|
||||||
assert_eq!(theme.bar_margin_top, 0);
|
assert_eq!(theme.bar_margin_top, 0);
|
||||||
|
|||||||
Reference in new issue
Block a user