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