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
This commit is contained in:
11 files changed
+339
-45
No files matched your search
+95
-6
@@ -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
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
));
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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)),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user