srdusr
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-06-24 01:47:00 +0200
committersrdusr <[email protected]>2024-06-24 01:47:00 +0200
commit2cc10b0704d7352198374597075ef184253026be (patch)
treecc295c04270a8daa535e8edb31e98daa14943759
parent0818b56dd9c70b1097c5ef24be0f62baaf9ec999 (diff)
downloadsrdwm-2cc10b0704d7352198374597075ef184253026be.tar.gz
srdwm-2cc10b0704d7352198374597075ef184253026be.zip
Real rounded corners on client content (GLES/winit backend), via a custom shader
decoration.rs's round_top_corners only ever clipped the compositor's own titlebar/border bitmap - its own doc comment already said why nothing more had been done: clipping arbitrary client content needs a real per-pixel mask, "a much bigger change than this cosmetic pass". That's this change, for the one backend that can do it cheaply: the udev backend's PixmanRenderer is software-only with no shader stage at all, but GlesRenderer (winit) has a real custom-shader path (`compile_custom_texture_shader`, `TextureShaderElement`) that a first look at smithay's higher-level convenience APIs missed entirely. crates/wayland/src/rounded_corners.rs: a GLSL fragment shader masking a window's texture against a rounded-rect signed-distance field while sampling it (the same technique cosmic-comp/niri use for GPU-side rounded corners) - built by hand from the surface's own committed texture/view/ damage state (`RendererSurfaceState`'s public accessors), since no smithay convenience wrapper builds a masked element at all (`CropRenderElement` only crops to a rectangle). A decorated window rounds only its bottom two corners - the top two are already rounded, on the titlebar's own CPU bitmap, by decoration.rs, at the exact same `CORNER_RADIUS` (now `pub(crate)`, shared between the two so the curve reads as one continuous radius, not two different ones meeting at a seam) - an undecorated/CSD window rounds all four, since its content is the window's whole visible extent. Falls back to plain unrounded content on any failure (shader didn't compile, no committed buffer yet, a single-pixel-buffer surface), same "always show something over a prettier maybe-nothing" reasoning cursor.rs's built-in-arrow fallback already uses. Deliberately scoped to a window's *main* surface only, not subsurfaces - documented as a real, if narrow, follow-up rather than attempted here. `TextureShaderElement` only implements `RenderElement<GlesRenderer>`, not the generic `RenderElement<R>` every `OverlayElement<R>` variant needs, so it can't be added to that shared enum without breaking `OverlayElement< PixmanRenderer>` (used identically by udev.rs) the moment a GLES-only variant showed up in it. `WinitElement<=GlesRenderer>` (new, winit.rs-only) wraps the existing enum as one variant instead of touching it - this is the same lesson as the fullscreen-hiding investigation earlier this session, just resolved cleanly this time: nesting a *foreign* generic type inside your own hits real bound-resolution walls; wrapping your own already-working type inside a new concrete-renderer enum doesn't, because smithay's own `render_elements!` macro documents exactly this `<=ConcreteRenderer>` form. Config: `general.rounded_corners` (default `true`), `srd.window` unaffected - this is a `general.*` compositor-behavior knob, not a per-window rule action like `opacity`. Verified live: shader compiles without error on this machine's real Mesa/ llvmpipe GL driver, and a decorated wezterm window's bottom-left and bottom-right corners both show a real, smoothly anti-aliased curve on the actual client-rendered pixels (not a compositor bitmap) in a host-session screenshot - qualitatively sharper than `round_top_corners`' deliberate hard cutoff, since a GPU shader can afford a ~2px smoothstep a CPU bitmap pass isn't worth adding for. cargo build --workspace (all 9 crates), cargo clippy --workspace (0 new warnings), cargo test --workspace (197 tests, 0 failed).
-rw-r--r--crates/config/src/lib.rs1
-rw-r--r--crates/core/src/manager.rs7
-rw-r--r--crates/srdwm/src/main.rs2
-rw-r--r--crates/wayland/src/decoration.rs9
-rw-r--r--crates/wayland/src/lib.rs1
-rw-r--r--crates/wayland/src/rounded_corners.rs215
-rw-r--r--crates/wayland/src/state.rs11
-rw-r--r--crates/wayland/src/udev.rs1
-rw-r--r--crates/wayland/src/winit.rs70
-rw-r--r--docs/DEFAULTS.md1
10 files changed, 306 insertions, 12 deletions
diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs
index 7749371..43577f8 100644
--- a/crates/config/src/lib.rs
+++ b/crates/config/src/lib.rs
@@ -1008,6 +1008,7 @@ fn default_config() -> HashMap<String, ConfigValue> {
set("general.animation_duration", Number(200.0));
set("general.shadows", Bool(true));
set("general.resize_margin", Number(6.0));
+ set("general.rounded_corners", Bool(true));
set("general.focus_follows_mouse", Bool(false));
set("general.mouse_follows_focus", Bool(true));
set("general.auto_raise", Bool(false));
diff --git a/crates/core/src/manager.rs b/crates/core/src/manager.rs
index d45eaf6..08284c5 100644
--- a/crates/core/src/manager.rs
+++ b/crates/core/src/manager.rs
@@ -70,6 +70,12 @@ pub struct WindowManager {
/// read from `general.resize_margin`. See [`crate::window::RESIZE_MARGIN`]'s
/// doc comment for the default and why it's what it is.
pub resize_margin: i32,
+ /// Whether a decorated window's content rounds its bottom two corners
+ /// to match the titlebar's own curve (an undecorated/CSD window rounds
+ /// all four). Read from `general.rounded_corners`. GLES-backend-only
+ /// in practice - see `crates/wayland/src/rounded_corners.rs` - so
+ /// this is a no-op on the udev/Pixman backend regardless of its value.
+ pub rounded_corners_enabled: bool,
/// Default decoration colours and border width, read from `theme.colors.*`/
/// `theme.decorations.*`. See `ThemeConfig`'s own doc comment.
pub theme: ThemeConfig,
@@ -114,6 +120,7 @@ impl WindowManager {
animation_duration_ms: 200,
shadows_enabled: true,
resize_margin: RESIZE_MARGIN,
+ rounded_corners_enabled: true,
theme: ThemeConfig::default(),
drag: None,
resize: None,
diff --git a/crates/srdwm/src/main.rs b/crates/srdwm/src/main.rs
index d334556..36da572 100644
--- a/crates/srdwm/src/main.rs
+++ b/crates/srdwm/src/main.rs
@@ -151,6 +151,7 @@ fn apply_general_settings(engine: &Engine, wm: &Rc<RefCell<WindowManager>>) {
let duration = engine.get_f64("general.animation_duration", 200.0).max(0.0) as u32;
let shadows = engine.get_bool("general.shadows", true);
let resize_margin = engine.get_f64("general.resize_margin", srdwm_core::RESIZE_MARGIN as f64).max(1.0) as i32;
+ let rounded_corners = engine.get_bool("general.rounded_corners", true);
// Only the three `theme.*` keys with an unambiguous, already-rendered
// counterpart are wired - see `srdwm_core::ThemeConfig`'s doc comment.
@@ -180,6 +181,7 @@ fn apply_general_settings(engine: &Engine, wm: &Rc<RefCell<WindowManager>>) {
wm.animation_duration_ms = duration;
wm.shadows_enabled = shadows;
wm.resize_margin = resize_margin;
+ wm.rounded_corners_enabled = rounded_corners;
wm.theme = theme;
wm.auto_back_and_forth = engine.get_bool("workspace.auto_back_and_forth", false);
}
diff --git a/crates/wayland/src/decoration.rs b/crates/wayland/src/decoration.rs
index 8d300aa..4aa0b2d 100644
--- a/crates/wayland/src/decoration.rs
+++ b/crates/wayland/src/decoration.rs
@@ -354,7 +354,14 @@ pub fn render_titlebar(width: u32, height: u32, title: &str, background: (u8, u8
/// cosmetic nicety, not a feature surface worth a `srd.theme` knob, and a
/// value this small barely reads as "rounded" if it gets any larger at the
/// titlebar heights this compositor actually uses.
-const CORNER_RADIUS: u32 = 6;
+///
+/// `pub(crate)`, not private: `rounded_corners.rs` reuses the exact same
+/// value for a decorated window's own content (its bottom two corners,
+/// where the GLES backend can round the client's actual pixels, unlike
+/// the CPU bitmap clip this file does for the titlebar/border), so a
+/// bordered window's curve reads as one continuous radius from titlebar
+/// to content rather than two different ones meeting at a seam.
+pub(crate) const CORNER_RADIUS: u32 = 6;
/// Clips the top-left and top-right corners of a titlebar buffer to a
/// quarter-circle by making the pixels outside it fully transparent, so
diff --git a/crates/wayland/src/lib.rs b/crates/wayland/src/lib.rs
index e8a50ef..7687812 100644
--- a/crates/wayland/src/lib.rs
+++ b/crates/wayland/src/lib.rs
@@ -49,6 +49,7 @@ mod lock;
mod output_management;
mod output_power;
mod protocols;
+mod rounded_corners;
mod screencopy;
mod state;
mod udev;
diff --git a/crates/wayland/src/rounded_corners.rs b/crates/wayland/src/rounded_corners.rs
new file mode 100644
index 0000000..90f9afb
--- /dev/null
+++ b/crates/wayland/src/rounded_corners.rs
@@ -0,0 +1,215 @@
+//! Real rounded corners on a window's own client content - GLES backend
+//! only (the winit nested backend; the udev backend's `PixmanRenderer` is
+//! software-only and has no shader stage to hook this into at all).
+//!
+//! `decoration.rs`'s `round_top_corners` only ever clipped the compositor's
+//! *own* titlebar/border bitmap - its own doc comment says why: clipping
+//! arbitrary client surface content needs a real per-pixel mask, "a much
+//! bigger change than this cosmetic pass". This is that bigger change, for
+//! the one backend that can actually do it cheaply: a custom GLES fragment
+//! shader (`smithay::backend::renderer::gles::element::TextureShaderElement`,
+//! compiled once via `GlesRenderer::compile_custom_texture_shader`) that
+//! masks a window's texture against a rounded-rect signed-distance field
+//! while sampling it, the same technique every compositor with GPU-side
+//! rounded corners uses (cosmic-comp, niri). No smithay convenience wrapper
+//! does this - `CropRenderElement` only crops to a rectangle - so this
+//! builds the `TextureRenderElement` by hand from the surface's own
+//! committed texture/view/damage state (`RendererSurfaceState`, the same
+//! data `WaylandSurfaceRenderElement::from_state` reads, just via the
+//! public accessors instead of that private constructor).
+//!
+//! Scope, deliberately: only a window's *main* surface. A window with
+//! subsurfaces (a video overlay, a game's separately-committed content
+//! layer) won't have them rendered at all through this path - rounding a
+//! main surface correctly while leaving unclipped subsurfaces poking out
+//! past the curve would look worse than not rounding, and walking a whole
+//! subsurface tree through a shape mask is real additional work belonging
+//! to a follow-up, not this pass. `elements::surface_content_elements`
+//! (plain, unrounded, subsurface-aware) stays the fallback - see this
+//! module's own call site in `winit.rs`.
+
+use smithay::backend::renderer::element::texture::TextureRenderElement;
+use smithay::backend::renderer::element::Kind;
+use smithay::backend::renderer::gles::element::TextureShaderElement;
+use smithay::backend::renderer::gles::{GlesError, GlesRenderer, GlesTexProgram, Uniform, UniformName, UniformType};
+use smithay::backend::renderer::utils::{with_renderer_surface_state, RendererSurfaceState};
+use smithay::backend::renderer::Renderer;
+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
+use smithay::utils::Point;
+
+// `winit.rs`'s `custom_elements` element type. `crate::elements::
+// OverlayElement<GlesRenderer>` already covers everything the render loop
+// draws (cursor, decoration, borders, popups, plain content, layer-shell
+// surfaces) but can't also carry `TextureShaderElement`: that type only
+// implements `RenderElement<GlesRenderer>`, not the generic `RenderElement<R>`
+// every `OverlayElement<R>` variant needs (`OverlayElement<PixmanRenderer>`,
+// used identically by `udev.rs`, would stop compiling the moment a
+// GLES-only variant were added to the shared enum). Wrapping the whole
+// existing enum as one variant here, concrete to `GlesRenderer` from the
+// start (`<=GlesRenderer>`, not `<R>`), sidesteps that without touching
+// the shared type at all - `udev.rs` never sees this module.
+smithay::backend::renderer::element::render_elements! {
+ pub(crate) WinitElement<=GlesRenderer>;
+ Base=crate::elements::OverlayElement<GlesRenderer>,
+ Rounded=TextureShaderElement,
+}
+
+/// `//_DEFINES_` is smithay's own template placeholder (see `texture_program`
+/// in its `shaders` module) - `#define NO_ALPHA`/`EXTERNAL`/`DEBUG_FLAGS`
+/// get substituted there for whichever variant is actually compiled.
+/// Everything else here mirrors the bundled `texture.frag` (uniform names
+/// `tex`/`alpha`, varying `v_coords`) plus this module's own two: `size`
+/// (the element's own logical size in pixels, for turning `v_coords`
+/// - 0..1 across the quad - back into a pixel position) and `radius`.
+///
+/// `corners` (top-left, top-right, bottom-right, bottom-left; `1.0` = round,
+/// `0.0` = square) lets a decorated window round only its bottom two - the
+/// top two are already hidden under the titlebar band's own rounded
+/// bitmap, so rounding them here too would just be redundant work, not a
+/// visual difference.
+///
+/// The mask itself is a standard rounded-rect SDF: clamp the pixel position
+/// into the rect inset by `radius` on every side, then measure distance
+/// from that clamped point. Zero everywhere but the four corner zones (the
+/// clamp is a no-op there, so `dist` is always `0`, `mask` always `1`);
+/// inside a corner zone it's the distance to that corner's own circle
+/// center, smoothed over roughly 2 physical pixels so the edge reads as an
+/// anti-aliased curve instead of `round_top_corners`' deliberate hard
+/// cutoff (a GPU shader can afford the smoothstep; the CPU bitmap path that
+/// cuts corners elsewhere in this codebase can't justify one for a purely
+/// cosmetic pass).
+const FRAGMENT_SHADER: &str = r#"
+#version 100
+
+//_DEFINES_
+
+#if defined(EXTERNAL)
+#extension GL_OES_EGL_image_external : require
+#endif
+
+precision mediump float;
+#if defined(EXTERNAL)
+uniform samplerExternalOES tex;
+#else
+uniform sampler2D tex;
+#endif
+
+uniform float alpha;
+varying vec2 v_coords;
+
+uniform vec2 size;
+uniform float radius;
+uniform vec4 corners;
+
+void main() {
+ vec4 color = texture2D(tex, v_coords);
+#if defined(NO_ALPHA)
+ color = vec4(color.rgb, 1.0) * alpha;
+#else
+ color = color * alpha;
+#endif
+
+ vec2 pos = v_coords * size;
+ bool is_left = pos.x < size.x * 0.5;
+ bool is_top = pos.y < size.y * 0.5;
+ float corner_flag = is_top
+ ? (is_left ? corners.x : corners.y)
+ : (is_left ? corners.w : corners.z);
+
+ if (corner_flag > 0.5 && radius > 0.0) {
+ vec2 clamped = clamp(pos, vec2(radius), size - vec2(radius));
+ float dist = distance(pos, clamped);
+ float mask = 1.0 - smoothstep(radius - 1.0, radius + 1.0, dist);
+ color *= mask;
+ }
+
+ gl_FragColor = color;
+}
+"#;
+
+/// Which corners a call to [`rounded_content_element`] should actually
+/// round - see this module's doc comment on why a decorated window only
+/// wants its bottom two.
+#[derive(Clone, Copy)]
+pub(crate) struct RoundedCorners {
+ pub top_left: bool,
+ pub top_right: bool,
+ pub bottom_right: bool,
+ pub bottom_left: bool,
+}
+
+impl RoundedCorners {
+ pub(crate) const ALL: Self = Self { top_left: true, top_right: true, bottom_right: true, bottom_left: true };
+ pub(crate) const BOTTOM_ONLY: Self = Self { top_left: false, top_right: false, bottom_right: true, bottom_left: true };
+
+ fn as_uniform_value(self) -> (f32, f32, f32, f32) {
+ let f = |b: bool| if b { 1.0 } else { 0.0 };
+ (f(self.top_left), f(self.top_right), f(self.bottom_right), f(self.bottom_left))
+ }
+}
+
+/// Compiles [`FRAGMENT_SHADER`] once. Cheap to call more than once (it's a
+/// real GL program compile+link each time), so the caller caches the
+/// result - see `CompState`'s own field for where.
+pub(crate) fn compile(renderer: &mut GlesRenderer) -> Result<GlesTexProgram, GlesError> {
+ renderer.compile_custom_texture_shader(
+ FRAGMENT_SHADER,
+ &[UniformName::new("size", UniformType::_2f), UniformName::new("radius", UniformType::_1f), UniformName::new("corners", UniformType::_4f)],
+ )
+}
+
+/// Builds a rounded-corner-masked render element for `surface`'s own main
+/// content - not subsurfaces, not popups, see this module's doc comment
+/// for the scope this is deliberately narrower than
+/// `elements::surface_content_elements`'s.
+///
+/// `None` if the surface has no committed buffer yet (nothing to round) or
+/// isn't backed by a GLES texture at all (a single-pixel-buffer solid-color
+/// surface, `wp_single_pixel_buffer_v1` - rare, and trivially "already a
+/// rectangle" regardless, so not worth a second code path here).
+pub(crate) fn rounded_content_element(
+ renderer: &mut GlesRenderer,
+ program: &GlesTexProgram,
+ surface: &WlSurface,
+ location: (i32, i32),
+ alpha: f32,
+ radius: f32,
+ corners: RoundedCorners,
+) -> Option<TextureShaderElement> {
+ smithay::wayland::compositor::with_states(surface, |states| {
+ smithay::backend::renderer::utils::import_surface(renderer, states).ok()?;
+ Some(())
+ })?;
+
+ let (texture, view, buffer_scale, buffer_transform, damage_snapshot) = with_renderer_surface_state(surface, |state: &mut RendererSurfaceState| {
+ let texture = state.texture::<smithay::backend::renderer::gles::GlesTexture>(renderer.context_id())?.clone();
+ let view = state.view()?;
+ Some((texture, view, state.buffer_scale(), state.buffer_transform(), state.damage()))
+ })??;
+
+ let id = smithay::backend::renderer::element::Id::from_wayland_resource(surface);
+ let location: Point<f64, smithay::utils::Physical> = Point::from((location.0 as f64 + view.offset.x as f64, location.1 as f64 + view.offset.y as f64));
+ let inner = TextureRenderElement::from_texture_with_damage(
+ id,
+ renderer.context_id(),
+ location,
+ texture,
+ buffer_scale,
+ buffer_transform,
+ Some(alpha),
+ Some(view.src),
+ Some(view.dst),
+ None,
+ damage_snapshot,
+ Kind::Unspecified,
+ );
+
+ let (w, h) = (view.dst.w as f32, view.dst.h as f32);
+ let (c0, c1, c2, c3) = corners.as_uniform_value();
+ let uniforms = vec![
+ Uniform::new("size", (w, h)),
+ Uniform::new("radius", radius.min(w / 2.0).min(h / 2.0)),
+ Uniform::new("corners", (c0, c1, c2, c3)),
+ ];
+ Some(TextureShaderElement::new(inner, program.clone(), uniforms))
+}
diff --git a/crates/wayland/src/state.rs b/crates/wayland/src/state.rs
index 8d04d20..4416869 100644
--- a/crates/wayland/src/state.rs
+++ b/crates/wayland/src/state.rs
@@ -248,6 +248,17 @@ pub(crate) struct CompState {
/// call site for why those don't get a shadow at all rather than a
/// zero-alpha one.
pub(crate) shadow_buffers: HashMap<WindowId, MemoryRenderBuffer>,
+ /// The compiled rounded-corner GLES shader program (`rounded_corners::
+ /// compile`), if that succeeded - `None` on the udev backend always
+ /// (it never even tries, `PixmanRenderer` has no shader stage) and on
+ /// winit only if compilation itself failed (an old/software GL driver
+ /// missing something the shader needs), in which case content falls
+ /// back to plain, unrounded rendering rather than the compositor
+ /// refusing to start over a cosmetic feature. A concrete, non-generic
+ /// smithay type (`GlesTexProgram`), so this field costs nothing to
+ /// declare on the shared `CompState` even though only one backend ever
+ /// populates it.
+ pub(crate) rounded_corners_program: Option<smithay::backend::renderer::gles::GlesTexProgram>,
/// Persistent solid-colour buffers backing a window's other three
/// border strips (bottom, left, right - `decoration::border_strips`'
/// order past index 0), reused by position every frame rather than
diff --git a/crates/wayland/src/udev.rs b/crates/wayland/src/udev.rs
index 75abcba..40c0ce5 100644
--- a/crates/wayland/src/udev.rs
+++ b/crates/wayland/src/udev.rs
@@ -1079,6 +1079,7 @@ impl UdevPlatform {
decorations: HashMap::new(),
border_top_decorations: HashMap::new(),
shadow_buffers: HashMap::new(),
+ rounded_corners_program: None,
border_side_buffers: HashMap::new(),
last_synced_size: HashMap::new(),
pending: pending.clone(),
diff --git a/crates/wayland/src/winit.rs b/crates/wayland/src/winit.rs
index 2c55dda..5f072b6 100644
--- a/crates/wayland/src/winit.rs
+++ b/crates/wayland/src/winit.rs
@@ -165,6 +165,18 @@ impl WaylandPlatform {
let mut dmabuf_state = DmabufState::new();
dmabuf_state.create_global::<CompState>(&dh, backend.renderer().dmabuf_formats());
+ // See `state.rs`'s `rounded_corners_program` doc comment: `None` on
+ // any failure (an old/software GL driver missing something the
+ // shader needs) rather than refusing to start over a cosmetic
+ // feature - content just renders unrounded in that case.
+ let rounded_corners_program = match crate::rounded_corners::compile(backend.renderer()) {
+ Ok(program) => Some(program),
+ Err(e) => {
+ log::warn!("winit: rounded-corner shader failed to compile, content will render unrounded: {e}");
+ None
+ }
+ };
+
let pending = Rc::new(RefCell::new(Vec::new()));
let state = CompState {
compositor_state,
@@ -229,6 +241,7 @@ impl WaylandPlatform {
decorations: HashMap::new(),
border_top_decorations: HashMap::new(),
shadow_buffers: HashMap::new(),
+ rounded_corners_program,
border_side_buffers: HashMap::new(),
last_synced_size: HashMap::new(),
pending: pending.clone(),
@@ -337,7 +350,15 @@ impl WaylandPlatform {
return Ok(());
}
- let mut custom_elements: Vec<crate::elements::OverlayElement<GlesRenderer>> = Vec::new();
+ // `WinitElement`, not `OverlayElement<GlesRenderer>` directly - see
+ // `rounded_corners.rs`'s own doc comment on why content that gets
+ // rounded needs a wider element type than everything else here,
+ // and why that couldn't just be added as a new `OverlayElement`
+ // variant instead. Every existing push below wraps its
+ // `OverlayElement` in `WinitElement::Base`; only the new
+ // rounded-content push (further down) uses `WinitElement::Rounded`
+ // directly.
+ let mut custom_elements: Vec<crate::rounded_corners::WinitElement> = Vec::new();
// The right-click titlebar menu, if open - pushed first so it's
// topmost over every window (this backend draws no cursor of its
// own, see this module's doc comment, so there's no "stay under
@@ -345,7 +366,7 @@ impl WaylandPlatform {
if let (Some(menu), Some(buffer)) = (self.state.context_menu.as_ref(), self.state.context_menu_buffer.as_ref()) {
let pos = (menu.pos.0 as f64, menu.pos.1 as f64);
match MemoryRenderBufferRenderElement::from_buffer(renderer, pos, buffer, None, None, None, Kind::Unspecified) {
- Ok(elem) => custom_elements.push(crate::elements::OverlayElement::Memory(elem)),
+ Ok(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Memory(elem))),
Err(e) => log::warn!("failed to import context menu buffer: {e}"),
}
}
@@ -381,12 +402,17 @@ impl WaylandPlatform {
// 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)));
+ custom_elements.extend(crate::elements::popup_render_elements(&popup_targets, renderer, (0, 0)).into_iter().map(crate::rounded_corners::WinitElement::Base));
// 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);
+ let rounded_corners_enabled = self.wm.borrow().rounded_corners_enabled;
if !hide_top_layers {
- custom_elements.extend(crate::elements::output_layer_elements(renderer, &self.output, (0, 0), |layer| matches!(layer, Layer::Top | Layer::Overlay)));
+ custom_elements.extend(
+ crate::elements::output_layer_elements(renderer, &self.output, (0, 0), |layer| matches!(layer, Layer::Top | Layer::Overlay))
+ .into_iter()
+ .map(crate::rounded_corners::WinitElement::Base),
+ );
}
// Windows stacked in front of whichever one border/decoration is
// being built right now - `ids` is already front-to-back, so this
@@ -413,7 +439,7 @@ impl WaylandPlatform {
let rect = decoration::shadow_rect(geom);
let pos = (rect.x as f64, rect.y as f64);
match MemoryRenderBufferRenderElement::from_buffer(renderer, pos, shadow, None, None, None, Kind::Unspecified) {
- Ok(elem) => custom_elements.push(crate::elements::OverlayElement::Memory(elem)),
+ Ok(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Memory(elem))),
Err(e) => log::warn!("failed to import shadow buffer for window {id}: {e}"),
}
}
@@ -431,7 +457,7 @@ impl WaylandPlatform {
Size::from((fragment.width as f64, fragment.height as f64)),
);
match MemoryRenderBufferRenderElement::from_buffer(renderer, pos, deco, None, Some(src), None, Kind::Unspecified) {
- Ok(elem) => custom_elements.push(crate::elements::OverlayElement::Memory(elem)),
+ Ok(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Memory(elem))),
Err(e) => log::warn!("failed to import titlebar buffer for window {id}: {e}"),
}
}
@@ -458,7 +484,7 @@ impl WaylandPlatform {
if strips[0].width > 0 && strips[0].height > 0 && !strips[0].subtract_all(&occluders).is_empty() {
if let Some(buffer) = self.state.border_top_decorations.get(&id) {
match MemoryRenderBufferRenderElement::from_buffer(renderer, (strips[0].x as f64, strips[0].y as f64), buffer, None, None, None, Kind::Unspecified) {
- Ok(elem) => custom_elements.push(crate::elements::OverlayElement::Memory(elem)),
+ Ok(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Memory(elem))),
Err(e) => log::warn!("failed to import top border buffer for window {id}: {e}"),
}
}
@@ -472,7 +498,7 @@ impl WaylandPlatform {
for fragment in crate::elements::visible_border_fragments(*strip, &occluders) {
let buf = crate::elements::border_fragment_buffer(pool, buf_index);
buf_index += 1;
- custom_elements.push(crate::elements::OverlayElement::Solid(crate::elements::border_side_render_element(buf, fragment, color, (0, 0))));
+ custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Solid(crate::elements::border_side_render_element(buf, fragment, color, (0, 0)))));
}
}
}
@@ -480,11 +506,29 @@ impl WaylandPlatform {
// 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).
+ // does). Rounded via `rounded_corners::rounded_content_element`
+ // when the feature's on and the shader compiled - a decorated
+ // window only rounds its bottom two corners (the top two are
+ // already rounded, on the titlebar's own bitmap, by
+ // `decoration.rs`), an undecorated/CSD one rounds all four,
+ // since its content *is* the window's whole visible extent.
+ // Falls back to the plain, unrounded `surface_content_elements`
+ // on any failure - no committed buffer yet, a single-pixel
+ // solid-colour buffer, or the feature simply being off - same
+ // "always show something over a prettier maybe-nothing"
+ // reasoning `cursor.rs`'s built-in-arrow fallback already uses.
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));
+ let pos = (geom.x, geom.y + band);
+ let rounded = rounded_corners_enabled.then_some(self.state.rounded_corners_program.as_ref()).flatten().and_then(|program| {
+ let corners = if w.decorated { crate::rounded_corners::RoundedCorners::BOTTOM_ONLY } else { crate::rounded_corners::RoundedCorners::ALL };
+ crate::rounded_corners::rounded_content_element(renderer, program, &surface, pos, w.opacity, crate::decoration::CORNER_RADIUS as f32, corners)
+ });
+ match rounded {
+ Some(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Rounded(elem)),
+ None => custom_elements.extend(crate::elements::surface_content_elements(renderer, &surface, pos, w.opacity).into_iter().map(crate::rounded_corners::WinitElement::Base)),
+ }
}
}
occluders.push(geom);
@@ -492,7 +536,11 @@ impl WaylandPlatform {
// 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)));
+ custom_elements.extend(
+ crate::elements::output_layer_elements(renderer, &self.output, (0, 0), |layer| matches!(layer, Layer::Background | Layer::Bottom))
+ .into_iter()
+ .map(crate::rounded_corners::WinitElement::Base),
+ );
// Not `smithay::desktop::space::render_output`: see `udev.rs`'s
// matching call site for why (per-window opacity, fullscreen-aware
diff --git a/docs/DEFAULTS.md b/docs/DEFAULTS.md
index 5c93c4e..dc390e1 100644
--- a/docs/DEFAULTS.md
+++ b/docs/DEFAULTS.md
@@ -15,6 +15,7 @@ srd.set("general.animations", true) -- Default: true
srd.set("general.animation_duration", 200) -- Default: 200ms
srd.set("general.shadows", true) -- Default: true
srd.set("general.resize_margin", 6) -- Default: 6px
+srd.set("general.rounded_corners", true) -- Default: true (GLES/winit backend only)
srd.set("general.focus_follows_mouse", false) -- Default: false
srd.set("general.mouse_follows_focus", true) -- Default: true
srd.set("general.auto_raise", false) -- Default: false