1use crate::primitives::split_layout::{PaneLayout, SplitAxis, SplitDividerLayout, SplitLayout};
10use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
11use ratatui::{
12 layout::Rect,
13 style::{Color, Modifier, Style},
14 text::Line,
15 widgets::{Block, BorderType, Borders, StatefulWidget, Widget},
16 Frame,
17};
18
19#[derive(Debug, Clone, Copy, Default)]
24pub struct SplitLayoutWidgetState {
25 pub hovered_divider: Option<usize>,
27 pub dragging_divider: Option<usize>,
29}
30
31#[derive(Debug)]
52pub struct SplitLayoutWidget<'a> {
53 layout: &'a mut SplitLayout,
55 state: SplitLayoutWidgetState,
57 divider_width: u16,
59 hit_threshold: u16,
61 hover_style: Style,
63 drag_style: Style,
65 divider_style: Style,
67 block: Option<Block<'a>>,
69 show_pane_borders: bool,
71}
72
73impl<'a> SplitLayoutWidget<'a> {
74 pub fn new(layout: &'a mut SplitLayout) -> Self {
76 Self {
77 layout,
78 state: SplitLayoutWidgetState::default(),
79 divider_width: 1,
80 hit_threshold: 2,
81 hover_style: Style::default()
82 .fg(Color::Cyan)
83 .add_modifier(Modifier::BOLD),
84 drag_style: Style::default()
85 .fg(Color::Yellow)
86 .add_modifier(Modifier::BOLD),
87 divider_style: Style::default(),
88 block: None,
89 show_pane_borders: true,
90 }
91 }
92
93 pub fn with_state(mut self, state: SplitLayoutWidgetState) -> Self {
95 self.state = state;
96 self
97 }
98
99 pub fn state(&self) -> SplitLayoutWidgetState {
101 self.state
102 }
103
104 pub fn optimal_poll_duration(&self) -> std::time::Duration {
116 if self.state.dragging_divider.is_some() {
117 std::time::Duration::from_millis(8)
118 } else {
119 std::time::Duration::from_millis(50)
120 }
121 }
122
123 pub fn layout(&self) -> &SplitLayout {
125 self.layout
126 }
127
128 pub fn layout_mut(&mut self) -> &mut SplitLayout {
130 self.layout
131 }
132
133 pub fn with_divider_width(mut self, width: u16) -> Self {
135 self.divider_width = width.max(1);
136 self
137 }
138
139 pub fn with_hit_threshold(mut self, threshold: u16) -> Self {
141 self.hit_threshold = threshold.max(1);
142 self
143 }
144
145 pub fn with_hover_style(mut self, style: Style) -> Self {
147 self.hover_style = style;
148 self
149 }
150
151 pub fn with_drag_style(mut self, style: Style) -> Self {
153 self.drag_style = style;
154 self
155 }
156
157 pub fn with_divider_style(mut self, style: Style) -> Self {
159 self.divider_style = style;
160 self
161 }
162
163 pub fn with_block(mut self, block: Block<'a>) -> Self {
165 self.block = Some(block);
166 self
167 }
168
169 pub fn with_pane_borders(mut self, show: bool) -> Self {
171 self.show_pane_borders = show;
172 self
173 }
174
175 pub fn is_hovering(&self) -> bool {
177 self.state.hovered_divider.is_some()
178 }
179
180 pub fn is_dragging(&self) -> bool {
182 self.state.dragging_divider.is_some()
183 }
184
185 pub fn hovered_divider(&self) -> Option<usize> {
187 self.state.hovered_divider
188 }
189
190 pub fn dragging_divider(&self) -> Option<usize> {
192 self.state.dragging_divider
193 }
194
195 pub fn handle_mouse(&mut self, mouse: MouseEvent, area: Rect) {
208 match mouse.kind {
209 MouseEventKind::Moved => {
210 if self.state.dragging_divider.is_none() {
211 self.state.hovered_divider =
212 self.find_divider_at(mouse.column, mouse.row, area);
213 }
214 }
215 MouseEventKind::Down(MouseButton::Left) => {
216 if let Some(pane_id) = self.find_divider_at(mouse.column, mouse.row, area) {
217 self.state.dragging_divider = Some(pane_id);
218 }
219 }
220 MouseEventKind::Drag(MouseButton::Left) => {
221 if let Some(pane_id) = self.state.dragging_divider {
222 self.resize_divider(pane_id, mouse.column, mouse.row, area);
223 }
224 }
225 MouseEventKind::Up(MouseButton::Left) => {
226 self.state.dragging_divider = None;
227 self.state.hovered_divider = self.find_divider_at(mouse.column, mouse.row, area);
228 }
229 _ => {}
230 }
231 }
232
233 fn find_divider_at(&self, column: u16, row: u16, area: Rect) -> Option<usize> {
237 let layouts = self.layout.layout_dividers(area);
238 let threshold = self.hit_threshold;
239 let mut best_match: Option<(usize, u16, u32)> = None;
240
241 for divider in &layouts {
242 let rect = divider.area();
243 match divider.axis() {
244 SplitAxis::Vertical => {
245 let divider_x = rect.x.saturating_add(
246 ((rect.width as u32 * divider.ratio() as u32) / 100) as u16,
247 );
248 let distance = divider_x.abs_diff(column);
249 if distance <= threshold
250 && column <= divider_x.saturating_add(threshold)
251 && row >= rect.y
252 && row <= rect.y.saturating_add(rect.height)
253 {
254 let area_size = rect.width as u32 * rect.height as u32;
255 if best_match
256 .map(|(_, best_distance, best_area)| {
257 distance < best_distance
258 || (distance == best_distance && area_size < best_area)
259 })
260 .unwrap_or(true)
261 {
262 best_match = Some((divider.split_index(), distance, area_size));
263 }
264 }
265 }
266 SplitAxis::Horizontal => {
267 let divider_y = rect.y.saturating_add(
268 ((rect.height as u32 * divider.ratio() as u32) / 100) as u16,
269 );
270 let distance = divider_y.abs_diff(row);
271 if distance <= threshold
272 && row <= divider_y.saturating_add(threshold)
273 && column >= rect.x
274 && column <= rect.x.saturating_add(rect.width)
275 {
276 let area_size = rect.width as u32 * rect.height as u32;
277 if best_match
278 .map(|(_, best_distance, best_area)| {
279 distance < best_distance
280 || (distance == best_distance && area_size < best_area)
281 })
282 .unwrap_or(true)
283 {
284 best_match = Some((divider.split_index(), distance, area_size));
285 }
286 }
287 }
288 }
289 }
290
291 best_match.map(|(split_index, _, _)| split_index)
292 }
293
294 fn resize_divider(&mut self, split_index: usize, column: u16, row: u16, area: Rect) {
299 let layouts = self.layout.layout_dividers(area);
300 let divider_layout = layouts
301 .iter()
302 .find(|divider| divider.split_index() == split_index);
303
304 if let Some(divider) = divider_layout {
305 let rect = divider.area();
306 match divider.axis() {
307 SplitAxis::Vertical => {
308 let content_width = rect.width;
309 if content_width > 0 {
310 let relative_x = column.saturating_sub(rect.x);
311 let percent = ((relative_x as u32 * 100) / content_width as u32) as u16;
312 let _ = self.layout.resize_split(split_index, percent);
313 }
314 }
315 SplitAxis::Horizontal => {
316 let content_height = rect.height;
317 if content_height > 0 {
318 let relative_y = row.saturating_sub(rect.y);
319 let percent = ((relative_y as u32 * 100) / content_height as u32) as u16;
320 let _ = self.layout.resize_split(split_index, percent);
321 }
322 }
323 }
324 }
325 }
326
327 pub fn pane_layouts(&self, area: Rect) -> Vec<PaneLayout> {
332 self.layout.layout_panes(area)
333 }
334}
335
336impl<'a> Widget for SplitLayoutWidget<'a> {
337 fn render(self, area: Rect, buf: &mut ratatui::buffer::Buffer) {
338 let mut render_area = area;
339
340 if let Some(ref block) = self.block {
342 let block = block.clone();
343 render_area = block.inner(area);
344 block.render(area, buf);
345 }
346
347 let pane_layouts = self.layout.layout_panes(render_area);
348 let divider_layouts = self.layout.layout_dividers(render_area);
349
350 for pane_layout in &pane_layouts {
352 let pane_id = pane_layout.pane_id();
353 let pane_area = pane_layout.area();
354
355 let border_style = self.divider_style;
356
357 if self.show_pane_borders {
359 let pane_block = Block::default()
360 .borders(Borders::ALL)
361 .border_type(BorderType::Rounded)
362 .border_style(border_style)
363 .title(Line::from(format!(" {}", pane_id)));
364
365 pane_block.render(pane_area, buf);
366 }
367
368 }
370
371 for divider in ÷r_layouts {
372 let divider_style = if self.state.dragging_divider == Some(divider.split_index()) {
373 self.drag_style
374 } else if self.state.hovered_divider == Some(divider.split_index()) {
375 self.hover_style
376 } else {
377 continue;
378 };
379
380 self.render_divider_overlay(divider, divider_style, buf);
381 }
382 }
383}
384
385impl<'a> SplitLayoutWidget<'a> {
386 fn render_divider_overlay(
388 &self,
389 divider: &SplitDividerLayout,
390 style: Style,
391 buf: &mut ratatui::buffer::Buffer,
392 ) {
393 let width = self.divider_width;
394 let rect = divider.area();
395
396 match divider.axis() {
397 SplitAxis::Vertical => {
398 let divider_x = rect
399 .x
400 .saturating_add(((rect.width as u32 * divider.ratio() as u32) / 100) as u16);
401 for y in rect.top()..rect.bottom() {
402 for dx in 0..width {
403 let x = divider_x.saturating_sub(dx);
404 let cell = buf.get_mut(x, y);
405 cell.set_style(style);
406 cell.set_char('│');
407 }
408 }
409 }
410 SplitAxis::Horizontal => {
411 let divider_y = rect
412 .y
413 .saturating_add(((rect.height as u32 * divider.ratio() as u32) / 100) as u16);
414 for x in rect.left()..rect.right() {
415 for dy in 0..width {
416 let y = divider_y.saturating_sub(dy);
417 let cell = buf.get_mut(x, y);
418 cell.set_style(style);
419 cell.set_char('─');
420 }
421 }
422 }
423 }
424 }
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430 use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
431
432 #[test]
433 fn test_widget_creation() {
434 let mut layout = SplitLayout::new(0);
435 let widget = SplitLayoutWidget::new(&mut layout);
436 assert!(!widget.is_hovering());
437 assert!(!widget.is_dragging());
438 }
439
440 #[test]
441 fn test_hover_on_vertical_divider() {
442 let mut layout = SplitLayout::new(0);
443 let _pane_2 = layout.split_pane_horizontally(0).unwrap();
444
445 let mut widget = SplitLayoutWidget::new(&mut layout);
446
447 let area = Rect::new(0, 0, 80, 24);
448
449 let mouse = MouseEvent {
451 kind: MouseEventKind::Moved,
452 column: 26, row: 5,
454 modifiers: KeyModifiers::empty(),
455 };
456
457 widget.handle_mouse(mouse, area);
458
459 assert!(widget.is_hovering());
461 assert_eq!(widget.hovered_divider(), Some(0));
462 }
463
464 #[test]
465 fn test_drag_vertical_divider() {
466 let mut layout = SplitLayout::new(0);
467 let _pane_2 = layout.split_pane_horizontally(0).unwrap();
468
469 let mut widget = SplitLayoutWidget::new(&mut layout);
470
471 let area = Rect::new(0, 0, 80, 24);
472
473 let mouse_down = MouseEvent {
475 kind: MouseEventKind::Down(MouseButton::Left),
476 column: 26, row: 5,
478 modifiers: KeyModifiers::empty(),
479 };
480 widget.handle_mouse(mouse_down, area);
481
482 assert!(widget.is_dragging());
484 assert_eq!(widget.dragging_divider(), Some(0));
485
486 let mouse_drag = MouseEvent {
488 kind: MouseEventKind::Drag(MouseButton::Left),
489 column: 40, row: 5,
491 modifiers: KeyModifiers::empty(),
492 };
493 widget.handle_mouse(mouse_drag, area);
494
495 let layouts = layout.layout_panes(area);
497 let pane_0_area = layouts.iter().find(|p| p.pane_id() == 0).unwrap().area();
498
499 assert!(pane_0_area.width >= 38 && pane_0_area.width <= 42);
501 }
502
503 #[test]
504 fn test_hover_on_horizontal_divider() {
505 let mut layout = SplitLayout::new(0);
506 let _pane_2 = layout.split_pane_vertically(0).unwrap();
507
508 let mut widget = SplitLayoutWidget::new(&mut layout);
509
510 let area = Rect::new(0, 0, 80, 24);
511
512 let mouse = MouseEvent {
514 kind: MouseEventKind::Moved,
515 column: 40,
516 row: 11, modifiers: KeyModifiers::empty(),
518 };
519
520 widget.handle_mouse(mouse, area);
521
522 assert!(widget.is_hovering());
524 assert_eq!(widget.hovered_divider(), Some(0));
525 }
526
527 #[test]
528 fn test_no_hover_in_middle_of_pane() {
529 let mut layout = SplitLayout::new(0);
530 let _pane_2 = layout.split_pane_horizontally(0).unwrap();
531
532 let mut widget = SplitLayoutWidget::new(&mut layout);
533
534 let area = Rect::new(0, 0, 80, 24);
535
536 let mouse = MouseEvent {
538 kind: MouseEventKind::Moved,
539 column: 13, row: 12,
541 modifiers: KeyModifiers::empty(),
542 };
543
544 widget.handle_mouse(mouse, area);
545
546 assert!(!widget.is_hovering());
548 assert!(widget.hovered_divider().is_none());
549 }
550
551 #[test]
552 fn test_drag_stops_on_mouse_up() {
553 let mut layout = SplitLayout::new(0);
554 let _pane_2 = layout.split_pane_horizontally(0).unwrap();
555
556 let mut widget = SplitLayoutWidget::new(&mut layout);
557
558 let area = Rect::new(0, 0, 80, 24);
559
560 let mouse_down = MouseEvent {
562 kind: MouseEventKind::Down(MouseButton::Left),
563 column: 26,
564 row: 5,
565 modifiers: KeyModifiers::empty(),
566 };
567 widget.handle_mouse(mouse_down, area);
568 assert!(widget.is_dragging());
569
570 let mouse_up = MouseEvent {
572 kind: MouseEventKind::Up(MouseButton::Left),
573 column: 40,
574 row: 5,
575 modifiers: KeyModifiers::empty(),
576 };
577 widget.handle_mouse(mouse_up, area);
578 assert!(!widget.is_dragging());
579 }
580
581 #[test]
582 fn test_divider_hit_threshold() {
583 let mut layout = SplitLayout::new(0);
584 let _pane_2 = layout.split_pane_horizontally(0).unwrap();
585
586 let widget = SplitLayoutWidget::new(&mut layout).with_hit_threshold(5);
587
588 assert_eq!(widget.hit_threshold, 5);
589 }
590
591 #[test]
592 fn test_with_styling_methods() {
593 let mut layout = SplitLayout::new(0);
594 let hover_style = Style::default().fg(Color::Red);
595 let drag_style = Style::default().fg(Color::Blue);
596 let divider_style = Style::default().fg(Color::Green);
597
598 let widget = SplitLayoutWidget::new(&mut layout)
599 .with_hover_style(hover_style)
600 .with_drag_style(drag_style)
601 .with_divider_style(divider_style);
602
603 assert_eq!(widget.hover_style.fg, Some(Color::Red));
605 assert_eq!(widget.drag_style.fg, Some(Color::Blue));
606 assert_eq!(widget.divider_style.fg, Some(Color::Green));
607 }
608
609 #[test]
610 fn test_complex_grid_layout_hover() {
611 let mut layout = SplitLayout::new(0);
613 let pane_2 = layout.split_pane_horizontally(0).unwrap();
614 let _pane_3 = layout.split_pane_vertically(pane_2).unwrap();
615 let _pane_4 = layout.split_pane_vertically(1).unwrap();
616
617 let area = Rect::new(0, 0, 80, 24);
618 let (divider_x, divider_split_index) = {
619 let dividers = layout.layout_dividers(area);
620 let divider = dividers
621 .iter()
622 .find(|divider| divider.axis() == SplitAxis::Vertical)
623 .unwrap();
624 let divider_area = divider.area();
625 (
626 divider_area.x.saturating_add(
627 ((divider_area.width as u32 * divider.ratio() as u32) / 100) as u16,
628 ),
629 divider.split_index(),
630 )
631 };
632
633 let mut widget = SplitLayoutWidget::new(&mut layout);
634
635 let mouse = MouseEvent {
637 kind: MouseEventKind::Moved,
638 column: divider_x.saturating_sub(1),
639 row: 5,
640 modifiers: KeyModifiers::empty(),
641 };
642
643 widget.handle_mouse(mouse, area);
644
645 assert_eq!(widget.hovered_divider(), Some(divider_split_index));
647 }
648
649 #[test]
650 fn test_drag_resize_multiple_panes() {
651 let mut layout = SplitLayout::new(0);
653 let pane_2 = layout.split_pane_horizontally(0).unwrap();
654 let _pane_3 = layout.split_pane_vertically(pane_2).unwrap();
655
656 let area = Rect::new(0, 0, 80, 24);
657
658 let initial_pane_0_width = {
660 let initial_layouts = layout.layout_panes(area);
661 initial_layouts
662 .iter()
663 .find(|p| p.pane_id() == 0)
664 .unwrap()
665 .area()
666 .width
667 };
668
669 let mut widget = SplitLayoutWidget::new(&mut layout);
670
671 let mouse_down = MouseEvent {
673 kind: MouseEventKind::Down(MouseButton::Left),
674 column: 26,
675 row: 5,
676 modifiers: KeyModifiers::empty(),
677 };
678 widget.handle_mouse(mouse_down, area);
679
680 let mouse_drag = MouseEvent {
682 kind: MouseEventKind::Drag(MouseButton::Left),
683 column: 48, row: 5,
685 modifiers: KeyModifiers::empty(),
686 };
687 widget.handle_mouse(mouse_drag, area);
688
689 let final_layouts = layout.layout_panes(area);
691 let final_pane_0_width = final_layouts
692 .iter()
693 .find(|p| p.pane_id() == 0)
694 .unwrap()
695 .area()
696 .width;
697
698 assert!(final_pane_0_width > initial_pane_0_width);
699 }
700
701 #[test]
702 fn test_no_drag_on_single_pane() {
703 let mut layout = SplitLayout::new(0);
705
706 let mut widget = SplitLayoutWidget::new(&mut layout);
707
708 let area = Rect::new(0, 0, 80, 24);
709
710 let mouse_down = MouseEvent {
711 kind: MouseEventKind::Down(MouseButton::Left),
712 column: 40,
713 row: 10,
714 modifiers: KeyModifiers::empty(),
715 };
716 widget.handle_mouse(mouse_down, area);
717
718 assert!(!widget.is_dragging());
720 assert!(widget.dragging_divider().is_none());
721 }
722}