lv_tui/widgets/
splitpane.rs1use crate::component::{Component, EventCx, LayoutCx, MeasureCx};
2use crate::event::Event;
3use crate::geom::{Rect, Size};
4use crate::layout::Constraint;
5use crate::node::Node;
6use crate::render::RenderCx;
7use crate::style::Style;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum SplitDirection {
12 Horizontal,
14 Vertical,
16}
17
18pub struct SplitPane {
24 first: Option<Node>,
25 second: Option<Node>,
26 ratio: u16,
28 direction: SplitDirection,
29 rect: Rect,
30 style: Style,
31}
32
33impl SplitPane {
34 pub fn new() -> Self {
36 Self {
37 first: None,
38 second: None,
39 ratio: 50,
40 direction: SplitDirection::Horizontal,
41 rect: Rect::default(),
42 style: Style::default(),
43 }
44 }
45
46 pub fn first(mut self, component: impl Component + 'static) -> Self {
48 self.first = Some(Node::new(component));
49 self
50 }
51
52 pub fn second(mut self, component: impl Component + 'static) -> Self {
54 self.second = Some(Node::new(component));
55 self
56 }
57
58 pub fn ratio(mut self, ratio: u16) -> Self {
60 self.ratio = ratio.min(100);
61 self
62 }
63
64 pub fn direction(mut self, direction: SplitDirection) -> Self {
66 self.direction = direction;
67 self
68 }
69
70 pub fn style(mut self, style: Style) -> Self {
72 self.style = style;
73 self
74 }
75}
76
77impl Component for SplitPane {
78 fn render(&self, cx: &mut RenderCx) {
79 let is_h = self.direction == SplitDirection::Horizontal;
80
81 let (first_rect, second_rect) = self.child_rects();
83
84 if let Some(child) = &self.first {
86 let saved = child.rect();
87 child.set_rect(first_rect);
88 child.render_with_clip(cx.buffer, cx.focused_id, Some(first_rect));
89 child.set_rect(saved);
90 }
91
92 let div_style = Style::default().bg(crate::style::Color::White).fg(crate::style::Color::Black);
94 if is_h {
95 let div_x = second_rect.x.saturating_sub(1);
96 for y in self.rect.y..self.rect.y.saturating_add(self.rect.height) {
97 cx.buffer.write_text(
98 crate::geom::Pos { x: div_x, y },
99 self.rect, "│", &div_style,
100 );
101 }
102 } else {
103 let div_y = second_rect.y.saturating_sub(1);
104 for x in self.rect.x..self.rect.x.saturating_add(self.rect.width) {
105 cx.buffer.write_text(
106 crate::geom::Pos { x, y: div_y },
107 self.rect, "─", &div_style,
108 );
109 }
110 }
111
112 if let Some(child) = &self.second {
114 let saved = child.rect();
115 child.set_rect(second_rect);
116 child.render_with_clip(cx.buffer, cx.focused_id, Some(second_rect));
117 child.set_rect(saved);
118 }
119 }
120
121 fn measure(&self, constraint: Constraint, _cx: &mut MeasureCx) -> Size {
122 Size { width: constraint.max.width, height: constraint.max.height }
123 }
124
125 fn event(&mut self, event: &Event, cx: &mut EventCx) {
126 if let Event::Key(key_event) = event {
128 if key_event.modifiers.ctrl {
129 match self.direction {
130 SplitDirection::Horizontal => {
131 match &key_event.key {
132 crate::event::Key::Left => {
133 self.ratio = self.ratio.saturating_sub(5);
134 cx.invalidate_layout();
135 return;
136 }
137 crate::event::Key::Right => {
138 self.ratio = (self.ratio + 5).min(100);
139 cx.invalidate_layout();
140 return;
141 }
142 _ => {}
143 }
144 }
145 SplitDirection::Vertical => {
146 match &key_event.key {
147 crate::event::Key::Up => {
148 self.ratio = self.ratio.saturating_sub(5);
149 cx.invalidate_layout();
150 return;
151 }
152 crate::event::Key::Down => {
153 self.ratio = (self.ratio + 5).min(100);
154 cx.invalidate_layout();
155 return;
156 }
157 _ => {}
158 }
159 }
160 }
161 }
162 }
163
164 if cx.phase() == crate::event::EventPhase::Capture {
166 for child_opt in [&mut self.first, &mut self.second].iter_mut() {
167 if let Some(child) = child_opt {
168 let mut child_cx = EventCx::with_task_sender(
169 &mut child.dirty, cx.global_dirty, cx.quit,
170 cx.phase, cx.propagation_stopped, cx.task_sender.clone(),
171 );
172 child.component.event(event, &mut child_cx);
173 }
174 }
175 }
176 }
177
178 fn layout(&mut self, rect: Rect, _cx: &mut LayoutCx) {
179 self.rect = rect;
180 let (first_rect, second_rect) = self.child_rects();
181 if let Some(child) = &mut self.first { child.layout(first_rect); }
182 if let Some(child) = &mut self.second { child.layout(second_rect); }
183 }
184
185 fn for_each_child(&self, f: &mut dyn FnMut(&Node)) {
186 if let Some(child) = &self.first { f(child); }
187 if let Some(child) = &self.second { f(child); }
188 }
189 fn for_each_child_mut(&mut self, f: &mut dyn FnMut(&mut Node)) {
190 if let Some(child) = &mut self.first { f(child); }
191 if let Some(child) = &mut self.second { f(child); }
192 }
193 fn focusable(&self) -> bool { false }
194 fn style(&self) -> Style { self.style.clone() }
195}
196
197impl SplitPane {
198 fn child_rects(&self) -> (Rect, Rect) {
199 let is_h = self.direction == SplitDirection::Horizontal;
200 let total = if is_h { self.rect.width } else { self.rect.height };
201 let divider = 1u16;
202 let available = total.saturating_sub(divider);
203 let first_size = (available as u32 * self.ratio as u32 / 100) as u16;
204 let second_size = available.saturating_sub(first_size);
205
206 if is_h {
207 let first = Rect { x: self.rect.x, y: self.rect.y, width: first_size, height: self.rect.height };
208 let sx = self.rect.x.saturating_add(first_size).saturating_add(divider);
209 let second = Rect { x: sx, y: self.rect.y, width: second_size, height: self.rect.height };
210 (first, second)
211 } else {
212 let first = Rect { x: self.rect.x, y: self.rect.y, width: self.rect.width, height: first_size };
213 let sy = self.rect.y.saturating_add(first_size).saturating_add(divider);
214 let second = Rect { x: self.rect.x, y: sy, width: self.rect.width, height: second_size };
215 (first, second)
216 }
217 }
218}