1use ratatui::layout::Rect;
2use ratatui::Frame;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum SplitDirection {
7 Vertical,
9 Horizontal,
11}
12
13#[derive(Debug, Clone)]
15pub struct ResizableSplit {
16 pub split_percent: u16,
20 pub min_percent: u16,
22 pub max_percent: u16,
24 pub is_dragging: bool,
26 pub is_hovering: bool,
28 pub direction: SplitDirection,
30 pub divider_pos: u16,
32}
33
34impl ResizableSplit {
35 pub fn new(initial_percent: u16) -> Self {
37 Self::new_with_direction(initial_percent, SplitDirection::Vertical)
38 }
39
40 pub fn new_with_direction(initial_percent: u16, direction: SplitDirection) -> Self {
42 Self {
43 split_percent: initial_percent.clamp(5, 95),
44 min_percent: 10,
45 max_percent: 90,
46 is_dragging: false,
47 is_hovering: false,
48 direction,
49 divider_pos: 0,
50 }
51 }
52
53 pub fn update_divider_position(&mut self, area: Rect) {
55 match self.direction {
56 SplitDirection::Vertical => {
57 let left_width = (area.width as u32 * self.split_percent as u32 / 100) as u16;
60 self.divider_pos = area.x + left_width;
61 }
62 SplitDirection::Horizontal => {
63 let top_height = (area.height as u32 * self.split_percent as u32 / 100) as u16;
66 self.divider_pos = area.y + top_height;
67 }
68 }
69 }
70
71 pub fn is_on_divider(&self, mouse_column: u16, mouse_row: u16, area: Rect) -> bool {
74 match self.direction {
75 SplitDirection::Vertical => {
76 let divider_start = self.divider_pos.saturating_sub(1);
77 let divider_end = (self.divider_pos + 1).min(area.x + area.width.saturating_sub(1));
78 mouse_column >= divider_start && mouse_column <= divider_end
79 }
80 SplitDirection::Horizontal => {
81 let divider_start = self.divider_pos.saturating_sub(1);
82 let divider_end =
83 (self.divider_pos + 1).min(area.y + area.height.saturating_sub(1));
84 mouse_row >= divider_start && mouse_row <= divider_end
85 }
86 }
87 }
88
89 pub fn start_drag(&mut self) {
91 self.is_dragging = true;
92 }
93
94 pub fn stop_drag(&mut self) {
96 self.is_dragging = false;
97 }
98
99 pub fn update_from_mouse(&mut self, mouse_column: u16, mouse_row: u16, area: Rect) {
101 if !self.is_dragging {
102 return;
103 }
104
105 let new_percent = match self.direction {
106 SplitDirection::Vertical => {
107 let relative_column = mouse_column.saturating_sub(area.x);
109
110 if area.width > 0 {
112 ((relative_column as u32 * 100) / area.width as u32) as u16
113 } else {
114 self.split_percent
115 }
116 }
117 SplitDirection::Horizontal => {
118 let relative_row = mouse_row.saturating_sub(area.y);
120
121 if area.height > 0 {
123 ((relative_row as u32 * 100) / area.height as u32) as u16
124 } else {
125 self.split_percent
126 }
127 }
128 };
129
130 self.split_percent = new_percent.clamp(self.min_percent, self.max_percent);
132 }
133
134 pub fn right_percent(&self) -> u16 {
136 100 - self.split_percent
137 }
138
139 pub fn bottom_percent(&self) -> u16 {
141 self.right_percent()
142 }
143
144 pub fn render_divider_indicator(&self, _frame: &mut Frame, _area: Rect) {
147 }
149}
150
151impl Default for ResizableSplit {
152 fn default() -> Self {
153 Self::new(70)
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn test_new_clamps_percentage() {
163 let split = ResizableSplit::new(150);
164 assert_eq!(split.split_percent, 95);
165
166 let split = ResizableSplit::new(0);
167 assert_eq!(split.split_percent, 5);
168 }
169
170 #[test]
171 fn test_update_divider_position_vertical() {
172 let mut split = ResizableSplit::new(50);
173 let area = Rect::new(0, 0, 100, 20);
174
175 split.update_divider_position(area);
176 assert_eq!(split.divider_pos, 50);
178 }
179
180 #[test]
181 fn test_update_divider_position_horizontal() {
182 let mut split = ResizableSplit::new_with_direction(50, SplitDirection::Horizontal);
183 let area = Rect::new(0, 0, 100, 20);
184
185 split.update_divider_position(area);
186 assert_eq!(split.divider_pos, 10);
188 }
189
190 #[test]
191 fn test_is_on_divider_vertical() {
192 let mut split = ResizableSplit::new(50);
193 let area = Rect::new(0, 0, 100, 20);
194 split.update_divider_position(area);
195
196 assert!(split.is_on_divider(49, 10, area)); assert!(split.is_on_divider(50, 10, area)); assert!(split.is_on_divider(51, 10, area)); assert!(!split.is_on_divider(47, 10, area)); assert!(!split.is_on_divider(53, 10, area)); }
203
204 #[test]
205 fn test_is_on_divider_horizontal() {
206 let mut split = ResizableSplit::new_with_direction(50, SplitDirection::Horizontal);
207 let area = Rect::new(0, 0, 100, 20);
208 split.update_divider_position(area);
209
210 assert!(split.is_on_divider(50, 9, area)); assert!(split.is_on_divider(50, 10, area)); assert!(split.is_on_divider(50, 11, area)); assert!(!split.is_on_divider(50, 7, area)); assert!(!split.is_on_divider(50, 13, area)); }
217
218 #[test]
219 fn test_update_from_mouse_vertical() {
220 let mut split = ResizableSplit::new(50);
221 let area = Rect::new(0, 0, 100, 20);
222
223 split.update_from_mouse(75, 10, area);
225 assert_eq!(split.split_percent, 50);
226
227 split.start_drag();
229 split.update_from_mouse(75, 10, area);
230 assert_eq!(split.split_percent, 75);
231
232 split.update_from_mouse(99, 10, area);
234 assert_eq!(split.split_percent, 90); split.update_from_mouse(1, 10, area);
238 assert_eq!(split.split_percent, 10); }
240
241 #[test]
242 fn test_update_from_mouse_horizontal() {
243 let mut split = ResizableSplit::new_with_direction(50, SplitDirection::Horizontal);
244 let area = Rect::new(0, 0, 100, 20);
245
246 split.update_from_mouse(50, 15, area);
248 assert_eq!(split.split_percent, 50);
249
250 split.start_drag();
252 split.update_from_mouse(50, 15, area);
253 assert_eq!(split.split_percent, 75); split.update_from_mouse(50, 19, area);
257 assert_eq!(split.split_percent, 90); split.update_from_mouse(50, 1, area);
261 assert_eq!(split.split_percent, 10); }
263}