atagen

monolith

public
Star0
Watch0

Commit 47259c39793f

tarball

atagen <boss@atagen.co>2026-10-17 10:20 UTC

12 files changed+361-16
render-vk, monolith: round window corners and borders

Every fragment shader cuts its draw to an optional rounded rect from push constants, so windows, ghosts, spawn effects and borders share one clip. The radius comes from decoration.radius, as one number or four corners. Clipped windows report their corners as see-through, or the damage tracker would skip what shows through them.
Modified file: crates/config/default.toml+6 additions
@@ -17,10 +17,16 @@ focus_follows_mouse = false
1717 # Put the pointer in the middle of a window focused from the keyboard, a
1818 # rule or a new map. Bare metal only, a nested pointer belongs to the host.
1919 mouse_follows_focus = false
20+# A window landing on an empty tag that is not in view brings the tag into
21+# view, alongside the tags already shown.
22+view_tag_on_first_window = true
2023
2124 [decoration]
2225 # Borders sit in the gap around each window, so keep them under half the inner gap.
2326 border = 2
27+# Corner radius, one number or [top left, top right, bottom right, bottom left].
28+# Fullscreen windows stay square.
29+radius = 0
2430 focused = "#88bfd1"
2531 unfocused = "#3b4252"
2632
Modified file: crates/config/src/lib.rs+28 additions
@@ -33,6 +33,9 @@ pub struct General {
3333 pub move_mode: MoveMode,
3434 pub focus_follows_mouse: bool,
3535 pub mouse_follows_focus: bool,
36+ /// The first window on an empty tag nobody is viewing brings that tag
37+ /// into view on its output.
38+ pub view_tag_on_first_window: bool,
3639 }
3740
3841 #[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
@@ -53,6 +56,7 @@ impl Default for General {
5356 move_mode: MoveMode::Swap,
5457 focus_follows_mouse: false,
5558 mouse_follows_focus: false,
59+ view_tag_on_first_window: true,
5660 }
5761 }
5862 }
@@ -94,14 +98,38 @@ pub enum Vrr {
9498 pub struct Decoration {
9599 /// Border width in logical pixels, drawn in the gap around each window.
96100 pub border: u32,
101+ pub radius: Corners,
97102 pub focused: Color,
98103 pub unfocused: Color,
99104 }
100105
106+/// Corner radii in logical pixels, top left, top right, bottom right and
107+/// bottom left. One number rounds every corner alike.
108+#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
109+#[serde(from = "CornersToml")]
110+pub struct Corners(pub [u32; 4]);
111+
112+#[derive(Deserialize)]
113+#[serde(untagged)]
114+enum CornersToml {
115+ All(u32),
116+ Each([u32; 4]),
117+}
118+
119+impl From<CornersToml> for Corners {
120+ fn from(c: CornersToml) -> Self {
121+ match c {
122+ CornersToml::All(r) => Self([r; 4]),
123+ CornersToml::Each(r) => Self(r),
124+ }
125+ }
126+}
127+
101128 impl Default for Decoration {
102129 fn default() -> Self {
103130 Self {
104131 border: 2,
132+ radius: Corners([0; 4]),
105133 focused: Color::rgb(0x88, 0xbf, 0xd1),
106134 unfocused: Color::rgb(0x3b, 0x42, 0x52),
107135 }
Modified file: crates/monolith/src/scene.rs+210 additions−11 deletions
@@ -4,7 +4,7 @@
44 use crate::wm::Wm;
55 use monolith_config::Decoration;
66 use monolith_layout::{OutputId, Rect};
7-use monolith_render_vk::{Effect, VulkanFrame, VulkanRenderer, VulkanTexture};
7+use monolith_render_vk::{Clip, Effect, VulkanFrame, VulkanRenderer, VulkanTexture};
88 use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;
99 use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
1010 use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
@@ -12,13 +12,14 @@ use smithay::backend::renderer::element::surface::render_elements_from_surface_t
1212 use smithay::backend::renderer::element::texture::TextureRenderElement;
1313 use smithay::backend::renderer::element::utils::{CropRenderElement, RescaleRenderElement};
1414 use smithay::backend::renderer::element::{AsRenderElements, Element, Id, Kind, RenderElement};
15-use smithay::backend::renderer::utils::CommitCounter;
1615 use smithay::backend::renderer::utils::with_renderer_surface_state;
16+use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
1717 use smithay::backend::renderer::{ContextId, Renderer, Texture};
18-use smithay::desktop::Window;
18+use smithay::desktop::{PopupManager, Window};
1919 use smithay::output::Output;
2020 use smithay::utils::user_data::UserDataMap;
2121 use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
22+use smithay::wayland::compositor::{TraversalAction, with_surface_tree_downward};
2223 use smithay::wayland::shell::wlr_layer::Layer;
2324 use smithay::xwayland::X11Surface;
2425
@@ -31,6 +32,7 @@ smithay::backend::renderer::element::render_elements! {
3132 Border=SolidColorRenderElement,
3233 Memory=MemoryRenderBufferRenderElement<VulkanRenderer>,
3334 Rescaled=RescaleRenderElement<WaylandSurfaceRenderElement<VulkanRenderer>>,
35+ Clipped=ClipElement,
3436 }
3537
3638 /// Elements for output `o` in its own coordinates, front to back.
@@ -111,7 +113,12 @@ pub fn build(
111113 continue;
112114 };
113115 let effect = anim.effect(id, now);
116+ let first = out.len();
114117 window_elements(&mut out, renderer, &state.window, r, effect, gliding);
118+ let clip = framed.then(|| corner_clip(r, deco)).flatten();
119+ if let Some(clip) = clip {
120+ clip_window(&mut out, first, &state.window, clip);
121+ }
115122 if framed && deco.border > 0 {
116123 let color = if focused == Some(id) {
117124 deco.focused.0
@@ -120,7 +127,11 @@ pub fn build(
120127 };
121128 let alpha = effect.map_or(1.0, |e| e.progress);
122129 let borders = &mut state.borders;
123- border_elements(&mut out, borders, r, deco.border as i32, color, alpha);
130+ let first = out.len();
131+ border_elements(&mut out, borders, r, deco, color, alpha);
132+ if let Some(clip) = clip {
133+ ring_borders(&mut out, first, clip, deco.border as i32);
134+ }
124135 }
125136 }
126137 for (g, frame, r) in ghosts.into_iter().rev() {
@@ -131,7 +142,7 @@ pub fn build(
131142 frame.geometry.loc.to_f64(),
132143 (f64::from(size.0), f64::from(size.1)).into(),
133144 );
134- out.push(Draw::Effect(EffectElement::new(
145+ let ghost = Draw::Effect(EffectElement::new(
135146 &frame.texture,
136147 frame.scale,
137148 frame.transform,
@@ -139,7 +150,11 @@ pub fn build(
139150 at,
140151 size,
141152 anim.ghost_effect(g, now),
142- )));
153+ ));
154+ out.push(match corner_clip(r, deco) {
155+ Some(clip) => Draw::Clipped(ClipElement::new(ghost, clip)),
156+ None => ghost,
157+ });
143158 }
144159 if let Some(output) = output {
145160 crate::layers::elements(&mut out, renderer, output, Layer::Bottom);
@@ -236,22 +251,27 @@ fn window_elements(
236251 }
237252
238253 /// Four strips just outside `r`, in the gap, so the client keeps its size.
254+/// With rounded corners they reach in by the largest radius, to cover the
255+/// band the ring curves through inside the window's corners.
239256 fn border_elements(
240257 out: &mut Vec<Draw>,
241258 borders: &mut Option<[SolidColorBuffer; 4]>,
242259 r: Rect,
243- b: i32,
260+ deco: &Decoration,
244261 color: [f32; 4],
245262 alpha: f32,
246263 ) {
247264 let Some(((x, y), (w, h))) = pixels(r) else {
248265 return;
249266 };
267+ let b = deco.border as i32;
268+ let reach = deco.radius.0.into_iter().max().unwrap_or(0) as i32;
269+ let reach = reach.min(w / 2).min(h / 2);
250270 let strips = [
251- ((x - b, y - b), (w + 2 * b, b)),
252- ((x - b, y + h), (w + 2 * b, b)),
253- ((x - b, y), (b, h)),
254- ((x + w, y), (b, h)),
271+ ((x - b, y - b), (w + 2 * b, b + reach)),
272+ ((x - b, y + h - reach), (w + 2 * b, b + reach)),
273+ ((x - b, y + reach), (b + reach, h - 2 * reach)),
274+ ((x + w - reach, y + reach), (b + reach, h - 2 * reach)),
255275 ];
256276 let buffers = borders
257277 .get_or_insert_with(|| std::array::from_fn(|_| SolidColorBuffer::new((0, 0), color)));
@@ -416,3 +436,182 @@ impl RenderElement<VulkanRenderer> for EffectElement {
416436 )
417437 }
418438 }
439+
440+/// The rounded outline of a framed window at `r`, if it has one.
441+fn corner_clip(r: Rect, deco: &Decoration) -> Option<Clip> {
442+ let radii = deco.radius.0;
443+ if radii == [0; 4] {
444+ return None;
445+ }
446+ let (at, size) = pixels(r)?;
447+ Some(Clip {
448+ rect: Rectangle::new(at.into(), size.into()),
449+ radii: radii.map(|r| r as f32),
450+ ring: 0.0,
451+ })
452+}
453+
454+/// Clips what a window drew from `first` on, leaving its popups whole.
455+fn clip_window(out: &mut [Draw], first: usize, window: &Window, clip: Clip) {
456+ let mut popups = std::collections::HashSet::new();
457+ if let Some(root) = crate::wm::root(window) {
458+ for (popup, _) in PopupManager::popups_for_surface(&root) {
459+ with_surface_tree_downward(
460+ popup.wl_surface(),
461+ (),
462+ |_, _, _| TraversalAction::DoChildren(()),
463+ |s, _, _| {
464+ popups.insert(Id::from_wayland_resource(s));
465+ },
466+ |_, _, _| true,
467+ );
468+ }
469+ }
470+ for draw in &mut out[first..] {
471+ if !popups.contains(draw.id()) {
472+ wrap(draw, clip);
473+ }
474+ }
475+}
476+
477+/// Turns the border strips from `first` on into a ring around the window's
478+/// rounded outline.
479+fn ring_borders(out: &mut [Draw], first: usize, window: Clip, b: i32) {
480+ let grow = |r: f32| if r > 0.0 { r + b as f32 } else { 0.0 };
481+ let ring = Clip {
482+ rect: Rectangle::new(
483+ window.rect.loc - Point::from((b, b)),
484+ window.rect.size + Size::from((2 * b, 2 * b)),
485+ ),
486+ radii: window.radii.map(grow),
487+ ring: b as f32,
488+ };
489+ for draw in &mut out[first..] {
490+ wrap(draw, ring);
491+ }
492+}
493+
494+fn wrap(draw: &mut Draw, clip: Clip) {
495+ let inner = std::mem::replace(draw, Draw::Clipped(ClipElement::empty()));
496+ *draw = Draw::Clipped(ClipElement::new(inner, clip));
497+}
498+
499+/// Another element drawn through a rounded clip.
500+pub struct ClipElement {
501+ inner: Option<Box<Draw>>,
502+ clip: Clip,
503+ id: Id,
504+}
505+
506+impl ClipElement {
507+ fn new(inner: Draw, clip: Clip) -> Self {
508+ Self {
509+ id: inner.id().clone(),
510+ inner: Some(Box::new(inner)),
511+ clip,
512+ }
513+ }
514+
515+ /// A placeholder while an element is moved into its wrapper.
516+ fn empty() -> Self {
517+ Self {
518+ inner: None,
519+ clip: Clip {
520+ rect: Rectangle::default(),
521+ radii: [0.0; 4],
522+ ring: 0.0,
523+ },
524+ id: Id::new(),
525+ }
526+ }
527+
528+ fn inner(&self) -> &Draw {
529+ self.inner.as_deref().expect("only placeholders are empty")
530+ }
531+}
532+
533+impl Element for ClipElement {
534+ fn id(&self) -> &Id {
535+ &self.id
536+ }
537+
538+ fn current_commit(&self) -> CommitCounter {
539+ self.inner().current_commit()
540+ }
541+
542+ fn location(&self, scale: Scale<f64>) -> Point<i32, Physical> {
543+ self.inner().location(scale)
544+ }
545+
546+ fn src(&self) -> Rectangle<f64, Buffer> {
547+ self.inner().src()
548+ }
549+
550+ fn transform(&self) -> Transform {
551+ self.inner().transform()
552+ }
553+
554+ fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
555+ self.inner().geometry(scale)
556+ }
557+
558+ fn damage_since(
559+ &self,
560+ scale: Scale<f64>,
561+ commit: Option<CommitCounter>,
562+ ) -> DamageSet<i32, Physical> {
563+ self.inner().damage_since(scale, commit)
564+ }
565+
566+ /// A ring hides nothing behind it, and a rounded window hides nothing
567+ /// behind its corners.
568+ fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
569+ if self.clip.ring > 0.0 {
570+ return OpaqueRegions::default();
571+ }
572+ let loc = self.geometry(scale).loc;
573+ let c = self.clip.rect;
574+ let (right, bottom) = (c.loc.x + c.size.w, c.loc.y + c.size.h);
575+ let [tl, tr, br, bl] = self.clip.radii.map(|r| r.ceil() as i32);
576+ let corners = [
577+ Rectangle::new(c.loc, (tl, tl).into()),
578+ Rectangle::new((right - tr, c.loc.y).into(), (tr, tr).into()),
579+ Rectangle::new((right - br, bottom - br).into(), (br, br).into()),
580+ Rectangle::new((c.loc.x, bottom - bl).into(), (bl, bl).into()),
581+ ]
582+ .map(|mut r| {
583+ r.loc -= loc;
584+ r
585+ });
586+ Rectangle::subtract_rects_many(self.inner().opaque_regions(scale), corners)
587+ .into_iter()
588+ .collect()
589+ }
590+
591+ fn alpha(&self) -> f32 {
592+ self.inner().alpha()
593+ }
594+
595+ fn kind(&self) -> Kind {
596+ self.inner().kind()
597+ }
598+}
599+
600+impl RenderElement<VulkanRenderer> for ClipElement {
601+ fn draw(
602+ &self,
603+ frame: &mut VulkanFrame<'_, '_>,
604+ src: Rectangle<f64, Buffer>,
605+ dst: Rectangle<i32, Physical>,
606+ damage: &[Rectangle<i32, Physical>],
607+ opaque_regions: &[Rectangle<i32, Physical>],
608+ cache: Option<&UserDataMap>,
609+ ) -> Result<(), monolith_render_vk::Error> {
610+ frame.set_clip(Some(self.clip));
611+ let drawn = self
612+ .inner()
613+ .draw(frame, src, dst, damage, opaque_regions, cache);
614+ frame.set_clip(None);
615+ drawn
616+ }
617+}
Added file: crates/render-vk/shaders/clip.glsl+24 additions
@@ -0,0 +1,24 @@
1+// Coverage of the fragment under the push constant clip, a rounded rect in
2+// framebuffer pixels with radii TL, TR, BR, BL. A ring keeps only the band
3+// that wide inside the edge, for borders.
4+float rounded_box(vec2 p, vec2 half_size, float r) {
5+ vec2 q = abs(p) - half_size + r;
6+ return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
7+}
8+
9+float clip_alpha() {
10+ if (pc.clip.z <= 0.0) {
11+ return 1.0;
12+ }
13+ vec2 half_size = pc.clip.zw * 0.5;
14+ vec2 p = gl_FragCoord.xy - (pc.clip.xy + half_size);
15+ float r = p.x < 0.0 ? (p.y < 0.0 ? pc.radii.x : pc.radii.w)
16+ : (p.y < 0.0 ? pc.radii.y : pc.radii.z);
17+ float a = clamp(0.5 - rounded_box(p, half_size, r), 0.0, 1.0);
18+ float ring = pc.ring.x;
19+ if (ring > 0.0) {
20+ float inner = rounded_box(p, half_size - ring, max(r - ring, 0.0));
21+ a *= clamp(0.5 + inner, 0.0, 1.0);
22+ }
23+ return a;
24+}
Modified file: crates/render-vk/shaders/effect.frag+2 additions−1 deletions
@@ -1,6 +1,7 @@
11 #version 450
22 #extension GL_GOOGLE_include_directive : require
33 #include "push.glsl"
4+#include "clip.glsl"
45
56 layout(set = 0, binding = 0) uniform sampler2D tex;
67 layout(location = 0) in vec2 v_uv;
@@ -23,5 +24,5 @@ vec4 effect(vec4 color, vec2 uv, float progress, int kind) {
2324
2425 void main() {
2526 vec4 color = texture(tex, v_uv) * pc.color.a;
26- out_color = effect(color, v_uv, pc.effect.x, int(pc.effect.y));
27+ out_color = effect(color, v_uv, pc.effect.x, int(pc.effect.y)) * clip_alpha();
2728 }
Modified file: crates/render-vk/shaders/push.glsl+3 additions
@@ -6,4 +6,7 @@ layout(push_constant) uniform Push {
66 vec4 dst;
77 vec2 uv[4];
88 vec4 effect;
9+ vec4 clip;
10+ vec4 radii;
11+ vec4 ring;
912 } pc;
Modified file: crates/render-vk/shaders/solid.frag+2 additions−1 deletions
@@ -1,9 +1,10 @@
11 #version 450
22 #extension GL_GOOGLE_include_directive : require
33 #include "push.glsl"
4+#include "clip.glsl"
45
56 layout(location = 0) out vec4 out_color;
67
78 void main() {
8- out_color = pc.color;
9+ out_color = pc.color * clip_alpha();
910 }
Modified file: crates/render-vk/shaders/texture.frag+2 additions−1 deletions
@@ -1,11 +1,12 @@
11 #version 450
22 #extension GL_GOOGLE_include_directive : require
33 #include "push.glsl"
4+#include "clip.glsl"
45
56 layout(set = 0, binding = 0) uniform sampler2D tex;
67 layout(location = 0) in vec2 v_uv;
78 layout(location = 0) out vec4 out_color;
89
910 void main() {
10- out_color = texture(tex, v_uv) * pc.color.a;
11+ out_color = texture(tex, v_uv) * pc.color.a * clip_alpha();
1112 }
Modified file: crates/render-vk/src/frame.rs+45 additions
@@ -29,6 +29,18 @@ pub struct VulkanFrame<'frame, 'buffer> {
2929 output: Size<i32, Physical>,
3030 transform: Transform,
3131 ops: Vec<Op>,
32+ clip: Option<Clip>,
33+}
34+
35+/// A rounded rect in output space that later draws are cut to, until
36+/// [`VulkanFrame::set_clip`] changes it. A nonzero `ring` keeps only a band
37+/// that wide inside the edge.
38+#[derive(Clone, Copy, Debug, PartialEq)]
39+pub struct Clip {
40+ pub rect: Rectangle<i32, Physical>,
41+ /// Top left, top right, bottom right and bottom left, in pixels.
42+ pub radii: [f32; 4],
43+ pub ring: f32,
3244 }
3345
3446 impl std::fmt::Debug for VulkanFrame<'_, '_> {
@@ -53,9 +65,34 @@ impl<'frame, 'buffer> VulkanFrame<'frame, 'buffer> {
5365 output,
5466 transform,
5567 ops: Vec::new(),
68+ clip: None,
5669 }
5770 }
5871
72+ pub fn set_clip(&mut self, clip: Option<Clip>) {
73+ self.clip = clip;
74+ }
75+
76+ /// The clip's push constants in framebuffer space, where an output
77+ /// transform moves each radius to another corner.
78+ fn clip_push(&self) -> ([f32; 4], [f32; 4], [f32; 4]) {
79+ let Some(c) = self.clip else {
80+ return ([0.0; 4], [0.0; 4], [0.0; 4]);
81+ };
82+ let [tl, tr, br, bl] = c.radii;
83+ let corners = [[tl, 0.0], [tr, 0.0], [bl, 0.0], [br, 0.0]];
84+ let (fb, r) = to_framebuffer(c.rect, corners, self.output, self.transform);
85+ let rect = [
86+ fb.loc.x as f32,
87+ fb.loc.y as f32,
88+ fb.size.w as f32,
89+ fb.size.h as f32,
90+ ];
91+ let most = fb.size.w.min(fb.size.h) as f32 / 2.0;
92+ let radii = [r[0][0], r[1][0], r[3][0], r[2][0]].map(|r| r.min(most));
93+ (rect, radii, [c.ring, 0.0, 0.0, 0.0])
94+ }
95+
5996 fn fb_size(&self) -> Size<i32, Physical> {
6097 (self.target.width() as i32, self.target.height() as i32).into()
6198 }
@@ -139,11 +176,15 @@ impl<'frame, 'buffer> VulkanFrame<'frame, 'buffer> {
139176 }
140177 let uvs = texture_uvs(src, dst.size, texture.size(), src_transform);
141178 let (fb, uv) = to_framebuffer(dst, uvs, self.output, self.transform);
179+ let (clip, radii, ring) = self.clip_push();
142180 let push = Push {
143181 dst: self.ndc(fb),
144182 uv,
145183 color: [1.0, 1.0, 1.0, alpha],
146184 effect,
185+ clip,
186+ radii,
187+ ring,
147188 };
148189 self.ops.push(Op::Draw {
149190 kind,
@@ -196,11 +237,15 @@ impl Frame for VulkanFrame<'_, '_> {
196237 return Ok(());
197238 }
198239 let (fb, _) = to_framebuffer(dst, [[0.0; 2]; 4], self.output, self.transform);
240+ let (clip, radii, ring) = self.clip_push();
199241 let push = Push {
200242 dst: self.ndc(fb),
201243 uv: [[0.0; 2]; 4],
202244 color: color.components(),
203245 effect: [0.0; 4],
246+ clip,
247+ radii,
248+ ring,
204249 };
205250 self.ops.push(Op::Draw {
206251 kind: Kind::Solid,
Modified file: crates/render-vk/src/lib.rs+1 additions−1 deletions
@@ -23,6 +23,6 @@ mod transform;
2323
2424 pub use dmabuf::VulkanAllocator;
2525 pub use error::Error;
26-pub use frame::{Effect, Transition, VulkanFrame};
26+pub use frame::{Clip, Effect, Transition, VulkanFrame};
2727 pub use image::{VulkanImage, VulkanMapping, VulkanTarget, VulkanTexture};
2828 pub use renderer::{DmabufImporter, ShmStager, StagedShm, VulkanRenderer};
Modified file: crates/render-vk/src/pipeline.rs+4 additions
@@ -15,6 +15,10 @@ pub(crate) struct Push {
1515 pub uv: [[f32; 2]; 4],
1616 /// Transition progress and kind, read only by `shaders/effect.frag`.
1717 pub effect: [f32; 4],
18+ /// See `shaders/clip.glsl`. A zero width turns clipping off.
19+ pub clip: [f32; 4],
20+ pub radii: [f32; 4],
21+ pub ring: [f32; 4],
1822 }
1923
2024 impl Push {
Modified file: crates/render-vk/tests/golden.rs+34 additions−1 deletions
@@ -1,7 +1,7 @@
11 //! Renders small scenes offscreen and checks pixels. Runs on whatever Vulkan
22 //! driver is available. `nix flake check` forces lavapipe.
33
4-use monolith_render_vk::{VulkanImage, VulkanRenderer, VulkanTexture};
4+use monolith_render_vk::{Clip, VulkanImage, VulkanRenderer, VulkanTexture};
55 use smithay::backend::allocator::Fourcc;
66 use smithay::backend::renderer::{
77 Bind, Color32F, ExportMem, Frame, ImportMem, Offscreen, Renderer,
@@ -152,3 +152,36 @@ fn solid_fill_in_the_corner_keeps_its_colour() {
152152 });
153153 assert_eq!(at(&s.pixels(), 56, 56), RED);
154154 }
155+
156+/// The radius follows its corner through an output rotation.
157+#[test]
158+fn clip_radius_stays_on_its_corner_when_rotated() {
159+ let corners = [(0, 0), (W - 1, 0), (0, H - 1), (W - 1, H - 1)];
160+ let mut s = Scene::new();
161+ s.draw(Transform::_90, |f| {
162+ f.clear(rgba(BLACK), &[full()]).unwrap();
163+ let dst = Rectangle::new((0, 0).into(), (4, 4).into());
164+ f.draw_solid(dst, &[Rectangle::from_size(dst.size)], rgba(GREEN))
165+ .unwrap();
166+ });
167+ let px = s.pixels();
168+ let top_left = corners
169+ .into_iter()
170+ .find(|&(x, y)| at(&px, x, y) == GREEN)
171+ .unwrap();
172+ s.draw(Transform::_90, |f| {
173+ f.clear(rgba(BLACK), &[full()]).unwrap();
174+ f.set_clip(Some(Clip {
175+ rect: full(),
176+ radii: [20.0, 0.0, 0.0, 0.0],
177+ ring: 0.0,
178+ }));
179+ f.draw_solid(full(), &[full()], rgba(GREEN)).unwrap();
180+ });
181+ let px = s.pixels();
182+ for (x, y) in corners {
183+ let expected = if (x, y) == top_left { BLACK } else { GREEN };
184+ assert_eq!(at(&px, x, y), expected, "corner {x},{y}");
185+ }
186+ assert_eq!(at(&px, W / 2, H / 2), GREEN);
187+}