1use crate::{
3 buffer::{Buffer, BufferId, Buffers},
4 config::Config,
5 config_handle, die,
6 dot::{Cur, Dot},
7 editor::ViewPort,
8 fsys::InputFilter,
9 lsp::LspManagerHandle,
10 ziplist,
11 ziplist::{Position, ZipList},
12};
13use std::{
14 cmp::min,
15 io,
16 mem::swap,
17 path::Path,
18 sync::{Arc, RwLock, mpsc::channel},
19};
20use tracing::{debug, warn};
21use unicode_width::UnicodeWidthChar;
22
23pub const SCRATCH_ID: usize = usize::MAX;
27
28#[cfg(test)]
35macro_rules! assert_invariants {
36 ($self:expr) => {{
37 for (i, (_, col)) in $self.cols.iter().enumerate() {
38 for (j, (_, win)) in col.wins.iter().enumerate() {
39 assert!(
40 $self.buffers.contains_bufid(win.view.bufid),
41 "col {i} window {j} held unknown bufid ({})",
42 win.view.bufid
43 );
44 let b = $self.buffers.with_id(win.view.bufid).unwrap();
45 assert!(
46 win.view.row_off < b.len_lines(),
47 "col {i} window {j} has an OOB row_off ({} vs {})",
48 win.view.row_off,
49 b.len_lines()
50 );
51 }
52 }
53 for view in $self.views.iter() {
54 assert!(
55 $self.buffers.contains_bufid(view.bufid),
56 "stored view held unknown bufid ({})",
57 view.bufid
58 );
59 let b = $self.buffers.with_id(view.bufid).unwrap();
60 assert!(
61 view.row_off < b.len_lines(),
62 "stored view for bufid {} has an OOB row_off ({} vs {})",
63 view.bufid,
64 view.row_off,
65 b.len_lines()
66 );
67 }
68 }};
69}
70
71#[derive(Debug)]
75pub struct Layout {
76 buffers: Buffers,
78 config: Arc<RwLock<Config>>,
80 pub(crate) scratch: Scratch,
83 pub(crate) screen_rows: usize,
85 pub(crate) screen_cols: usize,
87 pub(super) cols: ZipList<Column>,
89 pub(super) views: Vec<View>,
91 changed_since_last_render: bool,
95}
96
97impl Layout {
98 pub fn new_with_stub_lsp_handle(
99 screen_rows: usize,
100 screen_cols: usize,
101 config: Arc<RwLock<Config>>,
102 ) -> Self {
103 let (tx, _rx) = channel();
104 let lsp_handle = LspManagerHandle::new_stubbed(tx);
105
106 Self::new(screen_rows, screen_cols, Arc::new(lsp_handle), config)
107 }
108
109 pub(crate) fn new(
110 screen_rows: usize,
111 screen_cols: usize,
112 lsp_handle: Arc<LspManagerHandle>,
113 config: Arc<RwLock<Config>>,
114 ) -> Self {
115 let scratch = Scratch::new(config.clone());
116 let buffers = Buffers::new(lsp_handle, config.clone());
117 let id = buffers.active().id;
118
119 let l = Self {
120 buffers,
121 config,
122 scratch,
123 screen_rows,
124 screen_cols,
125 cols: ziplist![Column::new(screen_rows, screen_cols, &[id])],
126 views: vec![],
127 changed_since_last_render: false,
128 };
129
130 #[cfg(test)]
131 assert_invariants!(l);
132
133 l
134 }
135
136 pub(crate) fn changed_since_last_render(&mut self) -> bool {
141 let had_change = self.changed_since_last_render
142 || self.buffers.iter().any(|b| b.changed_since_last_render)
143 || self.scratch.b.buffer().changed_since_last_render;
144 self.changed_since_last_render = false;
145 self.buffers
146 .iter_mut()
147 .for_each(|b| b.changed_since_last_render = false);
148 self.scratch.b.buffer_mut().changed_since_last_render = false;
149
150 had_change
151 }
152
153 pub(crate) fn buffers(&self) -> &Buffers {
154 &self.buffers
155 }
156
157 pub(crate) fn ids(&self) -> Vec<Vec<BufferId>> {
158 self.cols
159 .iter()
160 .map(|(_, col)| col.wins.iter().map(|(_, win)| win.view.bufid).collect())
161 .collect()
162 }
163
164 pub(crate) fn n_open_windows(&self) -> usize {
166 self.cols.iter().map(|(_, c)| c.wins.len()).sum()
167 }
168
169 pub(crate) fn ensure_file_is_open(&mut self, path: &str) {
170 self.buffers.ensure_file_is_open(path);
171
172 #[cfg(test)]
173 assert_invariants!(self);
174 }
175
176 pub(crate) fn is_empty_squirrel(&self) -> bool {
177 self.buffers.is_empty_squirrel()
178 }
179
180 fn buffer_is_visible(&self, id: BufferId) -> bool {
181 self.cols
182 .iter()
183 .any(|(_, c)| c.wins.iter().any(|(_, w)| w.view.bufid == id))
184 }
185
186 pub(crate) fn active_buffer_ignoring_scratch(&self) -> &Buffer {
188 self.buffers.active()
189 }
190
191 pub(crate) fn active_buffer_mut_ignoring_scratch(&mut self) -> &mut Buffer {
193 self.buffers.active_mut()
194 }
195
196 pub fn active_buffer(&self) -> &Buffer {
198 if self.scratch.is_focused {
199 self.scratch.b.buffer()
200 } else {
201 self.buffers.active()
202 }
203 }
204
205 pub(crate) fn active_buffer_mut(&mut self) -> &mut Buffer {
207 if self.scratch.is_focused {
208 self.scratch.b.buffer_mut()
209 } else {
210 self.buffers.active_mut()
211 }
212 }
213
214 pub(crate) fn buffer_with_id(&self, id: BufferId) -> Option<&Buffer> {
215 self.buffers.with_id(id)
216 }
217
218 pub(crate) fn buffer_with_id_mut(&mut self, id: BufferId) -> Option<&mut Buffer> {
219 self.buffers.with_id_mut(id)
220 }
221
222 fn focus_first_window_with_buffer(&mut self, id: BufferId) {
223 self.scratch.is_focused = false;
224 self.cols
225 .focus_element_by_mut(|c| c.wins.focus_element_by_mut(|w| w.view.bufid == id));
226 }
227
228 pub(crate) fn toggle_scratch(&mut self) {
229 self.scratch.toggle();
230 self.changed_since_last_render = true;
231 }
232
233 pub fn open_or_focus<P: AsRef<Path>>(
234 &mut self,
235 path: P,
236 mut new_window: bool,
237 ) -> io::Result<Option<BufferId>> {
238 self.scratch.is_focused = false;
239 self.changed_since_last_render = true;
240
241 if self.buffers.is_empty_squirrel() {
242 new_window = false;
245 }
246
247 let retain_empty_unnamed = new_window || self.n_open_windows() > 1;
248 let opt = self.buffers.open_or_focus(path, retain_empty_unnamed)?;
249 let id = self.active_buffer_ignoring_scratch().id;
250
251 if self.buffer_is_visible(id) {
252 self.focus_first_window_with_buffer(id);
253 } else if new_window {
254 self.show_buffer_in_new_window(id);
255 } else {
256 self.show_buffer_in_active_window(id);
257 }
258
259 #[cfg(test)]
260 assert_invariants!(self);
261
262 Ok(opt)
263 }
264
265 pub(crate) fn open_virtual(
269 &mut self,
270 name: impl Into<String>,
271 content: impl Into<String>,
272 new_window: bool,
273 ) {
274 self.scratch.is_focused = false;
275 self.changed_since_last_render = true;
276
277 let id = self.buffers.open_virtual(name.into(), content.into());
278
279 if self.buffer_is_visible(id) {
280 self.focus_first_window_with_buffer(id);
281 } else if new_window {
282 self.show_buffer_in_new_window(id);
283 } else {
284 self.show_buffer_in_active_window(id);
285 }
286
287 #[cfg(test)]
288 assert_invariants!(self);
289 }
290
291 pub(crate) fn open_transient_scratch(
297 &mut self,
298 name: impl Into<String>,
299 content: impl Into<String>,
300 ) {
301 self.changed_since_last_render = true;
302
303 self.scratch
304 .set_transient(name.into(), content.into(), self.config.clone());
305 }
306
307 pub(crate) fn close_buffer(&mut self, id: BufferId) -> bool {
317 self.scratch.is_focused = false;
318 self.changed_since_last_render = true;
319
320 if id == self.scratch.b.buffer().id {
321 self.scratch.is_visible = false;
322 return false;
323 }
324
325 if self.buffers.len() == 1 {
326 return self.active_buffer_ignoring_scratch().id == id;
329 }
330
331 debug_assert!(self.buffers.len() > 1, "we have at least two buffers");
332 self.views.retain(|v| v.bufid != id);
333 self.buffers.close_buffer(id);
334 let focused_id = self.active_buffer_ignoring_scratch().id;
335 let ix = self.views.iter().position(|v| v.bufid == id);
336 let existing_view = ix.map(|ix| self.views.remove(ix));
337
338 let only_closing_buffer = self
339 .cols
340 .iter()
341 .flat_map(|(_, c)| c.wins.iter().map(|(_, w)| w.view.bufid))
342 .all(|bufid| bufid == id);
343
344 if only_closing_buffer {
345 self.cols = ziplist![Column::new(
346 self.screen_rows,
347 self.screen_cols,
348 &[focused_id]
349 )];
350 if let Some(view) = existing_view {
351 self.cols.focus.wins.focus.view = view;
352 }
353
354 #[cfg(test)]
355 assert_invariants!(self);
356
357 return false;
358 }
359
360 let cols_before = self.cols.len();
362 self.cols
363 .filter_unchecked(|c| c.wins.iter().any(|(_, w)| w.view.bufid != id));
364
365 if self.cols.len() < cols_before {
366 self.balance_columns();
367 }
368
369 for (_, c) in self.cols.iter_mut() {
371 let wins_before = c.wins.len();
372 c.wins.filter_unchecked(|w| w.view.bufid != id);
373 if c.wins.len() < wins_before {
374 c.balance_windows(self.screen_rows);
375 }
376 }
377
378 #[cfg(test)]
379 assert_invariants!(self);
380
381 false
382 }
383
384 pub(crate) fn focus_id(&mut self, id: BufferId, force_active: bool) {
385 if id == self.scratch.b.buffer().id {
386 self.scratch.is_focused = true;
387 self.scratch.is_visible = true;
388 return;
389 }
390
391 self.scratch.is_focused = false;
392 self.changed_since_last_render = true;
393
394 if let Some(id) = self.buffers.focus_id(id) {
395 if !force_active && self.buffer_is_visible(id) {
396 self.focus_first_window_with_buffer(id);
397 } else {
398 self.show_buffer_in_active_window(id);
399 }
400 }
401 }
402
403 pub(crate) fn focus_id_silent(&mut self, id: BufferId) {
405 self.buffers.focus_id_silent(id);
406 }
407
408 pub fn focus_column_for_resize(&mut self, col_idx: usize) {
413 assert!(col_idx < self.cols.len(), "col_idx out of bounds");
414
415 self.scratch.is_focused = false;
416 self.changed_since_last_render = true;
417
418 self.cols.focus_head();
419 for _ in 0..col_idx {
420 self.cols.focus_down();
421 }
422
423 self.buffers.focus_id_silent(self.focused_view().bufid);
424 }
425
426 pub fn focus_window_for_resize(&mut self, win_idx: usize) {
432 let wins = &mut self.cols.focus.wins;
433 assert!(win_idx < wins.len(), "win_idx out of bounds");
434
435 self.scratch.is_focused = false;
436 self.changed_since_last_render = true;
437
438 wins.focus_head();
439 for _ in 0..win_idx {
440 wins.focus_down();
441 }
442
443 self.buffers.focus_id_silent(self.focused_view().bufid);
444 }
445
446 pub fn focus_column_and_window_for_resize(&mut self, col_idx: usize, win_idx: usize) {
451 self.focus_column_for_resize(col_idx);
452 self.focus_window_for_resize(win_idx);
453 }
454
455 pub(crate) fn focus_next_buffer(&mut self) -> BufferId {
456 self.scratch.is_focused = false;
457 self.changed_since_last_render = true;
458
459 self.buffers.next();
460 let id = self.active_buffer().id;
461 self.show_buffer_in_active_window(id);
462
463 id
464 }
465
466 pub(crate) fn focus_previous_buffer(&mut self) -> BufferId {
467 self.scratch.is_focused = false;
468 self.changed_since_last_render = true;
469
470 self.buffers.previous();
471 let id = self.active_buffer().id;
472 self.show_buffer_in_active_window(id);
473
474 id
475 }
476
477 pub(crate) fn close_active_window(&mut self) -> bool {
480 self.changed_since_last_render = true;
481
482 if self.scratch.is_focused {
483 self.scratch.toggle();
484 return false;
485 }
486
487 if self.cols.len() == 1 && self.cols.focus.wins.len() == 1 {
488 return true;
489 }
490
491 if self.cols.focus.wins.len() == 1 {
492 self.cols.remove_focused_unchecked();
493 self.balance_columns();
494 } else {
495 self.cols.focus.wins.remove_focused_unchecked();
496 self.balance_active_column();
497 }
498
499 let id = self.cols.focus.wins.focus.view.bufid;
500 self.buffers.focus_id(id);
501
502 #[cfg(test)]
503 assert_invariants!(self);
504
505 false
506 }
507
508 pub(crate) fn close_active_column(&mut self) -> bool {
511 self.changed_since_last_render = true;
512
513 if self.scratch.is_focused {
514 self.scratch.toggle();
515 return false;
516 }
517
518 if self.cols.len() == 1 {
519 return true;
520 }
521
522 self.cols.remove_focused_unchecked();
523 self.balance_columns();
524
525 let id = self.cols.focus.wins.focus.view.bufid;
526 self.buffers.focus_id(id);
527
528 #[cfg(test)]
529 assert_invariants!(self);
530
531 false
532 }
533
534 pub(crate) fn record_jump_position(&mut self) {
535 self.buffers.record_jump_position();
536 }
537
538 pub(crate) fn dirty_buffers(&self) -> Vec<String> {
539 self.buffers.dirty_buffers()
540 }
541
542 pub(crate) fn as_buffer_list(&self) -> Vec<String> {
543 self.buffers.as_buffer_list()
544 }
545
546 pub(crate) fn jump_forward(&mut self) -> Option<BufferId> {
547 let maybe_ids = self.buffers.jump_list_forward();
548 if let Some((prev_id, new_id)) = maybe_ids {
549 self.show_buffer_in_active_window(self.active_buffer_ignoring_scratch().id);
550 self.set_viewport(ViewPort::Center);
551 if new_id != prev_id {
552 return Some(new_id);
553 }
554 }
555
556 None
557 }
558
559 pub(crate) fn jump_backward(&mut self) -> Option<BufferId> {
560 let maybe_ids = self.buffers.jump_list_backward();
561 if let Some((prev_id, new_id)) = maybe_ids {
562 self.show_buffer_in_active_window(self.active_buffer_ignoring_scratch().id);
563 self.set_viewport(ViewPort::Center);
564 if new_id != prev_id {
565 return Some(new_id);
566 }
567 }
568
569 None
570 }
571
572 pub(crate) fn write_output_for_buffer(&mut self, id: usize, s: String, cwd: &Path) {
573 let id = self.buffers.write_output_for_buffer(id, s, cwd);
574 if !self.buffer_is_visible(id) {
575 self.show_buffer_in_new_window(id);
576 }
577
578 #[cfg(test)]
579 assert_invariants!(self);
580 }
581
582 pub(crate) fn next_column(&mut self) {
584 self.scratch.is_focused = false;
585 self.changed_since_last_render = true;
586
587 self.cols.focus_down();
588 self.buffers.focus_id(self.focused_view().bufid);
589 self.force_cursor_to_be_in_view();
590
591 #[cfg(test)]
592 assert_invariants!(self);
593 }
594
595 pub(crate) fn prev_column(&mut self) {
597 self.scratch.is_focused = false;
598 self.changed_since_last_render = true;
599
600 self.cols.focus_up();
601 self.buffers.focus_id(self.focused_view().bufid);
602 self.force_cursor_to_be_in_view();
603
604 #[cfg(test)]
605 assert_invariants!(self);
606 }
607
608 pub(crate) fn next_window_in_column(&mut self) {
610 self.scratch.is_focused = false;
611 self.changed_since_last_render = true;
612
613 self.cols.focus.wins.focus_down();
614 self.buffers.focus_id(self.focused_view().bufid);
615 self.force_cursor_to_be_in_view();
616
617 #[cfg(test)]
618 assert_invariants!(self);
619 }
620
621 pub(crate) fn prev_window_in_column(&mut self) {
623 self.scratch.is_focused = false;
624 self.changed_since_last_render = true;
625
626 self.cols.focus.wins.focus_up();
627 self.buffers.focus_id(self.focused_view().bufid);
628 self.force_cursor_to_be_in_view();
629
630 #[cfg(test)]
631 assert_invariants!(self);
632 }
633
634 pub(crate) fn drag_up(&mut self) {
636 self.scratch.is_focused = false;
637 self.changed_since_last_render = true;
638
639 self.cols.focus.wins.swap_up();
640
641 #[cfg(test)]
642 assert_invariants!(self);
643 }
644
645 pub(crate) fn drag_down(&mut self) {
647 self.scratch.is_focused = false;
648 self.changed_since_last_render = true;
649
650 self.cols.focus.wins.swap_down();
651
652 #[cfg(test)]
653 assert_invariants!(self);
654 }
655
656 pub(crate) fn drag_left(&mut self) {
668 self.scratch.is_focused = false;
669 self.changed_since_last_render = true;
670
671 if self.cols.up.is_empty() || self.cols.len() == 1 {
674 if self.cols.focus.wins.len() == 1 {
678 return;
679 }
680
681 let win = self.cols.focus.wins.remove_focused_unchecked();
683 self.balance_active_column(); let mut col = Column::new(self.screen_rows, self.screen_cols, &[win.view.bufid]);
685 col.wins.focus = win;
686 self.cols.insert_at(Position::Head, col);
687 self.cols.focus_up();
688 self.balance_columns();
689 } else if self.cols.focus.wins.len() == 1 {
690 let on_left = self.cols.up.is_empty();
695 let win = self.cols.remove_focused_unchecked().wins.focus;
696 self.balance_columns();
697 self.balance_active_column();
698 if !on_left {
699 self.cols.focus_up();
700 }
701 self.cols.focus.wins.insert(win);
702 self.balance_active_column();
703 } else {
704 let win = self.cols.focus.wins.remove_focused_unchecked();
707 self.balance_active_column();
708 self.cols.focus_up();
709 self.cols.focus.wins.insert(win);
710 self.balance_active_column();
711 }
712
713 #[cfg(test)]
714 assert_invariants!(self);
715 }
716
717 pub(crate) fn drag_right(&mut self) {
721 self.scratch.is_focused = false;
722 self.changed_since_last_render = true;
723
724 if self.cols.len() == 1 || self.cols.down.is_empty() {
727 if self.cols.focus.wins.len() == 1 {
731 return;
732 }
733
734 let win = self.cols.focus.wins.remove_focused_unchecked();
736 self.balance_active_column(); let mut col = Column::new(self.screen_rows, self.screen_cols, &[0]);
739 col.wins.focus = win;
740 self.cols.insert_at(Position::Tail, col);
741 self.cols.focus_down();
742 self.balance_columns();
743 } else if self.cols.focus.wins.len() == 1 {
744 let win = self.cols.remove_focused_unchecked().wins.focus;
747 self.cols.focus.wins.insert(win);
748 self.balance_active_column();
749 self.balance_columns();
750 } else {
751 let win = self.cols.focus.wins.remove_focused_unchecked();
754 self.balance_active_column();
755 self.cols.focus_down();
756 self.cols.focus.wins.insert(win);
757 self.balance_active_column();
758 }
759
760 #[cfg(test)]
761 assert_invariants!(self);
762 }
763
764 pub(crate) fn resize_active_column(&mut self, delta_cols: i16) {
770 self.changed_since_last_render = true;
771 self.cols.grow_focus(delta_cols);
772 }
773
774 pub(crate) fn resize_active_window(&mut self, delta_rows: i16) {
781 self.changed_since_last_render = true;
782 self.cols.focus.wins.grow_focus(delta_rows);
783 }
784
785 pub fn resize_active_column_against_next(&mut self, delta: i16) {
787 self.changed_since_last_render = true;
788 self.cols.grow_focus_against_next(delta);
789 }
790
791 pub fn resize_active_window_against_next(&mut self, delta: i16) {
793 self.changed_since_last_render = true;
794 self.cols.focus.wins.grow_focus_against_next(delta);
795 }
796
797 pub(crate) fn update_screen_size(&mut self, rows: usize, cols: usize) {
803 self.changed_since_last_render = true;
804
805 let col_ratio = (cols as f32) / (self.screen_cols as f32);
806 let row_ratio = (rows as f32) / (self.screen_rows as f32);
807
808 self.screen_rows = rows;
809 self.screen_cols = cols;
810
811 self.cols.scale_sizes(col_ratio, cols);
812 for (_, c) in self.cols.iter_mut() {
813 c.wins.scale_sizes(row_ratio, rows);
814 }
815
816 self.clamp_scroll();
817 }
818
819 pub(crate) fn balance_columns(&mut self) {
821 self.changed_since_last_render = true;
822
823 let (n_cols, slop) = calculate_dims(self.screen_cols, self.cols.len());
824 for (i, (_, col)) in self.cols.iter_mut().enumerate() {
825 col.n_cols = n_cols;
826 if i < slop {
827 col.n_cols += 1;
828 }
829 }
830 }
831
832 pub(crate) fn balance_active_column(&mut self) {
834 self.changed_since_last_render = true;
835 self.cols.focus.balance_windows(self.screen_rows);
836 }
837
838 pub(crate) fn balance_windows(&mut self) {
841 self.changed_since_last_render = true;
842 for (_, col) in self.cols.iter_mut() {
843 col.balance_windows(self.screen_rows);
844 }
845 }
846
847 pub(crate) fn balance_all(&mut self) {
849 self.balance_columns();
850 self.balance_windows();
851 }
852
853 #[inline]
854 pub(crate) fn focused_view(&self) -> &View {
855 &self.cols.focus.wins.focus.view
856 }
857
858 #[inline]
859 pub(crate) fn focused_view_mut(&mut self) -> &mut View {
860 &mut self.cols.focus.wins.focus.view
861 }
862
863 pub(crate) fn active_window_rows(&self) -> usize {
864 if self.scratch.is_focused {
865 self.scratch.w.n_rows
866 } else {
867 self.cols.focus.wins.focus.n_rows
868 }
869 }
870
871 pub(crate) fn show_buffer_in_active_window(&mut self, id: BufferId) {
873 self.scratch.is_focused = false;
874 self.changed_since_last_render = true;
875
876 if self.focused_view().bufid == id {
877 return;
878 }
879
880 let mut view = match self.views.iter().position(|v| v.bufid == id) {
881 Some(idx) => self.views.remove(idx),
882 None => View::new(id),
883 };
884
885 swap(self.focused_view_mut(), &mut view);
886 if self.buffers.contains_bufid(view.bufid) {
887 self.views.push(view);
888 }
889
890 #[cfg(test)]
891 assert_invariants!(self);
892 }
893
894 pub(crate) fn new_column(&mut self) {
897 self.scratch.is_focused = false;
898 self.changed_since_last_render = true;
899
900 let view = self.focused_view().clone();
901 let mut col = Column::new(self.screen_rows, self.screen_cols, &[view.bufid]);
902 col.wins.last_mut().view = view;
903 self.cols.insert_at(Position::Tail, col);
904 self.cols.focus_tail();
905 self.balance_columns();
906
907 #[cfg(test)]
908 assert_invariants!(self);
909 }
910
911 pub(crate) fn new_window(&mut self) {
914 self.scratch.is_focused = false;
915 self.changed_since_last_render = true;
916
917 let view = self.focused_view().clone();
918 let wins = &mut self.cols.focus.wins;
919 wins.insert_at(Position::Tail, Window { n_rows: 0, view });
920 wins.focus_tail();
921 self.balance_active_column();
922
923 #[cfg(test)]
924 assert_invariants!(self);
925 }
926
927 pub(crate) fn show_buffer_in_new_window(&mut self, id: BufferId) {
929 self.scratch.is_focused = false;
930 self.changed_since_last_render = true;
931
932 let view = if self.focused_view().bufid == id {
933 self.focused_view().clone()
934 } else {
935 match self.views.iter().position(|v| v.bufid == id) {
936 Some(idx) => self.views.remove(idx),
937 None => View::new(id),
938 }
939 };
940
941 if self.cols.len() == 1 {
942 let mut col = Column::new(self.screen_rows, self.screen_cols, &[id]);
943 col.wins.last_mut().view = view;
944 self.cols.insert_at(Position::Tail, col);
945 self.cols.focus_tail();
946 self.balance_columns();
947 } else {
948 self.cols.focus_tail();
949 let wins = &mut self.cols.focus.wins;
950 wins.insert_at(Position::Tail, Window { n_rows: 0, view });
951 wins.focus_tail();
952 self.balance_active_column();
953 }
954
955 self.buffers.focus_id(id);
956
957 #[cfg(test)]
958 assert_invariants!(self);
959 }
960
961 pub(crate) fn force_cursor_to_be_in_view(&mut self) {
962 self.changed_since_last_render = true;
963 let tabstop = config_handle!(self).tabstop;
964
965 if self.scratch.is_focused {
966 self.scratch.w.view.force_cursor_to_be_in_view(
967 self.scratch.b.buffer_mut(),
968 self.scratch.w.n_rows,
969 self.screen_cols,
970 tabstop,
971 );
972 } else {
973 let b = self.buffers.active_mut();
974 let cols = self.cols.focus.n_cols;
975 let rows = self.cols.focus.wins.focus.n_rows;
976
977 self.cols
978 .focus
979 .focused_view_mut()
980 .force_cursor_to_be_in_view(b, rows, cols, tabstop);
981 }
982
983 #[cfg(test)]
984 assert_invariants!(self);
985 }
986
987 pub(crate) fn clamp_scroll(&mut self) {
994 let tabstop = config_handle!(self).tabstop;
995
996 if self.scratch.is_visible {
999 self.scratch.w.view.clamp_scroll(
1000 self.scratch.b.buffer_mut(),
1001 self.scratch.w.n_rows,
1002 self.screen_cols,
1003 tabstop,
1004 );
1005 }
1006
1007 let b = self.buffers.active_mut();
1009 let cols = self.cols.focus.n_cols;
1010 let rows = self.cols.focus.wins.focus.n_rows;
1011
1012 self.cols
1013 .focus
1014 .focused_view_mut()
1015 .clamp_scroll(b, rows, cols, tabstop);
1016
1017 for (col_focused, col) in self.cols.iter_mut() {
1021 for (win_focused, win) in col.wins.iter_mut() {
1022 if col_focused && win_focused {
1023 continue; }
1025
1026 let b = self.buffers.with_id_mut(win.view.bufid).unwrap();
1027 let y_max = b.txt.len_lines() - 1;
1028 win.view.row_off = min(win.view.row_off, y_max);
1029 }
1030 }
1031
1032 #[cfg(test)]
1033 assert_invariants!(self);
1034 }
1035
1036 pub(crate) fn set_viewport(&mut self, vp: ViewPort) {
1037 self.changed_since_last_render = true;
1038 let tabstop = config_handle!(self).tabstop;
1039
1040 if self.scratch.is_focused {
1041 self.scratch.w.view.set_viewport(
1042 self.scratch.b.buffer_mut(),
1043 vp,
1044 self.scratch.w.n_rows,
1045 self.screen_cols,
1046 tabstop,
1047 );
1048 } else {
1049 let b = self.buffers.active_mut();
1050 let cols = self.cols.focus.n_cols;
1051 let rows = self.cols.focus.wins.focus.n_rows;
1052
1053 self.cols
1054 .focus
1055 .focused_view_mut()
1056 .set_viewport(b, vp, rows, cols, tabstop);
1057 }
1058
1059 #[cfg(test)]
1060 assert_invariants!(self);
1061 }
1062
1063 fn xy_offsets(&self) -> (usize, usize) {
1065 if self.scratch.is_focused {
1066 let y_offset = self.screen_rows - self.scratch.w.n_rows + 1; return (0, y_offset);
1068 }
1069
1070 let cols_before = &self.cols.up;
1071 let wins_above = &self.cols.focus.wins.up;
1072 let x_offset = cols_before.iter().map(|c| c.n_cols).sum::<usize>() + cols_before.len();
1073 let y_offset = wins_above.iter().map(|w| w.n_rows).sum::<usize>() + wins_above.len();
1074
1075 (x_offset, y_offset)
1076 }
1077
1078 pub(crate) fn ui_xy(&self) -> (usize, usize) {
1080 let (x_offset, y_offset) = self.xy_offsets();
1081 let (x, y) = if self.scratch.is_focused {
1082 self.scratch.w.view.ui_xy(self.scratch.b.buffer())
1083 } else {
1084 self.focused_view().ui_xy(self.active_buffer())
1085 };
1086
1087 (x + x_offset, y + y_offset)
1088 }
1089
1090 fn row_is_scratch(&self, y: usize) -> bool {
1092 if !self.scratch.is_visible {
1093 return false;
1094 }
1095
1096 y > (self.screen_rows - self.scratch.w.n_rows + 1)
1097 }
1098
1099 pub fn border_at_coords(&self, x: usize, y: usize) -> Option<Border> {
1100 if self.row_is_scratch(y) {
1101 return None;
1102 }
1103
1104 let n_cols = self.cols.len();
1105 let mut x_offset = 0;
1106
1107 for (col_idx, (_, col)) in self.cols.iter().enumerate() {
1108 let border_x = x_offset + col.n_cols + 1;
1109
1110 if x == border_x && col_idx < n_cols - 1 {
1111 return Some(Border::Vertical { col_idx });
1112 } else if x > border_x {
1113 x_offset = border_x;
1114 continue;
1115 }
1116
1117 let n_wins = col.wins.len();
1118 let mut y_offset = 0;
1119
1120 for (win_idx, (_, win)) in col.wins.iter().enumerate() {
1121 let border_y = y_offset + win.n_rows + 1;
1122
1123 if y == border_y && win_idx < n_wins - 1 {
1124 return Some(Border::Horizontal { col_idx, win_idx });
1125 } else if y > border_y {
1126 y_offset = border_y;
1127 continue;
1128 }
1129
1130 return None; }
1132
1133 return None;
1134 }
1135
1136 None
1137 }
1138
1139 fn buffer_for_screen_coords(&self, x: usize, y: usize) -> BufferId {
1142 let mut x_offset = 0;
1143 let mut y_offset = 0;
1144
1145 if self.row_is_scratch(y) {
1146 return SCRATCH_ID;
1147 }
1148
1149 for (_, col) in self.cols.iter() {
1150 if x > x_offset + col.n_cols {
1151 x_offset += col.n_cols + 1;
1152 continue;
1153 }
1154 for (_, win) in col.wins.iter() {
1155 if y > y_offset + win.n_rows {
1156 y_offset += win.n_rows + 1;
1157 continue;
1158 }
1159 return win.view.bufid;
1160 }
1161 }
1162
1163 debug!("click out of bounds (x, y)=({x}, {y})");
1164 self.active_buffer().id
1165 }
1166
1167 fn focus_buffer_for_screen_coords(&mut self, x: usize, y: usize) -> BufferId {
1168 self.changed_since_last_render = true;
1169 let mut x_offset = 0;
1170 let mut y_offset = 0;
1171
1172 if self.row_is_scratch(y) {
1173 self.scratch.is_focused = true;
1174 return SCRATCH_ID;
1175 }
1176
1177 self.scratch.is_focused = false;
1178
1179 self.cols.focus_head();
1180 for _ in 0..self.cols.len() {
1181 let col = &self.cols.focus;
1182 if x > x_offset + col.n_cols {
1183 x_offset += col.n_cols + 1;
1184 self.cols.focus_down();
1185 continue;
1186 }
1187
1188 self.cols.focus.wins.focus_head();
1189 for _ in 0..self.cols.focus.wins.len() {
1190 let win = &self.cols.focus.wins.focus;
1191 if y > y_offset + win.n_rows {
1192 y_offset += win.n_rows + 1;
1193 self.cols.focus.wins.focus_down();
1194 continue;
1195 }
1196 self.buffers.focus_id(win.view.bufid);
1197 return win.view.bufid;
1198 }
1199 }
1200
1201 debug!("click out of bounds (x, y)=({x}, {y})");
1202 self.active_buffer().id
1203 }
1204
1205 fn cur_from_screen_coords(&mut self, x: usize, y: usize) -> Cur {
1207 let (x_offset, y_offset) = self.xy_offsets();
1208 let (b, win) = if self.scratch.is_focused {
1209 (self.scratch.b.buffer_mut(), &mut self.scratch.w)
1210 } else {
1211 (self.buffers.active_mut(), &mut self.cols.focus.wins.focus)
1212 };
1213
1214 let row_off = win.view.row_off;
1215
1216 let (_, w_sgncol) = b.sign_col_dims();
1217 let rx = x
1218 .saturating_sub(1)
1219 .saturating_sub(x_offset)
1220 .saturating_sub(w_sgncol);
1221 let y = min(
1222 y.saturating_sub(y_offset).saturating_add(row_off),
1223 b.len_lines(),
1224 )
1225 .saturating_sub(1);
1226
1227 win.view.rx = rx;
1228 b.cached_rx = rx;
1229
1230 let mut cur = Cur::from_yx(y, b.x_from_provided_rx(y, rx), b);
1231 cur.clamp_idx(b.len_chars());
1232
1233 cur
1234 }
1235
1236 pub(crate) fn try_active_cur_from_screen_coords(&mut self, x: usize, y: usize) -> Option<Cur> {
1239 let id = self.buffer_for_screen_coords(x, y);
1240 if id == self.active_buffer().id {
1241 Some(self.cur_from_screen_coords(x, y))
1242 } else {
1243 None
1244 }
1245 }
1246
1247 pub(crate) fn focus_cur_from_screen_coords(&mut self, x: usize, y: usize) -> (BufferId, Cur) {
1250 let bufid = self.focus_buffer_for_screen_coords(x, y);
1251 let cur = self.cur_from_screen_coords(x, y);
1252
1253 (bufid, cur)
1254 }
1255
1256 pub(crate) fn set_dot_from_screen_coords(&mut self, x: usize, y: usize) -> bool {
1261 self.changed_since_last_render = true;
1262 let current_bufid = self.active_buffer().id;
1263 let bufid = self.focus_buffer_for_screen_coords(x, y);
1264 let c = self.cur_from_screen_coords(x, y);
1265 self.active_buffer_mut().dot = Dot::Cur { c };
1266
1267 #[cfg(test)]
1268 assert_invariants!(self);
1269
1270 bufid == current_bufid
1271 }
1272
1273 pub fn scroll_view(&mut self, x: usize, y: usize, up: bool, scroll_rows: usize) {
1275 self.changed_since_last_render = true;
1276 let tabstop = config_handle!(self).tabstop;
1277 let mut x_offset = 0;
1278 let mut y_offset = 0;
1279
1280 if self.row_is_scratch(y) {
1281 apply_scroll(
1282 self.scratch.b.buffer_mut(),
1283 &mut self.scratch.w,
1284 self.screen_cols,
1285 tabstop,
1286 self.scratch.is_focused,
1287 up,
1288 scroll_rows,
1289 );
1290
1291 #[cfg(test)]
1292 assert_invariants!(self);
1293
1294 return;
1295 }
1296
1297 for (focused_col, col) in self.cols.iter_mut() {
1298 if x > x_offset + col.n_cols {
1299 x_offset += col.n_cols + 1;
1300 continue;
1301 }
1302 for (focused_win, win) in col.wins.iter_mut() {
1303 if y > y_offset + win.n_rows {
1304 y_offset += win.n_rows + 1;
1305 continue;
1306 }
1307
1308 let b = self.buffers.with_id_mut(win.view.bufid).unwrap_or_else(|| {
1309 die!("invalid buffer ID {}", win.view.bufid);
1310 });
1311 let focused = focused_col && focused_win;
1312 apply_scroll(b, win, col.n_cols, tabstop, focused, up, scroll_rows);
1313
1314 #[cfg(test)]
1315 assert_invariants!(self);
1316
1317 return;
1318 }
1319 }
1320
1321 let n_cols = self.cols.focus.n_cols;
1323 let win = &mut self.cols.focus.wins.focus;
1324 let b = self.buffers.with_id_mut(win.view.bufid).unwrap();
1325 apply_scroll(b, win, n_cols, tabstop, true, up, scroll_rows);
1326
1327 #[cfg(test)]
1328 assert_invariants!(self);
1329 }
1330
1331 pub(crate) fn update_visible_ts_state(&mut self) {
1332 let it = self.cols.iter().flat_map(|(_, c)| {
1333 c.wins
1334 .iter()
1335 .map(|(_, w)| (w.view.bufid, w.view.row_off, w.n_rows))
1336 });
1337
1338 for (bufid, from, n_rows) in it {
1339 let b = self.buffers.with_id_mut(bufid).unwrap_or_else(|| {
1340 die!("invalid buffer ID {bufid}");
1341 });
1342
1343 b.update_ts_state(from, n_rows);
1344 }
1345
1346 #[cfg(test)]
1347 assert_invariants!(self);
1348 }
1349
1350 pub(crate) fn try_set_input_filter(&mut self, bufid: BufferId, filter: InputFilter) -> bool {
1352 let b = match self.buffer_with_id_mut(bufid) {
1353 Some(b) => b,
1354 None => return false,
1355 };
1356
1357 if b.input_filter.is_some() {
1358 warn!("attempt to set an input filter when one is already in place. id={bufid:?}");
1359 return false;
1360 }
1361
1362 let scratch_filter = filter.paired_tag_filter();
1363 b.input_filter = Some(filter);
1364 self.scratch.b.main.input_filter = Some(scratch_filter);
1367
1368 true
1369 }
1370
1371 pub(crate) fn clear_input_filter(&mut self, bufid: usize) {
1373 if let Some(b) = self.buffer_with_id_mut(bufid) {
1374 b.input_filter = None;
1375 }
1376
1377 self.scratch.b.main.input_filter = None;
1380 }
1381}
1382
1383#[derive(Debug, Clone)]
1384pub(crate) struct Column {
1385 pub(crate) n_cols: usize,
1387 pub(crate) wins: ZipList<Window>,
1389}
1390
1391impl Column {
1392 pub(crate) fn new(n_rows: usize, n_cols: usize, buf_ids: &[BufferId]) -> Self {
1393 let (win_rows, slop) = calculate_dims(n_rows, buf_ids.len());
1394 let mut wins = ZipList::try_from_iter(buf_ids.iter().map(|id| Window::new(win_rows, *id)))
1395 .expect("can't have an empty column");
1396
1397 for (i, (_, w)) in wins.iter_mut().enumerate() {
1398 if i < slop {
1399 w.n_rows += 1;
1400 }
1401 }
1402
1403 Self { n_cols, wins }
1404 }
1405
1406 #[inline]
1408 fn focused_view_mut(&mut self) -> &mut View {
1409 &mut self.wins.focus.view
1410 }
1411
1412 fn balance_windows(&mut self, screen_rows: usize) {
1414 let (n_rows, slop) = calculate_dims(screen_rows, self.wins.len());
1415 for (i, (_, win)) in self.wins.iter_mut().enumerate() {
1416 win.n_rows = n_rows;
1417 if i < slop {
1418 win.n_rows += 1;
1419 }
1420 }
1421 }
1422}
1423
1424#[derive(Debug)]
1426pub(crate) struct Scratch {
1427 pub(crate) b: ScratchBuf,
1428 pub(super) w: Window,
1429 pub(super) is_visible: bool,
1430 pub(super) is_focused: bool,
1431}
1432
1433#[derive(Debug)]
1434pub(crate) struct ScratchBuf {
1435 main: Buffer,
1436 transient: Option<Buffer>,
1437}
1438
1439impl ScratchBuf {
1440 pub(crate) fn buffer(&self) -> &Buffer {
1441 self.transient.as_ref().unwrap_or(&self.main)
1442 }
1443
1444 pub(crate) fn buffer_mut(&mut self) -> &mut Buffer {
1445 self.transient.as_mut().unwrap_or(&mut self.main)
1446 }
1447
1448 pub(crate) fn clear(&mut self) {
1449 self.main.clear();
1450 }
1451}
1452
1453impl Scratch {
1454 fn new(config: Arc<RwLock<Config>>) -> Self {
1456 let n_rows = config.read().unwrap().minibuffer_lines;
1457
1458 Self {
1459 b: ScratchBuf {
1460 main: Buffer::new_virtual(SCRATCH_ID, "*scratch*", "", config),
1461 transient: None,
1462 },
1463 w: Window::new(n_rows, SCRATCH_ID),
1464 is_visible: false,
1465 is_focused: false,
1466 }
1467 }
1468
1469 fn set_transient(&mut self, name: String, content: String, config: Arc<RwLock<Config>>) {
1470 self.b.transient = Some(Buffer::new_virtual(SCRATCH_ID, name, content, config));
1471 self.is_visible = true;
1472 self.is_focused = true;
1473 }
1474
1475 fn toggle(&mut self) {
1479 if self.is_visible && self.b.transient.is_some() {
1480 self.b.transient = None;
1481 }
1482
1483 self.is_visible = !self.is_visible;
1484 self.is_focused = self.is_visible;
1485 }
1486}
1487
1488#[derive(Debug, Clone)]
1489pub(crate) struct Window {
1490 pub(crate) n_rows: usize,
1492 pub(crate) view: View,
1494}
1495
1496impl Window {
1497 pub(crate) fn new(n_rows: usize, bufid: BufferId) -> Self {
1498 Self {
1499 n_rows,
1500 view: View::new(bufid),
1501 }
1502 }
1503}
1504
1505#[derive(Debug, Clone)]
1506pub(crate) struct View {
1507 pub(crate) bufid: BufferId,
1508 pub(crate) col_off: usize,
1509 pub(crate) row_off: usize,
1510 pub(crate) rx: usize,
1511 cur: Cur,
1512}
1513
1514impl View {
1515 pub(crate) fn new(bufid: BufferId) -> Self {
1516 Self {
1517 bufid,
1518 col_off: 0,
1519 row_off: 0,
1520 rx: 0,
1521 cur: Cur::default(),
1522 }
1523 }
1524
1525 fn ui_xy(&self, b: &Buffer) -> (usize, usize) {
1527 let (_, w_sgncol) = b.sign_col_dims();
1528 let (y, _) = b.dot.active_cur().as_yx(b);
1529 let x = self.rx - self.col_off + w_sgncol;
1530 let y = y - self.row_off;
1531
1532 (x, y)
1533 }
1534
1535 pub(crate) fn rx_from_x(&self, b: &Buffer, y: usize, x: usize, tabstop: usize) -> usize {
1536 if y >= b.len_lines() {
1537 return 0;
1538 }
1539
1540 let mut rx = 0;
1541 for c in b.txt.line(y).chars().take(x) {
1542 if c == '\t' {
1543 rx += (tabstop - 1) - (rx % tabstop);
1544 }
1545 rx += UnicodeWidthChar::width(c).unwrap_or(1);
1546 }
1547
1548 rx
1549 }
1550
1551 fn force_cursor_to_be_in_view(
1553 &mut self,
1554 b: &mut Buffer,
1555 rows: usize,
1556 cols: usize,
1557 tabstop: usize,
1558 ) {
1559 b.dot = self.cur.into();
1560 self.clamp_scroll(b, rows, cols, tabstop);
1561 }
1562
1563 pub(crate) fn clamp_scroll(
1565 &mut self,
1566 b: &mut Buffer,
1567 rows: usize,
1568 cols: usize,
1569 tabstop: usize,
1570 ) {
1571 self.cur = b.dot.active_cur();
1572 let (y, x) = self.cur.as_yx(b);
1573 let (_, w_sgncol) = b.sign_col_dims();
1574 self.rx = self.rx_from_x(b, y, x, tabstop);
1575 b.cached_rx = self.rx;
1576
1577 if y < self.row_off {
1578 self.row_off = y;
1579 }
1580
1581 if y >= self.row_off + rows {
1582 self.row_off = y + 1 - rows;
1583 }
1584
1585 if self.rx < self.col_off {
1586 self.col_off = self.rx;
1587 }
1588
1589 if self.rx >= self.col_off + cols - w_sgncol {
1590 self.col_off = self.rx + w_sgncol + 1 - cols;
1591 }
1592 }
1593
1594 pub(crate) fn set_viewport(
1596 &mut self,
1597 b: &mut Buffer,
1598 vp: ViewPort,
1599 screen_rows: usize,
1600 screen_cols: usize,
1601 tabstop: usize,
1602 ) {
1603 let (y, _) = b.dot.active_cur().as_yx(b);
1604
1605 self.row_off = match vp {
1606 ViewPort::Top => y,
1607 ViewPort::Center => y.saturating_sub(screen_rows / 2),
1608 ViewPort::Bottom => y.saturating_sub(screen_rows),
1609 };
1610
1611 self.clamp_scroll(b, screen_rows, screen_cols, tabstop);
1612 }
1613}
1614
1615const MIN_DIM: usize = 5;
1617
1618pub trait Growable {
1619 fn size(&mut self) -> &mut usize;
1620
1621 fn clamped_sub(&mut self, delta: usize, min_val: usize) -> usize {
1622 let clamped = (*self.size()).saturating_sub(delta);
1623 if clamped >= min_val {
1624 *self.size() = clamped;
1625 delta
1626 } else {
1627 let actual = *self.size() - min_val;
1628 *self.size() = min_val;
1629 actual
1630 }
1631 }
1632}
1633
1634impl Growable for Column {
1635 fn size(&mut self) -> &mut usize {
1636 &mut self.n_cols
1637 }
1638}
1639
1640impl Growable for Window {
1641 fn size(&mut self) -> &mut usize {
1642 &mut self.n_rows
1643 }
1644}
1645
1646impl<T> ZipList<T>
1647where
1648 T: Growable,
1649{
1650 fn grow_focus(&mut self, delta: i16) {
1655 if self.len() == 1 || delta == 0 {
1656 return; }
1658
1659 let other = if self.up.is_empty() {
1660 &mut self.down[0]
1661 } else {
1662 &mut self.up[0]
1663 };
1664
1665 if delta < 0 {
1666 let actual = self.focus.clamped_sub((-delta) as usize, MIN_DIM);
1667 *other.size() += actual;
1668 } else {
1669 let actual = other.clamped_sub(delta as usize, MIN_DIM);
1670 *self.focus.size() += actual;
1671 }
1672 }
1673
1674 fn grow_focus_against_next(&mut self, delta: i16) {
1678 if self.down.is_empty() || delta == 0 {
1679 return;
1680 }
1681
1682 let other = &mut self.down[0];
1683 if delta < 0 {
1684 let actual = self.focus.clamped_sub((-delta) as usize, MIN_DIM);
1685 *other.size() += actual;
1686 } else {
1687 let actual = other.clamped_sub(delta as usize, MIN_DIM);
1688 *self.focus.size() += actual;
1689 }
1690 }
1691
1692 fn scale_sizes(&mut self, ratio: f32, new_total: usize) {
1695 let mut total = 0;
1696 for (_, elem) in self.iter_mut() {
1697 let new_size = (*elem.size() as f32 * ratio) as usize;
1698 *elem.size() = new_size;
1699 total += new_size;
1700 }
1701
1702 total += self.len() - 1;
1703 let slop = new_total - total;
1704
1705 for (i, (_, elem)) in self.iter_mut().enumerate() {
1706 if i < slop {
1707 *elem.size() += 1;
1708 }
1709 }
1710 }
1711}
1712
1713#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1715pub enum Border {
1716 Vertical { col_idx: usize },
1718 Horizontal { col_idx: usize, win_idx: usize },
1720}
1721
1722fn calculate_dims(t: usize, n: usize) -> (usize, usize) {
1733 let size = (t + 1) / n - 1;
1734 let slop = t + 1 - n * (size + 1);
1735
1736 (size, slop)
1737}
1738
1739fn apply_scroll(
1743 b: &mut Buffer,
1744 win: &mut Window,
1745 n_cols: usize,
1746 tabstop: usize,
1747 focused: bool,
1748 up: bool,
1749 scroll_rows: usize,
1750) {
1751 let n_rows = win.n_rows;
1752 let view = &mut win.view;
1753 let mut cur = if focused {
1754 b.dot.active_cur()
1755 } else {
1756 view.cur
1757 };
1758 let (y, x) = cur.as_yx(b);
1759 let y_max = b.txt.len_lines() - 1;
1760 let mut need_clamp = false;
1761
1762 if up && view.row_off > 0 && y == view.row_off + n_rows - 1 {
1763 cur = Cur::from_yx(y.saturating_sub(scroll_rows), x, b);
1764 } else if !up && y == view.row_off && view.row_off < y_max {
1765 cur = Cur::from_yx(min(y + scroll_rows, y_max), x, b);
1766 need_clamp = true;
1767 };
1768
1769 if focused {
1770 b.dot.set_active_cur(cur);
1771 if need_clamp {
1772 b.dot.clamp_idx(b.txt.len_chars());
1773 b.xdot.clamp_idx(b.txt.len_chars());
1774 }
1775 } else {
1776 view.cur = cur;
1777 }
1778
1779 view.row_off = if up {
1780 view.row_off.saturating_sub(scroll_rows)
1781 } else {
1782 min(view.row_off + scroll_rows, y_max)
1783 };
1784
1785 if focused {
1786 view.clamp_scroll(b, n_rows, n_cols, tabstop);
1787 }
1788}
1789
1790#[cfg(test)]
1791mod tests {
1792 use super::*;
1793 use crate::{
1794 dot::{Dot, TextObject},
1795 key::Arrow,
1796 };
1797 use simple_test_case::test_case;
1798 use std::{path::PathBuf, sync::mpsc::channel};
1799
1800 impl Layout {
1801 pub fn column_widths(&self) -> Vec<usize> {
1802 self.cols.iter().map(|(_, c)| c.n_cols).collect()
1803 }
1804
1805 pub fn window_heights(&self) -> Vec<usize> {
1806 self.cols.focus.wins.iter().map(|(_, w)| w.n_rows).collect()
1807 }
1808
1809 pub fn cols_before_focus(&self) -> usize {
1810 self.cols.up.len()
1811 }
1812 }
1813
1814 fn test_layout(col_wins: &[usize], n_rows: usize, n_cols: usize) -> Layout {
1815 let mut cols = Vec::with_capacity(col_wins.len());
1816 let mut n = 0;
1817 let mut all_ids = Vec::new();
1818 let (col_size, slop) = calculate_dims(n_cols, col_wins.len());
1819
1820 for (i, m) in col_wins.iter().enumerate() {
1821 let ids: Vec<usize> = (n..(n + m)).collect();
1822 n += m;
1823 let col_n_cols = if i < slop { col_size + 1 } else { col_size };
1824 cols.push(Column::new(n_rows, col_n_cols, &ids));
1825 all_ids.extend(ids);
1826 }
1827
1828 let (tx, _) = channel();
1829 let config = Arc::new(RwLock::new(Config::default()));
1830 let scratch = Scratch::new(config.clone());
1831
1832 let mut l = Layout {
1833 buffers: Buffers::new_stubbed(&all_ids, tx, config.clone()),
1834 config,
1835 scratch,
1836 screen_rows: n_rows,
1837 screen_cols: n_cols,
1838 cols: ZipList::try_from_iter(cols).unwrap(),
1839 views: vec![],
1840 changed_since_last_render: false,
1841 };
1842 l.update_screen_size(n_rows, n_cols);
1843
1844 l
1845 }
1846
1847 fn ordered_window_ids(l: &Layout) -> Vec<usize> {
1848 l.cols
1849 .iter()
1850 .flat_map(|(_, c)| c.wins.iter().map(|(_, w)| w.view.bufid))
1851 .collect::<Vec<_>>()
1852 }
1853
1854 #[test]
1855 fn opening_file_with_unnamed_split_works() {
1856 let (tx, _) = channel();
1857 let config = Arc::new(RwLock::new(Config::default()));
1858 let scratch = Scratch::new(config.clone());
1859
1860 let buffers = Buffers::new_with_raw_sender(tx, config.clone());
1861 let id = buffers.active().id;
1862
1863 let mut l = Layout {
1864 buffers,
1865 config,
1866 scratch,
1867 screen_rows: 80,
1868 screen_cols: 100,
1869 cols: ziplist![Column::new(80, 100, &[id])],
1870 views: vec![],
1871 changed_since_last_render: false,
1872 };
1873
1874 l.new_column();
1875
1876 let _ = l.open_or_focus("test-buffer.txt", false);
1880 }
1881
1882 #[test]
1883 fn drag_left_works() {
1884 let mut l = test_layout(&[1, 1, 2], 80, 100);
1885 l.next_column();
1886 assert_eq!(l.active_buffer().id, 1);
1887 l.drag_left();
1888
1889 assert_eq!(l.cols.len(), 2);
1890 let first_col: Vec<usize> = l
1891 .cols
1892 .head()
1893 .wins
1894 .iter()
1895 .map(|(_, w)| w.view.bufid)
1896 .collect();
1897 let second_col: Vec<usize> = l
1898 .cols
1899 .last()
1900 .wins
1901 .iter()
1902 .map(|(_, w)| w.view.bufid)
1903 .collect();
1904
1905 assert_eq!(&first_col, &[1, 0]);
1906 assert_eq!(&second_col, &[2, 3]);
1907 }
1908
1909 #[test]
1910 fn drag_right_works() {
1911 let mut l = test_layout(&[1, 1, 2], 80, 100);
1912 assert_eq!(l.active_buffer().id, 0);
1913 l.drag_right();
1914
1915 assert_eq!(l.cols.len(), 2);
1916 let first_col: Vec<usize> = l
1917 .cols
1918 .head()
1919 .wins
1920 .iter()
1921 .map(|(_, w)| w.view.bufid)
1922 .collect();
1923 let second_col: Vec<usize> = l
1924 .cols
1925 .last()
1926 .wins
1927 .iter()
1928 .map(|(_, w)| w.view.bufid)
1929 .collect();
1930
1931 assert_eq!(&first_col, &[0, 1]);
1932 assert_eq!(&second_col, &[2, 3]);
1933 }
1934
1935 #[test]
1936 fn next_prev_column_methods_work() {
1937 let mut l = test_layout(&[1, 1, 2], 80, 100);
1938 assert_eq!(l.focused_view().bufid, 0);
1939
1940 l.next_column();
1942 assert_eq!(l.focused_view().bufid, 1);
1943 l.next_column();
1944 assert_eq!(l.focused_view().bufid, 2);
1945 l.next_column();
1946 assert_eq!(l.focused_view().bufid, 0);
1947
1948 l.prev_column();
1950 assert_eq!(l.focused_view().bufid, 2);
1951 l.prev_column();
1952 assert_eq!(l.focused_view().bufid, 1);
1953 l.prev_column();
1954 assert_eq!(l.focused_view().bufid, 0);
1955 }
1956
1957 #[test]
1958 fn next_prev_window_methods_work() {
1959 let mut l = test_layout(&[3, 1], 80, 100);
1960 assert_eq!(l.focused_view().bufid, 0);
1961
1962 l.next_window_in_column();
1964 assert_eq!(l.focused_view().bufid, 1);
1965 l.next_window_in_column();
1966 assert_eq!(l.focused_view().bufid, 2);
1967 l.next_window_in_column();
1968 assert_eq!(l.focused_view().bufid, 0);
1969
1970 l.prev_window_in_column();
1972 assert_eq!(l.focused_view().bufid, 2);
1973 l.prev_window_in_column();
1974 assert_eq!(l.focused_view().bufid, 1);
1975 l.prev_window_in_column();
1976 assert_eq!(l.focused_view().bufid, 0);
1977 }
1978
1979 #[test_case(&[1], 30, 40, 0; "one col one win")]
1980 #[test_case(&[1, 1], 30, 40, 0; "two cols one win each click in first")]
1981 #[test_case(&[1, 1], 60, 40, 1; "two cols one win each click in second")]
1982 #[test_case(&[1, 2], 60, 40, 1; "two cols second with two click in second window")]
1983 #[test_case(&[1, 2], 60, 60, 2; "two cols second with two click in third window")]
1984 #[test_case(&[1, 3], 60, 15, 1; "two cols second with three click in first window")]
1985 #[test_case(&[1, 3], 60, 35, 2; "two cols second with three click in second window")]
1986 #[test_case(&[1, 3], 60, 60, 3; "two cols second with three click in third window")]
1987 #[test_case(&[1, 4], 60, 70, 4; "two cols second with four click in fourth window")]
1988 #[test]
1989 fn buffer_for_screen_coords_works(col_wins: &[usize], x: usize, y: usize, expected: BufferId) {
1990 let mut l = test_layout(col_wins, 80, 100);
1991
1992 assert_eq!(
1993 l.buffer_for_screen_coords(x, y),
1994 expected,
1995 "bufid without mutation"
1996 );
1997 assert_eq!(
1998 l.cols.focus.wins.focus.view.bufid, 0,
1999 "focused id before mutation"
2000 );
2001 assert_eq!(
2002 l.focus_buffer_for_screen_coords(x, y),
2003 expected,
2004 "bufid with mutation"
2005 );
2006 assert_eq!(
2007 l.cols.focus.wins.focus.view.bufid, expected,
2008 "focused id after mutation"
2009 );
2010 }
2011
2012 #[test_case(1, 1, "f"; "before wide char SOB")]
2013 #[test_case(4, 1, " "; "immediately before wide char")]
2014 #[test_case(5, 1, "δΈ"; "on first wide char")]
2015 #[test_case(7, 1, "η"; "on second wide char")]
2016 #[test_case(9, 1, " "; "after second wide char")]
2017 #[test_case(1, 2, "π¦"; "second line first wide char")]
2018 #[test_case(3, 2, "β"; "second line multibyte single cell char")]
2019 #[test_case(6, 2, "a"; "second line ascii after wide and multibyte")]
2020 #[test]
2021 fn cur_from_screen_coords_handles_wide_utf8_chars(x: usize, y: usize, s: &str) {
2022 let mut l = test_layout(&[1], 80, 100);
2023 let content = "foo δΈη β \nπ¦β bar".to_string();
2028 l.active_buffer_mut().insert_xdot(content);
2029
2030 let (_, w_sgncol) = l.active_buffer().sign_col_dims();
2034 let c = l.cur_from_screen_coords(x + w_sgncol, y);
2035 l.active_buffer_mut().dot = Dot::Cur { c };
2036
2037 assert_eq!(l.active_buffer().dot_contents(), s, "click=({x}, {y})");
2038 }
2039
2040 #[test_case(0, &[1, 2, 3, 4]; "0")]
2041 #[test_case(1, &[0, 2, 3, 4]; "1")]
2042 #[test_case(2, &[0, 1, 3, 4]; "2")]
2043 #[test_case(3, &[0, 1, 2, 4]; "3")]
2044 #[test_case(4, &[0, 1, 2, 3]; "4")]
2045 #[test]
2046 fn close_buffer_works(id: usize, expected: &[usize]) {
2047 let mut l = test_layout(&[1, 4], 80, 100);
2048 assert_eq!(&ordered_window_ids(&l), &[0, 1, 2, 3, 4], "initial ids");
2049
2050 l.close_buffer(id);
2051 assert!(!l.buffers.contains_bufid(id), "buffer id should be removed");
2052
2053 for bufid in expected.iter() {
2054 assert!(
2055 l.buffers.contains_bufid(*bufid),
2056 "other buffers should still be there"
2057 );
2058 }
2059
2060 assert_eq!(
2061 &ordered_window_ids(&l),
2062 expected,
2063 "ids for each window should be correct"
2064 );
2065 }
2066
2067 #[test]
2068 fn focus_buffer_for_screen_coords_doesnt_reorder_windows() {
2069 let (x, y) = (60, 70);
2070 let expected = 4;
2071 let mut l = test_layout(&[1, 4], 80, 100);
2072
2073 assert_eq!(
2074 &ordered_window_ids(&l),
2075 &[0, 1, 2, 3, 4],
2076 "before first click"
2077 );
2078
2079 assert_eq!(
2080 l.focus_buffer_for_screen_coords(x, y),
2081 expected,
2082 "bufid with mutation"
2083 );
2084
2085 assert_eq!(
2086 &ordered_window_ids(&l),
2087 &[0, 1, 2, 3, 4],
2088 "after first click"
2089 );
2090
2091 assert_eq!(
2092 l.focus_buffer_for_screen_coords(x, y),
2093 expected,
2094 "bufid with mutation"
2095 );
2096
2097 assert_eq!(
2098 &ordered_window_ids(&l),
2099 &[0, 1, 2, 3, 4],
2100 "after second click"
2101 );
2102 }
2103
2104 #[test]
2109 fn ui_xy_correctly_handles_multibyte_characters() {
2110 let s = "abc δΈη π¦";
2111 let widths = &[1, 1, 1, 1, 2, 2, 1, 2];
2113 let mut b = Buffer::new_virtual(0, "test", s, Default::default());
2114 let mut view = View::new(0);
2115 let mut offset = 0;
2116
2117 for (idx, ch) in s.chars().enumerate() {
2119 assert_eq!(b.dot_contents(), ch.to_string());
2120 assert_eq!(b.dot, Dot::Cur { c: Cur { idx } });
2121 assert_eq!(
2122 view.ui_xy(&b),
2123 (3 + offset, 0),
2124 "idx={idx} content={:?}",
2125 b.dot_contents()
2126 );
2127
2128 b.set_dot(TextObject::Arr(Arrow::Right), 1);
2129 view.clamp_scroll(&mut b, 80, 80, 4);
2130 offset += widths[idx];
2131 }
2132 }
2133
2134 #[test_case(1, 0, 10, &[100]; "one col inc")]
2135 #[test_case(1, 0, -10, &[100]; "one col dec")]
2136 #[test_case(2, 0, 10, &[60, 39]; "two cols inc one")]
2137 #[test_case(2, 0, -10, &[40, 59]; "two cols dec one")]
2138 #[test_case(2, 1, 10, &[40, 59]; "two cols inc two")]
2139 #[test_case(2, 1, -10, &[60, 39]; "two cols dec two")]
2140 #[test_case(3, 1, 10, &[23, 43, 32]; "three cols inc two")]
2141 #[test_case(3, 1, -10, &[43, 23, 32]; "three cols dec two")]
2142 #[test_case(2, 0, -200, &[MIN_DIM, 100 - MIN_DIM - 1]; "two cols dec one clamping")]
2143 #[test_case(2, 0, 200, &[100 - MIN_DIM - 1, MIN_DIM]; "two cols inc one clamping")]
2144 #[test]
2145 fn resize_active_column_works(n_cols: usize, ix: usize, delta: i16, expected_cols: &[usize]) {
2146 assert_eq!(expected_cols.len(), n_cols, "malformed test case");
2147 let mut l = test_layout(&vec![1; n_cols], 80, 100);
2148 l.cols.focus_head();
2150 for _ in 0..ix {
2151 l.cols.focus_down();
2152 }
2153
2154 l.resize_active_column(delta);
2155
2156 for (i, (_, c)) in l.cols.iter().enumerate() {
2157 assert_eq!(c.n_cols, expected_cols[i], "column {i}");
2158 }
2159 }
2160
2161 #[test_case(1, 0, 10, &[80]; "one win inc")]
2162 #[test_case(1, 0, -10, &[80]; "one win dec")]
2163 #[test_case(2, 0, 10, &[50, 29]; "two wins inc one")]
2164 #[test_case(2, 0, -10, &[30, 49]; "two wins dec one")]
2165 #[test_case(2, 1, 10, &[30, 49]; "two wins inc two")]
2166 #[test_case(2, 1, -10, &[50, 29]; "two wins dec two")]
2167 #[test_case(3, 1, 10, &[16, 36, 26]; "three wins inc two")]
2168 #[test_case(3, 1, -10, &[36, 16, 26]; "three wins dec two")]
2169 #[test_case(2, 0, -200, &[MIN_DIM, 80 - MIN_DIM - 1]; "two wins dec one clamping")]
2170 #[test_case(2, 0, 200, &[80 - MIN_DIM - 1, MIN_DIM]; "two wins inc one clamping")]
2171 #[test]
2172 fn resize_active_window_works(n_wins: usize, ix: usize, delta: i16, expected_rows: &[usize]) {
2173 assert_eq!(expected_rows.len(), n_wins, "malformed test case");
2174 let mut l = test_layout(&[n_wins], 80, 100);
2175 l.cols.focus.wins.focus_head();
2177 for _ in 0..ix {
2178 l.cols.focus.wins.focus_down();
2179 }
2180
2181 l.resize_active_window(delta);
2182
2183 for (i, (_, w)) in l.cols.focus.wins.iter().enumerate() {
2184 assert_eq!(w.n_rows, expected_rows[i], "window {i}");
2185 }
2186 }
2187
2188 #[test_case(100, 120, (73, 46), (100, 63, 36); "increase width and height")]
2189 #[test_case(60, 80, (48, 31), (60, 38, 21); "decrease width and height")]
2190 #[test]
2191 fn update_screen_size_preserves_relative_sizes(
2192 w: usize,
2193 h: usize,
2194 expected_cols: (usize, usize),
2195 expected_wins: (usize, usize, usize),
2196 ) {
2197 let mut l = test_layout(&[1, 2], 80, 100);
2198
2199 l.cols.focus_head();
2200 l.resize_active_column(10);
2201 l.cols.focus_down();
2202 l.cols.focus.wins.focus_head();
2203 l.resize_active_window(10); let cols = |l: &Layout| (l.cols.up[0].n_cols, l.cols.focus.n_cols);
2206 let wins = |l: &Layout| {
2207 (
2208 l.cols.up[0].wins.focus.n_rows,
2209 l.cols.focus.wins.focus.n_rows,
2210 l.cols.focus.wins.down[0].n_rows,
2211 )
2212 };
2213
2214 assert_eq!(cols(&l), (60, 39), "initial column widths");
2216 assert_eq!(wins(&l), (80, 50, 29), "initial window heights");
2217
2218 l.update_screen_size(w, h);
2219
2220 assert_eq!(cols(&l), expected_cols, "updated column widths");
2221 assert_eq!(wins(&l), expected_wins, "updated window heights");
2222 }
2223
2224 #[test]
2225 fn writing_to_a_non_visible_output_buffer_creates_a_window() {
2226 let mut l = test_layout(&[1, 1], 80, 100);
2227 assert_eq!(l.n_open_windows(), 2);
2228 assert_eq!(l.cols[0].wins[0].n_rows, 80);
2229 assert_eq!(l.cols[1].wins[0].n_rows, 80);
2230
2231 l.write_output_for_buffer(0, "some output".into(), &PathBuf::from("/tmp"));
2232 assert_eq!(l.n_open_windows(), 3);
2233 assert_eq!(l.cols[0].wins[0].n_rows, 80);
2234 assert_eq!(l.cols[1].wins[0].n_rows, 40);
2235 assert_eq!(l.cols[1].wins[1].n_rows, 39);
2236 }
2237
2238 #[test]
2239 fn single_column_single_window_has_no_borders() {
2240 let l = test_layout(&[1], 80, 100);
2241
2242 for x in 1..=100 {
2243 for y in 1..=80 {
2244 assert_eq!(
2245 l.border_at_coords(x, y),
2246 None,
2247 "unexpected hit @ ({x}, {y})"
2248 );
2249 }
2250 }
2251 }
2252
2253 #[test]
2254 fn single_column_multiple_windows_has_horizontal_borders() {
2255 let l = test_layout(&[3], 80, 100);
2256
2257 assert_eq!(l.cols[0].wins[0].n_rows, 26);
2258 assert_eq!(l.cols[0].wins[1].n_rows, 26);
2259 assert_eq!(l.cols[0].wins[2].n_rows, 26);
2260
2261 assert_eq!(
2262 l.border_at_coords(50, 27),
2263 Some(Border::Horizontal {
2264 col_idx: 0,
2265 win_idx: 0
2266 })
2267 );
2268
2269 assert_eq!(
2270 l.border_at_coords(50, 54),
2271 Some(Border::Horizontal {
2272 col_idx: 0,
2273 win_idx: 1
2274 })
2275 );
2276
2277 assert_eq!(l.border_at_coords(50, 81), None); assert_eq!(l.border_at_coords(50, 1), None); assert_eq!(l.border_at_coords(50, 26), None); assert_eq!(l.border_at_coords(50, 28), None); }
2282
2283 #[test]
2284 fn multiple_columns_single_window_each_has_vertical_borders() {
2285 let l = test_layout(&[1, 1, 1], 80, 100);
2286
2287 assert_eq!(l.cols[0].n_cols, 33);
2288 assert_eq!(l.cols[1].n_cols, 33);
2289 assert_eq!(l.cols[2].n_cols, 32);
2290
2291 assert_eq!(
2292 l.border_at_coords(34, 40),
2293 Some(Border::Vertical { col_idx: 0 })
2294 );
2295
2296 assert_eq!(
2297 l.border_at_coords(68, 40),
2298 Some(Border::Vertical { col_idx: 1 })
2299 );
2300
2301 assert_eq!(l.border_at_coords(101, 40), None); assert_eq!(l.border_at_coords(1, 40), None); assert_eq!(l.border_at_coords(33, 40), None); assert_eq!(l.border_at_coords(35, 40), None); }
2306
2307 #[test]
2308 fn multiple_columns_multiple_windows_has_both_border_types() {
2309 let l = test_layout(&[2, 2], 80, 100);
2310
2311 let col0_width = l.cols[0].n_cols;
2312 let win0_height = l.cols[0].wins[0].n_rows;
2313
2314 assert_eq!(
2315 l.border_at_coords(col0_width + 1, 20),
2316 Some(Border::Vertical { col_idx: 0 })
2317 );
2318
2319 assert_eq!(
2320 l.border_at_coords(10, win0_height + 1),
2321 Some(Border::Horizontal {
2322 col_idx: 0,
2323 win_idx: 0
2324 })
2325 );
2326
2327 let col1_x = col0_width + 1 + 10; assert_eq!(
2329 l.border_at_coords(col1_x, win0_height + 1),
2330 Some(Border::Horizontal {
2331 col_idx: 1,
2332 win_idx: 0
2333 })
2334 );
2335
2336 assert_eq!(l.border_at_coords(10, 10), None); assert_eq!(l.border_at_coords(col1_x, 10), None); }
2339
2340 #[test]
2341 fn border_coords_at_screen_edges() {
2342 let l = test_layout(&[1, 1], 80, 100);
2343
2344 assert_eq!(l.border_at_coords(1, 1), None); assert_eq!(l.border_at_coords(101, 40), None); assert_eq!(l.border_at_coords(10, 81), None); }
2348
2349 #[test]
2350 fn focus_column_for_resize_works() {
2351 let mut l = test_layout(&[1, 1, 1], 80, 100);
2352 assert_eq!(l.cols.up.len(), 0);
2353
2354 l.focus_column_for_resize(1);
2355 assert_eq!(l.cols.up.len(), 1);
2356 assert_eq!(l.cols.down.len(), 1);
2357
2358 l.focus_column_for_resize(2);
2359 assert_eq!(l.cols.up.len(), 2);
2360 assert_eq!(l.cols.down.len(), 0);
2361
2362 l.focus_column_for_resize(0);
2363 assert_eq!(l.cols.up.len(), 0);
2364 assert_eq!(l.cols.down.len(), 2);
2365 }
2366
2367 #[test]
2368 #[should_panic(expected = "col_idx out of bounds")]
2369 fn focus_column_for_resize_panics_on_out_of_bounds() {
2370 let mut l = test_layout(&[1, 1, 1], 80, 100);
2371 l.focus_column_for_resize(99);
2372 }
2373
2374 #[test]
2375 fn focus_window_for_resize_works() {
2376 let mut l = test_layout(&[3], 80, 100);
2377 assert_eq!(l.cols.focus.wins.up.len(), 0);
2378
2379 l.focus_window_for_resize(1);
2380 assert_eq!(l.cols.focus.wins.up.len(), 1);
2381 assert_eq!(l.cols.focus.wins.down.len(), 1);
2382
2383 l.focus_window_for_resize(2);
2384 assert_eq!(l.cols.focus.wins.up.len(), 2);
2385 assert_eq!(l.cols.focus.wins.down.len(), 0);
2386
2387 l.focus_window_for_resize(0);
2388 assert_eq!(l.cols.focus.wins.up.len(), 0);
2389 assert_eq!(l.cols.focus.wins.down.len(), 2);
2390 }
2391
2392 #[test]
2393 #[should_panic(expected = "win_idx out of bounds")]
2394 fn focus_window_for_resize_panics_on_out_of_bounds() {
2395 let mut l = test_layout(&[3], 80, 100);
2396 l.focus_window_for_resize(99);
2397 }
2398
2399 #[test]
2400 fn focus_column_and_window_for_resize_works() {
2401 let mut l = test_layout(&[2, 2], 80, 100);
2402
2403 l.focus_column_and_window_for_resize(1, 1);
2404 assert_eq!(l.cols.up.len(), 1);
2405 assert_eq!(l.cols.focus.wins.up.len(), 1);
2406 }
2407
2408 #[test]
2409 #[should_panic(expected = "col_idx out of bounds")]
2410 fn focus_column_and_window_for_resize_panics_on_bad_col() {
2411 let mut l = test_layout(&[2, 2], 80, 100);
2412 l.focus_column_and_window_for_resize(99, 0);
2413 }
2414
2415 #[test]
2416 #[should_panic(expected = "win_idx out of bounds")]
2417 fn focus_column_and_window_for_resize_panics_on_bad_win() {
2418 let mut l = test_layout(&[2, 2], 80, 100);
2419 l.focus_column_and_window_for_resize(0, 99);
2420 }
2421
2422 #[test_case(2, 0, 10, &[60, 39]; "two cols grow first against second")]
2423 #[test_case(2, 0, -10, &[40, 59]; "two cols shrink first against second")]
2424 #[test_case(3, 1, 10, &[33, 43, 22]; "three cols grow middle against last")]
2425 #[test_case(3, 1, -10, &[33, 23, 42]; "three cols shrink middle against last")]
2426 #[test_case(2, 0, -200, &[MIN_DIM, 100 - MIN_DIM - 1]; "clamps to MIN_DIM")]
2427 #[test_case(2, 1, 10, &[50, 49]; "last column has no next so noop")]
2428 #[test]
2429 fn resize_column_against_next(n_cols: usize, focus_idx: usize, delta: i16, expected: &[usize]) {
2430 let mut l = test_layout(&vec![1; n_cols], 80, 100);
2431 l.focus_column_for_resize(focus_idx);
2432 l.resize_active_column_against_next(delta);
2433
2434 for (i, (_, c)) in l.cols.iter().enumerate() {
2435 assert_eq!(c.n_cols, expected[i], "column {i}");
2436 }
2437 }
2438
2439 #[test_case(2, 0, 10, &[50, 29]; "two wins grow first against second")]
2440 #[test_case(2, 0, -10, &[30, 49]; "two wins shrink first against second")]
2441 #[test_case(3, 1, 10, &[26, 36, 16]; "three wins grow middle against last")]
2442 #[test_case(3, 1, -10, &[26, 16, 36]; "three wins shrink middle against last")]
2443 #[test_case(2, 0, -200, &[MIN_DIM, 80 - MIN_DIM - 1]; "clamps to MIN_DIM")]
2444 #[test_case(2, 1, 10, &[40, 39]; "last window has no next so noop")]
2445 #[test]
2446 fn resize_window_against_next(n_wins: usize, focus_idx: usize, delta: i16, expected: &[usize]) {
2447 let mut l = test_layout(&[n_wins], 80, 100);
2448 l.focus_window_for_resize(focus_idx);
2449 l.resize_active_window_against_next(delta);
2450
2451 for (i, (_, w)) in l.cols.focus.wins.iter().enumerate() {
2452 assert_eq!(w.n_rows, expected[i], "window {i}");
2453 }
2454 }
2455
2456 #[test]
2457 fn clamp_scroll_clamps_all_visible_views() {
2458 let mut l = test_layout(&[2, 3], 80, 100);
2459
2460 for (_, col) in l.cols.iter_mut() {
2461 for (_, win) in col.wins.iter_mut() {
2462 let b = l.buffers.with_id_mut(win.view.bufid).unwrap();
2463 b.insert_xdot("line1\nline2\nline3".to_string());
2464 win.view.row_off = 100;
2465 }
2466 }
2467
2468 l.clamp_scroll();
2469
2470 for (_, col) in l.cols.iter() {
2471 for (_, win) in col.wins.iter() {
2472 assert_eq!(
2473 win.view.row_off, 2,
2474 "bufid {} had row_off={}",
2475 win.view.bufid, win.view.row_off
2476 );
2477 }
2478 }
2479 }
2480
2481 #[test]
2482 fn apply_scroll_for_unfocused_window_clamps_row_off() {
2483 let config = Arc::new(RwLock::new(Config::default()));
2484 let mut b = Buffer::new_unnamed(0, "line1\nline2\nline3\nline4\nline5", config);
2485 let y_max = b.txt.len_lines() - 1;
2486 assert_eq!(y_max, 4);
2487
2488 let mut win = Window::new(0, 3);
2489 win.view.row_off = 3;
2490
2491 let focused = false;
2492 let up = false;
2493 let n_cols = 80;
2494 let tabstop = 4;
2495 let scroll_rows = 5;
2496
2497 apply_scroll(&mut b, &mut win, n_cols, tabstop, focused, up, scroll_rows);
2498
2499 assert_eq!(win.view.row_off, y_max);
2500 }
2501}