feat: theme-coloured region dim
This commit is contained in:
1 parent
2178b840ad
commit
201f43ef34
4 files changed
+83
-31
No files matched your search
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user