diff options
Diffstat (limited to 'src/ui')
| -rw-r--r-- | src/ui/sidebar.rs | 177 | ||||
| -rw-r--r-- | src/ui/sidebar/tokens.rs | 9 |
2 files changed, 157 insertions, 29 deletions
diff --git a/src/ui/sidebar.rs b/src/ui/sidebar.rs index 68e5549d..ebe7d0a1 100644 --- a/src/ui/sidebar.rs +++ b/src/ui/sidebar.rs @@ -218,11 +218,19 @@ fn workspace_row_height_in_body( workspace_row_height(app, workspace, indented).min(body_height) } -fn workspace_entry_gap(entries: &[WorkspaceListEntry], entry_idx: usize, indented: bool) -> u16 { - u16::from( - entry_idx + 1 < entries.len() - && !(indented && next_entry_is_indented_workspace(entries, entry_idx)), - ) +fn workspace_entry_gap( + app: &AppState, + entries: &[WorkspaceListEntry], + entry_idx: usize, + indented: bool, +) -> u16 { + if entry_idx + 1 < entries.len() + && !(indented && next_entry_is_indented_workspace(entries, entry_idx)) + { + app.sidebar_spaces.row_gap + } else { + 0 + } } fn workspace_attention_priority(state: AgentState, seen: bool) -> u8 { @@ -456,7 +464,7 @@ fn workspace_list_visible_count(app: &AppState, area: Rect, scroll: usize) -> us }; ( workspace_row_height_in_body(app, ws, *indented, body.height), - workspace_entry_gap(&entries, entry_idx, *indented), + workspace_entry_gap(app, &entries, entry_idx, *indented), ) } }; @@ -465,9 +473,7 @@ fn workspace_list_visible_count(app: &AppState, area: Rect, scroll: usize) -> us } used_rows = used_rows.saturating_add(row_height); visible += 1; - if gap > 0 && used_rows < body.height { - used_rows = used_rows.saturating_add(1); - } + used_rows = used_rows.saturating_add(gap).min(body.height); } visible } @@ -482,7 +488,7 @@ fn workspace_list_bottom_start(app: &AppState, area: Rect) -> usize { let Some(workspace) = app.workspaces.get(*ws_idx) else { continue; }; - let gap = workspace_entry_gap(&entries, entry_idx, *indented); + let gap = workspace_entry_gap(app, &entries, entry_idx, *indented); let needed = workspace_row_height_in_body(app, workspace, *indented, body.height) .saturating_add(gap); if used_rows.saturating_add(needed) > body.height { @@ -552,6 +558,14 @@ pub(crate) fn agent_entry_height_in_body( .min(body_height) } +pub(crate) fn agent_entry_gap(app: &AppState, entry_idx: usize, entry_count: usize) -> u16 { + if entry_idx + 1 < entry_count { + app.sidebar_agents.row_gap + } else { + 0 + } +} + fn agent_panel_visible_count_from(app: &AppState, area: Rect, scroll: usize) -> usize { let body = agent_panel_body_rect(area, false); if body.width == 0 || body.height == 0 { @@ -560,16 +574,17 @@ fn agent_panel_visible_count_from(app: &AppState, area: Rect, scroll: usize) -> let mut used_rows = 0u16; let mut visible = 0usize; - for entry in agent_panel_entries(app).iter().skip(scroll) { + let entries = agent_panel_entries(app); + for (index, entry) in entries.iter().enumerate().skip(scroll) { let height = agent_entry_height_in_body(app, entry, body.height); if used_rows.saturating_add(height) > body.height { break; } used_rows = used_rows.saturating_add(height); visible += 1; - if used_rows < body.height { - used_rows = used_rows.saturating_add(1); - } + used_rows = used_rows + .saturating_add(agent_entry_gap(app, index, entries.len())) + .min(body.height); } visible } @@ -580,7 +595,7 @@ fn agent_panel_bottom_start(app: &AppState, area: Rect) -> usize { let mut used_rows = 0u16; let mut start = entries.len(); for (index, entry) in entries.iter().enumerate().rev() { - let gap = u16::from(index + 1 < entries.len()); + let gap = agent_entry_gap(app, index, entries.len()); let needed = agent_entry_height_in_body(app, entry, body.height).saturating_add(gap); if used_rows.saturating_add(needed) > body.height { break; @@ -664,7 +679,7 @@ pub(crate) fn compute_workspace_list_areas( continue; }; let row_height = workspace_row_height_in_body(app, ws, *indented, body.height); - let gap = workspace_entry_gap(&entries, entry_idx, *indented); + let gap = workspace_entry_gap(app, &entries, entry_idx, *indented); if row_y.saturating_add(row_height) > body_bottom { break; } @@ -673,10 +688,10 @@ pub(crate) fn compute_workspace_list_areas( rect: Rect::new(body.x, row_y, body.width, row_height), indented: *indented, }); - row_y = row_y.saturating_add(row_height); - if gap > 0 && row_y < body_bottom { - row_y = row_y.saturating_add(1); - } + row_y = row_y + .saturating_add(row_height) + .saturating_add(gap) + .min(body_bottom); } } } @@ -1298,7 +1313,7 @@ fn render_agent_detail( let mut row_y = body.y; let body_bottom = body.y + body.height; - for detail in details.iter().skip(app.agent_panel_scroll) { + for (index, detail) in details.iter().enumerate().skip(app.agent_panel_scroll) { let label_color = state_label_color(detail.state, detail.seen, p); let rows = resolved_agent_rows(app, detail); let height = (rows.len().max(1) as u16).min(body.height); @@ -1343,10 +1358,10 @@ fn render_agent_detail( Rect::new(body.x, row_y + row_index as u16, body.width, 1), ); } - row_y = row_y.saturating_add(height); - if row_y < body_bottom { - row_y += 1; - } + row_y = row_y + .saturating_add(height) + .saturating_add(agent_entry_gap(app, index, details.len())) + .min(body_bottom); } if let Some(track) = scrollbar_rect { @@ -1446,6 +1461,36 @@ mod tests { } #[test] + fn default_agent_row_gap_packs_rendering_and_scroll_geometry() { + let mut app = crate::app::state::AppState::test_new(); + app.workspaces = vec![Workspace::test_new("one"), Workspace::test_new("two")]; + app.ensure_test_terminals(); + for (workspace, agent) in app.workspaces.iter().zip([Agent::Pi, Agent::Claude]) { + let pane_id = workspace.tabs[0].root_pane; + let terminal_id = workspace.tabs[0].panes[&pane_id] + .attached_terminal_id + .clone(); + app.terminals.get_mut(&terminal_id).unwrap().detected_agent = Some(agent); + } + app.sidebar_agents.rows = vec![vec![crate::config::AgentSidebarToken::Agent]]; + assert_eq!(app.sidebar_agents.row_gap, 0); + + let area = Rect::new(0, 0, 20, 5); + let metrics = agent_panel_scroll_metrics(&app, area); + let body = agent_panel_body_rect(area, false); + let mut terminal = Terminal::new(TestBackend::new(20, 5)).unwrap(); + terminal + .draw(|frame| render_agent_detail(&app, &TerminalRuntimeRegistry::new(), frame, area)) + .unwrap(); + let buffer = terminal.backend().buffer(); + + assert_eq!(metrics.viewport_rows, 2); + assert_eq!(metrics.max_offset_from_bottom, 0); + assert_eq!(row_text(buffer, body.y, body.width), " pi"); + assert_eq!(row_text(buffer, body.y + 1, body.width), " claude"); + } + + #[test] fn narrow_agent_rows_preserve_later_tab_tokens() { let mut app = crate::app::state::AppState::test_new(); let mut workspace = Workspace::test_new("very-long-workspace-name"); @@ -2020,6 +2065,7 @@ mod tests { workspace_with_worktree_space("main", Some("repo-key"), "/repo/herdr"), workspace_with_worktree_space("issue", Some("repo-key"), "/repo/herdr-issue"), ]; + app.sidebar_spaces.row_gap = 1; let (cards, headers) = compute_workspace_list_areas(&app, Rect::new(0, 0, 30, 20)); @@ -2032,6 +2078,87 @@ mod tests { } #[test] + fn space_row_gap_preserves_compact_worktree_children() { + let mut app = AppState::test_new(); + app.workspaces = vec![ + workspace_with_worktree_space("main", Some("repo-key"), "/repo/herdr"), + workspace_with_worktree_space("issue", Some("repo-key"), "/repo/herdr-issue"), + workspace_with_worktree_space("review", Some("repo-key"), "/repo/herdr-review"), + Workspace::test_new("notes"), + ]; + app.sidebar_spaces.rows = vec![vec![crate::config::SpaceSidebarToken::Workspace]]; + app.sidebar_spaces.row_gap = 2; + + let (spacious, _) = compute_workspace_list_areas(&app, Rect::new(0, 0, 30, 30)); + assert_eq!( + spacious[1].rect.y, + spacious[0].rect.y + spacious[0].rect.height + 2 + ); + assert_eq!( + spacious[2].rect.y, + spacious[1].rect.y + spacious[1].rect.height + ); + assert_eq!( + spacious[3].rect.y, + spacious[2].rect.y + spacious[2].rect.height + 2 + ); + let spacious_metrics = workspace_list_scroll_metrics(&app, Rect::new(0, 0, 30, 7)); + assert_eq!(spacious_metrics.viewport_rows, 2); + assert_eq!(spacious_metrics.max_offset_from_bottom, 2); + + app.sidebar_spaces.row_gap = 0; + let (packed, _) = compute_workspace_list_areas(&app, Rect::new(0, 0, 30, 30)); + assert!(packed + .windows(2) + .all(|pair| pair[1].rect.y == pair[0].rect.y + pair[0].rect.height)); + let packed_metrics = workspace_list_scroll_metrics(&app, Rect::new(0, 0, 30, 7)); + assert_eq!(packed_metrics.viewport_rows, 4); + assert_eq!(packed_metrics.max_offset_from_bottom, 0); + } + + #[test] + fn packed_workspace_drag_indicator_overlays_an_internal_boundary() { + let mut app = AppState::test_new(); + app.workspaces = vec![ + Workspace::test_new("a"), + Workspace::test_new("b"), + Workspace::test_new("c"), + ]; + app.sidebar_spaces.rows = vec![vec![crate::config::SpaceSidebarToken::Workspace]]; + app.sidebar_spaces.row_gap = 0; + let area = Rect::new(0, 0, 30, 20); + app.view.workspace_card_areas = compute_workspace_card_areas(&app, area); + let list_area = workspace_list_rect(area, app.sidebar_section_split); + let indicator_row = + workspace_drop_indicator_row(&app.view.workspace_card_areas, list_area, 2).unwrap(); + assert_eq!(indicator_row, app.view.workspace_card_areas[1].rect.y); + app.drag = Some(crate::app::state::DragState { + target: crate::app::state::DragTarget::WorkspaceReorder { + source_ws_idx: 0, + insert_idx: Some(2), + }, + }); + + let mut terminal = Terminal::new(TestBackend::new(area.width, area.height)).unwrap(); + terminal + .draw(|frame| { + render_workspace_list( + &app, + &TerminalRuntimeRegistry::new(), + frame, + list_area, + false, + ) + }) + .unwrap(); + + assert_eq!( + terminal.backend().buffer()[(list_area.x, indicator_row)].symbol(), + "─" + ); + } + + #[test] fn linked_only_worktree_members_do_not_form_parentless_group() { let mut app = AppState::test_new(); app.workspaces = vec![ diff --git a/src/ui/sidebar/tokens.rs b/src/ui/sidebar/tokens.rs index 43c24d05..5c003617 100644 --- a/src/ui/sidebar/tokens.rs +++ b/src/ui/sidebar/tokens.rs @@ -153,7 +153,7 @@ mod tests { vec![AgentSidebarToken::Custom("missing".into())], vec![AgentSidebarToken::Agent], ], - rows_by_agent: Default::default(), + ..Default::default() }; let rows = agent_rows(&config, &entry, "working"); @@ -174,7 +174,7 @@ mod tests { AgentSidebarToken::StateText, AgentSidebarToken::Custom("summary".into()), ]], - rows_by_agent: Default::default(), + ..Default::default() }; assert_eq!( @@ -200,7 +200,7 @@ mod tests { AgentSidebarToken::TerminalTitleStripped, AgentSidebarToken::Custom("terminal_title".into()), ]], - rows_by_agent: Default::default(), + ..Default::default() }; assert_eq!( @@ -217,7 +217,7 @@ mod tests { fn known_agent_override_replaces_default_rows() { let mut config = AgentsSidebarConfig { rows: vec![vec![AgentSidebarToken::Workspace]], - rows_by_agent: Default::default(), + ..Default::default() }; config .rows_by_agent @@ -265,6 +265,7 @@ mod tests { let tokens = std::collections::HashMap::from([("jj_status".into(), "2 changes".into())]); let config = SpacesSidebarConfig { rows: vec![vec![SpaceSidebarToken::Custom("jj_status".into())]], + ..Default::default() }; assert_eq!( |
