feat: theme-coloured region dim

This commit is contained in:
elwrcl committed 2026-10-03 20:35:44 +03:00
1 parent 2178b840ad
commit 201f43ef34
4 files changed
+83 -31

No files matched your search

+1 -1
View File
@@ -18,7 +18,7 @@ mod selection;
mod shadow; mod shadow;
mod sound; mod sound;
mod stack; mod stack;
// The theme lands in pieces; Task 2 of the theme plan removes this. // Parts of the theme are used from Task 4 of the theme plan on; that task removes this.
#[allow(dead_code)] #[allow(dead_code)]
mod theme; mod theme;
mod thumbnail; mod thumbnail;
+11 -3
View File
@@ -87,6 +87,8 @@ pub fn select(wl: &mut Wayland, outputs: &[Output], frames: &[Frame]) -> Result<
.context("compositor does not support wp-viewporter") .context("compositor does not support wp-viewporter")
.hint("run `valw doctor` to see what the compositor supports")?; .hint("run `valw doctor` to see what the compositor supports")?;
// The theme's colour for the overlay outside the selection.
let palette = crate::theme::current();
let mut surfaces = Vec::new(); let mut surfaces = Vec::new();
for (output, frame) in outputs.iter().zip(frames) { for (output, frame) in outputs.iter().zip(frames) {
let surface = s.compositor.create_surface(&qh); let surface = s.compositor.create_surface(&qh);
@@ -102,7 +104,7 @@ pub fn select(wl: &mut Wayland, outputs: &[Output], frames: &[Frame]) -> Result<
layer.set_keyboard_interactivity(KeyboardInteractivity::Exclusive); layer.set_keyboard_interactivity(KeyboardInteractivity::Exclusive);
layer.commit(); layer.commit();
let viewport = viewporter.get_viewport(layer.wl_surface(), &qh, ()); let viewport = viewporter.get_viewport(layer.wl_surface(), &qh, ());
surfaces.push(Surface::new(layer, viewport, frame, &s.shm)?); surfaces.push(Surface::new(layer, viewport, frame, &palette, &s.shm)?);
} }
tracing::debug!( tracing::debug!(
@@ -126,7 +128,13 @@ pub fn select(wl: &mut Wayland, outputs: &[Output], frames: &[Frame]) -> Result<
} }
impl Surface { impl Surface {
fn new(layer: LayerSurface, viewport: WpViewport, frame: &Frame, shm: &Shm) -> Result<Surface> { fn new(
layer: LayerSurface,
viewport: WpViewport,
frame: &Frame,
palette: &crate::theme::Palette,
shm: &Shm,
) -> Result<Surface> {
let (width, height) = frame.pixels.dimensions(); let (width, height) = frame.pixels.dimensions();
let len = (width * height * 4) as usize; let len = (width * height * 4) as usize;
Ok(Surface { Ok(Surface {
@@ -135,7 +143,7 @@ impl Surface {
geom: frame.output.clone(), geom: frame.output.clone(),
width, width,
height, height,
dark: render::dim(frame.pixels.as_raw(), render::DIM), dark: render::tint(frame.pixels.as_raw(), palette.base, render::TINT),
bright: Arc::clone(&frame.pixels), bright: Arc::clone(&frame.pixels),
// Room for two buffers so one can be on screen while we draw the next. // Room for two buffers so one can be on screen while we draw the next.
pool: SlotPool::new(len * 2, shm).context("could not create shm pool")?, pool: SlotPool::new(len * 2, shm).context("could not create shm pool")?,
+29 -20
View File
@@ -1,19 +1,25 @@
use crate::frame::PixelRect; use crate::frame::PixelRect;
/// Overlay brightness outside the selection (60%, i.e. darkened by 40%). /// How far the overlay outside the selection leans towards the theme colour.
pub const DIM: u8 = 153; pub const TINT: f32 = 0.55;
/// B, G, R, X pixels with every byte scaled by about `brightness / 255`. /// B, G, R, X pixels blended `amount` of the way towards `colour` (RGB 0–1).
/// /// Runs once per output before the overlay shows: a lookup table per
/// This runs on every output before the overlay can appear, so it is one /// channel keeps it as cheap as the old integer dim.
/// branch-free pass the compiler can vectorise. Scaling X too is harmless: pub fn tint(pixels: &[u8], colour: [f32; 3], amount: f32) -> Vec<u8> {
/// the overlay buffer is XRGB8888, which ignores it. let keep = 1.0 - amount;
pub fn dim(pixels: &[u8], brightness: u8) -> Vec<u8> { let table = |c: f32| -> [u8; 256] {
let factor = brightness as u16 + 1; std::array::from_fn(|v| (v as f32 * keep + c * 255.0 * amount).round() as u8)
pixels };
.iter() let [r, g, b] = colour;
.map(|&b| ((b as u16 * factor) >> 8) as u8) let tables = [table(b), table(g), table(r)];
.collect() let mut out = pixels.to_vec();
for px in out.as_chunks_mut::<4>().0 {
for (v, t) in px.iter_mut().zip(&tables) {
*v = t[*v as usize];
}
}
out
} }
/// Draws one overlay frame into `dst`: `dark` everywhere, `bright` inside /// Draws one overlay frame into `dst`: `dark` everywhere, `bright` inside
@@ -92,18 +98,21 @@ pub fn blend(dst: &mut [u8], dst_width: u32, src: &tiny_skia::Pixmap, at: (u32,
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn dims_every_byte() {
assert_eq!(dim(&[200, 100, 50, 0], 255), vec![200, 100, 50, 0]);
assert_eq!(dim(&[200, 100, 50, 255], DIM), vec![120, 60, 30, 153]);
assert_eq!(dim(&[255, 0, 1, 2], 0), vec![0, 0, 0, 0]);
}
fn px(buf: &[u8], width: u32, x: u32, y: u32) -> [u8; 4] { fn px(buf: &[u8], width: u32, x: u32, y: u32) -> [u8; 4] {
let i = ((y * width + x) * 4) as usize; let i = ((y * width + x) * 4) as usize;
buf[i..i + 4].try_into().unwrap() buf[i..i + 4].try_into().unwrap()
} }
#[test]
fn tint_blends_towards_the_colour() {
// B, G, R, X = 200, 100, 0, 255; colour pure red (R = 1).
let out = tint(&[200, 100, 0, 255], [1.0, 0.0, 0.0], 0.55);
assert_eq!(out[0], (200.0 * 0.45f32).round() as u8, "blue fades");
assert_eq!(out[1], (100.0 * 0.45f32).round() as u8);
assert_eq!(out[2], (255.0 * 0.55f32).round() as u8, "red rises");
assert_eq!(tint(&[10, 20, 30, 0], [0.0; 3], 0.0), vec![10, 20, 30, 0]);
}
#[test] #[test]
fn compose_outline_and_inside() { fn compose_outline_and_inside() {
let (w, h) = (6, 6); let (w, h) = (6, 6);
+42 -7
View File
@@ -150,21 +150,41 @@ pub fn current() -> Palette {
} }
fn noctalia_palette() -> Option<Palette> { fn noctalia_palette() -> Option<Palette> {
let scheme = noctalia(&["color-scheme-get"])?; // "<source> <name>" // Noctalia keeps the live choice in its state settings (theme-mode-set
let (source, name) = scheme.split_once(' ')?; // writes there); its config holds the initial one. Reading the files is
// instant, unlike `noctalia msg`, and this runs before every overlay.
let state = state_dir().join("noctalia/settings.toml");
let config = crate::config::home().join(".config/noctalia/config.toml");
let (source, name, mode) = [state, config]
.iter()
.find_map(|p| theme_choice(&std::fs::read_to_string(p).ok()?))?;
if source != "community" { if source != "community" {
return None; return None;
} }
let mode = noctalia(&["theme-mode-get"])?; let file = state_dir()
let dir = std::env::var_os("XDG_STATE_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| crate::config::home().join(".local/state"));
let file = dir
.join("noctalia/community-palettes") .join("noctalia/community-palettes")
.join(format!("{}.json", name.replace(' ', "%20"))); .join(format!("{}.json", name.replace(' ', "%20")));
from_noctalia_file(&std::fs::read_to_string(file).ok()?, &mode) from_noctalia_file(&std::fs::read_to_string(file).ok()?, &mode)
} }
fn state_dir() -> PathBuf {
std::env::var_os("XDG_STATE_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| crate::config::home().join(".local/state"))
}
/// `(source, community palette, mode)` from a Noctalia settings file.
fn theme_choice(text: &str) -> Option<(String, String, String)> {
let doc: toml::Table = text.parse().ok()?;
let theme = doc.get("theme")?.as_table()?;
let get = |k: &str| theme.get(k)?.as_str().map(str::to_string);
Some((
get("source")?,
get("community_palette").unwrap_or_default(),
get("mode").unwrap_or_else(|| "dark".into()),
))
}
/// The wallpaper Noctalia shows on `connector`. /// The wallpaper Noctalia shows on `connector`.
pub fn wallpaper(connector: &str) -> Option<PathBuf> { pub fn wallpaper(connector: &str) -> Option<PathBuf> {
noctalia(&["wallpaper-get", connector]) noctalia(&["wallpaper-get", connector])
@@ -236,6 +256,21 @@ mod tests {
} }
} }
#[test]
fn theme_choice_from_settings() {
let text = "[bar]\nx = 1\n\n[theme]\ncommunity_palette = \"Kemuri Susu\"\nmode = \"dark\"\nsource = \"community\"\n\n[wallpaper]\n";
assert_eq!(
theme_choice(text),
Some(("community".into(), "Kemuri Susu".into(), "dark".into()))
);
assert_eq!(
theme_choice("[theme]\nmode = \"dark\"\n"),
None,
"no source"
);
assert_eq!(theme_choice("not toml ["), None);
}
#[test] #[test]
fn lerp_eases() { fn lerp_eases() {
let a = Palette::DEFAULT; let a = Palette::DEFAULT;