bread-capture: rainbow-gradient isolation background instead of flat color
A solid background can't reveal whether a surface that's supposed to be translucent (breadbox/breadclip/breadsearch's full-screen overlay windows, breadbar's alpha-blended notification/OSD surfaces) is actually compositing as translucent — a flat color showing through a flat color still just looks flat. Generates a diagonal rainbow gradient (full hue sweep, via the `image` crate) sized exactly to the capture canvas and sets it as Sway's output background instead. Needed installing swaybg (the external helper Sway's `output ... bg` config directive shells out to) — without it the bg command silently no-ops and the canvas stays black, which is why the first attempt at this looked identical to the old flat-color version.
This commit is contained in:
parent
ff17a028a3
commit
e3df426996
3 changed files with 128 additions and 8 deletions
64
Cargo.lock
generated
64
Cargo.lock
generated
|
|
@ -155,6 +155,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"bread-utils",
|
||||
"clap",
|
||||
"image",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -222,6 +223,18 @@ version = "3.20.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "cairo-rs"
|
||||
version = "0.22.0"
|
||||
|
|
@ -591,6 +604,15 @@ version = "2.4.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "field-offset"
|
||||
version = "0.3.6"
|
||||
|
|
@ -1183,6 +1205,19 @@ dependencies = [
|
|||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
|
|
@ -1362,6 +1397,16 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"pxfm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndarray"
|
||||
version = "0.17.2"
|
||||
|
|
@ -1519,6 +1564,19 @@ version = "0.3.33"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.14.0"
|
||||
|
|
@ -1570,6 +1628,12 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pxfm"
|
||||
version = "0.1.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.46"
|
||||
|
|
|
|||
|
|
@ -16,3 +16,6 @@ path = "src/main.rs"
|
|||
bread-utils = { path = "../bread-utils" }
|
||||
clap = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
# Generates the isolated capture canvas's rainbow-gradient background — see
|
||||
# isolation.rs. png-only: no decoding, no other format support needed.
|
||||
image = { version = "0.25", default-features = false, features = ["png"] }
|
||||
|
|
|
|||
|
|
@ -39,6 +39,17 @@
|
|||
//! breadbar already has to tolerate a missing/dead Hyprland connection
|
||||
//! gracefully (it survives Hyprland restarting), so this just exercises that
|
||||
//! same fallback path instead of a real error case.
|
||||
//!
|
||||
//! The background is a generated rainbow gradient, not a flat colour —
|
||||
//! deliberately: a solid fill can't tell you whether a window that's
|
||||
//! *supposed* to be translucent (breadbox/breadclip/breadsearch's
|
||||
//! full-screen overlay windows, breadbar's notification/OSD surfaces) is
|
||||
//! actually compositing as translucent, since a flat colour showing through
|
||||
//! a flat colour still just looks flat. A continuously-varying gradient
|
||||
//! makes any real transparency immediately obvious (multiple hues bleed
|
||||
//! through) and any accidentally-opaque surface just as obvious (it blocks
|
||||
//! the gradient out entirely, a flat rectangle where there should be colour
|
||||
//! variation).
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use std::collections::HashSet;
|
||||
|
|
@ -48,15 +59,11 @@ use std::time::{Duration, Instant};
|
|||
|
||||
const DISCOVERY_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
/// Matches bread-theme's `FIXED_BACKGROUND` (`bread-theme/src/palette.rs`) —
|
||||
/// so the empty canvas behind a capture reads as "the app's own dark theme",
|
||||
/// not an arbitrary compositor default.
|
||||
const BACKGROUND_COLOR: &str = "#0c0c0c";
|
||||
|
||||
pub struct Isolation {
|
||||
child: Child,
|
||||
pub wayland_display: String,
|
||||
config_path: PathBuf,
|
||||
background_path: PathBuf,
|
||||
runtime_dir: PathBuf,
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +77,8 @@ impl Isolation {
|
|||
let runtime_dir = PathBuf::from(
|
||||
std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/run/user/1000".to_string()),
|
||||
);
|
||||
let config_path = write_headless_config(width, height)?;
|
||||
let background_path = write_rainbow_background(width, height)?;
|
||||
let config_path = write_headless_config(width, height, &background_path)?;
|
||||
let before_sockets: HashSet<String> = dir_names(&runtime_dir)
|
||||
.into_iter()
|
||||
.filter(|n| is_wayland_socket_name(n))
|
||||
|
|
@ -91,6 +99,7 @@ impl Isolation {
|
|||
child,
|
||||
wayland_display: String::new(),
|
||||
config_path,
|
||||
background_path,
|
||||
runtime_dir: runtime_dir.clone(),
|
||||
};
|
||||
|
||||
|
|
@ -122,6 +131,7 @@ impl Drop for Isolation {
|
|||
let _ = self.child.kill();
|
||||
let _ = self.child.wait();
|
||||
let _ = std::fs::remove_file(&self.config_path);
|
||||
let _ = std::fs::remove_file(&self.background_path);
|
||||
// Killing sway doesn't unlink the socket it bound — confirmed
|
||||
// empirically, a killed instance leaves both files behind — so
|
||||
// without this, every capture run permanently orphans a
|
||||
|
|
@ -133,16 +143,59 @@ impl Drop for Isolation {
|
|||
}
|
||||
}
|
||||
|
||||
fn write_headless_config(width: u32, height: u32) -> Result<PathBuf> {
|
||||
fn write_headless_config(width: u32, height: u32, background_path: &Path) -> Result<PathBuf> {
|
||||
let path = std::env::temp_dir().join(format!("bread-capture-sway-{}.conf", std::process::id()));
|
||||
let bg = background_path.display();
|
||||
let contents = format!(
|
||||
"output HEADLESS-1 resolution {width}x{height}\n\
|
||||
output HEADLESS-1 bg {BACKGROUND_COLOR} solid_color\n"
|
||||
output HEADLESS-1 bg {bg} stretch\n"
|
||||
);
|
||||
std::fs::write(&path, contents).with_context(|| format!("writing {}", path.display()))?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Renders a diagonal rainbow (full hue sweep, both x and y contribute) to a
|
||||
/// PNG at exactly `width`x`height`, for use as the isolated canvas's
|
||||
/// background — see the module doc for why a gradient instead of a flat
|
||||
/// colour. Diagonal rather than a simple left-to-right sweep so a capture
|
||||
/// showing color variation isn't just luck-of-the-x-position: a purely
|
||||
/// horizontal gradient would still make a tall, narrow surface look like a
|
||||
/// near-flat single hue.
|
||||
fn write_rainbow_background(width: u32, height: u32) -> Result<PathBuf> {
|
||||
let path = std::env::temp_dir().join(format!("bread-capture-bg-{}.png", std::process::id()));
|
||||
let mut img = image::RgbImage::new(width.max(1), height.max(1));
|
||||
let denom = (width + height).max(1) as f32;
|
||||
for y in 0..img.height() {
|
||||
for x in 0..img.width() {
|
||||
let hue = ((x + y) as f32 / denom) * 360.0;
|
||||
img.put_pixel(x, y, image::Rgb(hsv_to_rgb(hue, 0.85, 0.95)));
|
||||
}
|
||||
}
|
||||
img.save(&path).with_context(|| format!("writing {}", path.display()))?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Standard HSV -> RGB conversion. `h` in degrees [0, 360), `s`/`v` in [0, 1].
|
||||
fn hsv_to_rgb(h: f32, s: f32, v: f32) -> [u8; 3] {
|
||||
let c = v * s;
|
||||
let h_prime = (h / 60.0) % 6.0;
|
||||
let x = c * (1.0 - (h_prime % 2.0 - 1.0).abs());
|
||||
let m = v - c;
|
||||
let (r1, g1, b1) = match h_prime as u32 {
|
||||
0 => (c, x, 0.0),
|
||||
1 => (x, c, 0.0),
|
||||
2 => (0.0, c, x),
|
||||
3 => (0.0, x, c),
|
||||
4 => (x, 0.0, c),
|
||||
_ => (c, 0.0, x),
|
||||
};
|
||||
[
|
||||
((r1 + m) * 255.0).round() as u8,
|
||||
((g1 + m) * 255.0).round() as u8,
|
||||
((b1 + m) * 255.0).round() as u8,
|
||||
]
|
||||
}
|
||||
|
||||
fn dir_names(path: &Path) -> HashSet<String> {
|
||||
std::fs::read_dir(path)
|
||||
.into_iter()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue