T20: on Wayland the window asks for ext_background_effect_v1 on its surface, through a guest connection on gpui's display; one region wider than any window, which the compositor clips, comes and goes with Blurred; the log names the way blur goes; a wayland-server compositor tests it, and scripts/linux.sh runs the tests on Linux in a container
CI / platforms (windows-latest) (push) Failing after 9s
CI / macos (push) Failing after 8m24s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-15 00:42:30 -04:00
1 parent 6523632ade
commit 107069b3c8
8 files changed
+529 -13

No files matched your search

+34 -5
View File
@@ -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<wayland::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,
#[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);
}
}
}
+171
View File
@@ -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<Connection>,
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<Self> {
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<c_void>,
surface: NonNull<c_void>,
) -> anyhow::Result<Option<Self>> {
// 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::<Offers>(&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<WlRegistry, GlobalListContents> for Offers {
fn event(
_: &mut Self,
_: &WlRegistry,
_: <WlRegistry as Proxy>::Event,
_: &GlobalListContents,
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<ExtBackgroundEffectManagerV1, ()> for Offers {
fn event(
offers: &mut Self,
_: &ExtBackgroundEffectManagerV1,
event: ext_background_effect_manager_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
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;
+259
View File
@@ -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<Vec<[i32; 4]>>;
/// A compositor that writes down what the window asks of it.
struct Compositor {
heard: Arc<Mutex<Vec<String>>>,
}
impl Compositor {
fn hear(&self, line: String) {
self.heard.lock().expect("the notes").push(line);
}
}
struct Peer;
impl ClientData for Peer {}
impl GlobalDispatch<wl_compositor::WlCompositor, ()> for Compositor {
fn bind(
_: &mut Self,
_: &DisplayHandle,
_: &Client,
resource: New<wl_compositor::WlCompositor>,
_: &(),
init: &mut DataInit<'_, Self>,
) {
init.init(resource, ());
}
}
impl Serve<wl_compositor::WlCompositor, ()> 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<wl_surface::WlSurface, ()> for Compositor {
fn request(
_: &mut Self,
_: &Client,
_: &wl_surface::WlSurface,
_: wl_surface::Request,
_: &(),
_: &DisplayHandle,
_: &mut DataInit<'_, Self>,
) {
}
}
impl Serve<wl_region::WlRegion, Rects> 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<manager::ExtBackgroundEffectManagerV1, ()> for Compositor {
fn bind(
_: &mut Self,
_: &DisplayHandle,
_: &Client,
resource: New<manager::ExtBackgroundEffectManagerV1>,
_: &(),
init: &mut DataInit<'_, Self>,
) {
init.init(resource, ())
.capabilities(manager::Capability::Blur);
}
}
impl Serve<manager::ExtBackgroundEffectManagerV1, ()> 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<effect::ExtBackgroundEffectSurfaceV1, ()> 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::<Rects>().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<WlRegistry, GlobalListContents> for Gpui {
fn event(
_: &mut Self,
_: &WlRegistry,
_: <WlRegistry as Proxy>::Event,
_: &GlobalListContents,
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
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<Mutex<Vec<String>>>,
stop: Arc<AtomicBool>,
) -> thread::JoinHandle<()> {
thread::spawn(move || {
let mut display = Display::<Compositor>::new().expect("a display");
let mut handle = display.handle();
handle.create_global::<Compositor, wl_compositor::WlCompositor, ()>(6, ());
if offered {
handle.create_global::<Compositor, manager::ExtBackgroundEffectManagerV1, ()>(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<Effect>)) -> (Vec<String>, 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::<Gpui>(&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"
));
}
+6 -7
View File
@@ -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<Files>,
git: Entity<GitPanel>,
/// 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::<SettingsStore>(window, |this, window, cx| {
blur::follow(window, &mut this.background, SettingsStore::get(cx));
this.background.follow(window, SettingsStore::get(cx));
cx.notify();
}),
_acted: acted,