1use std::collections::{HashMap, HashSet};
2
3use crate::{DesktopError, IconAnimationSpec, IconSnapshot, MonitorInfo, Point, Rect};
4
5#[derive(Clone, Debug, PartialEq)]
8pub struct DesktopInfo {
9 pub bounds: Rect,
10 pub monitors: Vec<MonitorInfo>,
11 pub grids: Vec<IconGrid>,
12}
13
14#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct IconGrid {
19 pub monitor_id: String,
20 pub work_area: Rect,
21 pub icon_size: Point,
22 pub cell_size: Point,
23 pub origin: Option<Point>,
24 pub origin_inferred: bool,
25 pub columns: u32,
26 pub rows: u32,
27}
28
29impl IconGrid {
30 fn footprint(&self) -> Point {
31 Point::new(self.cell_size.x.max(self.icon_size.x), self.cell_size.y.max(self.icon_size.y))
32 }
33
34 pub fn new(
35 monitor_id: String,
36 work_area: Rect,
37 icon_size: Point,
38 cell_size: Point,
39 origin: Option<Point>,
40 origin_inferred: bool,
41 ) -> Result<Self, DesktopError> {
42 if cell_size.x <= 0 || cell_size.y <= 0 || icon_size.x <= 0 || icon_size.y <= 0
43 || work_area.right <= work_area.left || work_area.bottom <= work_area.top
44 {
45 return Err(DesktopError::InvalidGrid("non-positive desktop geometry".into()));
46 }
47 let (columns, rows) = if let Some(origin) = origin {
48 if !work_area.contains(origin) {
49 return Err(DesktopError::InvalidGrid("grid origin outside work area".into()));
50 }
51 (
52 ((i64::from(work_area.right) - i64::from(origin.x)) / i64::from(cell_size.x)) as u32,
53 ((i64::from(work_area.bottom) - i64::from(origin.y)) / i64::from(cell_size.y)) as u32,
54 )
55 } else {
56 (0, 0)
57 };
58 Ok(Self { monitor_id, work_area, icon_size, cell_size, origin, origin_inferred, columns, rows })
59 }
60
61 pub fn infer_origin(work: Rect, spacing: Point, positions: &[Point]) -> Option<Point> {
64 if spacing.x <= 0 || spacing.y <= 0 { return None; }
65 let mut counts = HashMap::new();
66 for position in positions.iter().filter(|position| work.contains(**position)) {
67 let phase = (
68 (i64::from(position.x) - i64::from(work.left)).rem_euclid(i64::from(spacing.x)),
69 (i64::from(position.y) - i64::from(work.top)).rem_euclid(i64::from(spacing.y)),
70 );
71 *counts.entry(phase).or_insert(0usize) += 1;
72 }
73 let maximum = counts.values().copied().max()?;
74 let mut winners = counts.into_iter().filter(|(_, count)| *count == maximum);
75 let ((horizontal, vertical), _) = winners.next()?;
76 if winners.next().is_some() { return None; }
77 Some(Point::new(
78 i32::try_from(i64::from(work.left) + horizontal).ok()?,
79 i32::try_from(i64::from(work.top) + vertical).ok()?,
80 ))
81 }
82}
83
84fn overlaps(left: Point, left_size: Point, right: Point, right_size: Point) -> bool {
85 (i64::from(left.x) - i64::from(right.x)).abs() < i64::from(left_size.x.max(right_size.x))
86 && (i64::from(left.y) - i64::from(right.y)).abs() < i64::from(left_size.y.max(right_size.y))
87}
88
89fn distance_to_rect(point: Point, bounds: Rect) -> i128 {
90 let horizontal = i128::from(point.x) - i128::from(point.x.clamp(bounds.left, bounds.right - 1));
91 let vertical = i128::from(point.y) - i128::from(point.y.clamp(bounds.top, bounds.bottom - 1));
92 horizontal * horizontal + vertical * vertical
93}
94
95pub fn resolve_grid_targets(
99 specs: &mut [IconAnimationSpec],
100 icons: &[IconSnapshot],
101 desktop: &DesktopInfo,
102) -> Result<(), DesktopError> {
103 let mut ids = HashSet::new();
104 for spec in specs.iter() {
105 if !ids.insert(&spec.id) {
106 return Err(DesktopError::InvalidGrid(format!("duplicate animation icon {}", spec.id)));
107 }
108 }
109 let existing: HashSet<_> = icons.iter().map(|icon| &icon.id).collect();
110 if specs.iter().all(|spec| !existing.contains(&spec.id)) { return Ok(()); }
111 if desktop.grids.is_empty() {
112 return Err(DesktopError::InvalidGrid("desktop has no icon grids".into()));
113 }
114 for grid in &desktop.grids {
115 let checked = IconGrid::new(grid.monitor_id.clone(), grid.work_area, grid.icon_size,
116 grid.cell_size, grid.origin, grid.origin_inferred)?;
117 if checked.columns != grid.columns || checked.rows != grid.rows
118 || u64::from(grid.columns) * u64::from(grid.rows) > 1_000_000
119 {
120 return Err(DesktopError::InvalidGrid("invalid or excessive grid capacity".into()));
121 }
122 }
123 let mut occupied: Vec<_> = icons.iter().filter(|icon| !ids.contains(&icon.id))
124 .map(|icon| {
125 let size = desktop.grids.iter()
126 .min_by_key(|grid| distance_to_rect(icon.position, grid.work_area))
127 .unwrap().footprint();
128 (icon.position, size)
129 }).collect();
130 let mut targets = Vec::with_capacity(specs.len());
131 for spec in specs.iter() {
132 if !existing.contains(&spec.id) {
133 targets.push(spec.target);
134 continue;
135 }
136 let grid = desktop.grids.iter()
137 .min_by_key(|grid| distance_to_rect(spec.target, grid.work_area)).unwrap();
138 let origin = grid.origin.ok_or_else(|| DesktopError::InvalidGrid(
139 format!("grid origin unavailable for {}", grid.monitor_id)))?;
140 let footprint = grid.footprint();
141 let mut best: Option<(i128, Point)> = None;
142 for column in 0..grid.columns {
143 for row in 0..grid.rows {
144 let candidate = Point::new(
145 (i64::from(origin.x) + i64::from(column) * i64::from(grid.cell_size.x)) as i32,
146 (i64::from(origin.y) + i64::from(row) * i64::from(grid.cell_size.y)) as i32,
147 );
148 if i64::from(candidate.x) + i64::from(footprint.x) > i64::from(grid.work_area.right)
149 || i64::from(candidate.y) + i64::from(footprint.y) > i64::from(grid.work_area.bottom)
150 || occupied.iter().any(|(point, size)| overlaps(candidate, footprint, *point, *size)) {
151 continue;
152 }
153 let horizontal = i128::from(candidate.x) - i128::from(spec.target.x);
154 let vertical = i128::from(candidate.y) - i128::from(spec.target.y);
155 let distance = horizontal * horizontal + vertical * vertical;
156 if best.is_none_or(|(previous, _)| distance < previous) {
157 best = Some((distance, candidate));
158 }
159 }
160 }
161 let target = best.ok_or_else(|| DesktopError::InvalidGrid(
162 format!("no free grid cell on {} for {}", grid.monitor_id, spec.id)))?.1;
163 occupied.push((target, footprint));
164 targets.push(target);
165 }
166 for (spec, target) in specs.iter_mut().zip(targets) { spec.target = target; }
167 Ok(())
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use crate::{Curve, Duration};
174
175 fn icon(id: &str, position: Point) -> IconSnapshot {
176 IconSnapshot::new(id.into(), id, None, false, position)
177 }
178
179 fn spec(id: &str, target: Point) -> IconAnimationSpec {
180 IconAnimationSpec::new(id.into(), target, Duration::fixed(std::time::Duration::ZERO), Curve::linear())
181 }
182
183 fn desktop() -> DesktopInfo {
184 let bounds = Rect::new(-300, -200, 0, 0);
185 DesktopInfo { bounds, monitors: vec![], grids: vec![IconGrid::new(
186 "left".into(), bounds, Point::new(40, 40), Point::new(100, 100),
187 Some(Point::new(-300, -200)), false).unwrap()] }
188 }
189
190 #[test]
191 fn grid_snaps_conflicts_and_protects_off_grid_stationary_icons() {
192 let desktop = desktop();
193 let icons = vec![icon("first", Point::new(-300, -200)),
194 icon("second", Point::new(-300, -100)), icon("fixed", Point::new(-150, -150))];
195 let mut specs = vec![spec("first", Point::new(-120, -110)), spec("second", Point::new(-120, -110))];
196 resolve_grid_targets(&mut specs, &icons, &desktop).unwrap();
197 assert_eq!(specs[0].target, Point::new(-300, -100));
198 assert_eq!(specs[1].target, Point::new(-300, -200));
199 }
200
201 #[test]
202 fn grid_failure_is_atomic_and_missing_icons_do_not_consume_cells() {
203 let mut desktop = desktop();
204 desktop.grids[0] = IconGrid::new("tiny".into(), Rect::new(0, 0, 100, 100),
205 Point::new(40, 40), Point::new(100, 100), Some(Point::new(0, 0)), false).unwrap();
206 let icons = vec![icon("first", Point::new(0, 0)), icon("second", Point::new(10, 10))];
207 let mut specs = vec![spec("first", Point::new(12, 12)), spec("second", Point::new(22, 22))];
208 assert!(resolve_grid_targets(&mut specs, &icons, &desktop).is_err());
209 assert_eq!(specs[0].target, Point::new(12, 12));
210 let mut specs = vec![spec("missing", Point::new(12, 12)), spec("first", Point::new(22, 22))];
211 resolve_grid_targets(&mut specs, &icons[..1], &desktop).unwrap();
212 assert_eq!(specs[1].target, Point::new(0, 0));
213 }
214
215 #[test]
216 fn grid_allows_swaps_and_clamps_extreme_targets() {
217 let icons = vec![icon("first", Point::new(-300, -200)), icon("second", Point::new(-100, -100))];
218 let mut specs = vec![spec("first", icons[1].position), spec("second", icons[0].position)];
219 resolve_grid_targets(&mut specs, &icons, &desktop()).unwrap();
220 assert_eq!(specs[0].target, icons[1].position);
221 assert_eq!(specs[1].target, icons[0].position);
222 specs[0].target = Point::new(i32::MAX, i32::MIN);
223 resolve_grid_targets(&mut specs, &icons, &desktop()).unwrap();
224 assert_eq!(specs[0].target, Point::new(-100, -200));
225 }
226
227 #[test]
228 fn grid_infers_phase_without_requiring_first_row_or_column() {
229 let work = Rect::new(-400, -300, 0, 0);
230 let spacing = Point::new(100, 100);
231 let points = [Point::new(-180, -90), Point::new(-80, -190), Point::new(-65, -70)];
232 assert_eq!(IconGrid::infer_origin(work, spacing, &points), Some(Point::new(-380, -290)));
233 assert_eq!(IconGrid::infer_origin(work, spacing, &points[1..]), None);
234 assert_eq!(IconGrid::infer_origin(work, spacing, &[]), None);
235 }
236
237 #[test]
238 fn grid_uses_target_monitor_spacing_and_reserves_large_icon_slots() {
239 let mut desktop = desktop();
240 desktop.grids.push(IconGrid::new("right".into(), Rect::new(0, 0, 600, 600),
241 Point::new(240, 240), Point::new(200, 200), Some(Point::new(0, 0)), false).unwrap());
242 let icons = vec![icon("first", Point::new(-300, -200)), icon("second", Point::new(-300, -100))];
243 let mut specs = vec![spec("first", Point::new(1, 1)), spec("second", Point::new(1, 1))];
244 assert!(resolve_grid_targets(&mut specs, &icons, &desktop).is_err());
245 desktop.grids[1] = IconGrid::new("right".into(), Rect::new(0, 0, 800, 800),
246 Point::new(240, 240), Point::new(200, 200), Some(Point::new(0, 0)), false).unwrap();
247 resolve_grid_targets(&mut specs, &icons, &desktop).unwrap();
248 assert_eq!(specs[0].target, Point::new(0, 0));
249 assert_eq!(specs[1].target, Point::new(0, 400));
250 }
251
252 #[test]
253 fn grid_rejects_duplicates_unknown_origins_and_invalid_sizes() {
254 let icons = vec![icon("first", Point::new(-300, -200))];
255 let mut specs = vec![spec("first", icons[0].position), spec("first", icons[0].position)];
256 assert!(resolve_grid_targets(&mut specs, &icons, &desktop()).is_err());
257 specs.pop();
258 let mut desktop = desktop();
259 desktop.grids[0].origin = None;
260 desktop.grids[0].columns = 0;
261 desktop.grids[0].rows = 0;
262 assert!(resolve_grid_targets(&mut specs, &icons, &desktop).is_err());
263 desktop.grids[0].cell_size.x = 0;
264 assert!(resolve_grid_targets(&mut specs, &icons, &desktop).is_err());
265 }
266}