ratatui_toolkit/resizable_split/
mod.rs1use ratatui::layout::Rect;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum SplitDirection {
10 Vertical,
12 Horizontal,
14}
15
16#[derive(Debug, Clone)]
18pub struct ResizableSplit {
19 pub split_percent: u16,
23 pub min_percent: u16,
25 pub max_percent: u16,
27 pub is_dragging: bool,
29 pub is_hovering: bool,
31 pub direction: SplitDirection,
33 pub divider_pos: u16,
35}
36
37impl ResizableSplit {
38 pub fn new(initial_percent: u16) -> Self {
40 Self::new_with_direction(initial_percent, SplitDirection::Vertical)
41 }
42
43 pub fn new_with_direction(initial_percent: u16, direction: SplitDirection) -> Self {
45 Self {
46 split_percent: initial_percent.clamp(5, 95),
47 min_percent: 10,
48 max_percent: 90,
49 is_dragging: false,
50 is_hovering: false,
51 direction,
52 divider_pos: 0,
53 }
54 }
55
56 pub fn update_divider_position(&mut self, area: Rect) {
58 match self.direction {
59 SplitDirection::Vertical => {
60 let left_width = (area.width as u32 * self.split_percent as u32 / 100) as u16;
61 self.divider_pos = area.x + left_width;
62 }
63 SplitDirection::Horizontal => {
64 let top_height = (area.height as u32 * self.split_percent as u32 / 100) as u16;
65 self.divider_pos = area.y + top_height;
66 }
67 }
68 }
69
70 pub fn is_on_divider(&self, mouse_column: u16, mouse_row: u16, area: Rect) -> bool {
72 match self.direction {
73 SplitDirection::Vertical => {
74 let divider_start = self.divider_pos.saturating_sub(1);
75 let divider_end = (self.divider_pos + 1).min(area.x + area.width.saturating_sub(1));
76 mouse_column >= divider_start && mouse_column <= divider_end
77 }
78 SplitDirection::Horizontal => {
79 let divider_start = self.divider_pos.saturating_sub(1);
80 let divider_end =
81 (self.divider_pos + 1).min(area.y + area.height.saturating_sub(1));
82 mouse_row >= divider_start && mouse_row <= divider_end
83 }
84 }
85 }
86
87 pub fn start_drag(&mut self) {
89 self.is_dragging = true;
90 }
91
92 pub fn stop_drag(&mut self) {
94 self.is_dragging = false;
95 }
96
97 pub fn update_from_mouse(&mut self, mouse_column: u16, mouse_row: u16, area: Rect) {
99 if !self.is_dragging {
100 return;
101 }
102
103 let new_percent = match self.direction {
104 SplitDirection::Vertical => {
105 let relative_column = mouse_column.saturating_sub(area.x);
106
107 if area.width > 0 {
108 ((relative_column as u32 * 100) / area.width as u32) as u16
109 } else {
110 self.split_percent
111 }
112 }
113 SplitDirection::Horizontal => {
114 let relative_row = mouse_row.saturating_sub(area.y);
115
116 if area.height > 0 {
117 ((relative_row as u32 * 100) / area.height as u32) as u16
118 } else {
119 self.split_percent
120 }
121 }
122 };
123
124 self.split_percent = new_percent.clamp(self.min_percent, self.max_percent);
125 }
126
127 pub fn right_percent(&self) -> u16 {
129 100 - self.split_percent
130 }
131
132 pub fn bottom_percent(&self) -> u16 {
134 self.right_percent()
135 }
136
137 pub fn render_divider_indicator(&self, _frame: &mut ratatui::Frame, _area: Rect) {
139 }
141}
142
143impl Default for ResizableSplit {
144 fn default() -> Self {
145 Self::new(70)
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn test_new_clamps_percentage() {
155 let split = ResizableSplit::new(150);
156 assert_eq!(split.split_percent, 95);
157
158 let split = ResizableSplit::new(0);
159 assert_eq!(split.split_percent, 5);
160 }
161
162 #[test]
163 fn test_update_divider_position_vertical() {
164 let mut split = ResizableSplit::new(50);
165 let area = Rect::new(0, 0, 100, 20);
166
167 split.update_divider_position(area);
168 assert_eq!(split.divider_pos, 50);
169 }
170
171 #[test]
172 fn test_update_divider_position_horizontal() {
173 let mut split = ResizableSplit::new_with_direction(50, SplitDirection::Horizontal);
174 let area = Rect::new(0, 0, 100, 20);
175
176 split.update_divider_position(area);
177 assert_eq!(split.divider_pos, 10);
178 }
179
180 #[test]
181 fn test_is_on_divider_vertical() {
182 let mut split = ResizableSplit::new(50);
183 let area = Rect::new(0, 0, 100, 20);
184 split.update_divider_position(area);
185
186 assert!(split.is_on_divider(49, 10, area));
187 assert!(split.is_on_divider(50, 10, area));
188 assert!(split.is_on_divider(51, 10, area));
189 assert!(!split.is_on_divider(47, 10, area));
190 assert!(!split.is_on_divider(53, 10, area));
191 }
192
193 #[test]
194 fn test_is_on_divider_horizontal() {
195 let mut split = ResizableSplit::new_with_direction(50, SplitDirection::Horizontal);
196 let area = Rect::new(0, 0, 100, 20);
197 split.update_divider_position(area);
198
199 assert!(split.is_on_divider(50, 9, area));
200 assert!(split.is_on_divider(50, 10, area));
201 assert!(split.is_on_divider(50, 11, area));
202 assert!(!split.is_on_divider(50, 7, area));
203 assert!(!split.is_on_divider(50, 13, area));
204 }
205
206 #[test]
207 fn test_update_from_mouse_vertical() {
208 let mut split = ResizableSplit::new(50);
209 let area = Rect::new(0, 0, 100, 20);
210
211 split.update_from_mouse(75, 10, area);
212 assert_eq!(split.split_percent, 50);
213
214 split.start_drag();
215 split.update_from_mouse(75, 10, area);
216 assert_eq!(split.split_percent, 75);
217
218 split.update_from_mouse(99, 10, area);
219 assert_eq!(split.split_percent, 90);
220
221 split.update_from_mouse(1, 10, area);
222 assert_eq!(split.split_percent, 10);
223 }
224
225 #[test]
226 fn test_update_from_mouse_horizontal() {
227 let mut split = ResizableSplit::new_with_direction(50, SplitDirection::Horizontal);
228 let area = Rect::new(0, 0, 100, 20);
229
230 split.update_from_mouse(50, 15, area);
231 assert_eq!(split.split_percent, 50);
232
233 split.start_drag();
234 split.update_from_mouse(50, 15, area);
235 assert_eq!(split.split_percent, 75);
236
237 split.update_from_mouse(50, 19, area);
238 assert_eq!(split.split_percent, 90);
239
240 split.update_from_mouse(50, 1, area);
241 assert_eq!(split.split_percent, 10);
242 }
243}