T21: on X11 the window names its pane in _KDE_NET_WM_BLUR_BEHIND_REGION and drops it when Blurred goes; Ely's new ResizeBorder::glass leaves a client-drawn window's pane to the content, without ground or shadow, so a Linux window shows the desktop at all; T20's region and this property cover the pane alone, worked out each frame and sent only on a change; the Ely pin moves to 5dc4dd1; the X11 test runs its own Xvfb
CI / platforms (windows-latest) (push) Failing after 8s
CI / macos (push) Failing after 8m43s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-15 01:09:47 -04:00
1 parent 8eeeedf12d
commit b5e4967071
11 files changed
+339 -45

No files matched your search

+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
# The Linux job container runs as root, which ignores the file permissions the tests check.
- if: runner.os == 'Linux'
run: |
apt-get update && apt-get install -y zsh fish
apt-get update && apt-get install -y zsh fish xvfb
useradd --create-home ci
chmod o+x "$HOME"
chown -R ci "$GITHUB_WORKSPACE" "$HOME/.cargo" "$HOME/.rustup"
+4 -3
View File
@@ -8,7 +8,7 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
- Ely (`ely-gpui-component`) from git.zacharyzhang.com, pinned by rev. gpui and gpui_platform from crates.io's `gpui-pre`, the exact version Ely pins, under their crate names through `package`.
- `alacritty_terminal` 0.26: grid, parser, pseudo-terminal, event loop.
- `serde`, `serde_json`: settings. `dirs`: the config folder. `log`, `env_logger`: logs to stderr. `unicode-normalization`, on macOS: Git's marks in composed form.
- On Linux, `wayland-client`, `wayland-backend` and `wayland-protocols` (staging) ask the compositor for `ext_background_effect_v1` on gpui's own connection; `wayland-server` hosts a compositor in the tests.
- On Linux, `wayland-client`, `wayland-backend` and `wayland-protocols` (staging) ask the compositor for `ext_background_effect_v1` on gpui's own connection; `wayland-server` hosts a compositor in the tests. `x11rb` sets `_KDE_NET_WM_BLUR_BEHIND_REGION`; its test runs Xvfb.
## Commands
@@ -24,7 +24,7 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
## Layout
- `src/main.rs`: start. `src/lib.rs`: the modules, public so tests and examples reach them. `src/app.rs`: fonts, settings, integration, then the main window. `src/blur.rs`: what the window shows of the desktop, from the opacity and blur settings, set as they change; `blur/wayland` asks a Wayland compositor for blur behind the window's surface through `ext_background_effect_v1`. `src/workspace.rs`: the window's content, Ely's `ColumnShell` with the sessions, terminal and right columns, their toggles, the right column's Git and Files switch, the title, and the sessions' lifecycle. `src/sessions.rs`: a session and its row. `src/files.rs`: the Files panel, the shown session's folder as Ely's `Tree` with Git's marks. `src/git.rs`: git through its command line: Files' marks, and the Git panel's status, stage, unstage, discard, commit and log; `src/git/diff.rs` reads git's diff into Ely's stretches. `src/git/panel.rs`: the Git panel's state and actions; `src/git/panel/view.rs`: how it draws.
- `src/main.rs`: start. `src/lib.rs`: the modules, public so tests and examples reach them. `src/app.rs`: fonts, settings, integration, then the main window. `src/blur.rs`: what the window shows of the desktop, from the opacity and blur settings, set as they change; `blur/wayland` asks a Wayland compositor for blur behind the window's surface through `ext_background_effect_v1`; `blur/x11` names the window's pane in `_KDE_NET_WM_BLUR_BEHIND_REGION`. `src/workspace.rs`: the window's content, Ely's `ColumnShell` with the sessions, terminal and right columns, their toggles, the right column's Git and Files switch, the title, and the sessions' lifecycle. `src/sessions.rs`: a session and its row. `src/files.rs`: the Files panel, the shown session's folder as Ely's `Tree` with Git's marks. `src/git.rs`: git through its command line: Files' marks, and the Git panel's status, stage, unstage, discard, commit and log; `src/git/diff.rs` reads git's diff into Ely's stretches. `src/git/panel.rs`: the Git panel's state and actions; `src/git/panel/view.rs`: how it draws.
- `src/settings`: the model, its ranges, and `SettingsStore`, the global that loads, checks and saves `settings.json`; `window` is the settings window, `window/appearance` its Appearance page, `window/terminal` its Terminal page. `tests/fonts.rs` brings its own main: gpui builds its Mac platform only on the main thread.
- `src/themes`: built-in terminal themes, the chrome palette each maps to, and `apply`, which dresses the app for the system's appearance.
- `src/term`: the terminal engine. `pty` starts a program through alacritty's event loop; `tap` wraps the pty so its reader scans OSC 7 and the mouse modes before the parser. `unix` starts the program itself, so a failed `chdir` fails the start; Windows uses alacritty's ConPTY. `shell` writes the integration scripts at start and builds each shell's launch. `frame` reads the grid into styled rows, `boxes` turns box drawing, block elements and powerline arrows into rects and polygons (`boxes/outline` strokes curves and diagonals into polygons), `ink` resolves colors, `paint` draws rows, decorations and the cursor, `view` is the entity: pty events, metrics, blink, resize. `keys` turns key events into bytes, the kitty protocol and, in `keys/legacy`, xterm's encoding, with no gpui types; `input` is the view's key listeners, the bindings that keep Tab and F6 from Ely's focus moves, and its IME handler; `mac` reads the Option side and repeat flag of AppKit's current event, keeps held keys repeating, and hands Command key-ups to the focused view. `mouse` encodes mouse reports, with no gpui types; `links` finds the link at a cell; `pointer` is the view's mouse listeners: selection, reports, wheel, links, copy and paste. `find` is the find box: Ely's `FindWidget`, and Ely's `find_all` over the grid's shown text.
@@ -72,4 +72,5 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
- 2026-10-14: Appearance shows every built-in theme as Ely's `RadioCard` in a `SimpleGrid`, each over a small window in the theme's colors. Separate dark theme copies the theme into `dark_theme`, so turning it on changes nothing until a dark card is picked. A theme change re-dresses the chrome at once. Font family comes from Ely's `FontPicker`; size, line height and padding from its `NumberInput`, which saves a number on Enter, on leaving the field and on each step. A font saves, and a settings file starts, only when gpui loads it as the terminal draws it: gpui falls back to another font without a word and loads none without an `m`, so the terminal draws with no font but its own. The cursor preview is a prompt in the terminal's font and theme, its cursor drawn as the terminal draws it, blinking on the terminal's beat. Opacity moves by whole percents, so the saved fraction carries no float noise. Blur saves here and reaches the window in T19. A change the store refuses leaves the setting as it was, and the page says why at its top; a number field drops its draft, and the font picker shows the saved family again. The settings window opens at 900 by 640, room for three theme columns.
- 2026-10-14: Terminal shows scrollback lines, Option as Alt, on macOS only, since elsewhere Alt is Alt, and copy on select. The engine reads each when it needs it, so a change applies at once to every session; a smaller scrollback drops the history past it.
- 2026-10-14: the main window lets the desktop through while Background opacity is under 1, blurred or clear as Background blur says, through gpui's `Blurred` (macOS's NSVisualEffectView, Windows' acrylic) or `Transparent`; at 1 it is `Opaque`. The workspace sets it as the settings change. Ely's `ColumnShell::glass`, added to Ely for this, grounds the columns in the palette's glass, which the opacity sets, so the title bar and every column let the desktop through alike; the palette's `bg` stays solid, so a dialog, the Git diff among them, hides what lies under it. The terminal paints no ground over the glass, or the two would stack; a program's own background (OSC 11) paints over it. Ely's own themes pass their palette too, Ely's colors with the glass set. The settings window stays opaque.
- 2026-10-15: on Wayland the window asks for `ext_background_effect_v1` beside gpui's `org_kde_kwin_blur`. A guest connection on gpui's own display binds it, takes the effect for the window's surface once, and sets a blur region of one rect wider and taller than any window, which the compositor clips to the surface, so a resize needs nothing; the region goes and comes back with Blurred and lands at the surface's next commit, which every frame makes. The guest connection reads the display only while the window opens, before gpui's loop runs, and is never dropped: a drop destroys its proxies through the display, which gpui may close first. The log names what the compositor offers, so which way blur goes: `ext_background_effect_v1`, gpui's `org_kde_kwin_blur`, or none, the desktop clear. Its test runs a `wayland-server` compositor on a socket pair.
- 2026-10-15: on Wayland the window asks for `ext_background_effect_v1` beside gpui's `org_kde_kwin_blur`. A guest connection on gpui's own display binds it, takes the effect for the window's surface once, and blurs the window's pane: the surface less Ely's resize margin on each free edge of a client-drawn window, worked out each frame and sent only when it changes; it goes and comes back with Blurred and lands at the surface's next commit, which every frame makes. The guest connection reads the display only while the window opens, before gpui's loop runs, and is never dropped: a drop destroys its proxies through the display, which gpui may close first. The log names what the compositor offers, so which way blur goes: `ext_background_effect_v1`, gpui's `org_kde_kwin_blur`, or none, the desktop clear. Its test runs a `wayland-server` compositor on a socket pair.
- 2026-10-15: on a client-drawn window, so on Linux, Ely's `ResizeBorder` paints its pane solid and shadowed under the content, which kept the desktop from showing at all. `ResizeBorder::glass`, added to Ely for this, leaves the pane to the content while the window is see-through: no ground, nor the shadow gpui draws under the box, which would tint it. So the blur covers the pane alone and the resize margin stays clear. On X11 the window names its pane in `_KDE_NET_WM_BLUR_BEHIND_REGION` (CARDINAL, 32: x, y, width, height in the window's pixels), as Ghostty names its own, and drops the property when Blurred goes; KWin reads it, other compositors pass it by. It goes over a connection of ELYTerminal's own to `$DISPLAY`, which gpui opens too: a property belongs to the window, whoever sets it. gpui's own `org_kde_kwin_blur` on KDE's Wayland still covers the whole surface, margin included, beyond ELYTerminal's reach.
Generated
+2 -1
View File
@@ -1701,7 +1701,7 @@ checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
[[package]]
name = "ely-gpui-component"
version = "0.1.1"
source = "git+https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git?rev=991ca7b548e0417146ec082dc89ccb8b8d0e09d6#991ca7b548e0417146ec082dc89ccb8b8d0e09d6"
source = "git+https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git?rev=5dc4dd1805550b8697130dce2c8eda903ce33f54#5dc4dd1805550b8697130dce2c8eda903ce33f54"
dependencies = [
"alacritty_terminal",
"anyhow",
@@ -1777,6 +1777,7 @@ dependencies = [
"wayland-client",
"wayland-protocols",
"wayland-server",
"x11rb",
]
[[package]]
+2 -1
View File
@@ -12,7 +12,7 @@ publish = false
alacritty_terminal = { version = "=0.26.0", default-features = false }
anyhow = "1"
dirs = "7"
ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git", rev = "991ca7b548e0417146ec082dc89ccb8b8d0e09d6" }
ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git", rev = "5dc4dd1805550b8697130dce2c8eda903ce33f54" }
env_logger = "0.11"
futures = "0.3"
gethostname = "1.1.0"
@@ -34,6 +34,7 @@ raw-window-handle = "0.6"
wayland-backend = { version = "0.3", features = ["client_system"] }
wayland-client = "0.31"
wayland-protocols = { version = "0.32", features = ["client", "staging"] }
x11rb = "0.13"
[target.'cfg(target_os = "linux")'.dev-dependencies]
wayland-protocols = { version = "0.32", features = ["server", "staging"] }
+2 -2
View File
@@ -6,7 +6,7 @@ docker build -q -t elyterminal-linux - >/dev/null <<'EOF'
FROM rust:1.95
RUN apt-get update \
&& apt-get install -y --no-install-recommends pkg-config libwayland-dev libxkbcommon-dev \
libxkbcommon-x11-dev libxcb1-dev libx11-xcb-dev libfontconfig-dev libfreetype-dev zsh fish \
libxkbcommon-x11-dev libxcb1-dev libx11-xcb-dev libfontconfig-dev libfreetype-dev zsh fish xvfb \
&& rm -rf /var/lib/apt/lists/* \
&& rustup component add clippy rustfmt
# Root ignores the file modes the tests check, as in CI.
@@ -19,7 +19,7 @@ WORKDIR /src
EOF
# Streamed, not shared, and dated now: cargo neither reads a stale file nor skips a changed one.
git -C "$repo" ls-files -co --exclude-standard -z |
COPYFILE_DISABLE=1 tar -C "$repo" --null -T - -cf - |
COPYFILE_DISABLE=1 tar -C "$repo" --no-xattrs --null -T - -cf - |
docker run -i --rm \
-v ely-linux-target:/target \
-v ely-linux-registry:/usr/local/cargo/registry \
+95 -6
View File
@@ -1,9 +1,13 @@
#[cfg(target_os = "linux")]
use gpui::{Bounds, Decorations, Pixels, Size, point};
use gpui::{Window, WindowBackgroundAppearance};
use crate::settings::Settings;
#[cfg(target_os = "linux")]
mod wayland;
#[cfg(target_os = "linux")]
mod x11;
/// What the window shows of the desktop: blurred or clear while the background lets it through.
pub fn appearance(settings: &Settings) -> WindowBackgroundAppearance {
@@ -16,26 +20,34 @@ pub fn appearance(settings: &Settings) -> WindowBackgroundAppearance {
}
}
/// What the main window shows of the desktop, as last set, and on Wayland the blur behind its surface.
/// What the main window shows of the desktop, as last set, and on Linux the compositor's blur behind its pane.
pub struct Blur {
shown: WindowBackgroundAppearance,
#[cfg(target_os = "linux")]
wayland: Option<wayland::Effect>,
#[cfg(target_os = "linux")]
x11: Option<x11::Effect>,
}
impl Blur {
/// The window as `WindowOptions` opened it from these settings.
pub fn new(window: &Window, settings: &Settings) -> Self {
let shown = appearance(settings);
#[cfg(not(target_os = "linux"))]
let _ = window;
Self {
shown,
shown: appearance(settings),
#[cfg(target_os = "linux")]
wayland: wayland::Effect::on(window, shown == WindowBackgroundAppearance::Blurred),
wayland: wayland::Effect::on(window),
#[cfg(target_os = "linux")]
x11: x11::Effect::on(window),
}
}
/// Whether the desktop shows through the window.
pub fn see_through(&self) -> bool {
self.shown != WindowBackgroundAppearance::Opaque
}
/// Sets the window to what the settings ask when it differs from what it shows; logs the way taken.
pub fn follow(&mut self, window: &Window, settings: &Settings) {
let wanted = appearance(settings);
@@ -45,13 +57,65 @@ impl Blur {
self.shown = wanted;
window.set_background_appearance(wanted);
log::info!("blur: the window is {wanted:?} on {}", std::env::consts::OS);
}
/// Keeps a Linux compositor's blur on the window's pane while Blurred; each frame, sending only a change.
pub fn fit(&mut self, window: &Window) {
#[cfg(not(target_os = "linux"))]
let _ = window;
#[cfg(target_os = "linux")]
if let Some(effect) = &self.wayland {
effect.blur(wanted == WindowBackgroundAppearance::Blurred);
{
let blurred = self.shown == WindowBackgroundAppearance::Blurred;
let size = window.viewport_size();
let pane =
blurred.then(|| pane(size, window.client_inset(), window.window_decorations()));
if let Some(effect) = &mut self.wayland {
effect.blur(pane.map(|pane| rect(pane, 1.0)));
}
if let Some(effect) = &mut self.x11 {
effect.blur(pane.map(|pane| rect(pane, window.scale_factor())));
}
}
}
}
/// The window's pane in its surface: all of it, less the inset on each free edge of a client-drawn window, as Ely's `ResizeBorder` pads it.
#[cfg(target_os = "linux")]
fn pane(size: Size<Pixels>, inset: Option<Pixels>, decorations: Decorations) -> Bounds<Pixels> {
let mut pane = Bounds::new(point(Pixels::ZERO, Pixels::ZERO), size);
let (Decorations::Client { tiling }, Some(inset)) = (decorations, inset) else {
return pane;
};
if !tiling.top {
pane.origin.y += inset;
pane.size.height -= inset;
}
if !tiling.bottom {
pane.size.height -= inset;
}
if !tiling.left {
pane.origin.x += inset;
pane.size.width -= inset;
}
if !tiling.right {
pane.size.width -= inset;
}
pane.size = pane.size.max(&Size::default());
pane
}
/// `pane` in whole pixels at `scale`: x, y, width, height.
#[cfg(target_os = "linux")]
fn rect(pane: Bounds<Pixels>, scale: f32) -> [i32; 4] {
let whole = |length: Pixels| (f32::from(length) * scale).round() as i32;
[
whole(pane.origin.x),
whole(pane.origin.y),
whole(pane.size.width),
whole(pane.size.height),
]
}
#[cfg(test)]
mod tests {
use gpui::WindowBackgroundAppearance::{Blurred, Opaque, Transparent};
@@ -72,4 +136,29 @@ mod tests {
assert_eq!(at(0.88, true), Blurred);
assert_eq!(at(0.3, false), Transparent);
}
#[cfg(target_os = "linux")]
#[test]
fn the_pane_leaves_the_margin_on_each_free_edge() {
use gpui::{Tiling, px, size};
let surface = size(px(1220.), px(780.));
let inset = Some(px(10.));
let at = |decorations| rect(pane(surface, inset, decorations), 2.0);
assert_eq!(at(Decorations::Server), [0, 0, 2440, 1560]);
let floating = Decorations::Client {
tiling: Tiling::default(),
};
assert_eq!(at(floating), [20, 20, 2400, 1520]);
let left = Tiling {
left: true,
top: true,
bottom: true,
right: false,
};
let half = Decorations::Client { tiling: left };
assert_eq!(at(half), [0, 0, 2420, 1560]);
assert_eq!(rect(pane(surface, None, floating), 1.0), [0, 0, 1220, 780]);
let tiny = size(px(15.), px(15.));
assert_eq!(rect(pane(tiny, inset, floating), 1.0), [10, 10, 0, 0]);
}
}
+34 -24
View File
@@ -17,15 +17,15 @@ use wayland_protocols::ext::background_effect::v1::client::{
ext_background_effect_surface_v1::ExtBackgroundEffectSurfaceV1,
};
/// Wider and taller than any window: the compositor clips the region to the surface.
const ALL: i32 = 1 << 20;
/// The compositor's blur behind a window's surface, through `ext_background_effect_v1`.
pub struct Effect {
// Never dropped: gpui may close the display first, and a drop touches it.
connection: ManuallyDrop<Connection>,
queue: QueueHandle<Offers>,
compositor: WlCompositor,
effect: ExtBackgroundEffectSurfaceV1,
region: WlRegion,
/// The rect the compositor blurs behind, as last sent.
blurs: Option<[i32; 4]>,
}
/// What the compositor offers, as its events say.
@@ -35,8 +35,8 @@ struct Offers {
}
impl Effect {
/// The effect on `window`'s surface, blurring when `blurred`; none off Wayland or where the compositor lacks it.
pub fn on(window: &Window, blurred: bool) -> Option<Self> {
/// The effect on `window`'s surface, blurring nothing yet; none off Wayland or where the compositor lacks it.
pub fn on(window: &Window) -> Option<Self> {
let handles = (
HasWindowHandle::window_handle(window),
window.display_handle(),
@@ -53,12 +53,7 @@ impl Effect {
};
// SAFETY: gpui's live display, and the window's live surface on it.
match unsafe { Self::new(display.display, surface.surface) } {
Ok(effect) => {
if blurred && let Some(effect) = &effect {
effect.blur(true);
}
effect
}
Ok(effect) => effect,
Err(error) => {
log::error!("blur: ext_background_effect_v1 failed: {error:#}");
None
@@ -109,28 +104,43 @@ impl Effect {
let surface =
WlSurface::from_id(&connection, id).context("blur: the window's surface is gone")?;
let effect = manager.get_background_effect(&surface, &qh, ());
let region = compositor.create_region(&qh, ());
region.add(0, 0, ALL, ALL);
log::info!(
"blur: ext_background_effect_v1 on the window's surface; the compositor blurs: {}, org_kde_kwin_blur too: {kde}",
offers.blur
);
Ok(Some(Self {
connection: ManuallyDrop::new(connection),
queue: qh,
compositor,
effect,
region,
blurs: None,
}))
}
/// Blurs behind the whole surface, or nothing, from the surface's next commit.
pub fn blur(&self, on: bool) {
self.effect.set_blur_region(on.then_some(&self.region));
match self.connection.flush() {
Ok(()) if on => log::info!("blur: ext_background_effect_v1 blurs behind the window"),
Ok(()) => log::info!("blur: ext_background_effect_v1 blurs nothing"),
Err(error) => {
log::error!("blur: ext_background_effect_v1 did not reach the compositor: {error}")
}
/// Blurs behind `rect`, in surface pixels, or nothing, from the surface's next commit; sends only a change.
pub fn blur(&mut self, rect: Option<[i32; 4]>) {
if rect == self.blurs {
return;
}
let turned = rect.is_some() != self.blurs.is_some();
self.blurs = rect;
let region = rect.map(|[x, y, width, height]| {
let region = self.compositor.create_region(&self.queue, ());
region.add(x, y, width, height);
region
});
self.effect.set_blur_region(region.as_ref());
// The effect copies the region.
if let Some(region) = region {
region.destroy();
}
let what = rect.map_or("nothing".into(), |rect| format!("{rect:?}"));
if let Err(error) = self.connection.flush() {
log::error!("blur: ext_background_effect_v1 did not reach the compositor: {error}");
} else if turned {
log::info!("blur: ext_background_effect_v1 blurs {what}");
} else {
log::debug!("blur: ext_background_effect_v1 blurs {what}");
}
}
}
+8 -6
View File
@@ -233,17 +233,19 @@ fn heard(offered: bool, ask: impl FnOnce(Option<Effect>)) -> (Vec<String>, u32)
#[test]
fn the_window_s_surface_blurs_while_asked() {
let (notes, surface) = heard(true, |effect| {
let effect = effect.expect("the compositor offers the protocol");
effect.blur(true);
effect.blur(false);
let mut effect = effect.expect("the compositor offers the protocol");
effect.blur(Some([10, 10, 300, 200]));
effect.blur(Some([10, 10, 300, 200]));
effect.blur(Some([0, 0, 320, 220]));
effect.blur(None);
});
let all = format!("blur [[0, 0, {ALL}, {ALL}]]");
let expected = [
format!("effect on surface {surface}"),
all,
"blur [[10, 10, 300, 200]]".into(),
"blur [[0, 0, 320, 220]]".into(),
"blur nothing".into(),
];
assert_eq!(notes, expected);
assert_eq!(notes, expected, "a repeat sends nothing");
assert!(crate::testlog::logged(
"blur: ext_background_effect_v1 on the window's surface; the compositor blurs: true, org_kde_kwin_blur too: false"
));
+97
View File
@@ -0,0 +1,97 @@
use anyhow::Context as _;
use gpui::Window;
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
use x11rb::{
protocol::xproto::{Atom, AtomEnum, ConnectionExt as _, PropMode},
rust_connection::RustConnection,
wrapper::ConnectionExt as _,
};
/// KWin blurs behind the rects a window names here, in its own pixels.
const BLUR_BEHIND: &str = "_KDE_NET_WM_BLUR_BEHIND_REGION";
/// The compositor's blur behind an X11 window, through `_KDE_NET_WM_BLUR_BEHIND_REGION`.
pub struct Effect {
connection: RustConnection,
window: u32,
atom: Atom,
/// The rect the property names, as last set.
blurs: Option<[i32; 4]>,
}
impl Effect {
/// The effect on `window`'s X11 window, blurring nothing yet; none off X11.
pub fn on(window: &Window) -> Option<Self> {
let Ok(handle) = HasWindowHandle::window_handle(window) else {
log::error!("blur: gpui gave no handle to the window");
return None;
};
let RawWindowHandle::Xcb(handle) = handle.as_raw() else {
log::info!("blur: no X11 window, so no {BLUR_BEHIND}");
return None;
};
// gpui opens $DISPLAY too; a property belongs to the window, whoever sets it.
match Self::new(None, handle.window.get()) {
Ok(effect) => Some(effect),
Err(error) => {
log::error!("blur: {BLUR_BEHIND} failed: {error:#}");
None
}
}
}
/// An effect on `window` of the X server `display` names, or `$DISPLAY`'s.
fn new(display: Option<&str>, window: u32) -> anyhow::Result<Self> {
let (connection, _) = x11rb::connect(display).context("blur: no X server")?;
let atom = (connection.intern_atom(false, BLUR_BEHIND.as_bytes()))
.context("blur: the atom did not go out")?
.reply()
.context("blur: the atom")?
.atom;
log::info!(
"blur: {BLUR_BEHIND} on X11 window {window}; KWin blurs behind it, other compositors pass it by"
);
Ok(Self {
connection,
window,
atom,
blurs: None,
})
}
/// Names `rect`, in the window's pixels, or removes the property; the X server has it on return; sends only a change.
pub fn blur(&mut self, rect: Option<[i32; 4]>) {
if rect == self.blurs {
return;
}
let turned = rect.is_some() != self.blurs.is_some();
self.blurs = rect;
let asked = match rect {
Some(rect) => {
let cardinals = rect.map(|side| side.max(0) as u32);
(self.connection)
.change_property32(
PropMode::REPLACE,
self.window,
self.atom,
AtomEnum::CARDINAL,
&cardinals,
)
.map(|cookie| cookie.check())
}
None => (self.connection)
.delete_property(self.window, self.atom)
.map(|cookie| cookie.check()),
};
let what = rect.map_or("nothing".into(), |rect| format!("{rect:?}"));
match asked {
Ok(Ok(())) if turned => log::info!("blur: {BLUR_BEHIND} names {what}"),
Ok(Ok(())) => log::debug!("blur: {BLUR_BEHIND} names {what}"),
Ok(Err(error)) => log::error!("blur: the X server refused {BLUR_BEHIND}: {error}"),
Err(error) => log::error!("blur: {BLUR_BEHIND} did not reach the X server: {error}"),
}
}
}
#[cfg(test)]
mod tests;
+91
View File
@@ -0,0 +1,91 @@
use std::{
io::{BufRead, BufReader},
process::{Child, Command, Stdio},
};
use x11rb::{
COPY_DEPTH_FROM_PARENT,
connection::Connection as _,
protocol::xproto::{CreateWindowAux, WindowClass},
};
use super::*;
/// An X server of the test's own, gone with it.
struct Xvfb {
child: Child,
display: String,
}
impl Xvfb {
fn start() -> Self {
let mut child = Command::new("Xvfb")
.args(["-displayfd", "1", "-nolisten", "tcp"])
.stdout(Stdio::piped())
.spawn()
.expect("Xvfb starts: the X11 tests need it");
let mut number = String::new();
let stdout = child.stdout.take().expect("Xvfb's stdout");
BufReader::new(stdout)
.read_line(&mut number)
.expect("Xvfb names its display");
let display = format!(":{}", number.trim());
Self { child, display }
}
}
impl Drop for Xvfb {
fn drop(&mut self) {
if let Err(error) = self.child.kill().and_then(|()| self.child.wait().map(drop)) {
eprintln!("Xvfb {} did not stop: {error}", self.display);
}
}
}
#[test]
fn the_window_names_its_pane_while_blurred() {
crate::testlog::init();
let xvfb = Xvfb::start();
let (connection, screen) = x11rb::connect(Some(&xvfb.display)).expect("the test connects");
let root = connection.setup().roots[screen].root;
let window = connection.generate_id().expect("a window id");
let aux = CreateWindowAux::new();
let class = WindowClass::INPUT_OUTPUT;
(connection.create_window(
COPY_DEPTH_FROM_PARENT,
window,
root,
0,
0,
640,
400,
0,
class,
0,
&aux,
))
.expect("the request")
.check()
.expect("a window");
let mut effect = Effect::new(Some(&xvfb.display), window).expect("an effect");
let atom = effect.atom;
let worn = || {
let property = (connection.get_property(false, window, atom, AtomEnum::ANY, 0, 16))
.expect("the request")
.reply()
.expect("the property");
let cardinal = property.type_ == u32::from(AtomEnum::CARDINAL);
let values = property.value32().map(Iterator::collect::<Vec<_>>);
(property.type_ != 0).then_some((cardinal, values))
};
assert_eq!(worn(), None, "none at first");
effect.blur(Some([20, 20, 1200, 760]));
assert_eq!(worn(), Some((true, Some(vec![20, 20, 1200, 760]))));
effect.blur(Some([0, 0, 1240, 800]));
assert_eq!(worn(), Some((true, Some(vec![0, 0, 1240, 800]))));
effect.blur(None);
assert_eq!(worn(), None, "removed");
assert!(crate::testlog::logged(&format!(
"blur: {BLUR_BEHIND} on X11 window {window}; KWin blurs behind it, other compositors pass it by"
)));
}
+3 -1
View File
@@ -320,7 +320,8 @@ fn show_tab(value: &SharedString, cx: &mut App) {
}
impl Render for Workspace {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
self.background.fit(window);
let settings = SettingsStore::get(cx);
let width = cx.theme().sidebar_width(false);
let mut shell = ColumnShell::new("workspace").glass();
@@ -434,6 +435,7 @@ impl Render for Workspace {
}))
.child(
ResizeBorder::new()
.when(self.background.see_through(), ResizeBorder::glass)
.child(FocusScope::new(&self.focus).root().size_full().child(shell)),
)
}