Skip to main content

tui/components/
split_panel.rs

1use super::component::{Component, Event};
2use crate::focus::{FocusOutcome, FocusRing};
3use crate::rendering::frame::{Cursor, Frame};
4use crate::rendering::line::Line;
5use crate::rendering::render_context::ViewContext;
6use crate::rendering::soft_wrap::{soft_wrap_lines_with_map, truncate_line};
7use crate::style::Style;
8use crossterm::event::KeyCode;
9
10pub struct SplitWidths {
11    pub left: u16,
12    pub right: u16,
13}
14
15pub struct SplitLayout {
16    base: SplitBase,
17    step: i16,
18    min_left: usize,
19}
20
21enum SplitBase {
22    Fraction { numer: usize, denom: usize, min: usize, max: usize },
23    Fixed(usize),
24}
25
26impl SplitLayout {
27    pub fn fraction(numer: usize, denom: usize, min: usize, max: usize) -> Self {
28        Self { base: SplitBase::Fraction { numer, denom, min, max }, step: 4, min_left: 12 }
29    }
30
31    pub fn fixed(width: usize) -> Self {
32        Self { base: SplitBase::Fixed(width), step: 4, min_left: 12 }
33    }
34
35    pub fn with_step(mut self, step: i16) -> Self {
36        self.step = step;
37        self
38    }
39
40    pub fn with_min_left(mut self, min: usize) -> Self {
41        self.min_left = min;
42        self
43    }
44
45    fn widths(&self, total_width: u16, delta: i16) -> SplitWidths {
46        let total = total_width as usize;
47        let base = match self.base {
48            SplitBase::Fraction { numer, denom, min, max } => (total * numer / denom).clamp(min, max),
49            SplitBase::Fixed(w) => w,
50        };
51        let left = base.saturating_add_signed(delta.into()).clamp(self.min_left, total / 2);
52        let right = total.saturating_sub(left + 1);
53        SplitWidths { left: u16::try_from(left).unwrap_or(u16::MAX), right: u16::try_from(right).unwrap_or(u16::MAX) }
54    }
55
56    fn step(&self) -> i16 {
57        self.step
58    }
59}
60
61pub enum Either<L, R> {
62    Left(L),
63    Right(R),
64}
65
66pub struct SplitPanel<L: Component, R: Component> {
67    left: L,
68    right: R,
69    layout: SplitLayout,
70    delta: i16,
71    focus: FocusRing,
72    separator: Option<(String, Style)>,
73    resize_keys: bool,
74}
75
76impl<L: Component, R: Component> SplitPanel<L, R> {
77    pub fn new(left: L, right: R, layout: SplitLayout) -> Self {
78        Self {
79            left,
80            right,
81            layout,
82            delta: 0,
83            focus: FocusRing::new(2).without_wrap(),
84            separator: None,
85            resize_keys: false,
86        }
87    }
88
89    pub fn with_separator(mut self, text: impl Into<String>, style: Style) -> Self {
90        self.separator = Some((text.into(), style));
91        self
92    }
93
94    pub fn with_resize_keys(mut self) -> Self {
95        self.resize_keys = true;
96        self
97    }
98
99    pub fn left(&self) -> &L {
100        &self.left
101    }
102
103    pub fn left_mut(&mut self) -> &mut L {
104        &mut self.left
105    }
106
107    pub fn right(&self) -> &R {
108        &self.right
109    }
110
111    pub fn right_mut(&mut self) -> &mut R {
112        &mut self.right
113    }
114
115    pub fn focus_left(&mut self) {
116        self.focus.focus(0);
117    }
118
119    pub fn focus_right(&mut self) {
120        self.focus.focus(1);
121    }
122
123    pub fn is_left_focused(&self) -> bool {
124        self.focus.is_focused(0)
125    }
126
127    pub fn widths(&self, total_width: u16) -> SplitWidths {
128        self.layout.widths(total_width, self.delta)
129    }
130
131    fn widen(&mut self) {
132        self.delta += self.layout.step();
133    }
134
135    fn narrow(&mut self) {
136        self.delta -= self.layout.step();
137    }
138
139    pub fn set_separator_style(&mut self, style: Style) {
140        if let Some((_, s)) = &mut self.separator {
141            *s = style;
142        }
143    }
144}
145
146impl<L: Component, R: Component> Component for SplitPanel<L, R> {
147    type Message = Either<L::Message, R::Message>;
148
149    async fn on_event(&mut self, event: &Event) -> Option<Vec<Self::Message>> {
150        if let Event::Key(key) = event {
151            if self.resize_keys {
152                match key.code {
153                    KeyCode::Char('<') => {
154                        self.narrow();
155                        return Some(vec![]);
156                    }
157                    KeyCode::Char('>') => {
158                        self.widen();
159                        return Some(vec![]);
160                    }
161                    _ => {}
162                }
163            }
164
165            let outcome = self.focus.handle_key(*key);
166            if matches!(outcome, FocusOutcome::FocusChanged) {
167                return Some(vec![]);
168            }
169        }
170
171        if self.focus.is_focused(0) {
172            self.left.on_event(event).await.map(|msgs| msgs.into_iter().map(Either::Left).collect())
173        } else {
174            self.right.on_event(event).await.map(|msgs| msgs.into_iter().map(Either::Right).collect())
175        }
176    }
177
178    fn render(&mut self, ctx: &ViewContext) -> Frame {
179        let widths = self.widths(ctx.size.width);
180
181        let left_ctx = ctx.with_size((widths.left, ctx.size.height));
182        let right_ctx = ctx.with_size((widths.right, ctx.size.height));
183
184        let (left_lines, left_cursor) = self.left.render(&left_ctx).into_parts();
185        let (right_lines, right_cursor) = self.right.render(&right_ctx).into_parts();
186
187        let (mut right_visual_lines, right_row_starts) = if widths.right == 0 {
188            (Vec::new(), vec![0; right_lines.len()])
189        } else {
190            soft_wrap_lines_with_map(&right_lines, widths.right)
191        };
192
193        for line in &mut right_visual_lines {
194            line.extend_bg_to_width(widths.right.into());
195        }
196
197        let max_rows = usize::from(ctx.size.height);
198        let sep_width = self.separator.as_ref().map_or(0, |(t, _)| t.len());
199        let mut merged = Vec::with_capacity(max_rows);
200
201        for i in 0..max_rows {
202            let mut line = match left_lines.get(i) {
203                Some(l) => {
204                    let mut l = truncate_line(l, widths.left.into());
205                    l.extend_bg_to_width(widths.left.into());
206                    l
207                }
208                None => Line::new(" ".repeat(widths.left.into())),
209            };
210            if let Some((text, style)) = &self.separator {
211                line.push_with_style(text, *style);
212            }
213            if let Some(right) = right_visual_lines.get(i) {
214                line.append_line(right);
215            }
216            merged.push(line);
217        }
218
219        let cursor = if self.focus.is_focused(0) && left_cursor.is_visible {
220            if left_cursor.row < max_rows { left_cursor } else { Cursor::hidden() }
221        } else if self.focus.is_focused(1) && right_cursor.is_visible && widths.right > 0 {
222            let mut row = right_row_starts
223                .get(right_cursor.row)
224                .copied()
225                .unwrap_or_else(|| right_visual_lines.len().saturating_sub(1));
226            let mut col = right_cursor.col;
227            let right_width = usize::from(widths.right);
228
229            row += col / right_width;
230            col %= right_width;
231
232            if row < max_rows {
233                Cursor::visible(row, col + usize::from(widths.left) + sep_width)
234            } else {
235                Cursor::hidden()
236            }
237        } else {
238            Cursor::hidden()
239        };
240
241        Frame::new(merged).with_cursor(cursor)
242    }
243}