1use gpui::{Axis, SharedString};
6use serde::{Deserialize, Serialize};
7
8#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
23#[non_exhaustive]
24pub struct GridArea {
25 pub id: SharedString,
27 pub col: usize,
29 pub row: usize,
31 pub col_span: usize,
33 pub row_span: usize,
35 #[serde(default = "one")]
37 pub min_col_span: usize,
38 #[serde(default = "one")]
40 pub min_row_span: usize,
41 #[serde(default)]
43 pub max_col_span: Option<usize>,
44 #[serde(default)]
46 pub max_row_span: Option<usize>,
47 #[serde(default)]
50 pub locked: bool,
51}
52
53fn one() -> usize {
54 1
55}
56
57impl GridArea {
58 pub fn new(id: impl Into<SharedString>) -> Self {
61 Self {
62 id: id.into(),
63 col: 0,
64 row: 0,
65 col_span: 1,
66 row_span: 1,
67 min_col_span: 1,
68 min_row_span: 1,
69 max_col_span: None,
70 max_row_span: None,
71 locked: false,
72 }
73 }
74
75 pub fn col(mut self, col: usize) -> Self {
77 self.col = col;
78 self
79 }
80
81 pub fn row(mut self, row: usize) -> Self {
83 self.row = row;
84 self
85 }
86
87 pub fn col_span(mut self, col_span: usize) -> Self {
89 self.col_span = col_span.max(1);
90 self
91 }
92
93 pub fn row_span(mut self, row_span: usize) -> Self {
95 self.row_span = row_span.max(1);
96 self
97 }
98
99 pub fn min_span(mut self, col_span: usize, row_span: usize) -> Self {
101 self.min_col_span = col_span.max(1);
102 self.min_row_span = row_span.max(1);
103 self
104 }
105
106 pub fn max_span(mut self, col_span: usize, row_span: usize) -> Self {
108 self.max_col_span = Some(col_span.max(1));
109 self.max_row_span = Some(row_span.max(1));
110 self
111 }
112
113 pub fn locked(mut self, locked: bool) -> Self {
115 self.locked = locked;
116 self
117 }
118
119 pub(crate) fn row_end(&self) -> usize {
121 self.row + self.row_span
122 }
123
124 pub(crate) fn col_end(&self) -> usize {
126 self.col + self.col_span
127 }
128
129 fn rect(&self) -> Rect {
130 Rect {
131 col: self.col,
132 row: self.row,
133 col_span: self.col_span,
134 row_span: self.row_span,
135 }
136 }
137
138 pub(crate) fn clamp_size(&self, w: usize, h: usize, columns: usize) -> (usize, usize) {
140 let max_col_span = self
141 .max_col_span
142 .unwrap_or(usize::MAX)
143 .min(columns.saturating_sub(self.col))
144 .max(1);
145 let max_row_span = self.max_row_span.unwrap_or(usize::MAX).max(1);
146 (
147 w.clamp(self.min_col_span.min(max_col_span), max_col_span),
148 h.clamp(self.min_row_span.min(max_row_span), max_row_span),
149 )
150 }
151}
152
153#[derive(Clone, Copy, Debug, PartialEq, Eq)]
154struct Rect {
155 col: usize,
156 row: usize,
157 col_span: usize,
158 row_span: usize,
159}
160
161impl Rect {
162 fn overlaps(&self, other: &Rect) -> bool {
163 self.col < other.col + other.col_span
164 && other.col < self.col + self.col_span
165 && self.row < other.row + other.row_span
166 && other.row < self.row + self.row_span
167 }
168}
169
170#[derive(Clone, Copy, Debug, PartialEq, Eq)]
172pub(crate) struct LayoutRules {
173 pub columns: usize,
174 pub compaction: Option<Axis>,
176 pub prevent_collision: bool,
179}
180
181pub(crate) fn rows(items: &[GridArea]) -> usize {
183 items.iter().map(GridArea::row_end).max().unwrap_or(0)
184}
185
186pub(crate) fn normalize(items: &mut [GridArea], rules: LayoutRules) {
189 let columns = rules.columns.max(1);
190 for item in items.iter_mut() {
191 let max_col_span = item.max_col_span.unwrap_or(usize::MAX).max(1);
194 let max_row_span = item.max_row_span.unwrap_or(usize::MAX).max(1);
195 item.col_span = item
196 .col_span
197 .clamp(item.min_col_span.min(max_col_span), max_col_span);
198 item.row_span = item
199 .row_span
200 .clamp(item.min_row_span.min(max_row_span), max_row_span);
201 item.col_span = item.col_span.clamp(1, columns);
202 item.row_span = item.row_span.max(1);
203 if item.col_end() > columns {
204 item.col = columns - item.col_span;
205 }
206 }
207
208 let locked = sorted_indices(items, rules.compaction)
211 .into_iter()
212 .filter(|&ix| items[ix].locked)
213 .collect::<Vec<_>>();
214 for (n, &ix) in locked.iter().enumerate() {
215 while let Some(other) = first_collision(items, &locked[..n], items[ix].rect(), ix) {
216 items[ix].row = items[other].row_end();
217 }
218 }
219
220 settle(items, rules, None);
221}
222
223fn settle(items: &mut [GridArea], rules: LayoutRules, priority: Option<usize>) {
227 if rules.compaction.is_some() {
228 compact(items, rules);
229 return;
230 }
231 let mut placed = (0..items.len())
232 .filter(|&ix| items[ix].locked)
233 .collect::<Vec<_>>();
234 if let Some(priority) = priority.filter(|&ix| !items[ix].locked) {
235 placed.push(priority);
236 }
237 for ix in sorted_indices(items, None) {
238 if placed.contains(&ix) {
239 continue;
240 }
241 while let Some(other) = first_collision(items, &placed, items[ix].rect(), ix) {
242 items[ix].row = items[other].row_end();
243 }
244 placed.push(ix);
245 }
246}
247
248pub(crate) fn reflowed(areas: &[GridArea], columns: usize) -> Vec<GridArea> {
254 let mut reflowed = areas.to_vec();
255 reflowed.sort_by_key(|area| (area.row, area.col));
259 for area in &mut reflowed {
260 area.locked = false;
261 }
262 normalize(
263 &mut reflowed,
264 LayoutRules {
265 columns,
266 compaction: Some(Axis::Vertical),
267 prevent_collision: false,
268 },
269 );
270 reflowed
271}
272
273pub(crate) fn compact(items: &mut [GridArea], rules: LayoutRules) {
275 let Some(axis) = rules.compaction else {
276 return;
277 };
278 let columns = rules.columns.max(1);
279 let mut placed = (0..items.len())
280 .filter(|&ix| items[ix].locked)
281 .collect::<Vec<_>>();
282 for ix in sorted_indices(items, Some(axis)) {
283 if items[ix].locked {
284 continue;
285 }
286 compact_item(items, &placed, ix, axis, columns);
287 placed.push(ix);
288 }
289}
290
291fn compact_item(items: &mut [GridArea], placed: &[usize], ix: usize, axis: Axis, columns: usize) {
292 match axis {
293 Axis::Vertical => {
294 let floor = placed
297 .iter()
298 .map(|&p| items[p].row_end())
299 .max()
300 .unwrap_or(0);
301 items[ix].row = items[ix].row.min(floor);
302 loop {
303 while items[ix].row > 0 {
304 let mut probe = items[ix].rect();
305 probe.row -= 1;
306 if first_collision(items, placed, probe, ix).is_some() {
307 break;
308 }
309 items[ix].row -= 1;
310 }
311 match first_collision(items, placed, items[ix].rect(), ix) {
312 Some(other) => items[ix].row = items[other].row_end(),
313 None => break,
314 }
315 }
316 }
317 Axis::Horizontal => loop {
318 while items[ix].col > 0 {
319 let mut probe = items[ix].rect();
320 probe.col -= 1;
321 if first_collision(items, placed, probe, ix).is_some() {
322 break;
323 }
324 items[ix].col -= 1;
325 }
326 if items[ix].col_end() > columns {
329 items[ix].col = 0;
330 items[ix].row += 1;
331 continue;
332 }
333 match first_collision(items, placed, items[ix].rect(), ix) {
334 Some(other) => {
335 items[ix].col = items[other].col_end();
336 if items[ix].col_end() > columns {
337 items[ix].col = 0;
338 items[ix].row += 1;
339 }
340 }
341 None => break,
342 }
343 },
344 }
345}
346
347pub(crate) fn move_item(
354 items: &[GridArea],
355 id: &str,
356 x: usize,
357 y: usize,
358 rules: LayoutRules,
359) -> Option<Vec<GridArea>> {
360 let ix = items.iter().position(|item| item.id.as_ref() == id)?;
361 if items[ix].locked {
362 return None;
363 }
364 let mut next = items.to_vec();
365 let x = x.min(rules.columns.saturating_sub(next[ix].col_span));
366 if rules.prevent_collision {
367 let mut probe = next[ix].rect();
368 probe.col = x;
369 probe.row = y;
370 if any_collision(&next, probe, ix) {
371 return None;
372 }
373 }
374 Push::new(rules, next.len()).move_element(&mut next, ix, Some(x), Some(y), true);
375 settle(&mut next, rules, Some(ix));
376 Some(next)
377}
378
379pub(crate) fn resize_item(
383 items: &[GridArea],
384 id: &str,
385 w: usize,
386 h: usize,
387 rules: LayoutRules,
388) -> Option<Vec<GridArea>> {
389 let ix = items.iter().position(|item| item.id.as_ref() == id)?;
390 if items[ix].locked {
391 return None;
392 }
393 let mut next = items.to_vec();
394 let (w, h) = next[ix].clamp_size(w, h, rules.columns);
395
396 let start = next[ix].clone();
399 let fits = |w: usize, h: usize, next: &[GridArea]| {
400 let probe = Rect {
401 col_span: w,
402 row_span: h,
403 ..start.rect()
404 };
405 !next
406 .iter()
407 .enumerate()
408 .any(|(other, item)| other != ix && item.locked && probe.overlaps(&item.rect()))
409 };
410 let (w, h) = [(w, h), (w, start.row_span), (start.col_span, h)]
411 .into_iter()
412 .find(|&(w, h)| fits(w, h, &next))?;
413
414 next[ix].col_span = w;
415 next[ix].row_span = h;
416 if rules.prevent_collision && any_collision(&next, next[ix].rect(), ix) {
417 return None;
418 }
419
420 let mut push = Push::new(rules, next.len());
421 push.moved[ix] = true;
422 for other in collisions(&next, sorted_indices(&next, rules.compaction), ix) {
423 let below = next[ix].row_end();
424 push.move_element(&mut next, other, None, Some(below), false);
425 }
426 settle(&mut next, rules, Some(ix));
427 Some(next)
428}
429
430struct Push {
433 rules: LayoutRules,
434 moved: Vec<bool>,
436}
437
438impl Push {
439 fn new(rules: LayoutRules, len: usize) -> Self {
440 Self {
441 rules,
442 moved: vec![false; len],
443 }
444 }
445
446 fn move_element(
447 &mut self,
448 items: &mut [GridArea],
449 ix: usize,
450 x: Option<usize>,
451 y: Option<usize>,
452 user_action: bool,
453 ) {
454 if items[ix].locked {
455 return;
456 }
457 let (old_x, old_y) = (items[ix].col, items[ix].row);
458 let x = x.unwrap_or(old_x);
459 let y = y.unwrap_or(old_y);
460 if (x, y) == (old_x, old_y) {
461 return;
462 }
463 items[ix].col = x;
464 items[ix].row = y;
465 self.moved[ix] = true;
466
467 let mut order = sorted_indices(items, self.rules.compaction);
470 let moving_back = match self.rules.compaction {
471 Some(Axis::Horizontal) => old_x >= x,
472 _ => old_y >= y,
473 };
474 if moving_back {
475 order.reverse();
476 }
477
478 for other in collisions(items, order, ix) {
479 if self.moved[other] {
480 continue;
481 }
482 if items[other].locked {
483 self.away(items, other, ix, user_action);
485 } else {
486 self.away(items, ix, other, user_action);
487 }
488 }
489 }
490
491 fn away(&mut self, items: &mut [GridArea], obstacle: usize, mover: usize, user_action: bool) {
493 let horizontal = self.rules.compaction == Some(Axis::Horizontal);
494 if user_action {
495 let (o, m) = (&items[obstacle], &items[mover]);
499 let probe = Rect {
500 col: if horizontal {
501 o.col.saturating_sub(m.col_span)
502 } else {
503 m.col
504 },
505 row: if horizontal {
506 m.row
507 } else {
508 o.row.saturating_sub(m.row_span)
509 },
510 col_span: m.col_span,
511 row_span: m.row_span,
512 };
513 let blocker = (0..items.len()).find(|&other| {
514 other != mover && other != obstacle && probe.overlaps(&items[other].rect())
515 });
516 let obstacle_rect = items[obstacle].rect();
517 match blocker {
518 None if !probe.overlaps(&obstacle_rect) => {
519 let (x, y) = if horizontal {
520 (Some(probe.col), None)
521 } else {
522 (None, Some(probe.row))
523 };
524 return self.move_element(items, mover, x, y, false);
525 }
526 _ => {}
527 }
528 }
529
530 let (x, y) = if horizontal {
532 (Some(items[obstacle].col_end()), None)
533 } else {
534 (None, Some(items[obstacle].row_end()))
535 };
536 self.move_element(items, mover, x, y, false);
537 }
538}
539
540fn sorted_indices(items: &[GridArea], compaction: Option<Axis>) -> Vec<usize> {
543 let mut order = (0..items.len()).collect::<Vec<_>>();
544 match compaction {
545 Some(Axis::Horizontal) => order.sort_by_key(|&ix| (items[ix].col, items[ix].row)),
546 _ => order.sort_by_key(|&ix| (items[ix].row, items[ix].col)),
547 }
548 order
549}
550
551fn first_collision(items: &[GridArea], among: &[usize], rect: Rect, skip: usize) -> Option<usize> {
552 among
553 .iter()
554 .copied()
555 .find(|&other| other != skip && rect.overlaps(&items[other].rect()))
556}
557
558fn any_collision(items: &[GridArea], rect: Rect, skip: usize) -> bool {
559 items
560 .iter()
561 .enumerate()
562 .any(|(other, item)| other != skip && rect.overlaps(&item.rect()))
563}
564
565fn collisions(items: &[GridArea], order: Vec<usize>, ix: usize) -> Vec<usize> {
566 let rect = items[ix].rect();
567 order
568 .into_iter()
569 .filter(|&other| other != ix && rect.overlaps(&items[other].rect()))
570 .collect()
571}
572
573#[cfg(test)]
574pub(crate) mod tests {
575 use super::*;
576
577 pub(crate) fn area(
579 id: impl Into<SharedString>,
580 col: usize,
581 row: usize,
582 col_span: usize,
583 row_span: usize,
584 ) -> GridArea {
585 GridArea::new(id)
586 .col(col)
587 .row(row)
588 .col_span(col_span)
589 .row_span(row_span)
590 }
591
592 fn rules(compaction: Option<Axis>) -> LayoutRules {
593 LayoutRules {
594 columns: 12,
595 compaction,
596 prevent_collision: false,
597 }
598 }
599
600 fn at<'a>(items: &'a [GridArea], id: &str) -> &'a GridArea {
601 items.iter().find(|item| item.id.as_ref() == id).unwrap()
602 }
603
604 fn pos(items: &[GridArea], id: &str) -> (usize, usize) {
605 let item = at(items, id);
606 (item.col, item.row)
607 }
608
609 fn assert_no_overlaps(items: &[GridArea]) {
610 for (a, first) in items.iter().enumerate() {
611 for second in &items[a + 1..] {
612 assert!(
613 !first.rect().overlaps(&second.rect()),
614 "{} overlaps {}",
615 first.id,
616 second.id
617 );
618 }
619 }
620 }
621
622 #[test]
623 fn test_grid_area_builder() {
624 let item = GridArea::new("a")
625 .col(1)
626 .row(2)
627 .col_span(0)
628 .row_span(3)
629 .min_span(2, 0)
630 .max_span(6, 4)
631 .locked(true);
632 assert_eq!(
633 (item.col, item.row, item.col_span, item.row_span),
634 (1, 2, 1, 3)
635 );
636 assert_eq!((item.min_col_span, item.min_row_span), (2, 1));
637 assert_eq!((item.max_col_span, item.max_row_span), (Some(6), Some(4)));
638 assert!(item.locked);
639 assert_eq!((item.col_end(), item.row_end()), (2, 5));
640 }
641
642 #[test]
643 fn vertical_compaction_floats_items_up() {
644 let mut items = vec![
645 area("a", 0, 3, 4, 2),
646 area("b", 0, 9, 4, 2),
647 area("c", 4, 7, 4, 1),
648 ];
649 compact(&mut items, rules(Some(Axis::Vertical)));
650 assert_eq!(pos(&items, "a"), (0, 0));
651 assert_eq!(pos(&items, "b"), (0, 2));
652 assert_eq!(pos(&items, "c"), (4, 0));
653 }
654
655 #[test]
656 fn horizontal_compaction_floats_items_leading() {
657 let mut items = vec![area("a", 5, 0, 2, 2), area("b", 9, 0, 2, 2)];
658 compact(&mut items, rules(Some(Axis::Horizontal)));
659 assert_eq!(pos(&items, "a"), (0, 0));
660 assert_eq!(pos(&items, "b"), (2, 0));
661 }
662
663 #[test]
664 fn compaction_flows_around_locked_items() {
665 let mut items = vec![
666 area("wall", 0, 0, 12, 1).locked(true),
667 area("a", 0, 0, 4, 2),
668 ];
669 normalize(&mut items, rules(Some(Axis::Vertical)));
670 assert_eq!(pos(&items, "wall"), (0, 0));
671 assert_eq!(pos(&items, "a"), (0, 1));
672 }
673
674 #[test]
675 fn normalize_fits_items_to_the_columns_and_removes_overlaps() {
676 let mut items = vec![
677 area("wide", 4, 0, 20, 1),
678 area("a", 0, 0, 2, 1),
679 area("b", 0, 0, 2, 1),
680 ];
681 normalize(&mut items, rules(None));
682 assert_eq!(at(&items, "wide").col_span, 12);
683 assert_eq!(at(&items, "wide").col, 0);
684 assert_no_overlaps(&items);
685 }
686
687 #[test]
688 fn normalize_applies_changed_size_limits() {
689 let mut items = vec![area("a", 0, 0, 2, 6).min_span(4, 1).max_span(12, 3)];
690 normalize(&mut items, rules(None));
691 assert_eq!((items[0].col_span, items[0].row_span), (4, 3));
692 }
693
694 #[test]
698 fn grid_area_json_is_a_stable_format() {
699 let saved = r#"{"id":"a","col":1,"row":2,"col_span":3,"row_span":4}"#;
700 let item: GridArea = serde_json::from_str(saved).unwrap();
701 assert_eq!(item, area("a", 1, 2, 3, 4));
702 assert_eq!(
703 serde_json::to_value(area("a", 1, 2, 3, 4).locked(true)).unwrap(),
704 serde_json::json!({
705 "id": "a", "col": 1, "row": 2, "col_span": 3, "row_span": 4,
706 "min_col_span": 1, "min_row_span": 1,
707 "max_col_span": null, "max_row_span": null, "locked": true
708 })
709 );
710 }
711
712 #[test]
713 fn reflowing_stacks_a_copy_in_reading_order() {
714 let areas = vec![
715 area("b", 6, 0, 6, 2),
716 area("a", 0, 0, 6, 3).min_span(4, 1),
717 area("c", 0, 3, 12, 1).locked(true),
718 ];
719 let stacked = reflowed(&areas, 1);
720 assert_eq!(pos(&stacked, "a"), (0, 0));
722 assert_eq!(pos(&stacked, "b"), (0, 3));
723 assert_eq!(pos(&stacked, "c"), (0, 5));
724 assert!(stacked.iter().all(|area| area.col_span == 1));
725 assert_no_overlaps(&stacked);
726 assert_eq!(at(&areas, "a").col_span, 6);
728 assert!(at(&areas, "c").locked);
729 }
730
731 #[test]
732 fn moving_onto_an_item_pushes_it_down() {
733 let items = vec![area("a", 0, 0, 4, 2), area("b", 4, 0, 4, 2)];
734 let next = move_item(&items, "a", 4, 1, rules(None)).unwrap();
735 assert_eq!(pos(&next, "a"), (4, 1));
736 assert_eq!(pos(&next, "b"), (4, 3));
737 assert_no_overlaps(&next);
738 }
739
740 #[test]
741 fn moving_up_onto_an_item_swaps_places() {
742 let items = vec![area("a", 0, 0, 4, 2), area("b", 0, 2, 4, 2)];
743 let next = move_item(&items, "b", 0, 0, rules(Some(Axis::Vertical))).unwrap();
744 assert_eq!(pos(&next, "b"), (0, 0));
745 assert_eq!(pos(&next, "a"), (0, 2));
746 assert_no_overlaps(&next);
747 }
748
749 #[test]
750 fn moves_and_resizes_never_leave_overlaps() {
751 let mut items = (0..7)
752 .map(|n| area(format!("item-{n}"), (n % 3) * 4, (n / 3) * 2, 4, 2 + n % 2))
753 .collect::<Vec<_>>();
754 items.push(area("wall", 5, 5, 2, 1).locked(true));
755 for compaction in [None, Some(Axis::Vertical), Some(Axis::Horizontal)] {
756 let rules = rules(compaction);
757 let mut start = items.clone();
758 normalize(&mut start, rules);
759 assert_no_overlaps(&start);
760 for n in 0..7 {
761 let id = format!("item-{n}");
762 for (x, y) in (0..12).flat_map(|x| (0..9).map(move |y| (x, y))) {
763 let next = move_item(&start, &id, x, y, rules).unwrap();
764 assert_no_overlaps(&next);
765 assert_eq!(pos(&next, "wall"), pos(&start, "wall"));
766 if let Some(next) = resize_item(&start, &id, x + 1, y + 1, rules) {
767 assert_no_overlaps(&next);
768 }
769 }
770 }
771 }
772 }
773
774 #[test]
775 fn a_move_is_clamped_to_the_columns() {
776 let items = vec![area("a", 0, 0, 4, 2)];
777 let next = move_item(&items, "a", 11, 0, rules(None)).unwrap();
778 assert_eq!(pos(&next, "a"), (8, 0));
779 }
780
781 #[test]
782 fn locked_items_neither_move_nor_get_pushed() {
783 let items = vec![area("wall", 4, 0, 4, 2).locked(true), area("a", 0, 0, 4, 2)];
784 assert!(move_item(&items, "wall", 0, 4, rules(None)).is_none());
785 let next = move_item(&items, "a", 4, 0, rules(Some(Axis::Vertical))).unwrap();
786 assert_eq!(pos(&next, "wall"), (4, 0));
787 assert_no_overlaps(&next);
788 }
789
790 #[test]
791 fn prevent_collision_refuses_overlapping_moves() {
792 let items = vec![area("a", 0, 0, 4, 2), area("b", 4, 0, 4, 2)];
793 let rules = LayoutRules {
794 prevent_collision: true,
795 ..rules(None)
796 };
797 assert!(move_item(&items, "a", 2, 0, rules).is_none());
798 assert_eq!(
799 pos(&move_item(&items, "a", 0, 3, rules).unwrap(), "a"),
800 (0, 3)
801 );
802 }
803
804 #[test]
805 fn resize_respects_limits_and_pushes_neighbours() {
806 let items = vec![
807 area("a", 0, 0, 4, 2).min_span(2, 2).max_span(8, 4),
808 area("b", 4, 0, 4, 2),
809 ];
810 let next = resize_item(&items, "a", 20, 1, rules(Some(Axis::Vertical))).unwrap();
811 assert_eq!((at(&next, "a").col_span, at(&next, "a").row_span), (8, 2));
812 assert_eq!(pos(&next, "b"), (4, 2));
813 assert_no_overlaps(&next);
814 }
815
816 #[test]
817 fn resize_stops_at_locked_items() {
818 let items = vec![area("a", 0, 0, 4, 2), area("wall", 4, 0, 4, 2).locked(true)];
819 let next = resize_item(&items, "a", 6, 3, rules(None)).unwrap();
820 assert_eq!((at(&next, "a").col_span, at(&next, "a").row_span), (4, 3));
821 assert_no_overlaps(&next);
822 }
823}