add shell typography and wallpaper support
This commit is contained in:
1 parent
dc3ef13501
commit
d0975631cf
5 files changed
+224
-17
No files matched your search
Generated
+2
@@ -1644,6 +1644,8 @@ dependencies = [
|
|||||||
"ferese-protocols",
|
"ferese-protocols",
|
||||||
"jiff",
|
"jiff",
|
||||||
"libcosmic",
|
"libcosmic",
|
||||||
|
"serde",
|
||||||
|
"toml 0.9.12+spec-1.1.0",
|
||||||
"wayland-client",
|
"wayland-client",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -132,6 +132,12 @@ image fill
|
|||||||
image fit
|
image fit
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[theme.background]
|
||||||
|
path = "/path/to/wallpaper.png"
|
||||||
|
mode = "fill" # fill or fit
|
||||||
|
```
|
||||||
|
|
||||||
Ferese SHOULD NOT globally blur the wallpaper.
|
Ferese SHOULD NOT globally blur the wallpaper.
|
||||||
|
|
||||||
Optional restrained adjustments MAY include:
|
Optional restrained adjustments MAY include:
|
||||||
@@ -235,7 +241,7 @@ base_unit = 8.0
|
|||||||
window_radius = 14.0
|
window_radius = 14.0
|
||||||
|
|
||||||
top_bar_height = 32.0
|
top_bar_height = 32.0
|
||||||
top_bar_margin_top = 8.0
|
top_bar_margin_top = 4.0
|
||||||
top_bar_margin_horizontal = 10.0
|
top_bar_margin_horizontal = 10.0
|
||||||
top_bar_radius = 16.0
|
top_bar_radius = 16.0
|
||||||
|
|
||||||
@@ -256,6 +262,11 @@ Only the top bar consumes persistent shell space.
|
|||||||
|
|
||||||
## 10. Typography
|
## 10. Typography
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[theme.typography]
|
||||||
|
font_family = "sans-serif"
|
||||||
|
```
|
||||||
|
|
||||||
Initial roles:
|
Initial roles:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -269,6 +280,10 @@ display 22-28 px semibold
|
|||||||
|
|
||||||
Use the system UI sans-serif by default.
|
Use the system UI sans-serif by default.
|
||||||
|
|
||||||
|
The configured family applies consistently to shell text. Icon assets use the
|
||||||
|
theme icon set and are sized from the same scale tokens rather than text glyph
|
||||||
|
metrics.
|
||||||
|
|
||||||
Clocks and changing numeric values SHOULD use tabular numerals where available.
|
Clocks and changing numeric values SHOULD use tabular numerals where available.
|
||||||
|
|
||||||
## 11. Iconography
|
## 11. Iconography
|
||||||
|
|||||||
@@ -9,4 +9,6 @@ rust-version = "1.93"
|
|||||||
ferese-protocols = { path = "../../crates/ferese-protocols", default-features = false, features = ["client"] }
|
ferese-protocols = { path = "../../crates/ferese-protocols", default-features = false, features = ["client"] }
|
||||||
jiff.workspace = true
|
jiff.workspace = true
|
||||||
libcosmic.workspace = true
|
libcosmic.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
toml.workspace = true
|
||||||
wayland-client.workspace = true
|
wayland-client.workspace = true
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
const DEFAULT_BACKGROUND: [u8; 3] = [11, 15, 20];
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub(crate) struct ShellConfig {
|
||||||
|
pub(crate) font_family: Option<String>,
|
||||||
|
pub(crate) wallpaper: WallpaperConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Deserialize)]
|
||||||
|
pub(crate) struct WallpaperConfig {
|
||||||
|
pub(crate) path: Option<PathBuf>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) mode: WallpaperMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub(crate) enum WallpaperMode {
|
||||||
|
#[default]
|
||||||
|
Fill,
|
||||||
|
Fit,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
struct FereseConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
theme: ThemeConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
struct ThemeConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
typography: TypographyConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
background: WallpaperConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
struct TypographyConfig {
|
||||||
|
font_family: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn load() -> ShellConfig {
|
||||||
|
let Some(path) = config_path() else {
|
||||||
|
return ShellConfig::default();
|
||||||
|
};
|
||||||
|
let Ok(source) = fs::read_to_string(&path) else {
|
||||||
|
return ShellConfig::default();
|
||||||
|
};
|
||||||
|
|
||||||
|
match toml::from_str::<FereseConfig>(&source) {
|
||||||
|
Ok(config) => ShellConfig {
|
||||||
|
font_family: config.theme.typography.font_family,
|
||||||
|
wallpaper: config.theme.background,
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("ferese-shell: failed to read {}: {error}", path.display());
|
||||||
|
ShellConfig::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn default_background() -> [u8; 3] {
|
||||||
|
DEFAULT_BACKGROUND
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_path() -> Option<PathBuf> {
|
||||||
|
if let Some(directory) = env::var_os("XDG_CONFIG_HOME").filter(|value| !value.is_empty()) {
|
||||||
|
return Some(PathBuf::from(directory).join("ferese/config.toml"));
|
||||||
|
}
|
||||||
|
|
||||||
|
env::var_os("HOME")
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.map(|home| home.join(".config/ferese/config.toml"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_theme_without_rejecting_compositor_sections() {
|
||||||
|
let config: FereseConfig = toml::from_str(
|
||||||
|
r#"
|
||||||
|
[layout]
|
||||||
|
mode = "scrolling"
|
||||||
|
|
||||||
|
[theme.typography]
|
||||||
|
font_family = "JetBrainsMono Nerd Font"
|
||||||
|
|
||||||
|
[theme.background]
|
||||||
|
path = "/tmp/wallpaper.png"
|
||||||
|
mode = "fit"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
config.theme.typography.font_family.as_deref(),
|
||||||
|
Some("JetBrainsMono Nerd Font")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
config.theme.background.path,
|
||||||
|
Some(PathBuf::from("/tmp/wallpaper.png"))
|
||||||
|
);
|
||||||
|
assert_eq!(config.theme.background.mode, WallpaperMode::Fit);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn defaults_to_the_ferese_visual_profile() {
|
||||||
|
let config: FereseConfig = toml::from_str("").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(config.theme.typography.font_family, None);
|
||||||
|
assert_eq!(config.theme.background.path, None);
|
||||||
|
assert_eq!(config.theme.background.mode, WallpaperMode::Fill);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
mod config;
|
||||||
mod control;
|
mod control;
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -11,10 +12,11 @@ use cosmic::iced::platform_specific::{
|
|||||||
shell::commands::layer_surface::{Anchor, KeyboardInteractivity, Layer},
|
shell::commands::layer_surface::{Anchor, KeyboardInteractivity, Layer},
|
||||||
};
|
};
|
||||||
use cosmic::iced::{
|
use cosmic::iced::{
|
||||||
Background, Border, Color, Event, Length, Shadow, Subscription, Vector, window,
|
Background, Border, Color, ContentFit, Event, Length, Limits, Shadow, Subscription, Vector,
|
||||||
|
window,
|
||||||
};
|
};
|
||||||
use cosmic::theme;
|
use cosmic::theme;
|
||||||
use cosmic::widget::{button, container, row, text};
|
use cosmic::widget::{button, container, image, row, text};
|
||||||
use ferese_protocols::effects::v1::client::{
|
use ferese_protocols::effects::v1::client::{
|
||||||
ferese_effects_manager_v1::FereseEffectsManagerV1,
|
ferese_effects_manager_v1::FereseEffectsManagerV1,
|
||||||
ferese_surface_effects_v1::{self, FereseSurfaceEffectsV1},
|
ferese_surface_effects_v1::{self, FereseSurfaceEffectsV1},
|
||||||
@@ -26,11 +28,12 @@ use wayland_client::{
|
|||||||
protocol::{wl_registry, wl_surface},
|
protocol::{wl_registry, wl_surface},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::config::{ShellConfig, WallpaperMode};
|
||||||
use crate::control::{ShellControl, ShellSnapshot};
|
use crate::control::{ShellControl, ShellSnapshot};
|
||||||
|
|
||||||
const APP_ID: &str = "dev.ferese.Shell";
|
const APP_ID: &str = "dev.ferese.Shell";
|
||||||
const BAR_HEIGHT: u32 = 32;
|
const BAR_HEIGHT: u32 = 32;
|
||||||
const BAR_MARGIN: i32 = 8;
|
const BAR_MARGIN: i32 = 4;
|
||||||
const EXCLUSIVE_ZONE: i32 = BAR_HEIGHT as i32;
|
const EXCLUSIVE_ZONE: i32 = BAR_HEIGHT as i32;
|
||||||
const BAR_TEXT_SIZE: u16 = (BAR_HEIGHT * 3 / 8) as u16;
|
const BAR_TEXT_SIZE: u16 = (BAR_HEIGHT * 3 / 8) as u16;
|
||||||
const BAR_ICON_SIZE: u16 = (BAR_HEIGHT / 2) as u16;
|
const BAR_ICON_SIZE: u16 = (BAR_HEIGHT / 2) as u16;
|
||||||
@@ -42,18 +45,25 @@ const DOT_MARKER_SIZE: f32 = BAR_HEIGHT as f32 * 0.1875;
|
|||||||
const EMPTY_MARKER_SIZE: f32 = BAR_HEIGHT as f32 * 0.125;
|
const EMPTY_MARKER_SIZE: f32 = BAR_HEIGHT as f32 * 0.125;
|
||||||
|
|
||||||
fn main() -> cosmic::iced::Result {
|
fn main() -> cosmic::iced::Result {
|
||||||
let settings = Settings::default()
|
let config = config::load();
|
||||||
|
let mut settings = Settings::default()
|
||||||
.no_main_window(true)
|
.no_main_window(true)
|
||||||
.client_decorations(false)
|
.client_decorations(false)
|
||||||
.transparent(true)
|
.transparent(true)
|
||||||
.is_daemon(true);
|
.is_daemon(true);
|
||||||
|
if let Some(font_family) = config.font_family.clone() {
|
||||||
|
let font_family = Box::leak(font_family.into_boxed_str());
|
||||||
|
|
||||||
cosmic::app::run::<FereseShell>(settings, ())
|
settings = settings.default_font(cosmic::font::Font::with_name(font_family));
|
||||||
|
}
|
||||||
|
|
||||||
|
cosmic::app::run::<FereseShell>(settings, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FereseShell {
|
struct FereseShell {
|
||||||
core: Core,
|
core: Core,
|
||||||
surface_id: window::Id,
|
bar_surface_id: window::Id,
|
||||||
|
config: ShellConfig,
|
||||||
control: Option<ShellControl>,
|
control: Option<ShellControl>,
|
||||||
snapshot: ShellSnapshot,
|
snapshot: ShellSnapshot,
|
||||||
clock: String,
|
clock: String,
|
||||||
@@ -70,7 +80,7 @@ enum Message {
|
|||||||
|
|
||||||
impl cosmic::Application for FereseShell {
|
impl cosmic::Application for FereseShell {
|
||||||
type Executor = cosmic::executor::Default;
|
type Executor = cosmic::executor::Default;
|
||||||
type Flags = ();
|
type Flags = ShellConfig;
|
||||||
type Message = Message;
|
type Message = Message;
|
||||||
|
|
||||||
const APP_ID: &'static str = APP_ID;
|
const APP_ID: &'static str = APP_ID;
|
||||||
@@ -83,11 +93,13 @@ impl cosmic::Application for FereseShell {
|
|||||||
&mut self.core
|
&mut self.core
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init(core: Core, _flags: Self::Flags) -> (Self, Task<Self::Message>) {
|
fn init(core: Core, config: Self::Flags) -> (Self, Task<Self::Message>) {
|
||||||
let surface_id = window::Id::unique();
|
let bar_surface_id = window::Id::unique();
|
||||||
|
let wallpaper_surface_id = window::Id::unique();
|
||||||
let app = Self {
|
let app = Self {
|
||||||
core,
|
core,
|
||||||
surface_id,
|
bar_surface_id,
|
||||||
|
config,
|
||||||
control: ShellControl::connect()
|
control: ShellControl::connect()
|
||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
eprintln!("ferese-shell: shell control unavailable: {error}");
|
eprintln!("ferese-shell: shell control unavailable: {error}");
|
||||||
@@ -97,10 +109,26 @@ impl cosmic::Application for FereseShell {
|
|||||||
clock: current_time(),
|
clock: current_time(),
|
||||||
effects: None,
|
effects: None,
|
||||||
};
|
};
|
||||||
let action = cosmic::surface::action::app_layer_shell::<Self>(
|
let wallpaper_action = cosmic::surface::action::app_layer_shell::<Self>(
|
||||||
|_| Default::default(),
|
|_| Default::default(),
|
||||||
move |_| SctkLayerSurfaceSettings {
|
move |_| SctkLayerSurfaceSettings {
|
||||||
id: surface_id,
|
id: wallpaper_surface_id,
|
||||||
|
layer: Layer::Background,
|
||||||
|
keyboard_interactivity: KeyboardInteractivity::None,
|
||||||
|
input_zone: Some(Vec::new()),
|
||||||
|
anchor: Anchor::TOP | Anchor::RIGHT | Anchor::BOTTOM | Anchor::LEFT,
|
||||||
|
output: IcedOutput::Active,
|
||||||
|
namespace: "ferese-shell-wallpaper".to_owned(),
|
||||||
|
size: Some((None, None)),
|
||||||
|
size_limits: Limits::NONE,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Some(Box::new(Self::view_wallpaper)),
|
||||||
|
);
|
||||||
|
let bar_action = cosmic::surface::action::app_layer_shell::<Self>(
|
||||||
|
|_| Default::default(),
|
||||||
|
move |_| SctkLayerSurfaceSettings {
|
||||||
|
id: bar_surface_id,
|
||||||
layer: Layer::Top,
|
layer: Layer::Top,
|
||||||
keyboard_interactivity: KeyboardInteractivity::None,
|
keyboard_interactivity: KeyboardInteractivity::None,
|
||||||
anchor: Anchor::TOP | Anchor::LEFT | Anchor::RIGHT,
|
anchor: Anchor::TOP | Anchor::LEFT | Anchor::RIGHT,
|
||||||
@@ -113,13 +141,18 @@ impl cosmic::Application for FereseShell {
|
|||||||
left: 10,
|
left: 10,
|
||||||
},
|
},
|
||||||
size: Some((None, Some(BAR_HEIGHT))),
|
size: Some((None, Some(BAR_HEIGHT))),
|
||||||
|
size_limits: Limits::NONE,
|
||||||
exclusive_zone: EXCLUSIVE_ZONE,
|
exclusive_zone: EXCLUSIVE_ZONE,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
Some(Box::new(Self::view_layer)),
|
Some(Box::new(Self::view_layer)),
|
||||||
);
|
);
|
||||||
|
|
||||||
(app, cosmic::task::message(cosmic::Action::Surface(action)))
|
let tasks = [wallpaper_action, bar_action]
|
||||||
|
.map(cosmic::Action::Surface)
|
||||||
|
.map(cosmic::task::message);
|
||||||
|
|
||||||
|
(app, Task::batch(tasks))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subscription(&self) -> Subscription<Self::Message> {
|
fn subscription(&self) -> Subscription<Self::Message> {
|
||||||
@@ -173,12 +206,14 @@ impl cosmic::Application for FereseShell {
|
|||||||
impl FereseShell {
|
impl FereseShell {
|
||||||
fn handle_event(&mut self, event: Event, id: window::Id) -> Task<Message> {
|
fn handle_event(&mut self, event: Event, id: window::Id) -> Task<Message> {
|
||||||
match event {
|
match event {
|
||||||
Event::Window(window::Event::Closed) if id == self.surface_id => cosmic::iced::exit(),
|
Event::Window(window::Event::Closed) if id == self.bar_surface_id => {
|
||||||
|
cosmic::iced::exit()
|
||||||
|
}
|
||||||
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Frame(
|
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Frame(
|
||||||
_,
|
_,
|
||||||
surface,
|
surface,
|
||||||
frame_id,
|
frame_id,
|
||||||
))) if frame_id == self.surface_id => {
|
))) if frame_id == self.bar_surface_id => {
|
||||||
self.attach_effects(&surface);
|
self.attach_effects(&surface);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
@@ -186,7 +221,7 @@ impl FereseShell {
|
|||||||
_,
|
_,
|
||||||
surface,
|
surface,
|
||||||
layer_id,
|
layer_id,
|
||||||
))) if layer_id == self.surface_id => {
|
))) if layer_id == self.bar_surface_id => {
|
||||||
self.attach_effects(&surface);
|
self.attach_effects(&surface);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
@@ -290,6 +325,26 @@ impl FereseShell {
|
|||||||
.class(theme::Container::custom(bar_style))
|
.class(theme::Container::custom(bar_style))
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn view_wallpaper(&self) -> Element<'_, cosmic::Action<Message>> {
|
||||||
|
let Some(path) = self.config.wallpaper.path.as_ref() else {
|
||||||
|
return container(text(""))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.class(theme::Container::custom(wallpaper_fallback_style))
|
||||||
|
.into();
|
||||||
|
};
|
||||||
|
let content_fit = match self.config.wallpaper.mode {
|
||||||
|
WallpaperMode::Fill => ContentFit::Cover,
|
||||||
|
WallpaperMode::Fit => ContentFit::Contain,
|
||||||
|
};
|
||||||
|
|
||||||
|
image(path.clone())
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.content_fit(content_fit)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn workspace_indicator(active: bool, occupied: bool) -> Element<'static, cosmic::Action<Message>> {
|
fn workspace_indicator(active: bool, occupied: bool) -> Element<'static, cosmic::Action<Message>> {
|
||||||
@@ -390,6 +445,15 @@ fn bar_style(_theme: &cosmic::Theme) -> container::Style {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn wallpaper_fallback_style(_theme: &cosmic::Theme) -> container::Style {
|
||||||
|
let [red, green, blue] = config::default_background();
|
||||||
|
|
||||||
|
container::Style {
|
||||||
|
background: Some(Background::Color(Color::from_rgb8(red, green, blue))),
|
||||||
|
..container::Style::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct EffectsBinding {
|
struct EffectsBinding {
|
||||||
_manager: FereseEffectsManagerV1,
|
_manager: FereseEffectsManagerV1,
|
||||||
_surface: FereseSurfaceEffectsV1,
|
_surface: FereseSurfaceEffectsV1,
|
||||||
|
|||||||
Reference in new issue
Block a user