b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 1 | use crate::geom::{Change, Rect}; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 2 | use crate::strategy::plan_group; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 3 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 4 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 5 | pub enum Style { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 6 | /// Every window moves or resizes along one axis per phase. |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 7 | Orthogonal, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 8 | /// Windows may change both axes in one phase when no pair loses its separation. |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 9 | Direct, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 10 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 11 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 12 | #[derive(Clone, Copy, Debug, PartialEq)] |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 13 | pub struct Config { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 14 | pub style: Style, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 15 | /// Gap between tiled windows. Neighbours closer than this are adjacent. |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 16 | pub gap: f64, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 17 | /// Tolerance for separation and adjacency tests. |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 18 | pub eps: f64, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 19 | /// Lanes thinner than this are not used. |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 20 | pub min_lane: f64, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 21 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 22 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 23 | impl Default for Config { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 24 | fn default() -> Self { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 25 | Self { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 26 | style: Style::Orthogonal, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 27 | gap: 10.0, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 28 | eps: 0.5, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 29 | min_lane: 6.0, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 30 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 31 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 32 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 33 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 34 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 35 | pub enum Strategy { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 36 | Unchanged, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 37 | Direct, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 38 | AxisOrder, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 39 | Lanes { lanes: usize }, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 40 | Grid { cells: usize }, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 41 | Pinch, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 42 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 43 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 44 | /// Windows that plan together. Groups never touch each other while moving. |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 45 | #[derive(Clone, Debug, PartialEq)] |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 46 | pub struct Group { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 47 | pub members: Vec<usize>, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 48 | pub strategy: Strategy, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 49 | pub phases: usize, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 50 | /// Whether the plan briefly separates two neighbours that touch at both ends. |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 51 | pub opens_hole: bool, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 52 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 53 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 54 | /// Keyframe layouts indexed like the input. Every phase lasts the same time |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 55 | /// and every window in a group moves on that group's curve. |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 56 | #[derive(Clone, Debug, PartialEq)] |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 57 | pub struct Plan { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 58 | pub keyframes: Vec<Vec<Rect>>, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 59 | pub groups: Vec<Group>, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 60 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 61 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 62 | impl Plan { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 63 | pub fn phases(&self) -> usize { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 64 | self.keyframes.len().saturating_sub(1) |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 65 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 66 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 67 | /// Window positions at `phase` with eased progress `s`. |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 68 | pub fn sample(&self, phase: usize, s: f64) -> Vec<Rect> { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 69 | match (self.keyframes.get(phase), self.keyframes.get(phase + 1)) { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 70 | (Some(a), Some(b)) => a.iter().zip(b).map(|(a, b)| a.lerp(b, s)).collect(), |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 71 | _ => self.keyframes.last().cloned().unwrap_or_default(), |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 72 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 73 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 74 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 75 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 76 | pub(crate) struct GroupPlan { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 77 | pub keys: Vec<Vec<Rect>>, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 78 | pub strategy: Strategy, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 79 | pub opens_hole: bool, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 80 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 81 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 82 | impl GroupPlan { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 83 | pub fn reversed(mut self) -> Self { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 84 | self.keys.reverse(); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 85 | self |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 86 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 87 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 88 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 89 | /// Plans the transition from `from` to `to`. Both slices index the same windows. |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 90 | pub fn plan(from: &[Rect], to: &[Rect], cfg: &Config) -> Plan { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 91 | assert_eq!( |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 92 | from.len(), |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 93 | to.len(), |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 94 | "layouts must describe the same windows" |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 95 | ); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 96 | let mut groups = Vec::new(); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 97 | let mut plans = Vec::new(); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 98 | for members in motion_groups(from, to, cfg) { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 99 | let f: Vec<Rect> = members.iter().map(|&i| from[i]).collect(); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 100 | let t: Vec<Rect> = members.iter().map(|&i| to[i]).collect(); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 101 | let gp = plan_canonical(&f, &t, cfg); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 102 | groups.push(Group { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 103 | members: members.clone(), |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 104 | strategy: gp.strategy, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 105 | phases: gp.keys.len() - 1, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 106 | opens_hole: gp.opens_hole, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 107 | }); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 108 | plans.push((members, gp)); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 109 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 110 | let phases = groups.iter().map(|g| g.phases).max().unwrap_or(0); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 111 | let mut keyframes = vec![to.to_vec(); phases + 1]; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 112 | for (members, gp) in &plans { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 113 | for (k, frame) in keyframes.iter_mut().enumerate() { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 114 | let key = &gp.keys[k.min(gp.keys.len() - 1)]; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 115 | for (local, &i) in members.iter().enumerate() { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 116 | frame[i] = key[local]; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 117 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 118 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 119 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 120 | Plan { keyframes, groups } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 121 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 122 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 123 | /// Plans in one fixed direction per pair of layouts, so planning back plays |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 124 | /// the same plan in reverse. |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 125 | pub(crate) fn plan_canonical(from: &[Rect], to: &[Rect], cfg: &Config) -> GroupPlan { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 126 | if canonical_order(from, to).is_le() { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 127 | plan_group(from, to, cfg) |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 128 | } else { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 129 | plan_group(to, from, cfg).reversed() |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 130 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 131 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 132 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 133 | fn canonical_order(a: &[Rect], b: &[Rect]) -> std::cmp::Ordering { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 134 | let q = |v: f64| (v * 10.0).round() as i64; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 135 | let key = |r: &Rect| [q(r.x), q(r.y), q(r.w), q(r.h)]; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 136 | a.iter().map(key).cmp(b.iter().map(key)) |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 137 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 138 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 139 | /// Partitions windows so that windows in different groups can never meet. |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 140 | /// A moving window joins every window its swept area comes within a gap of. |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 141 | fn motion_groups(from: &[Rect], to: &[Rect], cfg: &Config) -> Vec<Vec<usize>> { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 142 | let n = from.len(); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 143 | let swept: Vec<Rect> = (0..n) |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 144 | .map(|i| Rect::bounding([&from[i], &to[i]]).expanded(cfg.gap / 2.0 + cfg.eps)) |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 145 | .collect(); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 146 | let moves: Vec<bool> = (0..n) |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 147 | .map(|i| Change::between(&from[i], &to[i]).moves()) |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 148 | .collect(); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 149 | let mut parent: Vec<usize> = (0..n).collect(); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 150 | fn find(parent: &mut [usize], mut i: usize) -> usize { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 151 | while parent[i] != i { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 152 | parent[i] = parent[parent[i]]; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 153 | i = parent[i]; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 154 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 155 | i |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 156 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 157 | for i in 0..n { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 158 | for j in i + 1..n { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 159 | if (moves[i] || moves[j]) && swept[i].intersects(&swept[j]) { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 160 | let (a, b) = (find(&mut parent, i), find(&mut parent, j)); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 161 | parent[a] = b; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 162 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 163 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 164 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 165 | let mut groups: Vec<Vec<usize>> = Vec::new(); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 166 | let mut slot = vec![usize::MAX; n]; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 167 | for i in 0..n { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 168 | let root = find(&mut parent, i); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 169 | if slot[root] == usize::MAX { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 170 | slot[root] = groups.len(); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 171 | groups.push(Vec::new()); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 172 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 173 | groups[slot[root]].push(i); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 174 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 175 | groups |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 176 | } |