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