diff options
| -rw-r--r-- | crates/config/src/lib.rs | 20 | ||||
| -rw-r--r-- | crates/core/src/manager.rs | 31 | ||||
| -rw-r--r-- | crates/core/src/rules.rs | 2 | ||||
| -rw-r--r-- | crates/core/src/window.rs | 8 | ||||
| -rw-r--r-- | crates/wayland/src/elements.rs | 68 | ||||
| -rw-r--r-- | crates/wayland/src/udev.rs | 117 | ||||
| -rw-r--r-- | crates/wayland/src/winit.rs | 164 | ||||
| -rw-r--r-- | docs/DEFAULTS.md | 3 |
8 files changed, 316 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(()) diff --git a/docs/DEFAULTS.md b/docs/DEFAULTS.md index 7d13b95..5c93c4e 100644 --- a/docs/DEFAULTS.md +++ b/docs/DEFAULTS.md @@ -214,6 +214,9 @@ srd.rule(matcher, actions) given together to take effect - `decorated` (bool) - `border_color` (`{r, g, b}`), `border_width` (number) +- `pinned` (bool) - always-on-top +- `opacity` (number, `0.0`..=`1.0`) - content opacity; srdwm's own + titlebar/border/shadow always stay fully opaque regardless ```lua srd.rule({ class = "pavucontrol" }, { floating = true }) |