Merge feature/capture-rainbow-background: rainbow-gradient isolation background
All checks were successful
dev bread-theme / build (push) Successful in 21s
dev bakery / build (push) Successful in 52s

This commit is contained in:
Breadway 2026-07-29 17:02:23 +08:00
commit e402bb3cb7
3 changed files with 128 additions and 8 deletions

64
Cargo.lock generated
View file

@ -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"

View file

@ -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"] }

View file

@ -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()