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 sound;
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)]
mod theme;
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")
.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();
for (output, frame) in outputs.iter().zip(frames) {
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.commit();
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!(
@@ -126,7 +128,13 @@ pub fn select(wl: &mut Wayland, outputs: &[Output], frames: &[Frame]) -> Result<
}
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 len = (width * height * 4) as usize;
Ok(Surface {
@@ -135,7 +143,7 @@ impl Surface {
geom: frame.output.clone(),
width,
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),
// 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")?,
+29 -20
View File
@@ -1,19 +1,25 @@
use crate::frame::PixelRect;
/// Overlay brightness outside the selection (60%, i.e. darkened by 40%).
pub const DIM: u8 = 153;
/// How far the overlay outside the selection leans towards the theme colour.
pub const TINT: f32 = 0.55;
/// B, G, R, X pixels with every byte scaled by about `brightness / 255`.
///
/// This runs on every output before the overlay can appear, so it is one
/// branch-free pass the compiler can vectorise. Scaling X too is harmless:
/// the overlay buffer is XRGB8888, which ignores it.
pub fn dim(pixels: &[u8], brightness: u8) -> Vec<u8> {
let factor = brightness as u16 + 1;
pixels
.iter()
.map(|&b| ((b as u16 * factor) >> 8) as u8)
.collect()
/// 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
/// channel keeps it as cheap as the old integer dim.
pub fn tint(pixels: &[u8], colour: [f32; 3], amount: f32) -> Vec<u8> {
let keep = 1.0 - amount;
let table = |c: f32| -> [u8; 256] {
std::array::from_fn(|v| (v as f32 * keep + c * 255.0 * amount).round() as u8)
};
let [r, g, b] = colour;
let tables = [table(b), table(g), table(r)];
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
@@ -92,18 +98,21 @@ pub fn blend(dst: &mut [u8], dst_width: u32, src: &tiny_skia::Pixmap, at: (u32,
mod tests {
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] {
let i = ((y * width + x) * 4) as usize;
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]
fn compose_outline_and_inside() {
let (w, h) = (6, 6);
+42 -7
View File
@@ -150,21 +150,41 @@ pub fn current() -> Palette {
}
fn noctalia_palette() -> Option<Palette> {
let scheme = noctalia(&["color-scheme-get"])?; // "<source> <name>"
let (source, name) = scheme.split_once(' ')?;
// Noctalia keeps the live choice in its state settings (theme-mode-set
// 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" {
return None;
}
let mode = noctalia(&["theme-mode-get"])?;
let dir = std::env::var_os("XDG_STATE_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| crate::config::home().join(".local/state"));
let file = dir
let file = state_dir()
.join("noctalia/community-palettes")
.join(format!("{}.json", name.replace(' ', "%20")));
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`.
pub fn wallpaper(connector: &str) -> Option<PathBuf> {
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]
fn lerp_eases() {
let a = Palette::DEFAULT;