atagen

monolith

public
Star0
Watch0

Commit bbe43ba0672c

tarball

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

3 files changed+63-2
anim: slide a tiled window sent to another output across instead of dropping it

The source output's track lost the window outright, so it vanished in place, and an empty target grew it from its far short edge like a new window. A non-fading ghost now carries it off an output it leaves empty, and an empty target starts it where it was drawn. Both are limited to outputs with nothing else tiled, since crossing live neighbours forces the planner into a pinch.
Modified file: crates/monolith/src/anim.rs+8 additions−1 deletions
@@ -29,6 +29,9 @@ pub struct Ghost {
2929 /// Fades where it is for one phase instead of shrinking in the plan,
3030 /// like a dismissed scratchpad.
3131 pub fixed: Option<Rect>,
32+ /// Fades as it goes. A window sent to another output leaves one that
33+ /// slides off without fading, since the window lives on over there.
34+ pub closing: bool,
3235 pub opacity: f32,
3336 }
3437
@@ -221,7 +224,11 @@ impl Animator {
221224
222225 pub fn ghost_effect(&self, g: &Ghost, now: u64) -> Effect {
223226 Effect {
224- kind: Transition::Close,
227+ kind: if g.closing {
228+ Transition::Close
229+ } else {
230+ Transition::Move
231+ },
225232 progress: self.progress(g.born, now).unwrap_or(1.0),
226233 }
227234 }
Modified file: crates/monolith/src/ghosts.rs+23 additions
@@ -72,6 +72,7 @@ impl State {
7272 seed: seed.rect,
7373 born: now_ns(),
7474 fixed: None,
75+ closing: true,
7576 opacity,
7677 });
7778 }
@@ -92,6 +93,28 @@ impl State {
9293 seed: rect,
9394 born: now_ns(),
9495 fixed: Some(rect),
96+ closing: true,
97+ opacity,
98+ });
99+ }
100+ }
101+
102+ /// Slides a tiled window sent to another output off the one it left,
103+ /// towards where it lands.
104+ pub(crate) fn slide_out(&mut self, id: WindowId, output: OutputId, to: Rect) {
105+ let Some(window) = self.wm.window(id).cloned() else {
106+ return;
107+ };
108+ let opacity = self.wm.get(id).and_then(|s| s.rule.opacity).unwrap_or(1.0);
109+ if let Some(frame) = self.last_frame(&window) {
110+ self.wm.last_frames.insert(id, frame);
111+ self.wm.anim.add_ghost(Ghost {
112+ id,
113+ output,
114+ seed: to,
115+ born: now_ns(),
116+ fixed: None,
117+ closing: false,
95118 opacity,
96119 });
97120 }
Modified file: crates/monolith/src/state.rs+32 additions−1 deletions
@@ -377,6 +377,28 @@ impl State {
377377 _ => {}
378378 }
379379 }
380+ let sent: Vec<(WindowId, OutputId, Rect, Rect)> = tracks
381+ .iter()
382+ .flat_map(|(o, targets)| targets.iter().map(move |&(id, to)| (*o, id, to)))
383+ .filter_map(|(o, id, to)| match self.wm.get(id)?.placed? {
384+ (Kind::Tiled, rect, was) if was != o => {
385+ let from = self.wm.anim.tiled(was, id, now).unwrap_or(rect);
386+ Some((id, was, from, to))
387+ }
388+ _ => None,
389+ })
390+ .collect();
391+ let empty = |o: OutputId| {
392+ tracks
393+ .iter()
394+ .filter(|t| t.0 == o)
395+ .flat_map(|t| &t.1)
396+ .all(|&(_, r)| r.w <= 0.0 || r.h <= 0.0)
397+ };
398+ let leaving: Vec<_> = sent.iter().filter(|s| empty(s.1)).copied().collect();
399+ for (id, was, _, to) in leaving {
400+ self.slide_out(id, was, to);
401+ }
380402 let gap = self.wm.layout.gaps.inner;
381403 for (o, targets) in tracks {
382404 let new: Vec<WindowId> = targets
@@ -395,8 +417,17 @@ impl State {
395417 .map(|p| (p.window, p.rect))
396418 .collect()
397419 };
420+ let alone = |id| {
421+ targets
422+ .iter()
423+ .all(|&(t, r)| t == id || r.w <= 0.0 || r.h <= 0.0)
424+ };
398425 self.wm.anim.retarget(o, now, &targets, gap, |id| {
399- seeds.get(&id).copied().unwrap_or_default()
426+ sent.iter()
427+ .find(|s| s.0 == id && alone(id))
428+ .map(|s| s.2)
429+ .or_else(|| seeds.get(&id).copied())
430+ .unwrap_or_default()
400431 });
401432 }
402433 for (id, from, to) in glides {