atagen

monolith

public
Star0
Watch0

Commit 7373078d1149

tarball

atagen <boss@atagen.co>2026-10-16 11:25 UTC

3 files changed+156-24
monolith: shrink closed tiled windows into their seed

A closed window stays in its output plan as a ghost drawn from its last texture, aimed at the seed it would hide into. Its neighbours are planned in the same pass, so they close over it without ever overlapping it. A client that drops its buffer before closing still vanishes at once, since there is nothing left to draw.
Modified file: crates/monolith/src/anim.rs+40 additions
@@ -6,6 +6,8 @@ use monolith_config::{Animation, AnimationStyle};
66 use monolith_layout::{OutputId, Rect, WindowId};
77 use monolith_motion::{CubicBezier, Edges, Position, Timeline, group_initial_slope};
88 use monolith_planner::{Config as PlanConfig, Plan, Style};
9+use monolith_render_vk::VulkanTexture;
10+use smithay::utils::{Logical, Rectangle, Transform};
911 use std::collections::HashMap;
1012
1113 const VELOCITY_DT_NS: u64 = 1_000_000;
@@ -14,6 +16,20 @@ pub struct Animator {
1416 pub settings: Animation,
1517 tracks: HashMap<OutputId, Track>,
1618 glides: HashMap<WindowId, Glide>,
19+ ghosts: Vec<Ghost>,
20+}
21+
22+/// A closed tiled window that stays in its output's plan, shrinking to
23+/// `seed` with the last frame its client showed.
24+pub struct Ghost {
25+ pub id: WindowId,
26+ pub output: OutputId,
27+ pub seed: Rect,
28+ pub texture: VulkanTexture,
29+ pub scale: i32,
30+ pub transform: Transform,
31+ /// Window geometry inside the buffer.
32+ pub geometry: Rectangle<i32, Logical>,
1733 }
1834
1935 struct Track {
@@ -39,6 +55,7 @@ impl Animator {
3955 settings,
4056 tracks: HashMap::new(),
4157 glides: HashMap::new(),
58+ ghosts: Vec::new(),
4259 }
4360 }
4461
@@ -67,6 +84,12 @@ impl Animator {
6784 seed: impl Fn(WindowId) -> Rect,
6885 ) {
6986 let prev = self.tracks.get(&o);
87+ let ghosts = self
88+ .ghosts
89+ .iter()
90+ .filter(|g| g.output == o)
91+ .map(|g| (g.id, g.seed));
92+ let targets: Vec<(WindowId, Rect)> = ghosts.chain(targets.iter().copied()).collect();
7093 let ids: Vec<WindowId> = targets.iter().map(|(id, _)| *id).collect();
7194 let to: Vec<Rect> = targets.iter().map(|(_, r)| *r).collect();
7295 if prev.is_some_and(|t| t.ids == ids && t.plan.keyframes.last() == Some(&to)) {
@@ -167,6 +190,17 @@ impl Animator {
167190 }
168191 }
169192
193+ /// Keeps a closing tiled window in the plan until its track ends. The
194+ /// caller retargets afterwards.
195+ pub fn add_ghost(&mut self, ghost: Ghost) {
196+ self.ghosts.retain(|g| g.id != ghost.id);
197+ self.ghosts.push(ghost);
198+ }
199+
200+ pub fn ghosts(&self) -> &[Ghost] {
201+ &self.ghosts
202+ }
203+
170204 pub fn active(&self, now: u64) -> bool {
171205 self.tracks.values().any(|t| t.timeline.end_ns() > now)
172206 || self.glides.values().any(|g| g.timeline.end_ns() > now)
@@ -174,6 +208,12 @@ impl Animator {
174208
175209 pub fn prune(&mut self, now: u64) {
176210 self.glides.retain(|_, g| g.timeline.end_ns() > now);
211+ let tracks = &self.tracks;
212+ self.ghosts.retain(|g| {
213+ tracks
214+ .get(&g.output)
215+ .is_some_and(|t| t.timeline.end_ns() > now)
216+ });
177217 }
178218 }
179219
Modified file: crates/monolith/src/scene.rs+65 additions−23 deletions
@@ -4,14 +4,14 @@
44 use crate::anim::Animator;
55 use monolith_layout::{Layout, Rect, WindowId};
66 use monolith_render_vk::{VulkanRenderer, VulkanTexture};
7-use smithay::backend::renderer::Renderer;
87 use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
98 use smithay::backend::renderer::element::texture::TextureRenderElement;
109 use smithay::backend::renderer::element::utils::CropRenderElement;
1110 use smithay::backend::renderer::element::{AsRenderElements, Id, Kind};
1211 use smithay::backend::renderer::utils::with_renderer_surface_state;
12+use smithay::backend::renderer::{ContextId, Renderer};
1313 use smithay::desktop::Window;
14-use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size};
14+use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size, Transform};
1515 use std::collections::HashMap;
1616
1717 smithay::backend::renderer::element::render_elements! {
@@ -29,9 +29,15 @@ pub fn build(
2929 renderer: &mut VulkanRenderer,
3030 now: u64,
3131 ) -> Vec<Draw> {
32+ let mut ghosts = Vec::new();
3233 let mut tiled = Vec::new();
3334 let mut above = Vec::new();
3435 for (o, out) in layout.outputs() {
36+ for g in anim.ghosts().iter().filter(|g| g.output == o) {
37+ if let Some(r) = anim.tiled(o, g.id, now) {
38+ ghosts.push((g, r));
39+ }
40+ }
3541 let arr = layout.arrange(o);
3642 let fullscreen_at_rest = arr
3743 .fullscreen
@@ -61,6 +67,25 @@ pub fn build(
6167 window_elements(&mut out, renderer, window, r);
6268 }
6369 }
70+ let context = renderer.context_id();
71+ for (g, r) in ghosts.into_iter().rev() {
72+ let Some((at, size)) = pixels(r) else {
73+ continue;
74+ };
75+ let src = Rectangle::new(
76+ g.geometry.loc.to_f64(),
77+ (f64::from(size.0), f64::from(size.1)).into(),
78+ );
79+ out.push(texture(
80+ &context,
81+ &g.texture,
82+ g.scale,
83+ g.transform,
84+ src,
85+ at,
86+ size,
87+ ));
88+ }
6489 out
6590 }
6691
@@ -68,12 +93,9 @@ pub fn build(
6893 /// smaller has its last row and column stretched over the rest, so a resize
6994 /// in flight never shows a scaled or missing client.
7095 fn window_elements(out: &mut Vec<Draw>, renderer: &mut VulkanRenderer, window: &Window, r: Rect) {
71- let (x, y) = (r.x.round() as i32, r.y.round() as i32);
72- let w = r.right().round() as i32 - x;
73- let h = r.bottom().round() as i32 - y;
74- if w < 1 || h < 1 {
96+ let Some(((x, y), (w, h))) = pixels(r) else {
7597 return;
76- }
98+ };
7799 let geo = window.geometry();
78100 let loc = Point::<i32, Physical>::from((x - geo.loc.x, y - geo.loc.y));
79101 let elems: Vec<WaylandSurfaceRenderElement<VulkanRenderer>> =
@@ -93,7 +115,7 @@ fn window_elements(out: &mut Vec<Draw>, renderer: &mut VulkanRenderer, window: &
93115 return;
94116 };
95117 let context = renderer.context_id();
96- let Some((texture, scale, transform)) = with_renderer_surface_state(&root, |s| {
118+ let Some((tex, scale, transform)) = with_renderer_surface_state(&root, |s| {
97119 s.texture::<VulkanTexture>(context.clone())
98120 .cloned()
99121 .map(|t| (t, s.buffer_scale(), s.buffer_transform()))
@@ -105,22 +127,9 @@ fn window_elements(out: &mut Vec<Draw>, renderer: &mut VulkanRenderer, window: &
105127 let (cw, ch) = (w.min(gw), h.min(gh));
106128 let (gx, gy) = (geo.loc.x as f64, geo.loc.y as f64);
107129 let mut strip = |src: Rectangle<f64, Logical>, at: (i32, i32), size: (i32, i32)| {
108- if size.0 < 1 || size.1 < 1 {
109- return;
130+ if size.0 >= 1 && size.1 >= 1 {
131+ out.push(texture(&context, &tex, scale, transform, src, at, size));
110132 }
111- out.push(Draw::Texture(TextureRenderElement::from_static_texture(
112- Id::new(),
113- context.clone(),
114- Point::<f64, Physical>::from((f64::from(at.0), f64::from(at.1))),
115- texture.clone(),
116- scale,
117- transform,
118- None,
119- Some(src),
120- Some(Size::from(size)),
121- None,
122- Kind::Unspecified,
123- )));
124133 };
125134 let right = gx + f64::from(gw) - 1.0;
126135 let bottom = gy + f64::from(gh) - 1.0;
@@ -140,3 +149,36 @@ fn window_elements(out: &mut Vec<Draw>, renderer: &mut VulkanRenderer, window: &
140149 (w - gw, h - gh),
141150 );
142151 }
152+
153+/// Snaps a logical rect to whole pixels by rounding its edges, so neighbours
154+/// that share an edge stay flush.
155+fn pixels(r: Rect) -> Option<((i32, i32), (i32, i32))> {
156+ let (x, y) = (r.x.round() as i32, r.y.round() as i32);
157+ let w = r.right().round() as i32 - x;
158+ let h = r.bottom().round() as i32 - y;
159+ (w >= 1 && h >= 1).then_some(((x, y), (w, h)))
160+}
161+
162+fn texture(
163+ context: &ContextId<VulkanTexture>,
164+ texture: &VulkanTexture,
165+ scale: i32,
166+ transform: Transform,
167+ src: Rectangle<f64, Logical>,
168+ at: (i32, i32),
169+ size: (i32, i32),
170+) -> Draw {
171+ Draw::Texture(TextureRenderElement::from_static_texture(
172+ Id::new(),
173+ context.clone(),
174+ Point::<f64, Physical>::from((f64::from(at.0), f64::from(at.1))),
175+ texture.clone(),
176+ scale,
177+ transform,
178+ None,
179+ Some(src),
180+ Some(Size::from(size)),
181+ None,
182+ Kind::Unspecified,
183+ ))
184+}
Modified file: crates/monolith/src/state.rs+51 additions−1 deletions
@@ -1,11 +1,14 @@
1-use crate::anim::Animator;
1+use crate::anim::{Animator, Ghost};
22 use crate::commands::Binds;
33 use crate::nested::Host;
44 use monolith_config::Config;
55 use monolith_layout::{Layout, OutputId, Rect, WindowId};
66 use monolith_protocols::workspace::{Group, Snapshot, Workspace, WorkspaceState};
77 use monolith_render_vk::VulkanRenderer;
8+use monolith_render_vk::VulkanTexture;
89 use smithay::backend::renderer::ImportDma;
10+use smithay::backend::renderer::Renderer;
11+use smithay::backend::renderer::utils::{import_surface_tree, with_renderer_surface_state};
912 use smithay::desktop::{PopupManager, Space, Window, WindowSurfaceType};
1013 use smithay::input::keyboard::{Keycode, XkbConfig};
1114 use smithay::input::{Seat, SeatState};
@@ -219,12 +222,59 @@ impl State {
219222 if let Some(id) = self.window_id(surface) {
220223 if let Some(window) = self.windows.remove(&id) {
221224 self.space.unmap_elem(&window);
225+ self.leave_ghost(id, &window);
222226 }
223227 self.layout.remove_window(id);
224228 self.apply_layout();
225229 }
226230 }
227231
232+ /// Keeps a visible tiled window's last frame shrinking to its seed while
233+ /// its neighbours close over it.
234+ fn leave_ghost(&mut self, id: WindowId, window: &Window) {
235+ let Some(o) = self.layout.window(id).and_then(|w| w.output) else {
236+ return;
237+ };
238+ let tiled = self
239+ .layout
240+ .output(o)
241+ .is_some_and(|out| out.tree.contains(id));
242+ if !tiled || !self.layout.visible(id) {
243+ return;
244+ }
245+ let Some(seed) = self
246+ .layout
247+ .arrange_hiding(o, &[id])
248+ .tiled
249+ .into_iter()
250+ .find(|p| p.window == id)
251+ else {
252+ return;
253+ };
254+ let Some(surface) = window.toplevel().map(|t| t.wl_surface().clone()) else {
255+ return;
256+ };
257+ let _ = import_surface_tree(&mut self.renderer, &surface);
258+ let context = self.renderer.context_id();
259+ let snapshot = with_renderer_surface_state(&surface, |s| {
260+ s.texture::<VulkanTexture>(context.clone())
261+ .cloned()
262+ .map(|t| (t, s.buffer_scale(), s.buffer_transform()))
263+ })
264+ .flatten();
265+ if let Some((texture, scale, transform)) = snapshot {
266+ self.anim.add_ghost(Ghost {
267+ id,
268+ output: o,
269+ seed: seed.rect,
270+ texture,
271+ scale,
272+ transform,
273+ geometry: window.geometry(),
274+ });
275+ }
276+ }
277+
228278 pub fn window_id(&self, surface: &WlSurface) -> Option<WindowId> {
229279 self.windows
230280 .iter()