feat: add expanded sidebar collapse control (#291)

* feat: add expanded sidebar collapse control

refs #278

* fix: place sidebar collapse control inside content

refs #278

---------

Co-authored-by: Can Celik <ogulcancelik@gmail.com>
This commit is contained in:
Turgay Bulut 2026-06-01 04:15:26 +03:00 committed by GitHub
parent 09169d59c1
commit 4db4ffad99
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 108 additions and 19 deletions

View File

@ -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;

View File

@ -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();

View File

@ -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::{

View File

@ -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() {