54ca75b855
- shell.conf glass (0.05 to 1, default 1 = solid, as before): the surfaces' opacity; read live with the theme - surface.rs: shell() draws the island, the idle pill and the workspace pill (fill, rim, shadow), so a design is one file; the default is the look so far - see-through surfaces can't hide content under a veil, so with glass below 1 the open/close fades use .opacity() while they run - README: the Hyprland layer rule that frosts what's behind the shell (namespace match is a full match: ^magic-.*$; ignore_alpha keeps the transparent margin around the island sharp) - docs/design: the solid look and glass 0.5 with the rule, for review
217 lines
6.6 KiB
Rust
217 lines
6.6 KiB
Rust
use std::cell::Cell;
|
|
use std::path::PathBuf;
|
|
use std::sync::{Mutex, PoisonError};
|
|
use std::time::Duration;
|
|
use std::{env, fs, thread};
|
|
|
|
use amane::{Animation, Blend, Color, Service};
|
|
|
|
use crate::parse;
|
|
|
|
/*
|
|
* the shell's colours. they come from the wallpaper: skwd-wall runs matugen with
|
|
* matugen/amane.colors, which writes ~/.config/amane/theme. ~/.config/amane/shell.conf
|
|
* says whether the open media card takes the song cover's colours instead, and how long
|
|
* a change cross-fades. without a theme file the shell keeps its original pink on purple
|
|
*/
|
|
#[derive(Clone, Copy, PartialEq)]
|
|
pub struct Colors {
|
|
pub accent: Color,
|
|
pub on_accent: Color,
|
|
pub accent_soft: Color,
|
|
pub flame: Color,
|
|
pub text: Color,
|
|
pub dim: Color,
|
|
pub outline: Color,
|
|
pub surface: Color,
|
|
pub surface_high: Color,
|
|
pub surface_low: Color,
|
|
}
|
|
|
|
const PINK_ON_PURPLE: Colors = Colors {
|
|
accent: Color::rgb(247, 168, 200),
|
|
on_accent: Color::rgb(40, 18, 32),
|
|
accent_soft: Color::rgb(150, 82, 122),
|
|
flame: Color::rgb(255, 228, 240),
|
|
text: Color::rgb(241, 236, 246),
|
|
dim: Color::rgba(205, 192, 218, 160),
|
|
outline: Color::rgba(255, 255, 255, 26),
|
|
surface: Color::rgb(24, 18, 30),
|
|
surface_high: Color::rgb(35, 26, 44),
|
|
surface_low: Color::rgb(13, 10, 18),
|
|
};
|
|
|
|
impl Colors {
|
|
// the background gradient's three stops, light to dark
|
|
pub fn surfaces(&self) -> [Color; 3] {
|
|
[self.surface_high, self.surface, self.surface_low]
|
|
}
|
|
}
|
|
|
|
// so the colours can ride one Animation, all ten fading together
|
|
impl Blend for Colors {
|
|
fn blend(from: Self, to: Self, amount: f32) -> Self {
|
|
let mix = |from: Color, to: Color| Color::blend(from, to, amount);
|
|
|
|
Self {
|
|
accent: mix(from.accent, to.accent),
|
|
on_accent: mix(from.on_accent, to.on_accent),
|
|
accent_soft: mix(from.accent_soft, to.accent_soft),
|
|
flame: mix(from.flame, to.flame),
|
|
text: mix(from.text, to.text),
|
|
dim: mix(from.dim, to.dim),
|
|
outline: mix(from.outline, to.outline),
|
|
surface: mix(from.surface, to.surface),
|
|
surface_high: mix(from.surface_high, to.surface_high),
|
|
surface_low: mix(from.surface_low, to.surface_low),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct Theme {
|
|
colors: Animation<Colors>,
|
|
|
|
// the open media card and the equalizer take the cover's colours as their background
|
|
pub cover_colors: bool,
|
|
|
|
// how opaque the shell's surfaces are, 0.05 to 1; below 1 the desktop shows through
|
|
glass: f32,
|
|
}
|
|
|
|
impl Service for Theme {
|
|
fn new() -> Self {
|
|
Self {
|
|
colors: Animation::new(PINK_ON_PURPLE),
|
|
cover_colors: false,
|
|
glass: 1.0,
|
|
}
|
|
}
|
|
|
|
/*
|
|
* waits on the kernel instead of polling: amane::watch_file watches the folder, so
|
|
* matugen writing a new file and renaming it over the old one is seen too.
|
|
* shell.conf on its own thread, the theme here; either change re-reads both
|
|
*/
|
|
fn listen() {
|
|
reload();
|
|
|
|
thread::spawn(|| follow("shell.conf"));
|
|
|
|
follow("theme");
|
|
}
|
|
}
|
|
|
|
// what was read last, (theme, shell.conf); none until the first read
|
|
static LAST: Mutex<Option<(String, String)>> = Mutex::new(None);
|
|
|
|
fn follow(name: &str) {
|
|
for _ in amane::watch_file(&config_file(name).to_string_lossy()) {
|
|
// one save can arrive as several writes: let it finish, then read once
|
|
thread::sleep(Duration::from_millis(50));
|
|
|
|
reload();
|
|
}
|
|
}
|
|
|
|
// a deleted theme file sends no event: the defaults come back at the next start
|
|
fn reload() {
|
|
let read = (read("theme"), read("shell.conf"));
|
|
|
|
let mut last = LAST.lock().unwrap_or_else(PoisonError::into_inner);
|
|
|
|
// a write that changes nothing redraws nothing
|
|
if last.as_ref() == Some(&read) {
|
|
return;
|
|
}
|
|
|
|
// the colours at startup show at once, later changes cross-fade
|
|
apply(&read.0, &read.1, last.is_none());
|
|
|
|
*last = Some(read);
|
|
}
|
|
|
|
fn read(name: &str) -> String {
|
|
fs::read_to_string(config_file(name)).unwrap_or_default()
|
|
}
|
|
|
|
// ~/.config/amane/<name>, or under $XDG_CONFIG_HOME
|
|
pub fn config_file(name: &str) -> PathBuf {
|
|
let config = env::var_os("XDG_CONFIG_HOME")
|
|
.map(PathBuf::from)
|
|
.or_else(|| env::var_os("HOME").map(|home| PathBuf::from(home).join(".config")))
|
|
.unwrap_or_default();
|
|
|
|
config.join("amane").join(name)
|
|
}
|
|
|
|
fn apply(theme: &str, config: &str, startup: bool) {
|
|
let mut colors = PINK_ON_PURPLE;
|
|
|
|
// a missing or broken line keeps that colour's default
|
|
for (key, value) in parse::settings(theme) {
|
|
let Some([red, green, blue]) = parse::rgb(&value) else {
|
|
continue;
|
|
};
|
|
|
|
let color = Color::rgb(red, green, blue);
|
|
|
|
match key.as_str() {
|
|
"accent" => colors.accent = color,
|
|
"on_accent" => colors.on_accent = color,
|
|
"accent_soft" => colors.accent_soft = color,
|
|
"flame" => colors.flame = color,
|
|
"text" => colors.text = color,
|
|
"dim" => colors.dim = color,
|
|
"outline" => colors.outline = color,
|
|
"surface" => colors.surface = color,
|
|
"surface_high" => colors.surface_high = color,
|
|
"surface_low" => colors.surface_low = color,
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
let settings = parse::settings(config);
|
|
let setting = |name: &str| settings.iter().find(|(key, _)| key == name).map(|(_, value)| value.as_str());
|
|
|
|
let fade = if startup { 0 } else { setting("transition_ms").and_then(|ms| ms.parse().ok()).unwrap_or(400) };
|
|
|
|
let mut theme = Theme::write();
|
|
|
|
theme.cover_colors = setting("cover_colors") == Some("true");
|
|
theme.glass = setting("glass").and_then(|glass| glass.parse::<f32>().ok()).map_or(1.0, |glass| glass.clamp(0.05, 1.0));
|
|
|
|
// a change in the middle of a fade starts from the colours on screen now
|
|
let now = theme.colors.value();
|
|
|
|
theme.colors = Animation::new(now).duration(Duration::from_millis(fade));
|
|
theme.colors.to(colors);
|
|
}
|
|
|
|
// the colours and glass of the frame being drawn, set by the view that draws it
|
|
thread_local! {
|
|
static NOW: Cell<Colors> = const { Cell::new(PINK_ON_PURPLE) };
|
|
static GLASS: Cell<f32> = const { Cell::new(1.0) };
|
|
}
|
|
|
|
/*
|
|
* a view's first line: everything it draws this frame uses these colours (and
|
|
* reading Theme redraws it when they change). says whether the media card takes the cover's
|
|
*/
|
|
pub fn begin() -> bool {
|
|
let theme = Theme::read();
|
|
|
|
NOW.set(theme.colors.value());
|
|
GLASS.set(theme.glass);
|
|
|
|
theme.cover_colors
|
|
}
|
|
|
|
pub fn now() -> Colors {
|
|
NOW.get()
|
|
}
|
|
|
|
// the surfaces' opacity this frame, 0.05 to 1
|
|
pub fn glass() -> f32 {
|
|
GLASS.get()
|
|
}
|