Commit 47259c39793f
tarball12 files changed+361-16
@@ -17,10 +17,16 @@ focus_follows_mouse = false
1717
# Put the pointer in the middle of a window focused from the keyboard, a1818
# rule or a new map. Bare metal only, a nested pointer belongs to the host.1919
mouse_follows_focus = false20
+# A window landing on an empty tag that is not in view brings the tag into21
+# view, alongside the tags already shown.22
+view_tag_on_first_window = true2023
2124
[decoration]2225
# Borders sit in the gap around each window, so keep them under half the inner gap.2326
border = 227
+# Corner radius, one number or [top left, top right, bottom right, bottom left].28
+# Fullscreen windows stay square.29
+radius = 02430
focused = "#88bfd1"2531
unfocused = "#3b4252"2632
@@ -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 tag37
+ /// 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 and107
+/// 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
}@@ -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 the255
+/// 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 = borders257277
.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's478
+/// 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.id536
+ }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 nothing567
+ /// 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
+ r585
+ });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 = self612
+ .inner()613
+ .draw(frame, src, dst, damage, opaque_regions, cache);614
+ frame.set_clip(None);615
+ drawn616
+ }617
+}@@ -0,0 +1,24 @@
1
+// Coverage of the fragment under the push constant clip, a rounded rect in2
+// framebuffer pixels with radii TL, TR, BR, BL. A ring keeps only the band3
+// 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
+}@@ -1,6 +1,7 @@
11
#version 45022
#extension GL_GOOGLE_include_directive : require33
#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
}@@ -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;@@ -1,9 +1,10 @@
11
#version 45022
#extension GL_GOOGLE_include_directive : require33
#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
}@@ -1,11 +1,12 @@
11
#version 45022
#extension GL_GOOGLE_include_directive : require33
#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
}@@ -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, until36
+/// [`VulkanFrame::set_clip`] changes it. A nonzero `ring` keeps only a band37
+/// 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 output77
+ /// 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,@@ -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};@@ -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 {@@ -1,7 +1,7 @@
11
//! Renders small scenes offscreen and checks pixels. Runs on whatever Vulkan22
//! 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 = corners169
+ .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
+}