1use crate::{Floors, Rect, Settings};
28
29#[derive(Clone, Debug)]
31pub struct Column {
32 pub windows: Vec<u64>,
34 pub active: usize,
36 pub width: f64,
38}
39
40impl Column {
41 fn new(id: u64, width: f64) -> Self {
42 Self {
43 windows: vec![id],
44 active: 0,
45 width,
46 }
47 }
48}
49
50pub const PRESETS: [f64; 3] = [1.0 / 3.0, 0.5, 2.0 / 3.0];
60
61#[derive(Clone, Debug)]
63pub struct Scroller {
64 columns: Vec<Column>,
65 active: usize,
66 view_offset: f64,
68 widths: Vec<f64>,
79 preset: usize,
80 floors: Floors,
83}
84
85impl Default for Scroller {
86 fn default() -> Self {
87 Self {
88 columns: Vec::new(),
89 active: 0,
90 view_offset: 0.0,
91 widths: PRESETS.to_vec(),
92 preset: 0,
96 floors: Floors::new(),
97 }
98 }
99}
100
101impl Scroller {
102 #[must_use]
103 pub fn new() -> Self {
104 Self::default()
105 }
106
107 #[must_use]
123 pub fn with_widths(widths: &[f64], start: usize) -> Self {
124 let widths: Vec<f64> = if widths.is_empty() {
125 PRESETS.to_vec()
126 } else {
127 widths.to_vec()
128 };
129 let preset = start.min(widths.len() - 1);
130 Self {
131 widths,
132 preset,
133 ..Self::default()
134 }
135 }
136
137 fn width_now(&self) -> f64 {
144 self.widths
145 .get(self.preset)
146 .or_else(|| self.widths.first())
147 .copied()
148 .unwrap_or(PRESETS[0])
149 }
150
151 #[must_use]
152 pub fn is_empty(&self) -> bool {
153 self.columns.is_empty()
154 }
155
156 #[must_use]
157 pub fn columns(&self) -> &[Column] {
158 &self.columns
159 }
160
161 #[must_use]
162 pub fn active_column(&self) -> usize {
163 self.active
164 }
165
166 #[must_use]
168 pub fn focused(&self) -> Option<u64> {
169 let column = self.columns.get(self.active)?;
170 column.windows.get(column.active).copied()
171 }
172
173 #[must_use]
174 pub fn contains(&self, id: u64) -> bool {
175 self.columns
176 .iter()
177 .any(|column| column.windows.contains(&id))
178 }
179
180 pub fn set_floors(&mut self, floors: Floors) {
189 self.floors = floors;
190 }
191
192 fn floor_of(&self, index: usize) -> f64 {
194 self.columns.get(index).map_or(0.0, |column| {
195 column
196 .windows
197 .iter()
198 .filter_map(|id| self.floors.get(id))
199 .map(|floor| floor.w)
200 .filter(|width| width.is_finite())
201 .fold(0.0, f64::max)
202 })
203 }
204
205 fn width_of(&self, index: usize, view: Rect) -> f64 {
213 let share = self
214 .columns
215 .get(index)
216 .map_or(0.0, |column| (view.w * column.width).max(1.0));
217 share.max(self.floor_of(index))
218 }
219
220 pub fn insert(&mut self, id: u64, area: Rect, settings: Settings) {
222 if self.contains(id) {
223 return;
224 }
225 let width = self.width_now();
226 let at = if self.columns.is_empty() {
227 0
228 } else {
229 self.active + 1
230 };
231 self.columns.insert(at, Column::new(id, width));
232 self.focus_column(at, area, settings);
233 }
234
235 pub fn insert_into_active(&mut self, id: u64, area: Rect, settings: Settings) {
237 if self.contains(id) {
238 return;
239 }
240 let Some(column) = self.columns.get_mut(self.active) else {
241 self.insert(id, area, settings);
242 return;
243 };
244 column.windows.push(id);
245 column.active = column.windows.len() - 1;
246 }
247
248 pub fn remove(&mut self, id: u64) {
249 let Some(index) = self
250 .columns
251 .iter()
252 .position(|column| column.windows.contains(&id))
253 else {
254 return;
255 };
256 let column = &mut self.columns[index];
257 column.windows.retain(|other| *other != id);
258 if column.windows.is_empty() {
259 self.columns.remove(index);
260 if self.active >= self.columns.len() {
261 self.active = self.columns.len().saturating_sub(1);
262 }
263 } else {
264 column.active = column.active.min(column.windows.len() - 1);
265 }
266 }
267
268 pub fn focus_window(&mut self, id: u64, area: Rect, settings: Settings) {
275 let Some(column) = self
276 .columns
277 .iter()
278 .position(|column| column.windows.contains(&id))
279 else {
280 return;
281 };
282 if let Some(row) = self.columns[column].windows.iter().position(|w| *w == id) {
283 self.columns[column].active = row;
284 }
285 self.focus_column(column, area, settings);
286 }
287
288 pub fn focus_sideways(&mut self, by: isize, area: Rect, settings: Settings) {
290 if self.columns.is_empty() {
291 return;
292 }
293 let last = self.columns.len() - 1;
294 let target = usize::try_from(
295 isize::try_from(self.active)
296 .unwrap_or(0)
297 .saturating_add(by)
298 .clamp(0, isize::try_from(last).unwrap_or(0)),
299 )
300 .unwrap_or(0);
301 self.focus_column(target, area, settings);
302 }
303
304 pub fn focus_vertically(&mut self, by: isize) {
306 let Some(column) = self.columns.get_mut(self.active) else {
307 return;
308 };
309 let last = column.windows.len().saturating_sub(1);
310 column.active = usize::try_from(
311 isize::try_from(column.active)
312 .unwrap_or(0)
313 .saturating_add(by)
314 .clamp(0, isize::try_from(last).unwrap_or(0)),
315 )
316 .unwrap_or(0);
317 }
318
319 pub fn move_vertically(&mut self, by: isize) -> bool {
325 let Some(column) = self.columns.get_mut(self.active) else {
326 return false;
327 };
328 let Some(to) = column
329 .active
330 .checked_add_signed(by)
331 .filter(|to| *to < column.windows.len())
332 else {
333 return false;
334 };
335 if to == column.active {
336 return false;
337 }
338 column.windows.swap(column.active, to);
339 column.active = to;
340 true
341 }
342
343 pub fn move_column(&mut self, by: isize, area: Rect, settings: Settings) -> bool {
347 if self.columns.len() < 2 {
348 return false;
349 }
350 let last = self.columns.len() - 1;
351 let Ok(from) = isize::try_from(self.active) else {
352 return false;
353 };
354 let to = usize::try_from(
355 from.saturating_add(by)
356 .clamp(0, isize::try_from(last).unwrap_or(0)),
357 )
358 .unwrap_or(0);
359 if to == self.active {
360 return false;
361 }
362 self.columns.swap(self.active, to);
363 self.focus_column(to, area, settings);
364 true
365 }
366
367 pub fn consume(&mut self) {
369 let next = self.active + 1;
370 let Some(taken) = self.columns.get_mut(next).and_then(|column| {
371 let index = column.active.min(column.windows.len().saturating_sub(1));
372 (!column.windows.is_empty()).then(|| column.windows.remove(index))
373 }) else {
374 return;
375 };
376 if self.columns[next].windows.is_empty() {
377 self.columns.remove(next);
378 }
379 if let Some(column) = self.columns.get_mut(self.active) {
380 column.windows.push(taken);
381 column.active = column.windows.len() - 1;
382 }
383 }
384
385 pub fn expel(&mut self, area: Rect, settings: Settings) {
387 let Some(column) = self.columns.get_mut(self.active) else {
388 return;
389 };
390 if column.windows.len() < 2 {
391 return;
392 }
393 let index = column.active.min(column.windows.len() - 1);
394 let taken = column.windows.remove(index);
395 column.active = column.active.min(column.windows.len() - 1);
396 let width = column.width;
397 let at = self.active + 1;
398 self.columns.insert(at, Column::new(taken, width));
399 self.focus_column(at, area, settings);
400 }
401
402 pub fn move_to_column_of(&mut self, id: u64, target: u64, area: Rect, settings: Settings) {
408 if id == target {
409 return;
410 }
411 let Some(to) = self
412 .columns
413 .iter()
414 .position(|column| column.windows.contains(&target))
415 else {
416 return;
417 };
418 let Some(from) = self
419 .columns
420 .iter()
421 .position(|column| column.windows.contains(&id))
422 else {
423 return;
424 };
425 if from == to {
426 return;
427 }
428
429 self.columns[from].windows.retain(|other| *other != id);
430 let emptied = self.columns[from].windows.is_empty();
431 if emptied {
432 self.columns.remove(from);
433 } else {
434 let column = &mut self.columns[from];
435 column.active = column.active.min(column.windows.len() - 1);
436 }
437
438 let to = if emptied && to > from { to - 1 } else { to };
441 let Some(column) = self.columns.get_mut(to) else {
442 return;
443 };
444 let at = column
445 .windows
446 .iter()
447 .position(|other| *other == target)
448 .map_or(column.windows.len(), |index| index + 1);
449 column.windows.insert(at, id);
450 column.active = at;
451 self.focus_column(to, area, settings);
452 }
453
454 pub fn widen(&mut self, id: u64, by: f64, area: Rect, settings: Settings) {
461 let Some(index) = self
462 .columns
463 .iter()
464 .position(|column| column.windows.contains(&id))
465 else {
466 return;
467 };
468 let view = area.inset(settings.gap);
480 let least = (self.floor_of(index) / view.w.max(1.0)).clamp(0.1, 1.0);
481 let column = &mut self.columns[index];
482 column.width = (column.width.max(least) + by).clamp(least, 1.0);
483 self.focus_column(index, area, settings);
484 }
485
486 pub fn cycle_width(&mut self, area: Rect, settings: Settings) {
488 self.preset = (self.preset + 1) % self.widths.len();
489 let width = self.width_now();
490 if let Some(column) = self.columns.get_mut(self.active) {
491 column.width = width;
492 }
493 let active = self.active;
494 self.focus_column(active, area, settings);
495 }
496
497 pub fn configure_like(&mut self, like: &Self) {
507 self.widths.clone_from(&like.widths);
508 self.preset = like.preset;
509 }
510
511 pub fn scroll_by(&mut self, delta: f64) {
513 self.view_offset += delta;
514 }
515
516 #[must_use]
518 pub fn layout(&self, area: Rect, settings: Settings) -> Vec<(u64, Rect)> {
519 let view = area.inset(settings.gap);
520 let origin = self.view_position(view, settings);
521 let mut out = Vec::new();
522
523 for (index, column) in self.columns.iter().enumerate() {
524 let x = view.x + self.column_x(index, view, settings) - origin;
525 let width = self.width_of(index, view);
526 let count = column.windows.len().max(1);
527 #[expect(clippy::cast_precision_loss, reason = "a column holds a handful")]
528 let rows = count as f64;
529 let height = ((view.h - settings.gap * (rows - 1.0)) / rows).max(1.0);
530
531 for (row, id) in column.windows.iter().enumerate() {
532 #[expect(clippy::cast_precision_loss, reason = "as above")]
533 let row = row as f64;
534 out.push((
535 *id,
536 Rect::new(x, view.y + row * (height + settings.gap), width, height),
537 ));
538 }
539 }
540 out
541 }
542
543 fn view_position(&self, view: Rect, settings: Settings) -> f64 {
545 self.column_x(self.active, view, settings) + self.view_offset
546 }
547
548 fn column_x(&self, index: usize, view: Rect, settings: Settings) -> f64 {
550 (0..index.min(self.columns.len()))
551 .map(|each| self.width_of(each, view) + settings.gap)
552 .sum()
553 }
554
555 fn focus_column(&mut self, index: usize, area: Rect, settings: Settings) {
562 if self.columns.is_empty() {
563 return;
564 }
565 let index = index.min(self.columns.len() - 1);
566 let view = area.inset(settings.gap);
567
568 let old_x = self.column_x(self.active, view, settings);
569 let new_x = self.column_x(index, view, settings);
570 self.view_offset += old_x - new_x;
571 self.active = index;
572
573 let width = self.width_of(index, view);
574 let current = new_x + self.view_offset;
575 self.view_offset = fit(current, view.w, new_x, width, settings.gap);
576 }
577}
578
579fn fit(current: f64, view_width: f64, column_x: f64, column_width: f64, gap: f64) -> f64 {
591 if view_width <= column_width {
592 return 0.0;
593 }
594 let padding = ((view_width - column_width) / 2.0).clamp(0.0, gap);
595 let left = column_x - padding;
596 let right = column_x + column_width + padding;
597
598 if current <= left && right <= current + view_width {
599 return current - column_x;
600 }
601
602 let to_left = (current - left).abs();
603 let to_right = ((current + view_width) - right).abs();
604 if to_left <= to_right {
605 left - column_x
606 } else {
607 right - view_width - column_x
608 }
609}
610
611#[cfg(test)]
612mod tests {
613 use super::{PRESETS, Scroller};
614 use crate::{Rect, Settings};
615
616 fn area() -> Rect {
617 Rect::new(0.0, 0.0, 1000.0, 600.0)
618 }
619 fn settings() -> Settings {
620 Settings {
621 gap: 0.0,
622 ..Settings::default()
623 }
624 }
625 fn rect_of(scroller: &Scroller, id: u64) -> Rect {
626 scroller
627 .layout(area(), settings())
628 .into_iter()
629 .find(|(other, _)| *other == id)
630 .expect("window is in the strip")
631 .1
632 }
633 fn with(count: u64) -> Scroller {
634 let mut scroller = Scroller::new();
635 for id in 1..=count {
636 scroller.insert(id, area(), settings());
637 }
638 scroller
639 }
640
641 #[test]
646 fn move_vertically_swaps_within_the_column_and_stops_at_its_ends() {
647 let mut scroller = with(3);
648 scroller.focus_sideways(-2, area(), settings());
649 scroller.consume();
650 scroller.consume();
651 assert_eq!(
652 scroller.columns().len(),
653 1,
654 "the premise: one column of three"
655 );
656 assert_eq!(scroller.columns()[0].windows, vec![1, 2, 3]);
657 assert_eq!(scroller.focused(), Some(3));
658
659 assert!(scroller.move_vertically(-1));
660 assert_eq!(scroller.columns()[0].windows, vec![1, 3, 2]);
661 assert_eq!(scroller.focused(), Some(3), "focus went with the window");
662 assert!(scroller.move_vertically(-1));
663 assert_eq!(scroller.columns()[0].windows, vec![3, 1, 2]);
664 assert!(!scroller.move_vertically(-1), "at the top of the column");
665 assert_eq!(scroller.columns()[0].windows, vec![3, 1, 2]);
666 assert!(scroller.move_vertically(1));
667 assert_eq!(scroller.columns()[0].windows, vec![1, 3, 2]);
668
669 let mut alone = with(2);
670 assert!(!alone.move_vertically(1), "a column of one has no below");
671 assert!(!alone.move_vertically(-1));
672 assert!(!Scroller::new().move_vertically(1), "an empty strip");
673 }
674
675 #[test]
678 fn a_column_keeps_its_width_however_many_there_are() {
679 let two = rect_of(&with(2), 1).w;
680 let ten = rect_of(&with(10), 1).w;
681 assert!((two - ten).abs() < f64::EPSILON, "{two} vs {ten}");
682 }
683
684 #[test]
685 fn a_new_window_opens_beside_the_active_column_not_at_the_end() {
686 let mut scroller = with(3);
687 scroller.focus_sideways(-2, area(), settings());
688 assert_eq!(scroller.active_column(), 0);
689 scroller.insert(99, area(), settings());
690 assert_eq!(
691 scroller.active_column(),
692 1,
693 "it went right of the active one"
694 );
695 assert_eq!(scroller.focused(), Some(99));
696 }
697
698 #[test]
701 fn focusing_a_visible_column_does_not_move_the_view() {
702 let mut scroller = with(2); let before = rect_of(&scroller, 1).x;
704 scroller.focus_sideways(-1, area(), settings());
705 assert!((rect_of(&scroller, 1).x - before).abs() < 1.0);
706 }
707
708 #[test]
709 fn focusing_a_column_off_the_edge_brings_it_into_view() {
710 let scroller = with(4);
711 let last = rect_of(&scroller, 4);
712 assert!(last.x >= 0.0 && last.x + last.w <= 1000.0 + 1.0, "{last:?}");
713 }
714
715 #[test]
717 fn the_focused_column_is_always_visible() {
718 let mut scroller = with(6);
719 for step in [-1, -1, -1, 1, 1, -4, 5] {
720 scroller.focus_sideways(step, area(), settings());
721 let focused = scroller.focused().expect("something is focused");
722 let rect = rect_of(&scroller, focused);
723 assert!(
724 rect.x >= -1.0 && rect.x + rect.w <= 1001.0,
725 "focused column {focused} off screen at {rect:?}"
726 );
727 }
728 }
729
730 #[test]
731 fn windows_in_a_column_share_its_width_and_split_its_height() {
732 let mut scroller = with(1);
733 scroller.insert_into_active(2, area(), settings());
734 scroller.insert_into_active(3, area(), settings());
735 let (one, three) = (rect_of(&scroller, 1), rect_of(&scroller, 3));
736 assert!(
737 (one.w - three.w).abs() < f64::EPSILON,
738 "same column, same width"
739 );
740 assert!(
741 (one.h - 200.0).abs() < 1.0,
742 "three share the height: {one:?}"
743 );
744 assert!(three.y > one.y);
745 }
746
747 #[test]
748 fn consuming_takes_the_next_column_into_this_one() {
749 let mut scroller = with(2);
750 scroller.focus_sideways(-1, area(), settings());
751 scroller.consume();
752 assert_eq!(scroller.columns().len(), 1);
753 assert_eq!(scroller.columns()[0].windows.len(), 2);
754 }
755
756 #[test]
757 fn expelling_puts_a_window_back_in_its_own_column() {
758 let mut scroller = with(1);
759 scroller.insert_into_active(2, area(), settings());
760 assert_eq!(scroller.columns().len(), 1);
761 scroller.expel(area(), settings());
762 assert_eq!(scroller.columns().len(), 2);
763 assert_eq!(scroller.focused(), Some(2));
764 }
765
766 #[test]
767 fn cycling_width_walks_the_presets() {
768 let mut scroller = with(1);
769 let first = rect_of(&scroller, 1).w;
770 scroller.cycle_width(area(), settings());
771 let second = rect_of(&scroller, 1).w;
772 assert!((first - second).abs() > 1.0, "width changed");
773 for _ in 0..PRESETS.len() {
776 scroller.cycle_width(area(), settings());
777 }
778 assert!(
779 (rect_of(&scroller, 1).w - second).abs() < 1.0,
780 "and comes back around"
781 );
782 }
783
784 #[test]
794 fn a_strip_cycles_through_the_widths_it_was_given() {
795 let mut scroller = Scroller::with_widths(&[0.25, 0.8], 1);
799 scroller.insert(1, area(), settings());
800 assert!(
801 (rect_of(&scroller, 1).w - 800.0).abs() < 1.0,
802 "a new column opens at `default_width`, which is the second of the \
803 two given: {:?}",
804 rect_of(&scroller, 1)
805 );
806
807 scroller.cycle_width(area(), settings());
808 assert!(
809 (rect_of(&scroller, 1).w - 250.0).abs() < 1.0,
810 "and cycling wraps to the first of them: {:?}",
811 rect_of(&scroller, 1)
812 );
813
814 scroller.cycle_width(area(), settings());
815 assert!(
816 (rect_of(&scroller, 1).w - 800.0).abs() < 1.0,
817 "a lap of a two-entry list is two steps, not three"
818 );
819 }
820
821 #[test]
824 fn a_strip_configured_again_keeps_its_columns_and_takes_the_new_widths() {
825 let mut scroller = with(3);
826 scroller.cycle_width(area(), settings());
827 scroller.focus_sideways(-2, area(), settings());
828 let before = scroller.layout(area(), settings());
829
830 scroller.configure_like(&Scroller::with_widths(&[0.25, 0.8], 1));
831 assert_eq!(
832 scroller.layout(area(), settings()),
833 before,
834 "the columns, their widths, the focus and the view are unchanged"
835 );
836 scroller.insert(4, area(), settings());
837 assert!(
838 (rect_of(&scroller, 4).w - 800.0).abs() < 1.0,
839 "a new column opens at the new `default_width`: {:?}",
840 rect_of(&scroller, 4)
841 );
842 scroller.cycle_width(area(), settings());
843 assert!(
844 (rect_of(&scroller, 4).w - 250.0).abs() < 1.0,
845 "and the cycle walks the new list: {:?}",
846 rect_of(&scroller, 4)
847 );
848 }
849
850 #[test]
856 fn an_unusable_width_list_falls_back_rather_than_failing() {
857 let mut scroller = Scroller::with_widths(&[], 0);
858 scroller.insert(1, area(), settings());
859 assert!(
860 (rect_of(&scroller, 1).w - 1000.0 * PRESETS[0]).abs() < 1.0,
861 "an empty list is the shipped presets, not a column of no width"
862 );
863
864 let mut scroller = Scroller::with_widths(&[0.4, 0.6], 9);
865 scroller.insert(1, area(), settings());
866 assert!(
867 (rect_of(&scroller, 1).w - 600.0).abs() < 1.0,
868 "an index past the end is the last width, not a missing one"
869 );
870 }
871
872 #[test]
873 fn removing_a_column_keeps_focus_somewhere_real() {
874 let mut scroller = with(3);
875 scroller.remove(3);
876 assert!(scroller.focused().is_some());
877 scroller.remove(2);
878 scroller.remove(1);
879 assert!(scroller.is_empty());
880 assert!(scroller.layout(area(), settings()).is_empty());
881 }
882
883 #[test]
885 fn removing_one_of_a_stack_leaves_the_column() {
886 let mut scroller = with(1);
887 scroller.insert_into_active(2, area(), settings());
888 scroller.remove(1);
889 assert_eq!(scroller.columns().len(), 1);
890 assert_eq!(scroller.columns()[0].windows, vec![2]);
891 }
892
893 #[test]
894 fn moving_a_column_carries_focus_with_it() {
895 let mut scroller = with(3);
896 let focused = scroller.focused();
897 assert!(scroller.move_column(-1, area(), settings()));
898 assert_eq!(scroller.focused(), focused, "the window went with the move");
899 assert_eq!(scroller.active_column(), 1);
900 }
901
902 #[test]
907 fn move_column_says_whether_it_moved() {
908 let mut scroller = with(3);
909 let order = |scroller: &Scroller| -> Vec<u64> {
910 scroller
911 .columns()
912 .iter()
913 .flat_map(|column| column.windows.clone())
914 .collect()
915 };
916 assert!(
917 !scroller.move_column(1, area(), settings()),
918 "the last column"
919 );
920 assert_eq!(order(&scroller), vec![1, 2, 3]);
921 assert!(scroller.move_column(-1, area(), settings()));
922 assert!(scroller.move_column(-1, area(), settings()));
923 assert_eq!(order(&scroller), vec![3, 1, 2]);
924 assert!(
925 !scroller.move_column(-1, area(), settings()),
926 "the first column"
927 );
928 assert!(
929 !with(1).move_column(-1, area(), settings()),
930 "a strip of one"
931 );
932 }
933
934 #[test]
937 fn an_oversized_column_is_left_aligned() {
938 let mut scroller = Scroller::new();
939 scroller.insert(1, area(), settings());
940 for _ in 0..6 {
941 scroller.cycle_width(area(), settings());
942 }
943 let wide = Rect::new(0.0, 0.0, 300.0, 600.0);
944 let placed = scroller
945 .layout(wide, settings())
946 .into_iter()
947 .next()
948 .expect("one window");
949 assert!(placed.1.x.abs() < 1.0, "left-aligned: {:?}", placed.1);
950 }
951}
952
953#[cfg(test)]
954mod focus_and_width {
955 use super::Scroller;
956 use crate::{Rect, Settings};
957
958 fn area() -> Rect {
959 Rect::new(0.0, 0.0, 1000.0, 600.0)
960 }
961 fn settings() -> Settings {
962 Settings {
963 gap: 0.0,
964 ..Settings::default()
965 }
966 }
967 fn rect_of(scroller: &Scroller, id: u64) -> Rect {
968 scroller
969 .layout(area(), settings())
970 .into_iter()
971 .find(|(other, _)| *other == id)
972 .expect("in the strip")
973 .1
974 }
975
976 #[test]
980 fn focusing_a_half_visible_column_brings_it_fully_on_screen() {
981 let mut scroller = Scroller::new();
982 for id in 1..=4 {
983 scroller.insert(id, area(), settings());
984 }
985 scroller.focus_sideways(-3, area(), settings());
986 let off = rect_of(&scroller, 4);
987 assert!(
988 off.x + off.w > 1000.0,
989 "window 4 starts off the edge: {off:?}"
990 );
991
992 scroller.focus_window(4, area(), settings());
993 let now = rect_of(&scroller, 4);
994 assert!(
995 now.x >= -1.0 && now.x + now.w <= 1001.0,
996 "still not fully visible: {now:?}"
997 );
998 }
999
1000 #[test]
1001 fn widening_changes_the_column_and_every_window_in_it() {
1002 let mut scroller = Scroller::new();
1003 scroller.insert(1, area(), settings());
1004 scroller.insert_into_active(2, area(), settings());
1005 let before = rect_of(&scroller, 1).w;
1006 scroller.widen(1, 0.2, area(), settings());
1007 let (one, two) = (rect_of(&scroller, 1), rect_of(&scroller, 2));
1008 assert!(one.w > before, "the column grew");
1009 assert!((one.w - two.w).abs() < f64::EPSILON, "both windows with it");
1010 }
1011
1012 #[test]
1013 fn a_column_can_neither_vanish_nor_outgrow_the_view() {
1014 let mut scroller = Scroller::new();
1015 scroller.insert(1, area(), settings());
1016 scroller.widen(1, -10.0, area(), settings());
1017 assert!(rect_of(&scroller, 1).w >= 99.0, "floor holds");
1018 scroller.widen(1, 10.0, area(), settings());
1019 assert!(rect_of(&scroller, 1).w <= 1001.0, "ceiling holds");
1020 }
1021}
1022
1023#[cfg(test)]
1024mod moving {
1025 use super::Scroller;
1026 use crate::{Rect, Settings};
1027
1028 fn area() -> Rect {
1029 Rect::new(0.0, 0.0, 1000.0, 600.0)
1030 }
1031 fn settings() -> Settings {
1032 Settings {
1033 gap: 0.0,
1034 ..Settings::default()
1035 }
1036 }
1037
1038 #[test]
1039 fn a_window_dropped_on_another_column_joins_it() {
1040 let mut scroller = Scroller::new();
1041 for id in 1..=3 {
1042 scroller.insert(id, area(), settings());
1043 }
1044 assert_eq!(scroller.columns().len(), 3);
1045 scroller.move_to_column_of(3, 1, area(), settings());
1046 assert_eq!(scroller.columns().len(), 2, "one column absorbed another");
1047 assert!(scroller.columns()[0].windows.contains(&3));
1048 assert_eq!(scroller.focused(), Some(3), "focus went with it");
1049 }
1050
1051 #[test]
1052 fn moving_out_of_a_stack_leaves_the_column_standing() {
1053 let mut scroller = Scroller::new();
1054 scroller.insert(1, area(), settings());
1055 scroller.insert_into_active(2, area(), settings());
1056 scroller.insert(3, area(), settings());
1057 scroller.move_to_column_of(2, 3, area(), settings());
1058 assert_eq!(scroller.columns().len(), 2);
1059 assert_eq!(scroller.columns()[0].windows, vec![1]);
1060 assert!(scroller.columns()[1].windows.contains(&2));
1061 }
1062
1063 #[test]
1064 fn a_window_cannot_be_moved_onto_itself() {
1065 let mut scroller = Scroller::new();
1066 scroller.insert(1, area(), settings());
1067 scroller.insert(2, area(), settings());
1068 scroller.move_to_column_of(1, 1, area(), settings());
1069 assert_eq!(scroller.columns().len(), 2);
1070 }
1071}
1072
1073#[cfg(test)]
1076mod floors {
1077 use super::Scroller;
1078 use crate::{Floors, Minimum, Rect, Settings};
1079
1080 fn area() -> Rect {
1081 Rect::new(0.0, 0.0, 1000.0, 600.0)
1082 }
1083 fn settings() -> Settings {
1084 Settings {
1085 gap: 0.0,
1086 ..Settings::default()
1087 }
1088 }
1089 fn rect_of(scroller: &Scroller, id: u64) -> Rect {
1090 scroller
1091 .layout(area(), settings())
1092 .into_iter()
1093 .find(|(other, _)| *other == id)
1094 .expect("window is in the strip")
1095 .1
1096 }
1097 fn wide(of: &[(u64, f64)]) -> Floors {
1098 of.iter()
1099 .map(|&(id, w)| (id, Minimum { w, h: 0.0 }))
1100 .collect()
1101 }
1102
1103 #[test]
1106 fn a_column_is_never_narrower_than_its_widest_floor() {
1107 let mut scroller = Scroller::new();
1108 scroller.insert(1, area(), settings());
1109 scroller.insert(2, area(), settings());
1110 scroller.insert_into_active(3, area(), settings());
1111 scroller.set_floors(wide(&[(3, 600.0)]));
1112 for id in [2, 3] {
1113 let rect = rect_of(&scroller, id);
1114 assert!((rect.w - 600.0).abs() < 1e-6, "window {id}: {rect:?}");
1115 }
1116 assert!((rect_of(&scroller, 1).w - 1000.0 / 3.0).abs() < 1e-6);
1117 }
1118
1119 #[test]
1122 fn the_next_column_starts_after_a_widened_one() {
1123 let mut scroller = Scroller::new();
1124 for id in 1..=3 {
1125 scroller.insert(id, area(), settings());
1126 }
1127 scroller.set_floors(wide(&[(1, 600.0)]));
1128 let (one, two) = (rect_of(&scroller, 1), rect_of(&scroller, 2));
1129 assert!(
1130 (two.x - (one.x + 600.0)).abs() < 1e-6,
1131 "column 2 starts inside column 1: {one:?} {two:?}"
1132 );
1133 }
1134
1135 #[test]
1139 fn narrowing_a_column_stops_at_its_floor() {
1140 let mut scroller = Scroller::new();
1141 scroller.insert(1, area(), settings());
1142 scroller.set_floors(wide(&[(1, 500.0)]));
1143 scroller.widen(1, -0.1, area(), settings());
1144 assert!((rect_of(&scroller, 1).w - 500.0).abs() < 1e-6);
1145 scroller.widen(1, 0.1, area(), settings());
1146 let one = rect_of(&scroller, 1);
1147 assert!(
1148 (one.w - 600.0).abs() < 1e-6,
1149 "the column did not widen from its floor: {one:?}"
1150 );
1151 }
1152
1153 #[test]
1158 fn widening_a_column_its_floor_holds_widens_it_at_once() {
1159 let mut scroller = Scroller::new();
1160 scroller.insert(1, area(), settings());
1161 scroller.set_floors(wide(&[(1, 500.0)]));
1162 assert!(
1163 (rect_of(&scroller, 1).w - 500.0).abs() < 1e-6,
1164 "the premise"
1165 );
1166 scroller.widen(1, 0.1, area(), settings());
1167 let one = rect_of(&scroller, 1);
1168 assert!(
1169 (one.w - 600.0).abs() < 1e-6,
1170 "the press was swallowed by the floor: {one:?}"
1171 );
1172 }
1173}