Skip to main content

lv_tui/widgets/
splitpane.rs

1use 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/// Split direction.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum SplitDirection {
12    /// First on the left, second on the right.
13    Horizontal,
14    /// First on top, second on bottom.
15    Vertical,
16}
17
18/// A resizable split pane with two children.
19///
20/// The divider position is controlled by `ratio` (0–100), representing the
21/// percentage of space allocated to the first child. Use Ctrl+←/→ (horizontal)
22/// or Ctrl+↑/↓ (vertical) to adjust the ratio.
23pub struct SplitPane {
24    first: Option<Node>,
25    second: Option<Node>,
26    /// Percentage of space for the first child (0–100).
27    ratio: u16,
28    direction: SplitDirection,
29    rect: Rect,
30    style: Style,
31}
32
33impl SplitPane {
34    /// Creates an empty split pane.
35    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    /// Sets the first (left/top) child.
47    pub fn first(mut self, component: impl Component + 'static) -> Self {
48        self.first = Some(Node::new(component));
49        self
50    }
51
52    /// Sets the second (right/bottom) child.
53    pub fn second(mut self, component: impl Component + 'static) -> Self {
54        self.second = Some(Node::new(component));
55        self
56    }
57
58    /// Sets the ratio (0–100) of space for the first child.
59    pub fn ratio(mut self, ratio: u16) -> Self {
60        self.ratio = ratio.min(100);
61        self
62    }
63
64    /// Sets the split direction.
65    pub fn direction(mut self, direction: SplitDirection) -> Self {
66        self.direction = direction;
67        self
68    }
69
70    /// Builder: sets the splitter line style.
71    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        // Calculate child rects
82        let (first_rect, second_rect) = self.child_rects();
83
84        // Render first
85        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        // Divider line
93        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        // Render second
113        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        // Ctrl+arrows adjust ratio
127        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        // Forward events to both children (capture phase only)
165        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}