- 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).
221 lines
6.6 KiB
Rust
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()
|
|
}
|