diff --git a/src/app/input/mouse.rs b/src/app/input/mouse.rs index cc03c8f6..b991828a 100644 --- a/src/app/input/mouse.rs +++ b/src/app/input/mouse.rs @@ -449,12 +449,12 @@ impl AppState { } if in_sidebar { - if self.sidebar_collapsed { - if self.on_collapsed_sidebar_toggle(mouse.column, mouse.row) { - self.sidebar_collapsed = false; - return None; - } + if self.on_sidebar_toggle(mouse.column, mouse.row) { + self.sidebar_collapsed = !self.sidebar_collapsed; + return None; + } + if self.sidebar_collapsed { if let Some(idx) = self.collapsed_workspace_at_row(mouse.row) { self.switch_workspace(idx); self.mode = Mode::Terminal; diff --git a/src/app/input/sidebar.rs b/src/app/input/sidebar.rs index 8ba12bc6..43093a82 100644 --- a/src/app/input/sidebar.rs +++ b/src/app/input/sidebar.rs @@ -238,17 +238,25 @@ impl AppState { return false; } let sidebar = self.view.sidebar_rect; + let toggle = crate::ui::expanded_sidebar_toggle_rect(sidebar); + let on_toggle = toggle.width > 0 + && col >= toggle.x + && col < toggle.x + toggle.width + && row >= toggle.y + && row < toggle.y + toggle.height; sidebar.width > 0 + && !on_toggle && col == sidebar.x + sidebar.width.saturating_sub(1) && row >= sidebar.y && row < sidebar.y + sidebar.height } - pub(super) fn on_collapsed_sidebar_toggle(&self, col: u16, row: u16) -> bool { - if !self.sidebar_collapsed { - return false; - } - let rect = crate::ui::collapsed_sidebar_toggle_rect(self.view.sidebar_rect); + pub(super) fn on_sidebar_toggle(&self, col: u16, row: u16) -> bool { + let rect = if self.sidebar_collapsed { + crate::ui::collapsed_sidebar_toggle_rect(self.view.sidebar_rect) + } else { + crate::ui::expanded_sidebar_toggle_rect(self.view.sidebar_rect) + }; rect.width > 0 && col >= rect.x && col < rect.x + rect.width @@ -969,6 +977,24 @@ mod tests { assert!(!app.state.sidebar_collapsed); } + #[test] + fn clicking_expanded_sidebar_toggle_collapses_sidebar() { + let mut app = app_for_mouse_test(); + app.state.sidebar_collapsed = false; + app.state.view.sidebar_rect = Rect::new(0, 0, 26, 20); + app.state.view.terminal_area = Rect::new(26, 0, 80, 20); + + let toggle = crate::ui::expanded_sidebar_toggle_rect(app.state.view.sidebar_rect); + app.handle_mouse(mouse( + MouseEventKind::Down(MouseButton::Left), + toggle.x, + toggle.y, + )); + + assert!(app.state.sidebar_collapsed); + assert!(app.state.drag.is_none()); + } + #[test] fn clicking_workspace_switches_on_mouse_up() { let mut app = app_for_mouse_test(); @@ -1454,6 +1480,26 @@ mod tests { assert_eq!(snapshot.sidebar_width, Some(31)); } + #[test] + fn dragging_sidebar_bottom_divider_still_sets_manual_width() { + let mut app = app_for_mouse_test(); + let divider_col = app.state.view.sidebar_rect.x + app.state.view.sidebar_rect.width - 1; + let bottom_row = app.state.view.sidebar_rect.y + app.state.view.sidebar_rect.height - 1; + + app.handle_mouse(mouse( + MouseEventKind::Down(MouseButton::Left), + divider_col, + bottom_row, + )); + app.handle_mouse(mouse( + MouseEventKind::Drag(MouseButton::Left), + divider_col + 5, + bottom_row, + )); + + assert_eq!(app.state.sidebar_width, 31); + } + #[test] fn dragging_past_max_clamps_to_configured_max() { let mut app = app_for_mouse_test(); diff --git a/src/ui.rs b/src/ui.rs index eb958d02..f6fc54d4 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -68,9 +68,10 @@ pub(crate) use self::{ agent_panel_body_rect, agent_panel_entries, agent_panel_scroll_metrics, agent_panel_scrollbar_rect, agent_panel_toggle_rect, collapsed_sidebar_sections, collapsed_sidebar_toggle_rect, compute_workspace_card_areas, expanded_sidebar_sections, - normalized_workspace_scroll, sidebar_section_divider_rect, workspace_drop_indicator_row, - workspace_list_entries, workspace_list_rect, workspace_list_scroll_metrics, - workspace_list_scrollbar_rect, workspace_parent_group_state, WorkspaceListEntry, + expanded_sidebar_toggle_rect, normalized_workspace_scroll, sidebar_section_divider_rect, + workspace_drop_indicator_row, workspace_list_entries, workspace_list_rect, + workspace_list_scroll_metrics, workspace_list_scrollbar_rect, workspace_parent_group_state, + WorkspaceListEntry, }, }; pub(crate) use self::{ diff --git a/src/ui/sidebar.rs b/src/ui/sidebar.rs index 692e0236..7d1227df 100644 --- a/src/ui/sidebar.rs +++ b/src/ui/sidebar.rs @@ -1179,6 +1179,18 @@ pub(crate) fn collapsed_sidebar_toggle_rect(area: Rect) -> Rect { Rect::new(x, bottom_y, 1, 1) } +pub(crate) fn expanded_sidebar_toggle_rect(area: Rect) -> Rect { + if area.width <= 1 || area.height == 0 { + return Rect::default(); + } + Rect::new( + area.x + area.width.saturating_sub(2), + area.y + area.height.saturating_sub(1), + 1, + 1, + ) +} + fn render_sidebar_toggle( app: &AppState, frame: &mut Frame, @@ -1186,25 +1198,55 @@ fn render_sidebar_toggle( collapsed: bool, p: &Palette, ) { - if !collapsed { - return; - } - let toggle_area = collapsed_sidebar_toggle_rect(area); + let toggle_area = if collapsed { + collapsed_sidebar_toggle_rect(area) + } else { + expanded_sidebar_toggle_rect(area) + }; if toggle_area == Rect::default() { return; } - let icon_style = if app.global_menu_attention_badge_visible() { + let icon = if collapsed { "»" } else { "«" }; + let icon_style = if collapsed && app.global_menu_attention_badge_visible() { Style::default().fg(p.accent).add_modifier(Modifier::BOLD) } else { Style::default().fg(p.overlay0) }; - frame.render_widget(Paragraph::new(Span::styled("»", icon_style)), toggle_area); + frame.render_widget(Paragraph::new(Span::styled(icon, icon_style)), toggle_area); } #[cfg(test)] mod tests { use super::*; use crate::{detect::Agent, workspace::Workspace}; + use ratatui::{backend::TestBackend, Terminal}; + + #[test] + fn render_sidebar_toggle_draws_expanded_collapse_icon() { + let app = crate::app::state::AppState::test_new(); + let area = Rect::new(0, 0, 26, 20); + let mut terminal = + Terminal::new(TestBackend::new(26, 20)).expect("test terminal should initialize"); + + terminal + .draw(|frame| render_sidebar_toggle(&app, frame, area, false, &app.palette)) + .expect("sidebar toggle should render"); + + let toggle = expanded_sidebar_toggle_rect(area); + assert_eq!( + terminal.backend().buffer()[(toggle.x, toggle.y)].symbol(), + "«" + ); + } + + #[test] + fn expanded_sidebar_toggle_sits_inside_sidebar_content() { + let area = Rect::new(0, 0, 26, 20); + let toggle = expanded_sidebar_toggle_rect(area); + + assert_eq!(toggle.x, area.x + area.width - 2); + assert_eq!(toggle.y, area.y + area.height - 1); + } #[test] fn all_workspaces_agent_panel_entries_use_workspace_and_optional_tab_labels() {