diff --git a/AGENTS.md b/AGENTS.md index 932b18b..d27916b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,6 +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. ## Commands @@ -19,10 +20,11 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does; - `to:settings` and `to:main` aim the specs after them, and the picture, at that window, which becomes the app's key window, since AppKit sends keys there; the app stays inactive, and gpui sends an inactive window no focus or blur events, so press Enter to commit a field; - mouse gestures handed to gpui's view, at points from the content's top-left, modifiers as for keys: `click@x,y`, `click2@`, `click3@`, `rclick@`, `mclick@`, `drag@x,y@x,y`, `move@x,y`, the steps `ldown@`, `lup@`, `ldrag@`, `rdown@`, `rup@`, `mdown@`, `mup@`, and `scrollup@x,y@lines`, `scrolldown@x,y@lines`. - `scripts/check.sh`: fmt, clippy `-D warnings`, tests, the 500-line rule. +- `scripts/linux.sh ARGS` runs `cargo ARGS` on the working tree in a Linux container, the build kept in Docker volumes; `scripts/linux.sh test --locked` runs the tests as a user, not root, as CI does. The tree goes in as a tar stream, every file dated now: Colima's shared folder can show the VM a file as it was before an edit, and cargo judges by date, so a tree older than the last build would reuse that build. It needs a Docker daemon: on macOS, Colima (`colima start`). ## 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. `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`. `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. @@ -70,3 +72,4 @@ 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. diff --git a/Cargo.lock b/Cargo.lock index 6d735c1..38cc7fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1773,6 +1773,10 @@ dependencies = [ "serde_path_to_error", "signal-hook", "unicode-normalization", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-server", ] [[package]] @@ -7321,6 +7325,7 @@ dependencies = [ "wayland-backend", "wayland-client", "wayland-scanner", + "wayland-server", ] [[package]] @@ -7360,6 +7365,19 @@ dependencies = [ "quote", ] +[[package]] +name = "wayland-server" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dde9c29be0f723a573977de51ee455bf3dfa03652730a74f9dd3b337e374d75" +dependencies = [ + "bitflags 2.13.2", + "downcast-rs", + "rustix", + "wayland-backend", + "wayland-scanner", +] + [[package]] name = "wayland-sys" version = "0.31.11" diff --git a/Cargo.toml b/Cargo.toml index ceb0a6e..cb3dafc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,16 @@ libc = "0.2" rustix-openpty = "0.2" signal-hook = "0.4" +[target.'cfg(target_os = "linux")'.dependencies] +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"] } + +[target.'cfg(target_os = "linux")'.dev-dependencies] +wayland-protocols = { version = "0.32", features = ["server", "staging"] } +wayland-server = "0.31" + [target.'cfg(target_os = "macos")'.dependencies] block = "0.1" objc = "0.2" diff --git a/scripts/linux.sh b/scripts/linux.sh new file mode 100755 index 0000000..068e435 --- /dev/null +++ b/scripts/linux.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# linux.sh ARGS: runs `cargo ARGS` on the working tree in a Linux container. +set -eu +repo=$(cd "$(dirname "$0")/.." && pwd) +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 \ + && rm -rf /var/lib/apt/lists/* \ + && rustup component add clippy rustfmt +# Root ignores the file modes the tests check, as in CI. +RUN useradd --create-home ci \ + && mkdir -p /src /target /usr/local/cargo/registry /usr/local/cargo/git \ + && chown ci /src /target /usr/local/cargo/registry /usr/local/cargo/git +USER ci +ENV CARGO_TARGET_DIR=/target CARGO_PROFILE_DEV_DEBUG=0 CARGO_INCREMENTAL=0 +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 - | + docker run -i --rm \ + -v ely-linux-target:/target \ + -v ely-linux-registry:/usr/local/cargo/registry \ + -v ely-linux-git:/usr/local/cargo/git \ + elyterminal-linux sh -c 'tar -xmf - -C /src && exec cargo "$@"' cargo "$@" diff --git a/src/blur.rs b/src/blur.rs index d821303..337bbd5 100644 --- a/src/blur.rs +++ b/src/blur.rs @@ -2,6 +2,9 @@ use gpui::{Window, WindowBackgroundAppearance}; use crate::settings::Settings; +#[cfg(target_os = "linux")] +mod wayland; + /// What the window shows of the desktop: blurred or clear while the background lets it through. pub fn appearance(settings: &Settings) -> WindowBackgroundAppearance { if settings.background_opacity >= 1.0 { @@ -13,13 +16,39 @@ pub fn appearance(settings: &Settings) -> WindowBackgroundAppearance { } } -/// Sets `window` to what the settings ask when it differs from `shown`; logs the way taken. -pub fn follow(window: &Window, shown: &mut WindowBackgroundAppearance, settings: &Settings) { - let wanted = appearance(settings); - if wanted != *shown { - *shown = wanted; +/// What the main window shows of the desktop, as last set, and on Wayland the blur behind its surface. +pub struct Blur { + shown: WindowBackgroundAppearance, + #[cfg(target_os = "linux")] + wayland: Option, +} + +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, + #[cfg(target_os = "linux")] + wayland: wayland::Effect::on(window, shown == WindowBackgroundAppearance::Blurred), + } + } + + /// 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); + if wanted == self.shown { + return; + } + self.shown = wanted; window.set_background_appearance(wanted); log::info!("blur: the window is {wanted:?} on {}", std::env::consts::OS); + #[cfg(target_os = "linux")] + if let Some(effect) = &self.wayland { + effect.blur(wanted == WindowBackgroundAppearance::Blurred); + } } } diff --git a/src/blur/wayland.rs b/src/blur/wayland.rs new file mode 100644 index 0000000..748c91d --- /dev/null +++ b/src/blur/wayland.rs @@ -0,0 +1,171 @@ +use std::{ffi::c_void, mem::ManuallyDrop, ptr::NonNull}; + +use anyhow::Context as _; +use gpui::Window; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle}; +use wayland_backend::client::{Backend, ObjectId}; +use wayland_client::{ + Connection, Dispatch, Proxy, QueueHandle, delegate_noop, + globals::{BindError, GlobalListContents, registry_queue_init}, + protocol::{ + wl_compositor::WlCompositor, wl_region::WlRegion, wl_registry::WlRegistry, + wl_surface::WlSurface, + }, +}; +use wayland_protocols::ext::background_effect::v1::client::{ + ext_background_effect_manager_v1::{self, Capability, ExtBackgroundEffectManagerV1}, + 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, + effect: ExtBackgroundEffectSurfaceV1, + region: WlRegion, +} + +/// What the compositor offers, as its events say. +#[derive(Default)] +struct Offers { + blur: bool, +} + +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 { + let handles = ( + HasWindowHandle::window_handle(window), + window.display_handle(), + ); + let (Ok(surface), Ok(display)) = handles else { + log::error!("blur: gpui gave no handle to the window"); + return None; + }; + let (RawWindowHandle::Wayland(surface), RawDisplayHandle::Wayland(display)) = + (surface.as_raw(), display.as_raw()) + else { + log::info!("blur: no Wayland surface, so no ext_background_effect_v1"); + return None; + }; + // 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 + } + Err(error) => { + log::error!("blur: ext_background_effect_v1 failed: {error:#}"); + None + } + } + } + + /// An effect on `surface`, none where the compositor lacks the protocol; `display` must outlive every call on it. + unsafe fn new( + display: NonNull, + surface: NonNull, + ) -> anyhow::Result> { + // SAFETY: the caller's live display. + let backend = unsafe { Backend::from_foreign_display(display.as_ptr().cast()) }; + let connection = Connection::from_backend(backend); + let (globals, mut queue) = + registry_queue_init::(&connection).context("blur: Wayland's globals")?; + let qh = queue.handle(); + let kde = globals.contents().with_list(|list| { + list.iter() + .any(|global| global.interface == "org_kde_kwin_blur_manager") + }); + let manager: ExtBackgroundEffectManagerV1 = match globals.bind(&qh, 1..=1, ()) { + Ok(manager) => manager, + Err(BindError::NotPresent) => { + if kde { + log::info!( + "blur: the compositor offers no ext_background_effect_v1; gpui blurs through org_kde_kwin_blur" + ); + } else { + log::info!( + "blur: the compositor offers neither ext_background_effect_v1 nor org_kde_kwin_blur; a see-through window shows the desktop clear" + ); + } + return Ok(None); + } + Err(error) => { + return Err(error).context("blur: ext_background_effect_manager_v1 did not bind"); + } + }; + let compositor: WlCompositor = + (globals.bind(&qh, 1..=1, ())).context("blur: wl_compositor did not bind")?; + let mut offers = Offers::default(); + (queue.roundtrip(&mut offers)).context("blur: the compositor's capabilities")?; + // SAFETY: the caller's live surface. + let id = unsafe { ObjectId::from_ptr(WlSurface::interface(), surface.as_ptr().cast()) } + .context("blur: the window's surface is no wl_surface")?; + 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), + effect, + region, + })) + } + + /// 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}") + } + } + } +} + +impl Dispatch for Offers { + fn event( + _: &mut Self, + _: &WlRegistry, + _: ::Event, + _: &GlobalListContents, + _: &Connection, + _: &QueueHandle, + ) { + } +} + +impl Dispatch for Offers { + fn event( + offers: &mut Self, + _: &ExtBackgroundEffectManagerV1, + event: ext_background_effect_manager_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let ext_background_effect_manager_v1::Event::Capabilities { flags } = event { + // Bits this version does not know leave the blur bit readable. + offers.blur = u32::from(flags) & Capability::Blur.bits() != 0; + } + } +} + +delegate_noop!(Offers: WlCompositor); +delegate_noop!(Offers: WlRegion); +delegate_noop!(Offers: ExtBackgroundEffectSurfaceV1); + +#[cfg(test)] +mod tests; diff --git a/src/blur/wayland/tests.rs b/src/blur/wayland/tests.rs new file mode 100644 index 0000000..fd66ff1 --- /dev/null +++ b/src/blur/wayland/tests.rs @@ -0,0 +1,259 @@ +use std::{ + os::unix::net::UnixStream, + sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, + }, + thread, + time::Duration, +}; + +use wayland_protocols::ext::background_effect::v1::server::{ + ext_background_effect_manager_v1 as manager, ext_background_effect_surface_v1 as effect, +}; +use wayland_server::{ + Client, DataInit, Dispatch as Serve, Display, DisplayHandle, GlobalDispatch, New, Resource, + backend::ClientData, + protocol::{wl_compositor, wl_region, wl_surface}, +}; + +use super::*; + +/// The rects a region holds. +type Rects = Mutex>; + +/// A compositor that writes down what the window asks of it. +struct Compositor { + heard: Arc>>, +} + +impl Compositor { + fn hear(&self, line: String) { + self.heard.lock().expect("the notes").push(line); + } +} + +struct Peer; + +impl ClientData for Peer {} + +impl GlobalDispatch for Compositor { + fn bind( + _: &mut Self, + _: &DisplayHandle, + _: &Client, + resource: New, + _: &(), + init: &mut DataInit<'_, Self>, + ) { + init.init(resource, ()); + } +} + +impl Serve for Compositor { + fn request( + _: &mut Self, + _: &Client, + _: &wl_compositor::WlCompositor, + request: wl_compositor::Request, + _: &(), + _: &DisplayHandle, + init: &mut DataInit<'_, Self>, + ) { + match request { + wl_compositor::Request::CreateSurface { id } => { + init.init(id, ()); + } + wl_compositor::Request::CreateRegion { id } => { + init.init(id, Rects::default()); + } + _ => {} + } + } +} + +impl Serve for Compositor { + fn request( + _: &mut Self, + _: &Client, + _: &wl_surface::WlSurface, + _: wl_surface::Request, + _: &(), + _: &DisplayHandle, + _: &mut DataInit<'_, Self>, + ) { + } +} + +impl Serve for Compositor { + fn request( + _: &mut Self, + _: &Client, + _: &wl_region::WlRegion, + request: wl_region::Request, + rects: &Rects, + _: &DisplayHandle, + _: &mut DataInit<'_, Self>, + ) { + if let wl_region::Request::Add { + x, + y, + width, + height, + } = request + { + rects.lock().expect("the rects").push([x, y, width, height]); + } + } +} + +impl GlobalDispatch for Compositor { + fn bind( + _: &mut Self, + _: &DisplayHandle, + _: &Client, + resource: New, + _: &(), + init: &mut DataInit<'_, Self>, + ) { + init.init(resource, ()) + .capabilities(manager::Capability::Blur); + } +} + +impl Serve for Compositor { + fn request( + compositor: &mut Self, + _: &Client, + _: &manager::ExtBackgroundEffectManagerV1, + request: manager::Request, + _: &(), + _: &DisplayHandle, + init: &mut DataInit<'_, Self>, + ) { + if let manager::Request::GetBackgroundEffect { id, surface } = request { + init.init(id, ()); + compositor.hear(format!("effect on surface {}", surface.id().protocol_id())); + } + } +} + +impl Serve for Compositor { + fn request( + compositor: &mut Self, + _: &Client, + _: &effect::ExtBackgroundEffectSurfaceV1, + request: effect::Request, + _: &(), + _: &DisplayHandle, + _: &mut DataInit<'_, Self>, + ) { + if let effect::Request::SetBlurRegion { region } = request { + let line = match region { + Some(region) => { + let rects = region.data::().expect("a region's rects"); + format!("blur {:?}", rects.lock().expect("the rects")) + } + None => "blur nothing".to_string(), + }; + compositor.hear(line); + } + } +} + +/// The test's own client, holding the display and a surface where gpui would. +struct Gpui; + +impl Dispatch for Gpui { + fn event( + _: &mut Self, + _: &WlRegistry, + _: ::Event, + _: &GlobalListContents, + _: &Connection, + _: &QueueHandle, + ) { + } +} + +delegate_noop!(Gpui: WlCompositor); +delegate_noop!(Gpui: ignore WlSurface); + +/// Runs a compositor on `socket` until `stop`, offering `ext_background_effect_v1` when `offered`. +fn serve( + socket: UnixStream, + offered: bool, + heard: Arc>>, + stop: Arc, +) -> thread::JoinHandle<()> { + thread::spawn(move || { + let mut display = Display::::new().expect("a display"); + let mut handle = display.handle(); + handle.create_global::(6, ()); + if offered { + handle.create_global::(1, ()); + } + handle + .insert_client(socket, Arc::new(Peer)) + .expect("the window connects"); + let mut compositor = Compositor { heard }; + while !stop.load(Ordering::Acquire) { + display.dispatch_clients(&mut compositor).expect("requests"); + display.flush_clients().expect("events"); + thread::sleep(Duration::from_millis(1)); + } + }) +} + +/// What a compositor heard once `ask` used an effect on a fresh surface, and that surface's id. +fn heard(offered: bool, ask: impl FnOnce(Option)) -> (Vec, u32) { + crate::testlog::init(); + let (window_end, compositor_end) = UnixStream::pair().expect("a socket pair"); + let notes = Arc::new(Mutex::new(Vec::new())); + let stop = Arc::new(AtomicBool::new(false)); + let server = serve(compositor_end, offered, notes.clone(), stop.clone()); + let connection = Connection::from_socket(window_end).expect("libwayland connects"); + let (globals, mut queue) = registry_queue_init::(&connection).expect("globals"); + let compositor: WlCompositor = + (globals.bind(&queue.handle(), 1..=6, ())).expect("a compositor"); + let surface = compositor.create_surface(&queue.handle(), ()); + let display = NonNull::new(connection.backend().display_ptr().cast()).expect("a display"); + let pointer = NonNull::new(surface.id().as_ptr().cast()).expect("a surface"); + // SAFETY: the test's live display, which outlives the effect, and its live surface. + ask(unsafe { Effect::new(display, pointer) }.expect("no Wayland failure")); + queue + .roundtrip(&mut Gpui) + .expect("the compositor heard all"); + stop.store(true, Ordering::Release); + server.join().expect("the compositor ends"); + let notes = notes.lock().expect("the notes").clone(); + (notes, surface.id().protocol_id()) +} + +#[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 all = format!("blur [[0, 0, {ALL}, {ALL}]]"); + let expected = [ + format!("effect on surface {surface}"), + all, + "blur nothing".into(), + ]; + assert_eq!(notes, expected); + assert!(crate::testlog::logged( + "blur: ext_background_effect_v1 on the window's surface; the compositor blurs: true, org_kde_kwin_blur too: false" + )); +} + +#[test] +fn without_the_protocol_nothing_is_asked() { + let (notes, _) = heard(false, |effect| assert!(effect.is_none())); + assert!(notes.is_empty(), "{notes:?}"); + assert!(crate::testlog::logged( + "blur: the compositor offers neither ext_background_effect_v1 nor org_kde_kwin_blur; a see-through window shows the desktop clear" + )); +} diff --git a/src/workspace.rs b/src/workspace.rs index ce85e80..7b1807f 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -9,12 +9,11 @@ use ely_gpui_component::{ use gpui::{ Action, App, AppContext, Context, Entity, EntityId, FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding, ParentElement, Render, SharedString, StatefulInteractiveElement, - StyleRefinement, Styled, Subscription, Window, WindowBackgroundAppearance, actions, div, - prelude::FluentBuilder, + StyleRefinement, Styled, Subscription, Window, actions, div, prelude::FluentBuilder, }; use crate::{ - blur, + blur::Blur, files::Files, git::panel::{Acted, GitPanel}, sessions::{self, Session}, @@ -86,8 +85,8 @@ pub struct Workspace { active: usize, files: Entity, git: Entity, - /// What the window shows of the desktop, as last set. - background: WindowBackgroundAppearance, + /// What the window shows of the desktop, and the blur behind it. + background: Blur, _settings: Subscription, _acted: Subscription, } @@ -113,9 +112,9 @@ impl Workspace { active: 0, files: cx.new(|_| Files::new(launch.cwd.clone())), git, - background: blur::appearance(SettingsStore::get(cx)), + background: Blur::new(window, SettingsStore::get(cx)), _settings: cx.observe_global_in::(window, |this, window, cx| { - blur::follow(window, &mut this.background, SettingsStore::get(cx)); + this.background.follow(window, SettingsStore::get(cx)); cx.notify(); }), _acted: acted,