diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-16 02:18:35 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-16 02:18:35 +0300 |
| commit | 58c379f1f07adb4113e5f9ee673768b2e4b5be07 (patch) | |
| tree | d090f20c0b78f370b769a250c9bcc47126a9b738 | |
| parent | c3d74be01e6c3485f50f74e1ff566499ebe092b0 (diff) | |
fix: keep split resize hitboxes off pane content
fixes #120
| -rw-r--r-- | .pi/docs/CHANGELOG.md | 1 | ||||
| -rw-r--r-- | src/app/input/mouse.rs | 118 |
2 files changed, 117 insertions, 2 deletions
diff --git a/.pi/docs/CHANGELOG.md b/.pi/docs/CHANGELOG.md index ef523fab..6655806e 100644 --- a/.pi/docs/CHANGELOG.md +++ b/.pi/docs/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed - GitHub Copilot is now correctly detected when its process name is `copilot`. +- Split pane resize hit areas no longer overlap the first content column or row, making text selection work from the start of right and bottom panes. ## [0.5.9] - 2026-05-15 diff --git a/src/app/input/mouse.rs b/src/app/input/mouse.rs index 9b4055e5..a7a2159a 100644 --- a/src/app/input/mouse.rs +++ b/src/app/input/mouse.rs @@ -981,12 +981,14 @@ impl AppState { pub(super) fn find_border_at(&self, col: u16, row: u16) -> Option<&SplitBorder> { self.view.split_borders.iter().find(|b| match b.direction { Direction::Horizontal => { - (col as i32 - b.pos as i32).unsigned_abs() <= 1 + col >= b.pos.saturating_sub(1) + && col <= b.pos && row >= b.area.y && row < b.area.y + b.area.height } Direction::Vertical => { - (row as i32 - b.pos as i32).unsigned_abs() <= 1 + row >= b.pos.saturating_sub(1) + && row <= b.pos && col >= b.area.x && col < b.area.x + b.area.width } @@ -1467,6 +1469,118 @@ mod tests { assert_ne!(root_layout_ratio(&before), root_layout_ratio(&after)); } + #[test] + fn pane_split_hitbox_does_not_overlap_right_pane_content() { + let mut app = app_for_mouse_test(); + app.state.workspaces = vec![Workspace::test_new("test")]; + app.state.active = Some(0); + app.state.selected = 0; + app.state.workspaces[0].test_split(Direction::Horizontal); + crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20)); + let border = app.state.view.split_borders[0].clone(); + let row = border.area.y.saturating_add(1); + + assert!(app + .state + .find_border_at(border.pos.saturating_sub(1), row) + .is_some()); + assert!(app.state.find_border_at(border.pos, row).is_some()); + assert!(app + .state + .find_border_at(border.pos.saturating_add(1), row) + .is_none()); + } + + #[test] + fn pane_split_hitbox_does_not_overlap_bottom_pane_content() { + let mut app = app_for_mouse_test(); + app.state.workspaces = vec![Workspace::test_new("test")]; + app.state.active = Some(0); + app.state.selected = 0; + app.state.workspaces[0].test_split(Direction::Vertical); + crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20)); + let border = app.state.view.split_borders[0].clone(); + let col = border.area.x.saturating_add(1); + + assert!(app + .state + .find_border_at(col, border.pos.saturating_sub(1)) + .is_some()); + assert!(app.state.find_border_at(col, border.pos).is_some()); + assert!(app + .state + .find_border_at(col, border.pos.saturating_add(1)) + .is_none()); + } + + #[test] + fn selecting_from_right_pane_first_content_column_starts_selection() { + let mut app = app_for_mouse_test(); + let mut ws = Workspace::test_new("test"); + let second_pane = ws.test_split(Direction::Horizontal); + app.state.workspaces = vec![ws]; + app.state.active = Some(0); + app.state.selected = 0; + crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20)); + + let second_info = app + .state + .view + .pane_infos + .iter() + .find(|info| info.id == second_pane) + .expect("second pane info") + .clone(); + let col = second_info.inner_rect.x; + let row = second_info.inner_rect.y; + + assert!(app.state.find_border_at(col, row).is_none()); + app.handle_mouse(mouse(MouseEventKind::Down(MouseButton::Left), col, row)); + + assert!(app.state.drag.is_none()); + assert_eq!( + app.state + .selection + .as_ref() + .map(|selection| selection.pane_id), + Some(second_pane) + ); + } + + #[test] + fn selecting_from_bottom_pane_first_content_row_starts_selection() { + let mut app = app_for_mouse_test(); + let mut ws = Workspace::test_new("test"); + let second_pane = ws.test_split(Direction::Vertical); + app.state.workspaces = vec![ws]; + app.state.active = Some(0); + app.state.selected = 0; + crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20)); + + let second_info = app + .state + .view + .pane_infos + .iter() + .find(|info| info.id == second_pane) + .expect("second pane info") + .clone(); + let col = second_info.inner_rect.x; + let row = second_info.inner_rect.y; + + assert!(app.state.find_border_at(col, row).is_none()); + app.handle_mouse(mouse(MouseEventKind::Down(MouseButton::Left), col, row)); + + assert!(app.state.drag.is_none()); + assert_eq!( + app.state + .selection + .as_ref() + .map(|selection| selection.pane_id), + Some(second_pane) + ); + } + #[tokio::test] async fn dragging_vertical_pane_split_still_resizes_when_pane_mouse_reporting_is_enabled() { let mut app = app_for_mouse_test(); |
