aboutsummaryrefslogtreecommitdiff
path: root/src/app/input/sidebar.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/input/sidebar.rs')
-rw-r--r--src/app/input/sidebar.rs236
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();