diff --git a/README.md b/README.md index f20eb50..116718a 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,10 @@ raising the window; clicking raises it within its layer. ## 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 `$XDG_CONFIG_HOME`). Changes reload automatically. The complete [configuration reference](docs/configuration.md) covers every supported option; diff --git a/assets/wallpapers/ferese.png b/assets/wallpapers/ferese.png new file mode 100644 index 0000000..7c01d20 Binary files /dev/null and b/assets/wallpapers/ferese.png differ diff --git a/assets/wallpapers/ferese.svg b/assets/wallpapers/ferese.svg new file mode 100644 index 0000000..7387314 --- /dev/null +++ b/assets/wallpapers/ferese.svg @@ -0,0 +1,62 @@ +AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTo4MzkwZjY3Yi1lMWMzLTRkZjItYjc5Ni1kMThmOTcwMzJiOTgAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaHMKM0mGirl6EAsa915f3d8AAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDplYTUzOTY3Ni03MTE4LTQzMGItYTU3Ni02MDk2Zjk0ZmY0NTdscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNomXih8e+70OF/YLHlVgL9LgAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggnypnxLNZxd05KEbslyHWdZn0du7pvfZdyFM3FXgUiimkZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaEYNHg3OE8TdlNzzrWNuGkkAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCBmyCV0xk4L/Lqf9X1S9qWNTl5TCz9r70H5YR7X/R91+mRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBiaZmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjgzOTBmNjdiLWUxYzMtNGRmMi1iNzk2LWQxOGY5NzAzMmI5OC9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOmY4MGM0NTA5LTE1NmEtNDQxYi04ZDU4LTNjMzE5YTRiOWU4ZHJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCCfKmfEs1nF3TkoRuyXIdZ1mfR27um99l3IUzcVeBSKKaJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggBkna+FVWA7ttg4e5fN3SGCgCzCQlDA9sXSLaXIILGWiiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggaFremDqsMi+9hC8EXdAqcvUmalJPQpuJro7fci7a9hF0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYQCF+gEV93krhKWJZo9W7JOAutbiPIwiSmCwzd4sIsW1Qhwbqp1uFgraYgVYRPUkeUEywl5lZ0k47qZOOBpH4jps= + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/compositor/src/config.rs b/compositor/src/config.rs index 09ba67d..b26cf87 100644 --- a/compositor/src/config.rs +++ b/compositor/src/config.rs @@ -1674,7 +1674,7 @@ fn default_surface_base_color() -> String { } fn default_accent_color() -> String { - "#5B8CFF".to_owned() + "#3D7BE6".to_owned() } fn default_shadow_color() -> String { diff --git a/compositor/src/wallpaper.rs b/compositor/src/wallpaper.rs index 595f78a..dc3e4fa 100644 --- a/compositor/src/wallpaper.rs +++ b/compositor/src/wallpaper.rs @@ -17,13 +17,27 @@ use smithay::{ }; use std::{collections::HashMap, path::PathBuf, sync::mpsc}; -#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, PartialEq)] pub(crate) struct WallpaperConfig { + #[serde(default = "default_wallpaper_path")] pub path: Option, #[serde(default)] pub mode: WallpaperMode, } +fn default_wallpaper_path() -> Option { + 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)] #[serde(rename_all = "snake_case")] pub(crate) enum WallpaperMode { @@ -261,7 +275,7 @@ mod tests { let (sender, receiver) = mpsc::channel(); state.receiver = Some(receiver); state.reload(WallpaperConfig { - path: None, + path: state.config.path.clone(), mode: WallpaperMode::Fit, }); assert!(state.receiver.is_some()); @@ -284,7 +298,7 @@ mod tests { let (sender, receiver) = mpsc::channel(); state.receiver = Some(receiver); state.reload(WallpaperConfig { - path: None, + path: state.config.path.clone(), mode: WallpaperMode::Fit, }); state.reload(WallpaperConfig::default()); @@ -314,7 +328,18 @@ mod tests { #[test] 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()); } + + #[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()); + } } diff --git a/crates/ferese-config/src/lib.rs b/crates/ferese-config/src/lib.rs index 723fdc9..fcc2a70 100644 --- a/crates/ferese-config/src/lib.rs +++ b/crates/ferese-config/src/lib.rs @@ -27,6 +27,23 @@ pub fn config_path() -> Option { .map(|p| p.join("ferese/config.kdl")) } +pub fn default_wallpaper() -> &'static str { + static PATH: std::sync::OnceLock = 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 { if parent == "commands" { return name.to_owned(); diff --git a/docs/configuration.md b/docs/configuration.md index f321bf9..95b83e2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -105,6 +105,23 @@ Changing default width preserves manually resized columns. ## 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 | | --- | --- | --- | --- | | `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.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.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.material.style` | `"solid"`, `"translucent"` | `"solid"` | Shell background material | | `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 | | `text_primary` | `"#F4F7FB"` | Main text | -| `text_muted` | `"#7F8A98"` | Secondary text | -| `accent` | `"#5B8CFF"` | Active controls and solid focus ring | +| `text_muted` | `"#8793A2"` | Secondary text | +| `accent` | `"#3D7BE6"` | Active controls and solid focus ring | | `border` | `"#FFFFFF18"` | Unfocused window border | | `shadow` | `"#00000055"` | Window shadow color | diff --git a/packaging/config.kdl b/packaging/config.kdl index f2b49ee..d2a1efa 100644 --- a/packaging/config.kdl +++ b/packaging/config.kdl @@ -20,10 +20,10 @@ layout { theme { surface { bar { - background "#1C202E" + background "#111821" opacity 0.95 - text-primary "#F0F3FA" - text-muted "#AAB4C7" + text-primary "#F4F7FB" + text-muted "#8793A2" } } } diff --git a/scripts/install-session.sh b/scripts/install-session.sh index 7b3a531..512f3c3 100644 --- a/scripts/install-session.sh +++ b/scripts/install-session.sh @@ -42,6 +42,9 @@ fi for name in ferese ferese-shell ferese-settings feresectl; do [[ -x $repo_dir/target/release/$name ]] || { echo "Missing release binary: $name" >&2; exit 1; } 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/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-screenshot" "$release_dir/ferese-screenshot" 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 ln -sfn -- "$(readlink -- "$install_root/current")" "$install_root/previous" diff --git a/shell/ferese-settings/src/main.rs b/shell/ferese-settings/src/main.rs index 3c40f19..c24c327 100644 --- a/shell/ferese-settings/src/main.rs +++ b/shell/ferese-settings/src/main.rs @@ -416,7 +416,11 @@ impl cosmic::Application for App { Message::WallpaperPicked(Err(error)) => self.error = Some(error), Message::Thumbnail(path, result) => { 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; match result { Ok(handle) => { @@ -591,27 +595,31 @@ impl cosmic::Application for App { body = body.push(visuals::preview(&self.draft)); } if self.page == Page::Appearance { - let mut presets = row([]).spacing(10); - for (index, title) in ["Amberwood", "Monochrome", "Catppuccin"] - .into_iter() - .enumerate() - { - presets = presets.push( - button::custom( - row([]) - .spacing(10) - .align_y(Alignment::Center) + let mut presets = column([]).spacing(10); + for (row_index, choices) in visuals::PRESETS.chunks(3).enumerate() { + let mut tiles = row([]).spacing(10); + for (column_index, preset) in choices.iter().enumerate() { + let index = row_index * 3 + column_index; + let tile = button::custom( + column([]) + .spacing(6) .push(visuals::swatches(index)) - .push(self.label(title, 12.)), + .push(self.label(preset.name, 12.)) + .push(self.label(preset.description, 10.)), ) .width(Length::Fill) .padding(10) .class(visuals::button_style( palette, 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); } @@ -970,7 +978,9 @@ impl App { } fn load_thumbnail(&mut self) -> Task { - 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 { return Task::none(); } diff --git a/shell/ferese-settings/src/schema.rs b/shell/ferese-settings/src/schema.rs index f162a43..d5db414 100644 --- a/shell/ferese-settings/src/schema.rs +++ b/shell/ferese-settings/src/schema.rs @@ -558,7 +558,7 @@ pub fn fields(page: Page) -> Vec { "theme.colors.accent", "Accent color", "Focus rings and selected controls. Use a hex color.", - "#5B8CFF", + "#3D7BE6", ), text( "theme.colors.surface_base", @@ -607,7 +607,7 @@ pub fn fields(page: Page) -> Vec { "theme.background.path", "Image location", "Choose an image above, or enter its full path.", - "", + ferese_config::default_wallpaper(), ), choice( "theme.background.mode", diff --git a/shell/ferese-settings/src/store.rs b/shell/ferese-settings/src/store.rs index dad0dfa..dd1f1e1 100644 --- a/shell/ferese-settings/src/store.rs +++ b/shell/ferese-settings/src/store.rs @@ -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(); for edit in crate::visuals::preset(preset) { snapshot.edit(&edit).unwrap(); diff --git a/shell/ferese-settings/src/visuals.rs b/shell/ferese-settings/src/visuals.rs index 5bafbb2..34d1fcf 100644 --- a/shell/ferese-settings/src/visuals.rs +++ b/shell/ferese-settings/src/visuals.rs @@ -42,19 +42,19 @@ impl Palette { Color::from_rgb8(15, 16, 20), ); Self { - background: mix(base, Color::WHITE, 0.025), + background: mix(base, surface_shade(base), 0.025), sidebar: base, - card: mix(base, Color::WHITE, 0.055), + card: mix(base, surface_shade(base), 0.055), accent: color( - &snapshot.string("theme.colors.accent", "#5B8CFF"), - Color::from_rgb8(229, 200, 144), + &snapshot.string("theme.colors.accent", "#3D7BE6"), + Color::from_rgb8(61, 123, 230), ), text: color( - &snapshot.string("theme.colors.text_primary", "#e2e0e6"), + &snapshot.string("theme.colors.text_primary", "#F4F7FB"), Color::WHITE, ), muted: color( - &snapshot.string("theme.colors.text_muted", "#96959f"), + &snapshot.string("theme.colors.text_muted", "#8793A2"), Color::from_rgb8(150, 149, 159), ), 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 { Color::from_rgb( 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 { mix(p.sidebar, p.accent, if hover { 0.24 } else { 0.17 }) } else if hover { - mix(p.card, Color::WHITE, 0.06) + mix(p.card, surface_shade(p.sidebar), 0.06) } else if navigation { p.sidebar } else { p.card })), - text_color: Some(if selected { p.accent } else { p.text }), + text_color: Some(p.text), icon_color: None, border_radius: 9.into(), - border_width: 0., - border_color: Color::TRANSPARENT, + border_width: if selected { 1. } else { 0. }, + border_color: if selected { + p.accent + } else { + Color::TRANSPARENT + }, outline_width: 0., outline_color: Color::TRANSPARENT, 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 { - let mut theme = cosmic::theme::COSMIC_DARK.clone(); - if let Some(snapshot) = snapshot { - let p = Palette::from(snapshot); - let accent = cosmic::theme::CosmicColor::new(p.accent.r, p.accent.g, p.accent.b, 1.); - theme.accent.base = accent; - theme.accent_button.base = accent; - theme.text_button.on = accent; - } + let Some(snapshot) = snapshot else { + return cosmic::Theme::custom(std::sync::Arc::new(cosmic::theme::COSMIC_DARK.clone())); + }; + let palette = Palette::from(snapshot); + let builder = if luminance(palette.sidebar) > 0.5 { + cosmic::cosmic_theme::ThemeBuilder::light() + } 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)) } @@ -181,7 +215,18 @@ pub fn configured_font(snapshot: &Snapshot) -> cosmic::font::Font { } pub fn icon(page: Page, tint: Color) -> svg_icon::Icon { - svg_icon::from_svg_bytes(format!(r##""##, page.icon(),hex(tint)).into_bytes()).symbolic(false).icon().size(19) + let svg = format!( + r##" + +"##, + page.icon(), + hex(tint), + ); + svg_icon::from_svg_bytes(svg.into_bytes()) + .symbolic(false) + .icon() + .size(19) } 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 { - svg_icon::from_svg_bytes(format!(r##""##, hex(tint)).into_bytes()) + let svg = format!( + r##" + +"##, + hex(tint), + ); + svg_icon::from_svg_bytes(svg.into_bytes()) .symbolic(false) .icon() .size(16) } -const PRESETS: [(&str, &str, &str, &str, &str); 3] = [ - ("#e5c890", "#0f1014", "#c9c7cd", "#9998a8", "#795e40"), - ("#e5e5e5", "#101012", "#ededf0", "#97979f", "#696973"), - ("#cba6f7", "#1e1e2e", "#cdd6f4", "#a6adc8", "#89b4fa"), + +pub struct Preset { + pub name: &'static str, + 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 { - let (accent, base, text, muted, end) = PRESETS[index.min(2)]; + let Some(preset) = PRESETS.get(index) else { + return Vec::new(); + }; vec![ - set("theme.colors.accent", accent), - set("theme.colors.surface_base", base), - set("theme.colors.text_primary", text), - set("theme.colors.text_muted", muted), - set("theme.surface.bar.background", base), - set("theme.surface.bar.text_primary", text), - set("theme.surface.bar.text_muted", muted), - set("theme.focus_ring.gradient.from", accent), - set("theme.focus_ring.gradient.to", end), + set("theme.colors.accent", preset.accent), + set("theme.colors.surface_base", preset.base), + set("theme.colors.text_primary", preset.text), + set("theme.colors.text_muted", preset.muted), + set("theme.colors.border", preset.border), + set("theme.colors.shadow", "#00000055"), + set("theme.surface.bar.background", preset.base), + set("theme.surface.bar.text_primary", preset.text), + 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 { - let (accent, base, text, muted, _) = PRESETS[index]; + let Some(preset) = PRESETS.get(index) else { + return false; + }; [ - ("accent", accent), - ("surface_base", base), - ("text_primary", text), - ("text_muted", muted), + ("theme.colors.accent", preset.accent, "#3D7BE6"), + ("theme.colors.surface_base", preset.base, "#111821"), + ("theme.colors.text_primary", preset.text, "#F4F7FB"), + ("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() - .all(|(key, value)| { + .all(|(path, expected, fallback)| { + if index == 0 && snapshot.item(path).is_none() { + return true; + } snapshot - .string(&format!("theme.colors.{key}"), "") - .eq_ignore_ascii_case(value) - }) + .string(path, fallback) + .eq_ignore_ascii_case(expected) + }) && snapshot.number("theme.focus_ring.gradient.angle", 0.) == 0. } 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); - for value in [base, end, accent] { + for value in [preset.base, preset.gradient_end, preset.accent] { row = row.push( container(widget::Space::new().width(12).height(22)) .class(surface(color(value, Color::WHITE), 4.)), @@ -244,8 +387,7 @@ pub fn swatches(index: usize) -> Element<'static, Message> { } 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> { let p = Palette::from(snapshot); let base = hex(p.sidebar); @@ -291,3 +433,82 @@ pub fn preview(snapshot: &Snapshot) -> Element<'static, Message> { .content_fit(cosmic::iced::ContentFit::Contain) .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" + ); + } + } +} diff --git a/shell/ferese-shell/src/config.rs b/shell/ferese-shell/src/config.rs index 2131155..1eb4c4f 100644 --- a/shell/ferese-shell/src/config.rs +++ b/shell/ferese-shell/src/config.rs @@ -67,8 +67,8 @@ impl Default for ShellTheme { surface_base: [17, 24, 33, 255], surface_popover: [17, 24, 33, 245], text_primary: [244, 247, 251, 255], - text_muted: [127, 138, 152, 255], - accent: [91, 140, 255, 255], + text_muted: [135, 147, 162, 255], + accent: [61, 123, 230, 255], border: [255, 255, 255, 24], shadow: [0, 0, 0, 85], bar_height: 28.0, @@ -95,13 +95,27 @@ impl ShellTheme { } } -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub(crate) struct WallpaperConfig { + #[serde(default = "default_wallpaper_path")] pub(crate) path: Option, #[serde(default)] pub(crate) mode: WallpaperMode, } +fn default_wallpaper_path() -> Option { + 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)] #[serde(rename_all = "snake_case")] pub(crate) enum WallpaperMode { @@ -423,11 +437,11 @@ fn default_text_primary() -> String { } fn default_text_muted() -> String { - "#7F8A98".to_owned() + "#8793A2".to_owned() } fn default_accent() -> String { - "#5B8CFF".to_owned() + "#3D7BE6".to_owned() } fn default_border() -> String { @@ -555,7 +569,10 @@ theme { let config: FereseConfig = ferese_config::from_str("").unwrap(); 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); let theme = shell_theme(&config.theme); assert_eq!(theme.bar_margin_top, 0);