atagen

monolith

public
Star0
Watch0

Commit 2af1d8c55876

tarball

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

11 files changed+502-26
monolith: animate layout changes through the planner

Each output keeps one plan over every tiled window, with hidden and new windows as the zero-size seeds the layout already gives them, so spawning, viewing and hiding all go through the planner. The space keeps final geometry for input while the scene draws sampled rects. Clients get their final size at once, and their buffers are cropped or edge-stretched to the animated rect instead of scaled. A replan starts from the drawn positions and keeps each group moving at the speed it had.
Modified file: Cargo.lock+2 additions
@@ -398,6 +398,8 @@ dependencies = [
398398 "monolith-config",
399399 "monolith-ipc",
400400 "monolith-layout",
401+ "monolith-motion",
402+ "monolith-planner",
401403 "monolith-protocols",
402404 "monolith-render-vk",
403405 "pound",
Modified file: crates/config/default.toml+7 additions
@@ -10,6 +10,13 @@ startup = []
1010 mod = "super"
1111 gaps = { inner = 8, outer = 8 }
1212
13+[animation]
14+enabled = true
15+# orthogonal moves each window along one axis per phase, direct allows both.
16+style = "orthogonal"
17+phase_ms = 160
18+curve = [0.45, 0.0, 0.2, 1.0]
19+
1320 [keyboard]
1421 layout = ""
1522 repeat_delay = 200
Modified file: crates/config/src/lib.rs+33 additions
@@ -12,6 +12,7 @@ pub const DEFAULT: &str = include_str!("../default.toml");
1212 #[derive(Debug, Clone)]
1313 pub struct Config {
1414 pub general: General,
15+ pub animation: Animation,
1516 pub keyboard: Keyboard,
1617 pub binds: Vec<Bind>,
1718 pub rules: Vec<Rule>,
@@ -54,6 +55,35 @@ impl Default for Gaps {
5455
5556 #[derive(Deserialize, Debug, Clone, PartialEq)]
5657 #[serde(default, deny_unknown_fields)]
58+pub struct Animation {
59+ pub enabled: bool,
60+ pub style: AnimationStyle,
61+ /// Every phase of a planned transition lasts this long.
62+ pub phase_ms: u64,
63+ /// A cubic bezier, as in CSS.
64+ pub curve: [f64; 4],
65+}
66+
67+impl Default for Animation {
68+ fn default() -> Self {
69+ Self {
70+ enabled: true,
71+ style: AnimationStyle::Orthogonal,
72+ phase_ms: 160,
73+ curve: [0.45, 0.0, 0.2, 1.0],
74+ }
75+ }
76+}
77+
78+#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
79+#[serde(rename_all = "lowercase")]
80+pub enum AnimationStyle {
81+ Orthogonal,
82+ Direct,
83+}
84+
85+#[derive(Deserialize, Debug, Clone, PartialEq)]
86+#[serde(default, deny_unknown_fields)]
5787 pub struct Keyboard {
5888 pub rules: String,
5989 pub model: String,
@@ -166,6 +196,8 @@ struct Raw {
166196 #[serde(default)]
167197 keyboard: Keyboard,
168198 #[serde(default)]
199+ animation: Animation,
200+ #[serde(default)]
169201 binds: BTreeMap<String, Words>,
170202 #[serde(default)]
171203 rules: Vec<RawRule>,
@@ -243,6 +275,7 @@ impl Config {
243275 .collect::<Result<_, Error>>()?;
244276 Ok(Self {
245277 general: raw.general,
278+ animation: raw.animation,
246279 keyboard: raw.keyboard,
247280 binds,
248281 rules,
Modified file: crates/layout/src/lib.rs+9 additions−2 deletions
@@ -783,14 +783,21 @@ impl Layout {
783783 /// Target geometry for everything on `o`, in output-local logical
784784 /// coordinates offset by the output's area.
785785 pub fn arrange(&self, o: OutputId) -> Arrangement {
786+ self.arrange_hiding(o, &[])
787+ }
788+
789+ /// Like `arrange` with `hide` treated as hidden, which places them on the
790+ /// seeds they would grow from.
791+ pub fn arrange_hiding(&self, o: OutputId, hide: &[WindowId]) -> Arrangement {
786792 let Some(out) = self.outputs.get(o) else {
787793 return Arrangement::default();
788794 };
789795 let area = inset(out.area, self.gaps.outer);
790- let vis = self.vis_on(o);
796+ let shown = self.vis_on(o);
797+ let vis = &|w: WindowId| shown(w) && !hide.contains(&w);
791798 let tiled = out
792799 .tree
793- .arrange(area, self.gaps.inner, &vis)
800+ .arrange(area, self.gaps.inner, vis)
794801 .into_iter()
795802 .map(|(window, rect)| Placed {
796803 window,
Modified file: crates/monolith/Cargo.toml+2 additions
@@ -16,6 +16,8 @@ calloop-wayland-source = "0.4"
1616 monolith-config.workspace = true
1717 monolith-ipc.workspace = true
1818 monolith-layout.workspace = true
19+monolith-motion.workspace = true
20+monolith-planner.workspace = true
1921 monolith-protocols.workspace = true
2022 monolith-render-vk.workspace = true
2123 pound = "0.5"
Added file: crates/monolith/src/anim.rs+213 additions
@@ -0,0 +1,213 @@
1+//! Layout transitions. Each output has one track, a plan over every tiled
2+//! window it holds, with hidden windows as zero-size seeds. Switches between
3+//! tiled, floating and fullscreen glide directly above the tiled layer.
4+
5+use monolith_config::{Animation, AnimationStyle};
6+use monolith_layout::{OutputId, Rect, WindowId};
7+use monolith_motion::{CubicBezier, Edges, Position, Timeline, group_initial_slope};
8+use monolith_planner::{Config as PlanConfig, Plan, Style};
9+use std::collections::HashMap;
10+
11+const VELOCITY_DT_NS: u64 = 1_000_000;
12+
13+pub struct Animator {
14+ pub settings: Animation,
15+ tracks: HashMap<OutputId, Track>,
16+ glides: HashMap<WindowId, Glide>,
17+}
18+
19+struct Track {
20+ ids: Vec<WindowId>,
21+ plan: Plan,
22+ timeline: Timeline,
23+ base: CubicBezier,
24+ /// Each window's curve in the first phase. A replan can start a group
25+ /// with the speed it already had, later phases always use `base`.
26+ first: Vec<CubicBezier>,
27+}
28+
29+struct Glide {
30+ from: Rect,
31+ to: Rect,
32+ timeline: Timeline,
33+ curve: CubicBezier,
34+}
35+
36+impl Animator {
37+ pub fn new(settings: Animation) -> Self {
38+ Self {
39+ settings,
40+ tracks: HashMap::new(),
41+ glides: HashMap::new(),
42+ }
43+ }
44+
45+ fn base(&self) -> CubicBezier {
46+ let [x1, y1, x2, y2] = self.settings.curve;
47+ let c = CubicBezier::new(x1, y1, x2, y2);
48+ if c.is_valid() { c } else { CubicBezier::EASE }
49+ }
50+
51+ fn phase_ns(&self) -> u64 {
52+ if self.settings.enabled {
53+ self.settings.phase_ms * 1_000_000
54+ } else {
55+ 0
56+ }
57+ }
58+
59+ /// Plans `o`'s tiled windows from where they are drawn now to `targets`.
60+ /// Windows the track has never seen start from `seed`.
61+ pub fn retarget(
62+ &mut self,
63+ o: OutputId,
64+ now: u64,
65+ targets: &[(WindowId, Rect)],
66+ gap: f64,
67+ seed: impl Fn(WindowId) -> Rect,
68+ ) {
69+ let prev = self.tracks.get(&o);
70+ let ids: Vec<WindowId> = targets.iter().map(|(id, _)| *id).collect();
71+ let to: Vec<Rect> = targets.iter().map(|(_, r)| *r).collect();
72+ if prev.is_some_and(|t| t.ids == ids && t.plan.keyframes.last() == Some(&to)) {
73+ return;
74+ }
75+ let from: Vec<Rect> = ids
76+ .iter()
77+ .map(|&id| {
78+ prev.and_then(|t| t.sample(id, now))
79+ .unwrap_or_else(|| seed(id))
80+ })
81+ .collect();
82+ let cfg = PlanConfig {
83+ style: match self.settings.style {
84+ AnimationStyle::Orthogonal => Style::Orthogonal,
85+ AnimationStyle::Direct => Style::Direct,
86+ },
87+ gap,
88+ ..PlanConfig::default()
89+ };
90+ let plan = if self.phase_ns() == 0 {
91+ monolith_planner::plan(&to, &to, &cfg)
92+ } else {
93+ monolith_planner::plan(&from, &to, &cfg)
94+ };
95+ let base = self.base();
96+ let phase_secs = self.phase_ns() as f64 / 1e9;
97+ let mut first = vec![base; ids.len()];
98+ if plan.phases() > 0 {
99+ for g in &plan.groups {
100+ let movers = g.members.iter().map(|&i| {
101+ let v = prev.map_or([0.0; 4], |t| t.velocity(ids[i], now));
102+ let d = sub(edges(&plan.keyframes[1][i]), edges(&plan.keyframes[0][i]));
103+ (v, d)
104+ });
105+ if let Some(slope) = group_initial_slope(movers, phase_secs) {
106+ let curve = base.with_initial_slope(slope);
107+ for &i in &g.members {
108+ first[i] = curve;
109+ }
110+ }
111+ }
112+ }
113+ let timeline = Timeline {
114+ start_ns: now,
115+ phase_ns: self.phase_ns(),
116+ phases: plan.phases(),
117+ };
118+ self.tracks.insert(
119+ o,
120+ Track {
121+ ids,
122+ plan,
123+ timeline,
124+ base,
125+ first,
126+ },
127+ );
128+ }
129+
130+ /// Where a tiled window on `o` is drawn at `now`.
131+ pub fn tiled(&self, o: OutputId, id: WindowId, now: u64) -> Option<Rect> {
132+ self.tracks.get(&o)?.sample(id, now)
133+ }
134+
135+ /// Glides a window that changed between tiled, floating and fullscreen.
136+ pub fn glide(&mut self, id: WindowId, from: Rect, to: Rect, now: u64) {
137+ let phases = if self.phase_ns() == 0 { 0 } else { 2 };
138+ self.glides.insert(
139+ id,
140+ Glide {
141+ from,
142+ to,
143+ timeline: Timeline {
144+ start_ns: now,
145+ phase_ns: self.phase_ns(),
146+ phases,
147+ },
148+ curve: self.base(),
149+ },
150+ );
151+ }
152+
153+ pub fn stop_glide(&mut self, id: WindowId) {
154+ self.glides.remove(&id);
155+ }
156+
157+ pub fn gliding(&self, id: WindowId, now: u64) -> Option<Rect> {
158+ let g = self.glides.get(&id)?;
159+ match g.timeline.locate(now) {
160+ Position::Finished => None,
161+ Position::NotStarted => Some(g.from),
162+ Position::Phase { phase, t } => {
163+ let total = g.timeline.phases as f64;
164+ let s = g.curve.sample((phase as f64 + t) / total);
165+ Some(g.from.lerp(&g.to, s))
166+ }
167+ }
168+ }
169+
170+ pub fn active(&self, now: u64) -> bool {
171+ self.tracks.values().any(|t| t.timeline.end_ns() > now)
172+ || self.glides.values().any(|g| g.timeline.end_ns() > now)
173+ }
174+
175+ pub fn prune(&mut self, now: u64) {
176+ self.glides.retain(|_, g| g.timeline.end_ns() > now);
177+ }
178+}
179+
180+impl Track {
181+ fn sample(&self, id: WindowId, now: u64) -> Option<Rect> {
182+ let i = self.ids.iter().position(|&x| x == id)?;
183+ let keys = &self.plan.keyframes;
184+ Some(match self.timeline.locate(now) {
185+ Position::NotStarted => keys.first()?[i],
186+ Position::Finished => keys.last()?[i],
187+ Position::Phase { phase, t } => {
188+ let curve = if phase == 0 { self.first[i] } else { self.base };
189+ keys[phase][i].lerp(&keys[phase + 1][i], curve.sample(t))
190+ }
191+ })
192+ }
193+
194+ /// Edge velocities in logical pixels per second.
195+ fn velocity(&self, id: WindowId, now: u64) -> Edges {
196+ let (Some(a), Some(b)) = (
197+ self.sample(id, now.saturating_sub(VELOCITY_DT_NS)),
198+ self.sample(id, now),
199+ ) else {
200+ return [0.0; 4];
201+ };
202+ let d = sub(edges(&b), edges(&a));
203+ d.map(|x| x * 1e9 / VELOCITY_DT_NS as f64)
204+ }
205+}
206+
207+fn edges(r: &Rect) -> Edges {
208+ [r.x, r.right(), r.y, r.bottom()]
209+}
210+
211+fn sub(a: Edges, b: Edges) -> Edges {
212+ [a[0] - b[0], a[1] - b[1], a[2] - b[2], a[3] - b[3]]
213+}
Modified file: crates/monolith/src/commands.rs+1 additions
@@ -181,6 +181,7 @@ impl State {
181181 let keyboard_changed = config.keyboard != self.config.keyboard;
182182 self.binds = Binds::new(&config);
183183 self.layout.gaps = crate::state::gaps(&config);
184+ self.anim.settings = config.animation.clone();
184185 self.config = config;
185186 if keyboard_changed {
186187 self.reconfigure_keyboard();
Modified file: crates/monolith/src/main.rs+2 additions
@@ -1,9 +1,11 @@
1+mod anim;
12 mod commands;
23 mod handlers;
34 mod input;
45 mod ipc;
56 mod launch;
67 mod nested;
8+mod scene;
79 mod state;
810
911 use monolith_ipc::{Command, Query, Request};
Modified file: crates/monolith/src/nested.rs+20 additions−16 deletions
@@ -11,8 +11,6 @@ use smithay::backend::allocator::dmabuf::Dmabuf;
1111 use smithay::backend::allocator::{Format, Fourcc, Modifier, Slot, Swapchain};
1212 use smithay::backend::renderer::Bind;
1313 use smithay::backend::renderer::damage::OutputDamageTracker;
14-use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
15-use smithay::desktop::space::render_output;
1614 use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
1715 use smithay::reexports::calloop::EventLoop;
1816 use smithay::utils::{Logical, Point, Size, Transform};
@@ -148,6 +146,9 @@ pub fn render(state: &mut State) {
148146 space,
149147 needs_redraw,
150148 start,
149+ layout,
150+ windows,
151+ anim,
151152 ..
152153 } = state;
153154 let Some(host) = host.as_mut() else { return };
@@ -181,6 +182,7 @@ pub fn render(state: &mut State) {
181182 return;
182183 }
183184 };
185+ let now = crate::state::now_ns();
184186 let mut dmabuf = (*slot).clone();
185187 let damage = {
186188 let mut fb = match renderer.bind(&mut dmabuf) {
@@ -190,18 +192,11 @@ pub fn render(state: &mut State) {
190192 return;
191193 }
192194 };
193- let elements: &[WaylandSurfaceRenderElement<monolith_render_vk::VulkanRenderer>] = &[];
194- match render_output(
195- &host.output,
196- renderer,
197- &mut fb,
198- 1.0,
199- slot.age() as usize,
200- [&*space],
201- elements,
202- &mut host.damage,
203- CLEAR,
204- ) {
195+ let elements = crate::scene::build(layout, windows, anim, renderer, now);
196+ match host
197+ .damage
198+ .render_output(renderer, &mut fb, slot.age() as usize, &elements, CLEAR)
199+ {
205200 Ok(result) => {
206201 if let Err(e) = result.sync.wait() {
207202 tracing::warn!("waiting for the frame failed: {e:?}");
@@ -214,14 +209,23 @@ pub fn render(state: &mut State) {
214209 }
215210 }
216211 };
217- *needs_redraw = false;
212+ *needs_redraw = anim.active(now);
213+ anim.prune(now);
218214 let elapsed = start.elapsed();
219215 for window in space.elements() {
220216 window.send_frame(&host.output, elapsed, Some(Duration::ZERO), |_, _| {
221217 Some(host.output.clone())
222218 });
223219 }
224- let Some(damage) = damage else { return };
220+ let Some(damage) = damage else {
221+ if *needs_redraw {
222+ host.surface.frame(&host.qh, ());
223+ host.surface.commit();
224+ host.frame_pending = true;
225+ let _ = host.conn.flush();
226+ }
227+ return;
228+ };
225229 let buffer = match slot.userdata().get::<HostBuffer>() {
226230 Some(b) => b.0.clone(),
227231 None => {
Added file: crates/monolith/src/scene.rs+142 additions
@@ -0,0 +1,142 @@
1+//! What a frame draws. The space holds final geometry for input, while the
2+//! scene draws windows where the animator says they are right now.
3+
4+use crate::anim::Animator;
5+use monolith_layout::{Layout, Rect, WindowId};
6+use monolith_render_vk::{VulkanRenderer, VulkanTexture};
7+use smithay::backend::renderer::Renderer;
8+use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
9+use smithay::backend::renderer::element::texture::TextureRenderElement;
10+use smithay::backend::renderer::element::utils::CropRenderElement;
11+use smithay::backend::renderer::element::{AsRenderElements, Id, Kind};
12+use smithay::backend::renderer::utils::with_renderer_surface_state;
13+use smithay::desktop::Window;
14+use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size};
15+use std::collections::HashMap;
16+
17+smithay::backend::renderer::element::render_elements! {
18+ pub Draw<=VulkanRenderer>;
19+ Surface=WaylandSurfaceRenderElement<VulkanRenderer>,
20+ Cropped=CropRenderElement<WaylandSurfaceRenderElement<VulkanRenderer>>,
21+ Texture=TextureRenderElement<VulkanTexture>,
22+}
23+
24+/// Elements for every output, front to back.
25+pub fn build(
26+ layout: &Layout,
27+ windows: &HashMap<WindowId, Window>,
28+ anim: &Animator,
29+ renderer: &mut VulkanRenderer,
30+ now: u64,
31+) -> Vec<Draw> {
32+ let mut tiled = Vec::new();
33+ let mut above = Vec::new();
34+ for (o, out) in layout.outputs() {
35+ let arr = layout.arrange(o);
36+ let fullscreen_at_rest = arr
37+ .fullscreen
38+ .is_some_and(|w| anim.gliding(w, now).is_none());
39+ for p in &arr.tiled {
40+ if Some(p.window) == arr.fullscreen || fullscreen_at_rest {
41+ continue;
42+ }
43+ match anim.gliding(p.window, now) {
44+ Some(r) => above.push((p.window, r)),
45+ None => {
46+ let r = anim.tiled(o, p.window, now).unwrap_or(p.rect);
47+ tiled.push((p.window, r));
48+ }
49+ }
50+ }
51+ for &(w, r) in &arr.floating {
52+ above.push((w, anim.gliding(w, now).unwrap_or(r)));
53+ }
54+ if let Some(w) = arr.fullscreen {
55+ above.push((w, anim.gliding(w, now).unwrap_or(out.area)));
56+ }
57+ }
58+ let mut out = Vec::new();
59+ for (id, r) in tiled.into_iter().chain(above).rev() {
60+ if let Some(window) = windows.get(&id) {
61+ window_elements(&mut out, renderer, window, r);
62+ }
63+ }
64+ out
65+}
66+
67+/// Draws `window` filling `r`. A buffer bigger than `r` is cropped, and one
68+/// smaller has its last row and column stretched over the rest, so a resize
69+/// in flight never shows a scaled or missing client.
70+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 {
75+ return;
76+ }
77+ let geo = window.geometry();
78+ let loc = Point::<i32, Physical>::from((x - geo.loc.x, y - geo.loc.y));
79+ let elems: Vec<WaylandSurfaceRenderElement<VulkanRenderer>> =
80+ window.render_elements(renderer, loc, Scale::from(1.0), 1.0);
81+ if (w, h) == (geo.size.w, geo.size.h) {
82+ out.extend(elems.into_iter().map(Draw::Surface));
83+ return;
84+ }
85+ let crop = Rectangle::new((x, y).into(), (w, h).into());
86+ out.extend(
87+ elems
88+ .into_iter()
89+ .filter_map(|e| CropRenderElement::from_element(e, 1.0, crop))
90+ .map(Draw::Cropped),
91+ );
92+ let Some(root) = window.toplevel().map(|t| t.wl_surface().clone()) else {
93+ return;
94+ };
95+ let context = renderer.context_id();
96+ let Some((texture, scale, transform)) = with_renderer_surface_state(&root, |s| {
97+ s.texture::<VulkanTexture>(context.clone())
98+ .cloned()
99+ .map(|t| (t, s.buffer_scale(), s.buffer_transform()))
100+ })
101+ .flatten() else {
102+ return;
103+ };
104+ let (gw, gh) = (geo.size.w, geo.size.h);
105+ let (cw, ch) = (w.min(gw), h.min(gh));
106+ let (gx, gy) = (geo.loc.x as f64, geo.loc.y as f64);
107+ let mut strip = |src: Rectangle<f64, Logical>, at: (i32, i32), size: (i32, i32)| {
108+ if size.0 < 1 || size.1 < 1 {
109+ return;
110+ }
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+ )));
124+ };
125+ let right = gx + f64::from(gw) - 1.0;
126+ let bottom = gy + f64::from(gh) - 1.0;
127+ strip(
128+ Rectangle::new((right, gy).into(), (1.0, f64::from(ch)).into()),
129+ (x + gw, y),
130+ (w - gw, ch),
131+ );
132+ strip(
133+ Rectangle::new((gx, bottom).into(), (f64::from(cw), 1.0).into()),
134+ (x, y + gh),
135+ (cw, h - gh),
136+ );
137+ strip(
138+ Rectangle::new((right, bottom).into(), (1.0, 1.0).into()),
139+ (x + gw, y + gh),
140+ (w - gw, h - gh),
141+ );
142+}
Modified file: crates/monolith/src/state.rs+71 additions−8 deletions
@@ -1,3 +1,4 @@
1+use crate::anim::Animator;
12 use crate::commands::Binds;
23 use crate::nested::Host;
34 use monolith_config::Config;
@@ -14,7 +15,7 @@ use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::Sta
1415 use smithay::reexports::wayland_server::backend::{ClientData, ClientId, DisconnectReason};
1516 use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
1617 use smithay::reexports::wayland_server::{Display, DisplayHandle, Resource};
17-use smithay::utils::{Logical, Point, SERIAL_COUNTER, Size};
18+use smithay::utils::{Clock, Logical, Monotonic, Point, SERIAL_COUNTER, Size};
1819 use smithay::wayland::compositor::{CompositorClientState, CompositorState, with_states};
1920 use smithay::wayland::dmabuf::{DmabufGlobal, DmabufState};
2021 use smithay::wayland::output::OutputManagerState;
@@ -27,7 +28,7 @@ use std::collections::HashMap;
2728 use std::ffi::OsString;
2829 use std::path::PathBuf;
2930 use std::sync::Arc;
30-use std::time::Instant;
31+use std::time::{Duration, Instant};
3132
3233 pub struct State {
3334 pub start: Instant,
@@ -54,6 +55,10 @@ pub struct State {
5455 pub ipc: crate::ipc::Server,
5556 pub launches: crate::launch::Launches,
5657 pub workspaces: WorkspaceState,
58+ pub anim: Animator,
59+ /// How each shown window was placed by the last layout pass, to spot
60+ /// switches between tiled, floating and fullscreen.
61+ pub last_kind: HashMap<WindowId, (Kind, Rect, OutputId)>,
5762
5863 pub compositor_state: CompositorState,
5964 pub xdg_shell_state: XdgShellState,
@@ -111,6 +116,7 @@ impl State {
111116 let mut layout = Layout::default();
112117 layout.gaps = gaps(&config);
113118 let binds = Binds::new(&config);
119+ let animation = config.animation.clone();
114120 Ok(Self {
115121 start: Instant::now(),
116122 socket_name,
@@ -132,6 +138,8 @@ impl State {
132138 ipc: crate::ipc::Server::default(),
133139 launches: Default::default(),
134140 workspaces: WorkspaceState::new::<State>(&dh),
141+ anim: Animator::new(animation),
142+ last_kind: HashMap::new(),
135143 compositor_state: CompositorState::new::<State>(&dh),
136144 xdg_shell_state: XdgShellState::new::<State>(&dh),
137145 _xdg_decoration_state: XdgDecorationState::new::<State>(&dh),
@@ -226,27 +234,78 @@ impl State {
226234
227235 /// Pushes the layout's target geometry into the space and to clients.
228236 pub fn apply_layout(&mut self) {
237+ let now = now_ns();
229238 let mut shown = Vec::new();
239+ let mut tracks = Vec::new();
230240 let outputs: Vec<OutputId> = self.layout.outputs().map(|(o, _)| o).collect();
231241 for o in outputs {
232242 let arr = self.layout.arrange(o);
233243 let area = self.layout.output(o).map(|o| o.area).unwrap_or_default();
234244 for p in &arr.tiled {
235245 if p.visible && arr.fullscreen != Some(p.window) {
236- shown.push((p.window, p.rect, Kind::Tiled));
246+ shown.push((p.window, p.rect, Kind::Tiled, o));
237247 }
238248 }
239249 for &(w, rect) in &arr.floating {
240250 if arr.fullscreen != Some(w) {
241- shown.push((w, rect, Kind::Floating));
251+ shown.push((w, rect, Kind::Floating, o));
242252 }
243253 }
244254 if let Some(w) = arr.fullscreen {
245- shown.push((w, area, Kind::Fullscreen));
255+ shown.push((w, area, Kind::Fullscreen, o));
246256 }
257+ let targets: Vec<(WindowId, Rect)> =
258+ arr.tiled.iter().map(|p| (p.window, p.rect)).collect();
259+ tracks.push((o, targets));
247260 }
261+
262+ let mut glides = Vec::new();
263+ for &(id, rect, kind, _) in &shown {
264+ match self.last_kind.get(&id) {
265+ Some(&(was, from, o)) if was != kind => {
266+ let from = self
267+ .anim
268+ .gliding(id, now)
269+ .or_else(|| {
270+ (was == Kind::Tiled)
271+ .then(|| self.anim.tiled(o, id, now))
272+ .flatten()
273+ })
274+ .unwrap_or(from);
275+ glides.push((id, from, rect));
276+ }
277+ Some(&(Kind::Floating, from, _)) if from != rect => self.anim.stop_glide(id),
278+ _ => {}
279+ }
280+ }
281+ let gap = self.layout.gaps.inner;
282+ for (o, targets) in tracks {
283+ let new: Vec<WindowId> = targets
284+ .iter()
285+ .map(|t| t.0)
286+ .filter(|&id| self.anim.tiled(o, id, now).is_none())
287+ .collect();
288+ let seeds: HashMap<WindowId, Rect> = if new.is_empty() {
289+ HashMap::new()
290+ } else {
291+ self.layout
292+ .arrange_hiding(o, &new)
293+ .tiled
294+ .into_iter()
295+ .map(|p| (p.window, p.rect))
296+ .collect()
297+ };
298+ self.anim.retarget(o, now, &targets, gap, |id| {
299+ seeds.get(&id).copied().unwrap_or_default()
300+ });
301+ }
302+ for (id, from, to) in glides {
303+ self.anim.glide(id, from, to, now);
304+ }
305+ self.last_kind = shown.iter().map(|&(id, r, k, o)| (id, (k, r, o))).collect();
306+
248307 let mut mapped = Vec::new();
249- for (id, rect, kind) in shown {
308+ for (id, rect, kind, _) in shown {
250309 if let Some(window) = self.windows.get(&id)
251310 && configure(window, rect, kind)
252311 {
@@ -350,8 +409,8 @@ impl State {
350409 }
351410 }
352411
353-#[derive(Clone, Copy, PartialEq, Eq)]
354-enum Kind {
412+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
413+pub enum Kind {
355414 Tiled,
356415 Floating,
357416 Fullscreen,
@@ -395,6 +454,10 @@ fn configure(window: &Window, rect: Rect, kind: Kind) -> bool {
395454 true
396455 }
397456
457+pub fn now_ns() -> u64 {
458+ Duration::from(Clock::<Monotonic>::new().now()).as_nanos() as u64
459+}
460+
398461 pub fn identity(t: &smithay::wayland::shell::xdg::ToplevelSurface) -> (String, String) {
399462 with_states(t.wl_surface(), |s| {
400463 let data = s