diff options
Diffstat (limited to 'src/app/input/sidebar.rs')
| -rw-r--r-- | src/app/input/sidebar.rs | 236 |
1 files changed, 224 insertions, 12 deletions
diff --git a/src/app/input/sidebar.rs b/src/app/input/sidebar.rs index dc192e50..8ba12bc6 100644 --- a/src/app/input/sidebar.rs +++ b/src/app/input/sidebar.rs @@ -69,6 +69,11 @@ impl AppState { self.workspace_scroll = metrics .max_offset_from_bottom .saturating_sub(offset_from_bottom); + self.workspace_scroll = crate::ui::normalized_workspace_scroll( + self, + self.view.sidebar_rect, + self.workspace_scroll, + ); } pub(super) fn scroll_workspace_list(&mut self, delta: i16) { @@ -76,19 +81,25 @@ impl AppState { self.workspace_scroll = self .workspace_scroll .saturating_sub(delta.unsigned_abs() as usize); + self.workspace_scroll = crate::ui::normalized_workspace_scroll( + self, + self.view.sidebar_rect, + self.workspace_scroll, + ); return; } - for _ in 0..delta as usize { - let cards = crate::ui::compute_workspace_card_areas(self, self.view.sidebar_rect); - let Some(last) = cards.last() else { - break; - }; - if last.ws_idx + 1 >= self.workspaces.len() { - break; - } - self.workspace_scroll = self.workspace_scroll.saturating_add(1); - } + let area = self.workspace_list_rect(); + let metrics = crate::ui::workspace_list_scroll_metrics(self, area); + self.workspace_scroll = self + .workspace_scroll + .saturating_add(delta as usize) + .min(metrics.max_offset_from_bottom); + self.workspace_scroll = crate::ui::normalized_workspace_scroll( + self, + self.view.sidebar_rect, + self.workspace_scroll, + ); } pub(super) fn agent_panel_scrollbar_target_at( @@ -376,8 +387,23 @@ impl AppState { } let mut insert_indices = Vec::with_capacity(cards.len() + 1); - insert_indices.push(cards[0].ws_idx); - insert_indices.extend(cards.iter().skip(1).map(|card| card.ws_idx)); + for (idx, card) in cards.iter().enumerate() { + let card_group = self + .workspaces + .get(card.ws_idx) + .and_then(|ws| ws.worktree_space()) + .map(|space| space.key.as_str()); + let previous_group = idx.checked_sub(1).and_then(|prev_idx| { + self.workspaces + .get(cards[prev_idx].ws_idx) + .and_then(|ws| ws.worktree_space()) + .map(|space| space.key.as_str()) + }); + let inside_group_gap = card_group.is_some() && card_group == previous_group; + if !inside_group_gap { + insert_indices.push(card.ws_idx); + } + } insert_indices.push(cards.last().map(|card| card.ws_idx + 1).unwrap_or(0)); let mut best: Option<(usize, u16)> = None; @@ -970,6 +996,110 @@ mod tests { } #[test] + fn clicking_worktree_parent_row_focuses_workspace_without_toggling() { + let mut app = app_for_mouse_test(); + app.state.workspaces = vec![Workspace::test_new("main"), Workspace::test_new("issue")]; + for (idx, checkout_path) in ["/repo/herdr", "/repo/herdr-issue"].into_iter().enumerate() { + app.state.workspaces[idx].worktree_space = + Some(crate::workspace::WorktreeSpaceMembership { + key: "repo-key".into(), + label: "herdr".into(), + repo_root: "/repo/herdr".into(), + checkout_path: checkout_path.into(), + is_linked_worktree: idx > 0, + }); + } + app.state.active = None; + app.state.mode = Mode::Terminal; + crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20)); + let parent = app.state.view.workspace_card_areas[0].rect; + + app.handle_mouse(mouse( + MouseEventKind::Down(MouseButton::Left), + parent.x + 2, + parent.y, + )); + app.handle_mouse(mouse( + MouseEventKind::Up(MouseButton::Left), + parent.x + 2, + parent.y, + )); + + assert_eq!(app.state.active, Some(0)); + assert!(!app.state.collapsed_space_keys.contains("repo-key")); + } + + #[test] + fn clicking_worktree_parent_chevron_toggles_group_only() { + let mut app = app_for_mouse_test(); + app.state.workspaces = vec![Workspace::test_new("main"), Workspace::test_new("issue")]; + for (idx, checkout_path) in ["/repo/herdr", "/repo/herdr-issue"].into_iter().enumerate() { + app.state.workspaces[idx].worktree_space = + Some(crate::workspace::WorktreeSpaceMembership { + key: "repo-key".into(), + label: "herdr".into(), + repo_root: "/repo/herdr".into(), + checkout_path: checkout_path.into(), + is_linked_worktree: idx > 0, + }); + } + app.state.active = None; + app.state.mode = Mode::Terminal; + crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20)); + let parent = app.state.view.workspace_card_areas[0].rect; + + app.handle_mouse(mouse( + MouseEventKind::Down(MouseButton::Left), + parent.x, + parent.y, + )); + + assert_eq!(app.state.active, None); + assert!(app.state.workspace_press.is_none()); + assert!(app.state.collapsed_space_keys.contains("repo-key")); + + app.handle_mouse(mouse( + MouseEventKind::Down(MouseButton::Left), + parent.x, + parent.y, + )); + + assert!(!app.state.collapsed_space_keys.contains("repo-key")); + } + + #[test] + fn wheel_workspace_selection_follows_grouped_visual_order_without_scrollbar() { + let mut app = app_for_mouse_test(); + app.state.workspaces = vec![ + Workspace::test_new("main"), + Workspace::test_new("normal"), + Workspace::test_new("issue"), + ]; + for (idx, checkout_path) in [(0, "/repo/herdr"), (2, "/repo/herdr-issue")] { + app.state.workspaces[idx].worktree_space = + Some(crate::workspace::WorktreeSpaceMembership { + key: "repo-key".into(), + label: "herdr".into(), + repo_root: "/repo/herdr".into(), + checkout_path: checkout_path.into(), + is_linked_worktree: idx != 0, + }); + } + app.state.active = Some(0); + app.state.selected = 0; + app.state.mode = Mode::Navigate; + crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 30)); + let list = app.state.workspace_list_rect(); + assert!(!crate::ui::should_show_scrollbar( + crate::ui::workspace_list_scroll_metrics(&app.state, list) + )); + + app.handle_mouse(mouse(MouseEventKind::ScrollDown, list.x + 1, list.y + 1)); + + assert_eq!(app.state.selected, 2); + } + + #[test] fn dragging_workspace_reorders_without_changing_identity() { let mut app = app_for_mouse_test(); app.state.workspaces = vec![ @@ -1166,6 +1296,18 @@ mod tests { repo } + fn workspace_with_space(name: &str, key: &str) -> Workspace { + let mut ws = Workspace::test_new(name); + ws.worktree_space = Some(crate::workspace::WorktreeSpaceMembership { + key: key.into(), + label: "herdr".into(), + repo_root: "/repo/herdr".into(), + checkout_path: format!("/repo/{name}").into(), + is_linked_worktree: name != "main", + }); + ws + } + #[test] fn top_drop_slot_is_distinct_from_gap_below_first_workspace() { let mut app = app_for_mouse_test(); @@ -1231,6 +1373,76 @@ mod tests { } #[test] + fn grouped_sidebar_drop_slots_do_not_land_inside_compact_group() { + let mut app = app_for_mouse_test(); + app.state.workspaces = vec![ + workspace_with_space("main", "repo-key"), + Workspace::test_new("normal"), + workspace_with_space("issue", "repo-key"), + ]; + app.state.active = Some(1); + app.state.selected = 1; + crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 40)); + + let cards = &app.state.view.workspace_card_areas; + let order = cards.iter().map(|card| card.ws_idx).collect::<Vec<_>>(); + assert_eq!(order, vec![0, 2, 1]); + let issue = cards.iter().find(|card| card.ws_idx == 2).unwrap(); + let normal = cards.iter().find(|card| card.ws_idx == 1).unwrap(); + + assert_eq!(app.state.workspace_drop_index_at_row(issue.rect.y), Some(1)); + assert_eq!( + crate::ui::workspace_drop_indicator_row(cards, app.state.workspace_list_rect(), 2), + Some(normal.rect.y + normal.rect.height) + ); + } + + #[test] + fn dragging_worktree_space_member_does_not_reorder_workspaces() { + let mut app = app_for_mouse_test(); + app.state.workspaces = vec![ + workspace_with_space("main", "repo-key"), + Workspace::test_new("normal"), + workspace_with_space("issue", "repo-key"), + ]; + app.state.active = Some(0); + app.state.selected = 0; + crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 40)); + + let source = app + .state + .view + .workspace_card_areas + .iter() + .find(|card| card.ws_idx == 2) + .unwrap() + .rect; + let target_row = crate::ui::workspace_drop_indicator_row( + &app.state.view.workspace_card_areas, + app.state.workspace_list_rect(), + 0, + ) + .unwrap(); + + app.handle_mouse(mouse(MouseEventKind::Down(MouseButton::Left), 2, source.y)); + app.handle_mouse(mouse( + MouseEventKind::Drag(MouseButton::Left), + 2, + target_row, + )); + assert!(app.state.drag.is_none()); + app.handle_mouse(mouse(MouseEventKind::Up(MouseButton::Left), 2, target_row)); + + let names = app + .state + .workspaces + .iter() + .map(|ws| ws.display_name()) + .collect::<Vec<_>>(); + assert_eq!(names, vec!["main", "normal", "issue"]); + } + + #[test] fn dragging_sidebar_divider_sets_manual_width() { let mut app = app_for_mouse_test(); |
