srdusr
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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
-rw-r--r--docs/DEFAULTS.md3
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 })