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}