Files
amane-config/src/theme.rs
T
server2 6399844731 New workspaces grow in; config saves don't redraw everything or race; media peek skips the player in front
- A workspace Hyprland makes for a switch (createworkspacev2) grows into the row
  over the slide instead of appearing at full size; the window keeps the full
  room meanwhile.
- Every config.toml save restarted the colour Animation, and Animation::new
  runs its clock even from a colour to itself, so every window redrew for 0.4 s
  per save. The colours now only fade when they change.
- The file watch could read config.toml halfway through the shell's own save
  and reset settings to their defaults. Saves and rereads share one lock, the
  file is written to a new file and renamed over, and the shell's own saves
  are skipped by the watch.
- A song change or new player only pops the island when the focused window
  isn't that player's app (parse::same_app, tested).
2026-10-15 11:28:44 +02:00

221 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::{config, parse};
/*
* the shell's colours. they come from the wallpaper: skwd-wall runs matugen with
* matugen/amane.colors, which writes ~/.config/amane/theme. config.toml (config.rs) says
* whether the open media card takes the song cover's colours instead, how long a change
* cross-fades, and the colours used until there is a theme file. Theme also stands for
* config.toml: every view reads it, so a saved setting redraws the whole shell
*/
#[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,
}
// the colours without a theme file, from config.toml
fn fallback() -> Colors {
use config::appearance::fallback as colors;
Colors {
accent: colors::accent(),
on_accent: colors::on_accent(),
accent_soft: colors::accent_soft(),
flame: colors::flame(),
text: colors::text(),
dim: colors::dim(),
outline: colors::outline(),
surface: colors::surface(),
surface_high: colors::surface_high(),
surface_low: colors::surface_low(),
}
}
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>,
// where the colours are going; a reread that lands on them again starts no fade
target: Colors,
}
impl Service for Theme {
fn new() -> Self {
Self { colors: Animation::new(fallback()), target: fallback() }
}
/*
* 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.
* config.toml on its own thread, the theme here. a changed config.toml redraws
* everything; the colours fade only if it changed them (its fallback colours)
*/
fn listen() {
reload(true);
thread::spawn(|| {
for _ in amane::watch_file(&config_file("config.toml").to_string_lossy()) {
// one save can arrive as several writes: let it finish, then read once
thread::sleep(Duration::from_millis(50));
if config::reread() {
touch();
reload(true);
}
}
});
for _ in amane::watch_file(&config_file("theme").to_string_lossy()) {
thread::sleep(Duration::from_millis(50));
reload(false);
}
}
}
// redraws every view, for a setting changed in the config window
pub fn touch() {
let _ = Theme::write();
}
// the theme file as read last; none until the first read
static LAST: Mutex<Option<String>> = Mutex::new(None);
/*
* the theme file, read anew; `again` also when it's unchanged, for new fallback colours.
* a deleted theme file sends no event: the defaults come back at the next start
*/
fn reload(again: bool) {
let theme = fs::read_to_string(config_file("theme")).unwrap_or_default();
let mut last = LAST.lock().unwrap_or_else(PoisonError::into_inner);
if last.as_ref() == Some(&theme) && !again {
return;
}
// the colours at startup show at once, later changes cross-fade
apply(&theme, last.is_none());
*last = Some(theme);
}
// $XDG_CONFIG_HOME, or ~/.config
pub fn config_home() -> PathBuf {
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/amane/<name>, or under $XDG_CONFIG_HOME
pub fn config_file(name: &str) -> PathBuf {
config_home().join("amane").join(name)
}
fn apply(theme: &str, startup: bool) {
let mut colors = fallback();
// 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,
_ => {}
}
}
if !startup && Theme::read().target == colors {
return;
}
let fade = if startup { 0 } else { config::appearance::colors::transition_ms() };
let mut theme = Theme::write();
theme.target = colors;
// 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 of the frame being drawn, set by the view that draws it
thread_local! {
static NOW: Cell<Colors> = Cell::new(fallback());
}
/*
* a view's first line: everything it draws this frame uses these colours (and
* reading Theme redraws it when they or config.toml change). says whether the media
* card takes the cover's
*/
pub fn begin() -> bool {
let theme = Theme::read();
NOW.set(theme.colors.value());
config::appearance::colors::cover_colors()
}
pub fn now() -> Colors {
NOW.get()
}