ad_editor/ui/
layout.rs

1//! Layout of UI windows
2use 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
23/// The reserved ID for the scratch buffer.
24/// If we ever collide with this when creating a normal buffer then the user is
25/// doing something _very_ strange...
26pub const SCRATCH_ID: usize = usize::MAX;
27
28/// Similar to in ../buffer/internal.rs:/assert_line_endings/ this is used to hunt for exactly
29/// _where_ state becomes invalid between the actual buffer state in Buffers and the layout
30/// state referencing what should always be known IDs in Layout. This macro should be called
31/// at all points where the buffers & views are created / destroyed, as well as any time that
32/// views change their IDs. It should always be wrapped with #[cfg(test)] so that it doesn't
33/// affect the performance of the editor when it is actually in use.
34#[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/// Layout is a screen layout of the windows available for displaying buffer
72/// content to the user. The available screen space is split into a number of
73/// columns each containing a vertical stack of windows.
74#[derive(Debug)]
75pub struct Layout {
76    /// The managed buffer state
77    buffers: Buffers,
78    /// Global editor config
79    config: Arc<RwLock<Config>>,
80    /// An anonymous buffer that sits outside of the main buffer state and acts as though it is the
81    /// active buffer for the purposes of Load/Execute.
82    pub(crate) scratch: Scratch,
83    /// Available screen width in terms of characters
84    pub(crate) screen_rows: usize,
85    /// Available screen height in terms of characters
86    pub(crate) screen_cols: usize,
87    /// Left to right Columns of windows
88    pub(super) cols: ZipList<Column>,
89    /// Known Buffer views that are not currently active
90    pub(super) views: Vec<View>,
91    /// Whether or not the on-screen state changed since the last render of the UI.
92    /// Per-buffer state changes are tracked on each [Buffer], this flag is only for
93    /// changes to the UI layout itself.
94    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    /// Check to see if any actions taken since the last time this method was called resulted
137    /// in changes to the visible UI state.
138    ///
139    /// Calling this method will reset the internal flags used for checking these state changes.
140    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    /// The number of currently visible windows
165    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    /// Returns the active buffer, ignoring whether or not the scratch buffer is focused
187    pub(crate) fn active_buffer_ignoring_scratch(&self) -> &Buffer {
188        self.buffers.active()
189    }
190
191    /// Returns the active buffer, ignoring whether or not the scratch buffer is focused
192    pub(crate) fn active_buffer_mut_ignoring_scratch(&mut self) -> &mut Buffer {
193        self.buffers.active_mut()
194    }
195
196    /// Returns the active buffer or the scratch buffer if it is focused
197    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    /// Returns the active buffer or the scratch buffer if it is focused
206    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            // in the case where we only have an empty squirrel buffer present, we always replace
243            // the current buffer with the one that is newly opened.
244            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    /// Open a new virtual buffer that is not backed by a file on disk
266    ///
267    /// Opening the same virtual buffer a second time will replace the contents.
268    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    /// Open a new transient scratch buffer.
292    ///
293    /// This will replace the layout position of the main scratch buffer without altering it's
294    /// contents. When the transient buffer is closed, the main scratch buffer will be put back.
295    /// See [Scratch::toggle].
296    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    /// Returns true if this was the last buffer otherwise false.
308    ///
309    /// Closing a buffer also updates the UI:
310    ///   - any cached views for the buffer are cleared
311    ///   - any open windows containing the buffer are closed
312    ///   - if a buffer was the only window in a given column, the column is removed
313    ///   - if the active column is removed then focus moves to the next column
314    ///   - if there are no other columns then the "next buffer" is placed in the
315    ///     first column
316    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            // We could have been asked to close a non-existent buffer.
327            // If this was the last buffer then Editor::delete_buffer will exit
328            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        // Remove columns where there are only views of the closing buffer
361        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        // Remove remaining windows which were showing the closing buffer
370        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    /// Focus the given buffer ID without touching the jump list
404    pub(crate) fn focus_id_silent(&mut self, id: BufferId) {
405        self.buffers.focus_id_silent(id);
406    }
407
408    /// Focus the column at the given index without recording a jump.
409    ///
410    /// # Panics
411    /// Panics if col_idx is out of bounds.
412    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    /// Focus the window at the given index within the currently focused column,
427    /// without recording a jump.
428    ///
429    /// # Panics
430    /// Panics if win_idx is out of bounds.
431    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    /// Focus the column and window at the given indices without recording a jump.
447    ///
448    /// # Panics
449    /// Panics if either index is out of bounds.
450    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    /// Close the active window, if this was the last remaining window then
478    /// Editor::delete_active_window will exit.
479    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    /// Close the active column, if this was the last remaining column then
509    /// Editor::delete_active_column will exit.
510    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    /// Move focus to the column to the right of current focus (wrapping)
583    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    /// Move focus to the column to the left of current focus (wrapping)
596    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    /// Move focus to the window below in the current column (wrapping)
609    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    /// Move focus to the window above in the current column (wrapping)
622    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    /// Drag the focused window up through the column containing it (wrapping)
635    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    /// Drag the focused window down through the column containing it (wrapping)
646    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    /// Drag the focused window to the column on the left.
657    ///
658    /// # Semantics
659    /// - If the current columns contains multiple windows and the target exists
660    ///   then the current focus is moved to the focus position of the target column
661    /// - We anchor if the current column is the extreme left or right and this is
662    ///   the only window, otherwise a new column is created and the window is moved
663    ///   into it as the focus.
664    /// - If the focused window is the only window in and extremal column and the
665    ///   direction is towards other columns then the window is moved to that column
666    ///   and the previous column is removed.
667    pub(crate) fn drag_left(&mut self) {
668        self.scratch.is_focused = false;
669        self.changed_since_last_render = true;
670
671        // Strictly speaking, self.cols.up.is_empty() == true implies self.cols.len() == 1 but
672        // we keep the explicit check for clarity.
673        if self.cols.up.is_empty() || self.cols.len() == 1 {
674            // Single column or far left column
675
676            // If we only have a single window in this column then we're done...
677            if self.cols.focus.wins.len() == 1 {
678                return;
679            }
680
681            // Otherwise we need to create a new column containing only this window
682            let win = self.cols.focus.wins.remove_focused_unchecked();
683            self.balance_active_column(); // tidy up the column we've just popped from
684            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            // Column that is not on the far left containing only a single window
691
692            // If this column only has a single window then remove it an place the window in
693            // the column to the left
694            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            // Column that is not on the far left containing more than one window
705
706            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    /// Drag the focused window to the column on the right.
718    ///
719    /// See [Layout::drag_left] for semantics.
720    pub(crate) fn drag_right(&mut self) {
721        self.scratch.is_focused = false;
722        self.changed_since_last_render = true;
723
724        // Strictly speaking, self.cols.up.is_empty() == true implies self.cols.len() == 1 but
725        // we keep the explicit check for clarity.
726        if self.cols.len() == 1 || self.cols.down.is_empty() {
727            // Single column or far right column
728
729            // If we only have a single window in this column then we're done...
730            if self.cols.focus.wins.len() == 1 {
731                return;
732            }
733
734            // Otherwise we need to create a new column containing only this window
735            let win = self.cols.focus.wins.remove_focused_unchecked();
736            self.balance_active_column(); // tidy up the column we've just popped from
737
738            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            // Column that is not on the far right containing only a single window
745
746            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            // Column that is not on the far right containing more than one window
752
753            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    /// Adjust the size of the active [Column] by increasing or decreasing the number of
765    /// character columns it takes up.
766    ///
767    /// The adjustment is always applied from the left of the Column unless the active column
768    /// is the first in the [Layout], in which case the adjustment is made from the right.
769    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    /// Adjust the size of the active [Window] by increasing or decreasing the number of rows
775    /// it takes up.
776    ///
777    /// The adjustment is always applied from the top of the window unless the active window
778    /// is the first window in its [Column], in which case the adjustment is made from the
779    /// bottom of the window instead.
780    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    /// Resize the active column against the column to its right.
786    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    /// Resize the active window against the window below it.
792    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    /// Update the current layout state to reflect a new physical screen size given in terms
798    /// of the number of character rows and columns.
799    ///
800    /// Any existing layout customisation will be preserved as far as possible by converting
801    /// dimensions to be relative to the size of the full screen.
802    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    /// Force the columns within the layout to be equally sized.
820    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    /// Force the windows within the active column to be balanced.
833    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    /// Force the all windows within the layout to be equally sized within their respective
839    /// columns.
840    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    /// Force all columns and windows to be equally sized.
848    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    /// Set the currently focused window to contain the given buffer
872    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    /// Create a new column containing a single window showing the same view found in the
895    /// current active window.
896    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    /// Create a new window at the end of the current column showing the same view
912    /// found in the current active window.
913    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    /// Set the currently focused window to contain the given buffer
928    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    /// Clamp the active view to the current dot of the Buffer it is displaying and ensure that all
988    /// other views are within bounds for the end of the buffer.
989    ///
990    /// We need to do this for every visible view, not just the active one as external
991    /// inputs from systems such as the 9p filesystem and LSP servers can manipulate
992    /// state for non-active buffers.
993    pub(crate) fn clamp_scroll(&mut self) {
994        let tabstop = config_handle!(self).tabstop;
995
996        // Clamp the scratch buffer if it is visible unconditionally as we can't have multiple
997        // views of it.
998        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        // Clamp the active buffer fully to ensure that Dot is remaining within bounds
1008        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 all other visible Views, ensure that row_off is clamped to the end of the buffer but
1018        // don't _fully_ clamp to force the current Dot to be visible. This allows us to present
1019        // multiple Views of the same Buffer while only scrolling one of them.
1020        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; // handled above
1024                }
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    /// Coordinate offsets from the top left of the window layout to the top left of the active window.
1064    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; // +1 for status line
1067            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    /// Locate the absolute cursor position based on the current window layout
1079    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    /// Whether or not the given screen row is within a visible scratch buffer
1091    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; // Click was inside a window
1131            }
1132
1133            return None;
1134        }
1135
1136        None
1137    }
1138
1139    /// If the given coordinates lie within the scratch buffer return None, otherwise return the ID
1140    /// of the buffer containing the point.
1141    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    /// Map a given (x, y) point into a Cur for the active buffer or tag
1206    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    /// Determine the cursor position for a given set of coordinates and report whether or not
1237    /// these coordinates are inside of the currently active buffer (or tag).
1238    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    /// Focus the buffer (or tag) containing the given screen coordinates and return the current
1248    /// cursor position for updating held mouse state.
1249    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    /// Set the active buffer and dot based on a mouse click.
1257    ///
1258    /// Returns true if the click was in the currently active buffer and false if this click has
1259    /// changed the active buffer.
1260    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    /// Scroll the `View` under the given cursor coordinates up or down by `scroll_rows`
1274    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        // Default to scrolling the active window
1322        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    /// Returns `true` if the filter was successfully set, false if there was already one in place.
1351    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        // Deliberately self.scratch.b.main rather than self.scratch.b.buffer_mut() as we don't
1365        // support attaching an input filter to transient scratch buffers
1366        self.scratch.b.main.input_filter = Some(scratch_filter);
1367
1368        true
1369    }
1370
1371    /// Remove the input filter for the given buffer if one exists.
1372    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        // Deliberately self.scratch.b.main rather than self.scratch.b.buffer_mut() as we don't
1378        // support attaching an input filter to transient scratch buffers
1379        self.scratch.b.main.input_filter = None;
1380    }
1381}
1382
1383#[derive(Debug, Clone)]
1384pub(crate) struct Column {
1385    /// Number of character columns wide
1386    pub(crate) n_cols: usize,
1387    /// Windows within this column
1388    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    /// Needed to avoid borrowing all of Layout when calling [Layout::focused_view_mut].
1407    #[inline]
1408    fn focused_view_mut(&mut self) -> &mut View {
1409        &mut self.wins.focus.view
1410    }
1411
1412    /// Force the windows within this column to be balanced regardless of their current sizes
1413    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/// State for the scratch buffer
1425#[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    // n_rows is read from config on startup but then not modified after that
1455    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    /// Toggle the visibility of the scratch buffer and focus it if opening.
1476    ///
1477    /// If the scratch buffer was visible and contained a transient buffer, remove it.
1478    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    /// Number of character rows high
1491    pub(crate) n_rows: usize,
1492    /// Buffer view details currently shown in this window
1493    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    /// provides an (x, y) coordinate assuming that this window is in the top left
1526    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    /// Force the contained Buffer cursor to be visible if it currently isn't
1552    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    /// Clamp the current viewport to include the [Dot].
1564    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    /// Set the current [ViewPort] while accounting for screen size.
1595    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
1615/// Min window size is 5x5
1616const 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    /// Attempt to adjust the size of the focused element by a given delta.
1651    ///
1652    /// Clamp to [MIN_DIM] for both the focused element and the adjacent element
1653    /// that is being modified along with it.
1654    fn grow_focus(&mut self, delta: i16) {
1655        if self.len() == 1 || delta == 0 {
1656            return; // nothing to grow
1657        }
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    /// Resize the focused element against the next element (down[0]).
1675    ///
1676    /// No-op if there's no next element to resize against.
1677    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    /// Attempt to preserve the current relative size of each element when the
1693    /// overall available space changes.
1694    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/// A border that can be dragged to resize
1714#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1715pub enum Border {
1716    /// Vertical border after column at index `col_idx`
1717    Vertical { col_idx: usize },
1718    /// Horizontal border after window at index `win_idx` within column at index `col_idx`
1719    Horizontal { col_idx: usize, win_idx: usize },
1720}
1721
1722/// Calculate the size (rows/cols) for n blocks within an available space of t
1723/// while accounting for "slop" that will be added to some elements to make up
1724/// the correct total.
1725///
1726/// This calculation is derived from:  t = n(size) + (n - 1) + slop
1727///
1728/// where the (n - 1) is spacer rows/columns between each region. The use of
1729/// truncating division in computing "size" gets us an approximate answer for
1730/// an integer value that solve the equation above without "slop", which is then
1731/// calculated to get the correct total
1732fn 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
1739/// When we apply scrolling to a [View] we need to keep track of a preferred cursor position so
1740/// that when the user bounces between windows they don't get reset to a default position based on
1741/// the viewport alone.
1742fn 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        // This will panic if we've ended removing the original unnamed buffer from
1877        // self.buffers as part of opening the virtual buffer as the Layout state
1878        // will now contain references to an unknown buffer ID (via assert_invariants)
1879        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        // next wrapping
1941        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        // prev wrapping
1949        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        // next wrapping
1963        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        // prev wrapping
1971        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        // This is a mix of ascii and utf-8 multi-byte characters where some (but not all) of the
2024        // multi-byte characters have width > 1. Our handling of the raw x position given to us
2025        // from terminal input needs to be based on _character width_ rather than the number of
2026        // bytes in the character.
2027        let content = "foo δΈ–η•Œ ⌠\nπŸ¦ŠβŒ– bar".to_string();
2028        l.active_buffer_mut().insert_xdot(content);
2029
2030        // cur_from_screen_coords has to account for the additional UI elements we have in place
2031        // for the sign column so this gets added on here to allow the test case parameters to
2032        // represent the logical position within the buffer.
2033        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    // NOTE: there was a bug around misunderstanding terminal "cells" in relation to
2105    //       wide unicode characters
2106    //       - https://github.com/crossterm-rs/crossterm/issues/458
2107    //       - https://github.com/unicode-rs/unicode-width
2108    #[test]
2109    fn ui_xy_correctly_handles_multibyte_characters() {
2110        let s = "abc δΈ–η•Œ 🦊";
2111        // unicode display width for each character
2112        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        // sign column offset is 3
2118        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        // set focus to the target column
2149        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        // set focus to the target window
2176        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); // now focused on 1st window of 2nd column
2204
2205        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        // check that the initial column and window sizes are correct
2215        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); // past last window
2278        assert_eq!(l.border_at_coords(50, 1), None); // inside first window
2279        assert_eq!(l.border_at_coords(50, 26), None); // last row of first window
2280        assert_eq!(l.border_at_coords(50, 28), None); // first row of second window
2281    }
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); // past last column
2302        assert_eq!(l.border_at_coords(1, 40), None); // inside first column
2303        assert_eq!(l.border_at_coords(33, 40), None); // last char of first column
2304        assert_eq!(l.border_at_coords(35, 40), None); // first char of second column
2305    }
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; // inside col1
2328        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); // inside window
2337        assert_eq!(l.border_at_coords(col1_x, 10), None); // inside window in col1
2338    }
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); // top-left corner, inside first window
2345        assert_eq!(l.border_at_coords(101, 40), None); // past right edge
2346        assert_eq!(l.border_at_coords(10, 81), None); // past bottom edge
2347    }
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}