srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-06-22 22:34:00 +0200
committersrdusr <[email protected]>2024-06-22 22:34:00 +0200
commit0818b56dd9c70b1097c5ef24be0f62baaf9ec999 (patch)
tree4b0a739b9aaaa428e9d66eff9ae332c2f1f2d51b /crates
parentab0916391532031376817ed4ab6ab234428559a2 (diff)
downloadsrdwm-0818b56dd9c70b1097c5ef24be0f62baaf9ec999.tar.gz
srdwm-0818b56dd9c70b1097c5ef24be0f62baaf9ec999.zip
Bypass smithay's Space rendering for window/layer content: real per-window opacity
The user asked for per-window opacity (MISSING.md's `windowrule = opacity` gap) and pushed back on treating smithay's convenience wrappers as a hard ceiling: "don't rely on smithay, it won't have everything we need." Looked again at why opacity was ruled out earlier - `render_output`/ `space_render_elements` take one `alpha` for the whole frame's `self.space` content, no per-element control - and found a path that doesn't need nesting smithay's internal `SpaceRenderElements` type (the approach that hit an unresolvable generic-bounds wall investigating fullscreen-hiding earlier): call `render_elements_from_surface_tree` directly, once per window and once per layer-shell surface, each with its own alpha, wrapping the result in the *existing* `OverlayElement::Surface` variant. That primitive was already proven safe in this codebase (cursor.rs's client-image path, this file's own popup rendering) - reusing it here for a window's main content is the same call, not a new one. Both `udev.rs` and `winit.rs`'s render loops now build window content and layer-shell surfaces themselves (`elements.rs`: `surface_content_elements`, `output_layer_elements`, `window_wl_surface` for the Wayland/XWayland split), in the correct front-to-back order, then call `OutputDamageTracker::render_output` directly instead of the `space::render_output`/`space_render_elements` convenience wrappers. Content itself needs no occlusion clipping against `occluders` (unlike border/ titlebar bitmaps) - pushed in the same front-to-back order as everything else, ordinary painter's-algorithm draw order already occludes it correctly, the same property it had via `self.space`'s own order before. `self.space` stays mapped and `resync_stacking_order`-maintained exactly as before; only the render step stopped reading from it. A comment in winit.rs's render loop warned that a near-identical earlier attempt was reverted for a real ordering bug (whichever window was created first always painted in front, regardless of focus). That bug's actual root cause, identified and fixed since, was `Space::map_element` silently re-stacking on every geometry sync, independent of which render path was used - see `resync_stacking_order`. This rewrite never reads `Space`'s internal order for rendering at all (`ids` comes from `WindowManager.order` directly, the same source `hit_test` already trusts), so that specific bug class can't recur here regardless of whether `resync_stacking_order` ever drifts again. Bonus from the same infrastructure: the bar/dock now genuinely don't render at all (not just get covered) for a fullscreen window - `output_layer_elements` skips `Layer::Top`/`Overlay` entirely when any visible window is fullscreen, the hardening this work backed away from earlier for being too risky to build via the nested-SpaceRenderElements approach. `capture_offscreen` (winit.rs's screencopy path) picked up opacity-aware content and layer-shell inclusion too, though not full parity with the on-screen loop (still no border/shadow strips there - a pre-existing, separately-flagged gap). Opacity itself: `Window.opacity` (core), `WindowRuleActions.opacity` / `srd.rule(..., { opacity = 0.9 })`, `srd.window.set_opacity()`. Caught live, before commit: opacity was wired into `add_window`'s own rule match but not `reapply_rules_if_pending` - the *only* path a class-based rule actually takes effect through for a native Wayland client, since `add_window`'s own attempt always runs against a still-empty `app_id` (see the regression test next to the existing one covering the identical historical bug for `decorated`). Found by setting an isolated `SRDWM_CONFIG_PATH` test config with `srd.rule({ class = "Alacritty" }, { opacity = 0.4 })` against a nested instance and pixel-sampling a real screenshot: predicted blend (242,230,53) at 0.4 over (10,10,15) is (103,98,30); measured (105,100,33). Verified live in a nested session, screenshotting the *host* compositor (shows the real on-screen render, unlike grim against the nested socket, which - separately discovered this work - routes through `capture_offscreen`): stacking order correct with two overlapping windows (topmost fully occludes the one behind it, the exact scenario the reverted attempt got wrong), opacity blend matches prediction. Also confirmed, by testing the previous commit against the same scene, that upside-down content on this backend is a pre-existing bug unrelated to this change -- noted, not fixed here. cargo build --workspace (all 9 crates), cargo clippy --workspace (0 new warnings), cargo test --workspace (197 tests, 0 failed, includes 2 new regression tests).
Diffstat (limited to 'crates')
-rw-r--r--crates/config/src/lib.rs20
-rw-r--r--crates/core/src/manager.rs31
-rw-r--r--crates/core/src/rules.rs2
-rw-r--r--crates/core/src/window.rs8
-rw-r--r--crates/wayland/src/elements.rs68
-rw-r--r--crates/wayland/src/udev.rs117
-rw-r--r--crates/wayland/src/winit.rs164
7 files changed, 313 insertions, 97 deletions
diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs
index 12ba397..7749371 100644
--- a/crates/config/src/lib.rs
+++ b/crates/config/src/lib.rs
@@ -237,6 +237,7 @@ impl Engine {
window.set("set_decorations", self.fn_window_set_decorations()?)?;
window.set("set_border_color", self.fn_window_set_border_color()?)?;
window.set("set_border_width", self.fn_window_set_border_width()?)?;
+ window.set("set_opacity", self.fn_window_set_opacity()?)?;
window.set("set_floating", self.fn_window_set_floating()?)?;
window.set("toggle_floating", self.fn_window_action(WindowAction::ToggleFloating)?)?;
window.set("is_floating", self.fn_window_is_floating()?)?;
@@ -413,8 +414,8 @@ impl Engine {
/// class_regex = "...", instance = "..." }, { floating = true,
/// workspace = 2, x = .., y = .., width = .., height = ..,
/// decorated = false, border_color = {r,g,b}, border_width = 2,
- /// maximized = true })`. At least one matcher field is required;
- /// unmatched rules apply nothing.
+ /// maximized = true, opacity = 0.9 })`. At least one matcher field is
+ /// required; unmatched rules apply nothing.
///
/// `title`/`class` are plain substring/exact match, cheap and cover
/// most rules with no regex syntax to get right. `title_regex`/
@@ -468,6 +469,7 @@ impl Engine {
border_color,
border_width: actions.get("border_width")?,
pinned: actions.get("pinned")?,
+ opacity: actions.get("opacity")?,
},
};
state.borrow().wm.borrow_mut().add_rule(rule);
@@ -747,6 +749,20 @@ impl Engine {
})?)
}
+ fn fn_window_set_opacity(&self) -> Result<mlua::Function<'_>> {
+ let state = self.state.clone();
+ Ok(self.lua.create_function(move |_, opacity: f32| {
+ let wm = state.borrow().wm.clone();
+ let mut wm = wm.borrow_mut();
+ if let Some(id) = wm.focused_id() {
+ if let Some(w) = wm.window_mut(id) {
+ w.opacity = opacity.clamp(0.0, 1.0);
+ }
+ }
+ Ok(())
+ })?)
+ }
+
fn fn_window_set_floating(&self) -> Result<mlua::Function<'_>> {
let state = self.state.clone();
Ok(self.lua.create_function(move |_, floating: bool| {
diff --git a/crates/core/src/manager.rs b/crates/core/src/manager.rs
index 2c92ac1..d45eaf6 100644
--- a/crates/core/src/manager.rs
+++ b/crates/core/src/manager.rs
@@ -266,6 +266,9 @@ impl WindowManager {
if let Some(pinned) = a.pinned {
window.always_on_top = pinned;
}
+ if let Some(opacity) = a.opacity {
+ window.opacity = opacity.clamp(0.0, 1.0);
+ }
}
if let Some(monitor) = self.primary_monitor() {
@@ -338,6 +341,9 @@ impl WindowManager {
if let Some(pinned) = actions.pinned {
window.always_on_top = pinned;
}
+ if let Some(opacity) = actions.opacity {
+ window.opacity = opacity.clamp(0.0, 1.0);
+ }
if let Some(geometry) = actions.geometry {
window.geometry = geometry;
}
@@ -1807,6 +1813,31 @@ mod tests {
}
#[test]
+ fn opacity_rule_applies_on_the_deferred_retry_same_as_other_actions() {
+ // Regression test: `opacity` was added to `add_window`'s own rule
+ // application but missed here, in the deferred retry
+ // `reapply_rules_if_pending` - confirmed live: a rule like
+ // `srd.rule({ class = "Alacritty" }, { opacity = 0.4 })` never took
+ // effect for any real native Wayland client, since (per the test
+ // above) that's the *only* path a class-based rule actually
+ // matches through for one of those - `add_window`'s own match
+ // attempt always fails first, against an as-yet-empty `app_id`.
+ let mut wm = wm_with_monitor();
+ wm.add_rule(WindowRule {
+ matcher: crate::rules::WindowMatch { class: Some("alacritty".into()), ..Default::default() },
+ actions: crate::rules::WindowRuleActions { opacity: Some(0.4), ..Default::default() },
+ });
+ let id = wm.alloc_window_id();
+ wm.add_window(Window::new(id, ""));
+ assert_eq!(wm.window(id).unwrap().opacity, 1.0, "no app_id yet, so no match - must not have applied early");
+
+ let w = wm.window_mut(id).unwrap();
+ w.app_id = "Alacritty".into();
+ wm.reapply_rules_if_pending(id);
+ assert_eq!(wm.window(id).unwrap().opacity, 0.4, "app_id now known - the rule must apply on retry");
+ }
+
+ #[test]
fn fullscreen_from_maximized_still_restores_the_pre_maximize_size() {
// Both share `restore_geometry`; entering fullscreen from a
// maximised window must not overwrite it with the monitor rect, or
diff --git a/crates/core/src/rules.rs b/crates/core/src/rules.rs
index 148b627..e0e87cb 100644
--- a/crates/core/src/rules.rs
+++ b/crates/core/src/rules.rs
@@ -88,6 +88,8 @@ pub struct WindowRuleActions {
pub border_width: Option<u32>,
/// Always-on-top (Hyprland's `pin`).
pub pinned: Option<bool>,
+ /// Content opacity, `0.0`..=`1.0` (Hyprland's `windowrule = opacity`).
+ pub opacity: Option<f32>,
}
#[derive(Debug, Clone, Default)]
diff --git a/crates/core/src/window.rs b/crates/core/src/window.rs
index e30f501..774b0e2 100644
--- a/crates/core/src/window.rs
+++ b/crates/core/src/window.rs
@@ -90,6 +90,13 @@ pub struct Window {
pub always_on_top: bool,
pub border_color: (u8, u8, u8),
pub border_width: u32,
+ /// This window's own content opacity, `0.0`..=`1.0`. Only the content
+ /// (the client's own surface tree) is affected - srdwm's own
+ /// decoration (titlebar/border/shadow) always renders fully opaque
+ /// regardless, the same way a native macOS/Windows translucent-window
+ /// effect still keeps its frame legible. Set via `srd.window.
+ /// set_opacity()` or a rule's `opacity` action.
+ pub opacity: f32,
pub workspace: usize,
pub monitor: u32,
/// Whether `WindowManager`'s class/title-matched rules have already
@@ -135,6 +142,7 @@ impl Window {
always_on_top: false,
border_color: (136, 192, 208), // Nord accent, matches legacy theme default
border_width: 2,
+ opacity: 1.0,
workspace: 0,
monitor: 0,
rules_applied: false,
diff --git a/crates/wayland/src/elements.rs b/crates/wayland/src/elements.rs
index f9a569e..c9f1d6c 100644
--- a/crates/wayland/src/elements.rs
+++ b/crates/wayland/src/elements.rs
@@ -13,8 +13,10 @@ use smithay::backend::renderer::element::surface::{render_elements_from_surface_
use smithay::backend::renderer::element::Kind;
use smithay::backend::renderer::{Color32F, ImportAll, ImportMem, Renderer};
use smithay::desktop::{layer_map_for_output, PopupManager, Space, Window as DWindow};
+use smithay::output::Output;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Physical, Point, Rectangle, Scale};
+use smithay::wayland::shell::wlr_layer::Layer;
use srdwm_core::TITLEBAR_HEIGHT;
@@ -107,6 +109,72 @@ pub(crate) fn border_fragment_buffer(pool: &mut Vec<SolidColorBuffer>, index: us
&mut pool[index]
}
+/// A mapped window's own `WlSurface`, regardless of which protocol backs
+/// it - a native Wayland `xdg_toplevel` or an XWayland client. `None` for
+/// an X11 window between `MapRequest` and its first commit, which really
+/// has no surface yet.
+pub(crate) fn window_wl_surface(w: &DWindow) -> Option<WlSurface> {
+ if let Some(top) = w.toplevel() {
+ return Some(top.wl_surface().clone());
+ }
+ w.x11_surface().and_then(|x| x.wl_surface())
+}
+
+/// Renders one window's (or one layer-shell surface's) own content --
+/// nothing else, not decoration, not popups - at `location` (output-local
+/// physical space, the same convention every other `custom_elements` entry
+/// already uses) and `alpha`.
+///
+/// Deliberately not `Window::render_elements` (`AsRenderElements`): that
+/// bundles a window's own popups into the same call, which would double
+/// them with [`popup_render_elements`]'s own separate pass over
+/// [`popup_targets`]. `render_elements_from_surface_tree` walks only the
+/// given surface's own (sub)surface tree - the same low-level call this
+/// file's popup rendering and `cursor.rs`'s client-cursor-image path
+/// already use safely, so reusing it here for a window's *main* content is
+/// the same proven primitive, not a new one.
+///
+/// This is what lets content have its own `alpha`, at all: content used to
+/// only ever render via `self.space` passed whole to `render_output`,
+/// which takes one `alpha` for the entire frame's worth of space content,
+/// not one per window - there was no way to make one window translucent
+/// without also dimming everything else in `self.space`.
+pub(crate) fn surface_content_elements<R>(renderer: &mut R, surface: &WlSurface, location: (i32, i32), alpha: f32) -> Vec<OverlayElement<R>>
+where
+ R: Renderer + ImportAll + ImportMem,
+ R::TextureId: Clone + Send + 'static,
+{
+ render_elements_from_surface_tree(renderer, surface, location, 1.0, alpha, Kind::Unspecified)
+}
+
+/// Every mapped layer-shell surface on `output` whose [`Layer`] `include`
+/// accepts, each rendered via [`surface_content_elements`] at full opacity
+/// - layer-shell surfaces (bars, docks, wallpaper engines) don't have a
+/// per-surface opacity concept the way `srd.rule`'s `opacity` gives
+/// windows.
+///
+/// Order matches smithay's own `space_render_elements` (0.7.0): `.rev()`
+/// on `map.layers()` before rendering, so surfaces sharing one `Layer`
+/// keep the same relative stacking smithay's convenience wrapper gave
+/// them, now that this function replaces it.
+pub(crate) fn output_layer_elements<R>(renderer: &mut R, output: &Output, origin: (i32, i32), include: impl Fn(Layer) -> bool) -> Vec<OverlayElement<R>>
+where
+ R: Renderer + ImportAll + ImportMem,
+ R::TextureId: Clone + Send + 'static,
+{
+ let map = layer_map_for_output(output);
+ let mut elements = Vec::new();
+ for layer in map.layers().rev() {
+ if !include(layer.layer()) {
+ continue;
+ }
+ let Some(geo) = map.layer_geometry(layer) else { continue };
+ let location = (origin.0 + geo.loc.x, origin.1 + geo.loc.y);
+ elements.extend(surface_content_elements(renderer, layer.wl_surface(), location, 1.0));
+ }
+ elements
+}
+
/// A mapped toplevel's surface and on-screen (global-space, band-adjusted)
/// position - everything [`popup_render_elements`] needs to find and place
/// that window's popups, pre-extracted from `CompState` by
diff --git a/crates/wayland/src/udev.rs b/crates/wayland/src/udev.rs
index 7e7a69f..75abcba 100644
--- a/crates/wayland/src/udev.rs
+++ b/crates/wayland/src/udev.rs
@@ -44,9 +44,9 @@ use smithay::backend::renderer::pixman::PixmanRenderer;
use smithay::backend::renderer::{Bind, ImportDma};
use smithay::backend::session::{libseat::LibSeatSession, libseat::LibSeatSessionNotifier, Event as SessionEvent, Session};
use smithay::backend::udev::{self, UdevBackend, UdevEvent};
-use smithay::desktop::space::render_output;
use smithay::desktop::{layer_map_for_output, PopupManager, Space};
use smithay::backend::input::AxisSource;
+use smithay::wayland::shell::wlr_layer::Layer;
use smithay::input::pointer::AxisFrame;
use smithay::input::SeatState;
use smithay::output::{Mode as OutputMode, Output, PhysicalProperties, Subpixel};
@@ -277,22 +277,54 @@ impl CompState {
Err(e) => log::warn!("udev: failed to import context menu buffer: {e}"),
}
}
- // Content still comes from `render_output`'s own `spaces`
- // argument (`self.space`, passed below), not from a
- // per-window `render_elements` call here - see the long
- // comment on the matching loop in `winit.rs`, and `state.rs`'s
- // `resync_stacking_order` doc comment, for the real
- // stacking-order bug this was chasing and its actual fix.
+ // Popups next: always above every window's own content,
+ // matching this codebase's long-standing behavior from
+ // before content moved into this same `custom_elements`
+ // list (see below) - pushing them here, ahead of every
+ // window and every layer-shell surface, is what keeps that
+ // true now that "above everything in `self.space`" is no
+ // longer a free property of a separate tier.
+ custom_elements.extend(crate::elements::popup_render_elements(&popup_targets, &mut udev.renderer, (origin.x, origin.y)));
+
+ // The bar/dock/launcher (`Layer::Top`/`Overlay`): rendered
+ // ourselves via `output_layer_elements`, not through
+ // `render_output`'s automatic inclusion of `self.space` +
+ // `layer_map_for_output` - see this function's own call
+ // site further down for why content had to stop flowing
+ // through that convenience wrapper at all (per-window
+ // opacity), which took layer-shell inclusion down with it as
+ // a side effect. Skipped entirely - not just covered - for
+ // a fullscreen window: `we should not see the bar at all`,
+ // and unmapping it (`gtk_shell`) or covering it are two
+ // different guarantees. `ids` is already front-to-back, so
+ // checking every id for `fullscreen` here (rather than just
+ // the frontmost) covers a fullscreen window stacked behind
+ // an always-on-top one too.
+ let hide_top_layers = ids.iter().any(|&id| self.wm.borrow().window(id).is_some_and(|w| w.fullscreen));
+ if !hide_top_layers {
+ custom_elements.extend(crate::elements::output_layer_elements(
+ &mut udev.renderer,
+ &output,
+ (origin.x, origin.y),
+ |layer| matches!(layer, Layer::Top | Layer::Overlay),
+ ));
+ }
+
// Windows stacked in front of whichever one border/
// decoration is being built right now - `ids` is already
// front-to-back, so this only ever needs appending to, not
- // recomputing. Only a window's own *content* occludes
- // correctly on its own path (via `space`, real stacking
- // order respected); everything drawn here goes through
- // `custom_elements`, which composites above *all* content
- // unconditionally, so both the border strips and the
- // titlebar bitmap below need this explicit occlusion test
- // against it.
+ // recomputing. A window's own *content*, pushed inside this
+ // same loop below, needs no separate occlusion test: it
+ // draws in the same front-to-back push order as everything
+ // else here, so ordinary painter's-algorithm draw order
+ // already occludes it correctly (this is exactly why content
+ // used to occlude correctly via `self.space`'s own order,
+ // before it had to move into this list for per-window
+ // opacity to be possible at all). The border strips and
+ // titlebar bitmap are different: outside `geometry`, drawn
+ // via a bitmap that isn't itself window-shaped, so they
+ // still need `occluders`' explicit clip against whichever
+ // window is stacked in front.
let mut occluders: Vec<srdwm_core::Rect> = Vec::with_capacity(ids.len());
for &id in &ids {
let Some(w) = self.wm.borrow().window(id).cloned() else { continue };
@@ -413,9 +445,33 @@ impl CompState {
}
}
}
+ // The window's own content, at its own `opacity` --
+ // this, not decoration, is the entire reason content
+ // moved into this loop at all (see the doc comment on
+ // the popup push above). Positioned the same way
+ // `sync_geometry` maps it into `self.space` (band added
+ // for a decorated window's titlebar reservation), so
+ // switching rendering paths doesn't also shift content
+ // relative to where clicks still land (hit-testing is
+ // untouched, still `w.geometry`/`self.space`-based).
+ if let Some(dwindow) = self.id_to_window.get(&id) {
+ if let Some(surface) = crate::elements::window_wl_surface(dwindow) {
+ let band = if w.decorated { srdwm_core::TITLEBAR_HEIGHT as i32 } else { 0 };
+ let pos = (geom.x - origin.x, geom.y + band - origin.y);
+ custom_elements.extend(crate::elements::surface_content_elements(&mut udev.renderer, &surface, pos, w.opacity));
+ }
+ }
occluders.push(geom);
}
- custom_elements.extend(crate::elements::popup_render_elements(&popup_targets, &mut udev.renderer, (origin.x, origin.y)));
+ // Background/bottom layer-shell (wallpaper engines) last --
+ // bottommost, matching smithay's own `space_render_elements`
+ // ordering, which this whole custom loop now replaces.
+ custom_elements.extend(crate::elements::output_layer_elements(
+ &mut udev.renderer,
+ &output,
+ (origin.x, origin.y),
+ |layer| matches!(layer, Layer::Background | Layer::Bottom),
+ ));
}
let lock_elements = if locked {
crate::lock::lock_render_elements(lock_surface.as_ref(), &mut udev.renderer)
@@ -440,18 +496,25 @@ impl CompState {
.map(|r| (r.damage.is_some(), Vec::new()))
.map_err(|e| e.to_string())
} else {
- render_output(
- &head.output,
- &mut udev.renderer,
- &mut framebuffer,
- 1.0,
- head.ages[back],
- [&self.space],
- &custom_elements,
- &mut head.damage_tracker,
- [0.05, 0.05, 0.08, 1.0],
- )
- .map(|r| (r.damage.is_some(), r.damage.cloned().unwrap_or_default()))
+ // Not `smithay::desktop::space::render_output`: that
+ // convenience wrapper draws `self.space`'s window content at
+ // one `alpha` for the whole frame and pulls every
+ // layer-shell surface in unconditionally, neither of which
+ // leaves room for per-window opacity or hiding the bar/dock
+ // during fullscreen. `custom_elements` above already carries
+ // everything that wrapper would have built - window
+ // content (`surface_content_elements`, one call per window,
+ // each with its own `w.opacity`) and layer-shell surfaces
+ // (`output_layer_elements`, split Top/Overlay above content
+ // and Background/Bottom below it) - assembled by hand in
+ // the correct front-to-back order instead. `self.space`
+ // itself is untouched and still authoritative for
+ // hit-testing/stacking bookkeeping (`sync_geometry`'s
+ // `map_element` calls); only the *render* path stopped
+ // reading from it.
+ head.damage_tracker
+ .render_output(&mut udev.renderer, &mut framebuffer, head.ages[back], &custom_elements, [0.05, 0.05, 0.08, 1.0])
+ .map(|r| (r.damage.is_some(), r.damage.cloned().unwrap_or_default()))
// Both arms reduce to "was there damage" plus the damage
// rects themselves; the two error types differ, so they are
// flattened to a message here.
diff --git a/crates/wayland/src/winit.rs b/crates/wayland/src/winit.rs
index 11c2d1e..2c55dda 100644
--- a/crates/wayland/src/winit.rs
+++ b/crates/wayland/src/winit.rs
@@ -25,8 +25,8 @@ use smithay::backend::renderer::ImportDma;
use smithay::backend::winit::{self, WinitEvent, WinitEventLoop, WinitGraphicsBackend};
use smithay::reexports::winit::dpi::LogicalSize as WinitLogicalSize;
use smithay::reexports::winit::window::Window as WinitWindow;
-use smithay::desktop::space::render_output;
use smithay::desktop::{layer_map_for_output, PopupManager, Space};
+use smithay::wayland::shell::wlr_layer::Layer;
use smithay::input::SeatState;
use smithay::output::{Mode as OutputMode, Output, PhysicalProperties, Subpixel};
use smithay::reexports::calloop::EventLoop as CalloopEventLoop;
@@ -349,46 +349,54 @@ impl WaylandPlatform {
Err(e) => log::warn!("failed to import context menu buffer: {e}"),
}
}
- // Decoration/border built per window, front-to-back (topmost
- // first), so a window's own titlebar/border at least stay ordered
- // consistently relative to *other* windows' decoration/border.
+ // Content now renders here too, one window at a time, not through
+ // `render_output`'s own `spaces` argument - see this function's
+ // own call to `damage_tracker.render_output` further down for why,
+ // and `elements.rs`'s `surface_content_elements` doc comment for
+ // what it's actually for (per-window opacity, impossible through
+ // `spaces`, which takes one `alpha` for the whole frame).
//
- // Content deliberately still goes through `render_output`'s own
- // `spaces` argument below (`self.state.space`), not through this
- // loop: an earlier version of this fix pushed each window's own
- // `Window::render_elements` output into `custom_elements` here too,
- // in per-window stacking order, specifically to let decoration/
- // border interleave correctly with *other* windows' content
- // (custom_elements otherwise draws entirely above every window's
- // content, `spaces` or not - seeing `render_output`'s source is
- // what motivated that attempt). It was reverted: with two or more
- // native Wayland toplevels on screen, whichever was created
- // *first* always painted in front of later ones regardless of
- // real focus/stacking order, reproduced consistently across three
- // separate test windows, independent of push order, forced full
- // redraws (`age = 0`), and buffer age - i.e. a real ordering bug
- // in mixing multiple windows' own `render_elements` output this
- // way, not a damage-tracking artifact. The real root cause (found
- // later, by instrumenting a locally vendored smithay copy
- // directly) turned out to be unrelated to content-vs-decoration
- // mixing at all: `sync_geometry`'s `Space::map_element` call
- // silently re-stacked windows to the top of `Space`'s own order
- // any time position/size synced for *any* reason, independent of
- // which rendering path was used - see `state.rs`'s
+ // An earlier version of this exact change was reverted: with two or
+ // more native Wayland toplevels on screen, whichever was created
+ // *first* always painted in front of later ones regardless of real
+ // focus/stacking order. That bug's real root cause (found by
+ // instrumenting a locally vendored smithay copy directly) turned
+ // out to be `sync_geometry`'s `Space::map_element` call silently
+ // re-stacking windows to the top of `Space`'s *own* internal
+ // order as a side effect of updating position - see `state.rs`'s
// `resync_stacking_order` doc comment for the full story and the
- // actual fix. `self.state.space` stayed the content path here
- // since reverting it was never itself wrong, just insufficient on
- // its own.
+ // fix that landed for it (called after every `map_element` since).
+ // This loop never reads `Space`'s order at all: `ids` below comes
+ // from `WindowManager.order` (`visible_windows_front_to_back`),
+ // srdwm's own stacking model, the same source `hit_test` already
+ // trusts - so the specific bug that sank the earlier attempt
+ // can't recur here regardless of whether `resync_stacking_order`
+ // ever drifts again. `self.state.space` stays mapped and
+ // `resync_stacking_order`-maintained exactly as before; only the
+ // render step stopped reading from it.
let ids: Vec<WindowId> = self.wm.borrow().visible_windows_front_to_back().map(|w| w.id).collect();
let focused = self.wm.borrow().focused_id();
+ // Popups next: always above every window's own content - see the
+ // matching comment in `udev.rs`'s render loop for why this has to
+ // be pushed ahead of both the bar/dock and every window now that
+ // content shares this same list.
+ let popup_targets = crate::elements::popup_targets(&self.state);
+ custom_elements.extend(crate::elements::popup_render_elements(&popup_targets, renderer, (0, 0)));
+ // The bar/dock/launcher, skipped entirely for a fullscreen window --
+ // see `udev.rs`'s matching push for the full reasoning.
+ let hide_top_layers = self.wm.borrow().visible_windows_front_to_back().any(|w| w.fullscreen);
+ if !hide_top_layers {
+ custom_elements.extend(crate::elements::output_layer_elements(renderer, &self.output, (0, 0), |layer| matches!(layer, Layer::Top | Layer::Overlay)));
+ }
// Windows stacked in front of whichever one border/decoration is
// being built right now - `ids` is already front-to-back, so this
- // only ever needs appending to, not recomputing. Only a window's
- // own *content* occludes correctly on its own path (via `space`,
- // real stacking order respected); everything drawn here goes
- // through `custom_elements`, which composites above *all* content
- // unconditionally, so both the border strips and the titlebar
- // bitmap below need this explicit occlusion test against it.
+ // only ever needs appending to, not recomputing. A window's own
+ // *content*, pushed inside this same loop below, needs no separate
+ // occlusion test - see the matching comment in `udev.rs`'s render
+ // loop for why ordinary front-to-back push order already occludes
+ // it correctly. The border strips and titlebar bitmap are
+ // different: outside `geometry`, so they still need `occluders`'
+ // explicit clip against whichever window is stacked in front.
let mut occluders: Vec<srdwm_core::Rect> = Vec::with_capacity(ids.len());
for id in ids {
let Some(w) = self.wm.borrow().window(id).cloned() else { continue };
@@ -468,25 +476,32 @@ impl WaylandPlatform {
}
}
}
+ // The window's own content, at its own `opacity` - see the
+ // matching push in `udev.rs`'s render loop for why. Single
+ // output at the global origin, so no offset to subtract (see
+ // `elements.rs`'s doc comment on why `udev.rs`'s per-head call
+ // does).
+ if let Some(dwindow) = self.state.id_to_window.get(&id) {
+ if let Some(surface) = crate::elements::window_wl_surface(dwindow) {
+ let band = if w.decorated { srdwm_core::TITLEBAR_HEIGHT as i32 } else { 0 };
+ custom_elements.extend(crate::elements::surface_content_elements(renderer, &surface, (geom.x, geom.y + band), w.opacity));
+ }
+ }
occluders.push(geom);
}
- // Single output at the global origin - no offset to subtract, see
- // `elements.rs`'s doc comment on why udev.rs's per-head call does.
- let popup_targets = crate::elements::popup_targets(&self.state);
- custom_elements.extend(crate::elements::popup_render_elements(&popup_targets, renderer, (0, 0)));
-
- let result = render_output(
- &self.output,
- renderer,
- &mut framebuffer,
- 1.0,
- age,
- [&self.state.space],
- &custom_elements,
- &mut self.damage_tracker,
- [0.05, 0.05, 0.08, 1.0],
- )
- .map_err(err)?;
+ // Background/bottom layer-shell (wallpaper engines) last --
+ // bottommost, matching smithay's own `space_render_elements`
+ // ordering, which this whole custom loop now replaces.
+ custom_elements.extend(crate::elements::output_layer_elements(renderer, &self.output, (0, 0), |layer| matches!(layer, Layer::Background | Layer::Bottom)));
+
+ // Not `smithay::desktop::space::render_output`: see `udev.rs`'s
+ // matching call site for why (per-window opacity, fullscreen-aware
+ // layer-shell inclusion - `custom_elements` above already carries
+ // everything that wrapper would have built).
+ let result = self
+ .damage_tracker
+ .render_output(renderer, &mut framebuffer, age, &custom_elements, [0.05, 0.05, 0.08, 1.0])
+ .map_err(err)?;
let damage_rects: Vec<Rectangle<i32, Physical>> = result.damage.cloned().unwrap_or_default();
let has_damage = !damage_rects.is_empty();
drop(framebuffer);
@@ -588,29 +603,42 @@ impl WaylandPlatform {
renderer.create_buffer(Fourcc::Abgr8888, (size.w, size.h).into()).map_err(err)?;
let mut framebuffer = renderer.bind(&mut target).map_err(err)?;
+ // Not full parity with the on-screen render loop above (no border/
+ // shadow strips here, same as before this function's content/opacity
+ // fix) - a real, pre-existing gap in what a screenshot shows on
+ // this backend, flagged rather than grown further in this pass.
+ // Content (with each window's own `opacity`, unlike the
+ // `self.state.space`-based single-alpha call this replaced) and the
+ // bar/dock now render into the capture, at least: a screenshot used
+ // to only ever show titlebars for windows that had one, and never
+ // any layer-shell surface at all.
+ let hide_top_layers = self.wm.borrow().visible_windows_front_to_back().any(|w| w.fullscreen);
let mut custom_elements: Vec<crate::elements::OverlayElement<GlesRenderer>> = Vec::new();
- for (&id, deco) in self.state.decorations.iter() {
- let Some(geom) = self.wm.borrow().visible_windows().find(|w| w.id == id).map(|w| w.geometry) else { continue };
- if let Ok(elem) = MemoryRenderBufferRenderElement::from_buffer(renderer, (geom.x as f64, geom.y as f64), deco, None, None, None, Kind::Unspecified) {
- custom_elements.push(crate::elements::OverlayElement::Memory(elem));
+ if !hide_top_layers {
+ custom_elements.extend(crate::elements::output_layer_elements(renderer, &self.output, (0, 0), |layer| matches!(layer, Layer::Top | Layer::Overlay)));
+ }
+ for id in self.wm.borrow().visible_windows_front_to_back().map(|w| w.id).collect::<Vec<_>>() {
+ let Some(w) = self.wm.borrow().window(id).cloned() else { continue };
+ if let Some(deco) = self.state.decorations.get(&id) {
+ if let Ok(elem) = MemoryRenderBufferRenderElement::from_buffer(renderer, (w.geometry.x as f64, w.geometry.y as f64), deco, None, None, None, Kind::Unspecified) {
+ custom_elements.push(crate::elements::OverlayElement::Memory(elem));
+ }
+ }
+ if let Some(dwindow) = self.state.id_to_window.get(&id) {
+ if let Some(surface) = crate::elements::window_wl_surface(dwindow) {
+ let band = if w.decorated { srdwm_core::TITLEBAR_HEIGHT as i32 } else { 0 };
+ custom_elements.extend(crate::elements::surface_content_elements(renderer, &surface, (w.geometry.x, w.geometry.y + band), w.opacity));
+ }
}
}
+ custom_elements.extend(crate::elements::output_layer_elements(renderer, &self.output, (0, 0), |layer| matches!(layer, Layer::Background | Layer::Bottom)));
// A throwaway damage tracker, so this pass always draws the whole
// scene (age 0) and never perturbs the on-screen tracker's history.
let mut tracker = OutputDamageTracker::from_output(&self.output);
- render_output(
- &self.output,
- renderer,
- &mut framebuffer,
- 1.0,
- 0,
- [&self.state.space],
- &custom_elements,
- &mut tracker,
- [0.05, 0.05, 0.08, 1.0],
- )
- .map_err(err)?;
+ tracker
+ .render_output(renderer, &mut framebuffer, 0, &custom_elements, [0.05, 0.05, 0.08, 1.0])
+ .map_err(err)?;
screencopy::service_pending(captures, renderer, &framebuffer);
Ok(())