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
This commit is contained in:
8 files changed
+529
-13
No files matched your search
+34
-5
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user