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",
|
||||
"jiff",
|
||||
"libcosmic",
|
||||
"serde",
|
||||
"toml 0.9.12+spec-1.1.0",
|
||||
"wayland-client",
|
||||
]
|
||||
|
||||
|
||||
@@ -132,6 +132,12 @@ image fill
|
||||
image fit
|
||||
```
|
||||
|
||||
```toml
|
||||
[theme.background]
|
||||
path = "/path/to/wallpaper.png"
|
||||
mode = "fill" # fill or fit
|
||||
```
|
||||
|
||||
Ferese SHOULD NOT globally blur the wallpaper.
|
||||
|
||||
Optional restrained adjustments MAY include:
|
||||
@@ -235,7 +241,7 @@ base_unit = 8.0
|
||||
window_radius = 14.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_radius = 16.0
|
||||
|
||||
@@ -256,6 +262,11 @@ Only the top bar consumes persistent shell space.
|
||||
|
||||
## 10. Typography
|
||||
|
||||
```toml
|
||||
[theme.typography]
|
||||
font_family = "sans-serif"
|
||||
```
|
||||
|
||||
Initial roles:
|
||||
|
||||
```text
|
||||
@@ -269,6 +280,10 @@ display 22-28 px semibold
|
||||
|
||||
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.
|
||||
|
||||
## 11. Iconography
|
||||
|
||||
@@ -9,4 +9,6 @@ rust-version = "1.93"
|
||||
ferese-protocols = { path = "../../crates/ferese-protocols", default-features = false, features = ["client"] }
|
||||
jiff.workspace = true
|
||||
libcosmic.workspace = true
|
||||
serde.workspace = true
|
||||
toml.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;
|
||||
|
||||
use std::time::Duration;
|
||||
@@ -11,10 +12,11 @@ use cosmic::iced::platform_specific::{
|
||||
shell::commands::layer_surface::{Anchor, KeyboardInteractivity, Layer},
|
||||
};
|
||||
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::widget::{button, container, row, text};
|
||||
use cosmic::widget::{button, container, image, row, text};
|
||||
use ferese_protocols::effects::v1::client::{
|
||||
ferese_effects_manager_v1::FereseEffectsManagerV1,
|
||||
ferese_surface_effects_v1::{self, FereseSurfaceEffectsV1},
|
||||
@@ -26,11 +28,12 @@ use wayland_client::{
|
||||
protocol::{wl_registry, wl_surface},
|
||||
};
|
||||
|
||||
use crate::config::{ShellConfig, WallpaperMode};
|
||||
use crate::control::{ShellControl, ShellSnapshot};
|
||||
|
||||
const APP_ID: &str = "dev.ferese.Shell";
|
||||
const BAR_HEIGHT: u32 = 32;
|
||||
const BAR_MARGIN: i32 = 8;
|
||||
const BAR_MARGIN: i32 = 4;
|
||||
const EXCLUSIVE_ZONE: i32 = BAR_HEIGHT as i32;
|
||||
const BAR_TEXT_SIZE: u16 = (BAR_HEIGHT * 3 / 8) 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;
|
||||
|
||||
fn main() -> cosmic::iced::Result {
|
||||
let settings = Settings::default()
|
||||
let config = config::load();
|
||||
let mut settings = Settings::default()
|
||||
.no_main_window(true)
|
||||
.client_decorations(false)
|
||||
.transparent(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 {
|
||||
core: Core,
|
||||
surface_id: window::Id,
|
||||
bar_surface_id: window::Id,
|
||||
config: ShellConfig,
|
||||
control: Option<ShellControl>,
|
||||
snapshot: ShellSnapshot,
|
||||
clock: String,
|
||||
@@ -70,7 +80,7 @@ enum Message {
|
||||
|
||||
impl cosmic::Application for FereseShell {
|
||||
type Executor = cosmic::executor::Default;
|
||||
type Flags = ();
|
||||
type Flags = ShellConfig;
|
||||
type Message = Message;
|
||||
|
||||
const APP_ID: &'static str = APP_ID;
|
||||
@@ -83,11 +93,13 @@ impl cosmic::Application for FereseShell {
|
||||
&mut self.core
|
||||
}
|
||||
|
||||
fn init(core: Core, _flags: Self::Flags) -> (Self, Task<Self::Message>) {
|
||||
let surface_id = window::Id::unique();
|
||||
fn init(core: Core, config: Self::Flags) -> (Self, Task<Self::Message>) {
|
||||
let bar_surface_id = window::Id::unique();
|
||||
let wallpaper_surface_id = window::Id::unique();
|
||||
let app = Self {
|
||||
core,
|
||||
surface_id,
|
||||
bar_surface_id,
|
||||
config,
|
||||
control: ShellControl::connect()
|
||||
.map_err(|error| {
|
||||
eprintln!("ferese-shell: shell control unavailable: {error}");
|
||||
@@ -97,10 +109,26 @@ impl cosmic::Application for FereseShell {
|
||||
clock: current_time(),
|
||||
effects: None,
|
||||
};
|
||||
let action = cosmic::surface::action::app_layer_shell::<Self>(
|
||||
let wallpaper_action = cosmic::surface::action::app_layer_shell::<Self>(
|
||||
|_| Default::default(),
|
||||
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,
|
||||
keyboard_interactivity: KeyboardInteractivity::None,
|
||||
anchor: Anchor::TOP | Anchor::LEFT | Anchor::RIGHT,
|
||||
@@ -113,13 +141,18 @@ impl cosmic::Application for FereseShell {
|
||||
left: 10,
|
||||
},
|
||||
size: Some((None, Some(BAR_HEIGHT))),
|
||||
size_limits: Limits::NONE,
|
||||
exclusive_zone: EXCLUSIVE_ZONE,
|
||||
..Default::default()
|
||||
},
|
||||
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> {
|
||||
@@ -173,12 +206,14 @@ impl cosmic::Application for FereseShell {
|
||||
impl FereseShell {
|
||||
fn handle_event(&mut self, event: Event, id: window::Id) -> Task<Message> {
|
||||
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(
|
||||
_,
|
||||
surface,
|
||||
frame_id,
|
||||
))) if frame_id == self.surface_id => {
|
||||
))) if frame_id == self.bar_surface_id => {
|
||||
self.attach_effects(&surface);
|
||||
Task::none()
|
||||
}
|
||||
@@ -186,7 +221,7 @@ impl FereseShell {
|
||||
_,
|
||||
surface,
|
||||
layer_id,
|
||||
))) if layer_id == self.surface_id => {
|
||||
))) if layer_id == self.bar_surface_id => {
|
||||
self.attach_effects(&surface);
|
||||
Task::none()
|
||||
}
|
||||
@@ -290,6 +325,26 @@ impl FereseShell {
|
||||
.class(theme::Container::custom(bar_style))
|
||||
.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>> {
|
||||
@@ -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 {
|
||||
_manager: FereseEffectsManagerV1,
|
||||
_surface: FereseSurfaceEffectsV1,
|
||||
|
||||
Reference in new issue
Block a user