feat: add sidebar collapsible feature

This commit is contained in:
TomZz
2026-06-17 00:43:39 +08:00
parent 2a37f45ba2
commit daaebe4c2b
7 changed files with 224 additions and 22 deletions
+1
View File
@@ -133,6 +133,7 @@ press_new_key: "Press new key..."
settings_open_settings: "Open Settings"
settings_open_session: "Open Session"
settings_new_ssh: "New SSH"
settings_toggle_sidebar: "Toggle Sidebar"
settings_toggle_sftp_zoom: "Toggle SFTP Zoom"
settings_focus_pane_left: "Focus Pane Left"
settings_focus_pane_right: "Focus Pane Right"
+1
View File
@@ -136,6 +136,7 @@ press_new_key: "请按下新的快捷键..."
settings_open_settings: "打开设置"
settings_open_session: "打开会话"
settings_new_ssh: "新建 SSH"
settings_toggle_sidebar: "缩放侧边栏"
settings_toggle_sftp_zoom: "缩放 SFTP 面板"
settings_focus_pane_left: "向左切换焦点"
settings_focus_pane_right: "向右切换焦点"
+1
View File
@@ -1,6 +1,7 @@
pub(crate) const DEFAULT_COLS: u16 = 100;
pub(crate) const DEFAULT_ROWS: u16 = 30;
pub(crate) const SIDEBAR_WIDTH: f32 = 306.0;
pub(crate) const COLLAPSED_SIDEBAR_WIDTH: f32 = 52.0;
pub(crate) const TAB_BAR_HEIGHT: f32 = 52.0;
pub(crate) const TERMINAL_PADDING_X: f32 = 32.0;
pub(crate) const TERMINAL_PADDING_Y: f32 = 32.0;
+8
View File
@@ -17,6 +17,7 @@ gpui::actions!(
OpenSettings,
OpenSession,
NewSsh,
ToggleSidebar,
ToggleSftpZoom,
FocusPaneLeft,
FocusPaneRight,
@@ -55,6 +56,11 @@ pub(crate) const WORKSPACE_ACTIONS: &[WorkspaceAction] = &[
label_key: "settings_new_ssh",
default_suffix: "n",
},
WorkspaceAction {
id: "ToggleSidebar",
label_key: "settings_toggle_sidebar",
default_suffix: "s",
},
WorkspaceAction {
id: "ToggleSftpZoom",
label_key: "settings_toggle_sftp_zoom",
@@ -193,6 +199,7 @@ pub(crate) fn unbind_all_workspace_keys(cx: &mut App, config: &ConfigStore) {
unbind_action!("OpenSettings", crate::OpenSettings);
unbind_action!("OpenSession", crate::OpenSession);
unbind_action!("NewSsh", crate::NewSsh);
unbind_action!("ToggleSidebar", crate::ToggleSidebar);
unbind_action!("ToggleSftpZoom", crate::ToggleSftpZoom);
unbind_action!("FocusPaneLeft", crate::FocusPaneLeft);
unbind_action!("FocusPaneRight", crate::FocusPaneRight);
@@ -246,6 +253,7 @@ fn bind_workspace_actions(cx: &mut App, config: &ConfigStore) {
bind_action!("OpenSettings", crate::OpenSettings);
bind_action!("OpenSession", crate::OpenSession);
bind_action!("NewSsh", crate::NewSsh);
bind_action!("ToggleSidebar", crate::ToggleSidebar);
bind_action!("ToggleSftpZoom", crate::ToggleSftpZoom);
bind_action!("FocusPaneLeft", crate::FocusPaneLeft);
bind_action!("FocusPaneRight", crate::FocusPaneRight);
+4
View File
@@ -245,6 +245,8 @@ pub(crate) struct Ashell {
pub(crate) drag_split_origin: Option<gpui::Point<Pixels>>,
pub(crate) terminal_marked_text: Option<String>,
pub(crate) sftp_panel_minimized: bool,
pub(crate) sidebar_collapsed: bool,
pub(crate) collapsed_saved_scroll_handle: gpui::ScrollHandle,
pub(crate) prev_monitoring_size: Option<Pixels>,
pub(crate) status: SharedString,
pub(crate) config: ConfigStore,
@@ -467,6 +469,8 @@ impl Ashell {
dragging_splitter: None,
drag_split_origin: None,
sftp_panel_minimized: false,
sidebar_collapsed: false,
collapsed_saved_scroll_handle: gpui::ScrollHandle::new(),
prev_monitoring_size: None,
status: "ready".into(),
config,
+208 -22
View File
@@ -21,7 +21,7 @@ use rust_i18n::t;
use crate::{
Ashell, PaneLayout,
app::constants::{SIDEBAR_WIDTH, TERMINAL_KEY_CONTEXT},
app::constants::{COLLAPSED_SIDEBAR_WIDTH, SIDEBAR_WIDTH, TERMINAL_KEY_CONTEXT},
sftp::format_mtime,
sftp::ops::is_editable_text_file,
system::format_bytes,
@@ -1516,6 +1516,15 @@ impl Ashell {
.child("Ashell"),
)
.child(div().flex_1())
.child(
Button::new("sidebar-collapse-toggle")
.ghost()
.icon(IconName::PanelLeftClose)
.on_click(cx.listener(|this, _, _, cx| {
this.sidebar_collapsed = true;
cx.notify();
})),
)
.child(
Button::new("sidebar-settings")
.ghost()
@@ -1714,6 +1723,151 @@ impl Ashell {
)
}
fn render_collapsed_sidebar(&self, cx: &mut Context<Self>) -> impl IntoElement {
let sessions = self.config.sessions().to_vec();
let active_session_id = self.active_session_id().map(ToOwned::to_owned);
v_flex()
.w_full()
.h_full()
.min_w(px(0.))
.p_2()
.border_r_1()
.border_color(cx.theme().sidebar_border)
.bg(cx.theme().sidebar)
.overflow_hidden()
.items_center()
// Top: expand button only
.child(
div()
.w_full()
.flex()
.items_center()
.justify_center()
.pb_2()
.child(
Button::new("sidebar-expand-toggle")
.ghost()
.icon(IconName::PanelLeftOpen)
.on_click(cx.listener(|this, _, _, cx| {
this.sidebar_collapsed = false;
cx.notify();
})),
),
)
// Saved sessions as compact cards
.child(
div()
.relative()
.flex_1()
.min_h(px(0.))
.w_full()
.child(
v_flex()
.size_full()
.id("collapsed-saved-sessions-scroll")
.track_scroll(&self.collapsed_saved_scroll_handle)
.overflow_y_scroll()
.gap_2()
.items_center()
.children(sessions.into_iter().enumerate().map(
|(ix, session)| {
let connect_id = session.id.clone();
let is_active = active_session_id.as_deref()
== Some(session.id.as_str());
let name = session.name.clone();
// Abbreviate: first 1 char for CJK, first 2 chars for Latin
let abbrev = {
let mut chars = name.chars();
if let Some(first) = chars.next() {
if first > '\u{2E7F}' {
// CJK character range — show 1 char
first.to_string()
} else {
// Latin / ASCII — show first 2 chars
let mut s = first.to_string();
if let Some(second) = chars.next() {
s.push(second);
}
s
}
} else {
"?".to_string()
}
};
div()
.id(("collapsed-saved", ix))
.w(px(36.))
.h(px(36.))
.flex()
.items_center()
.justify_center()
.rounded_md()
.border_1()
.border_color(if is_active {
cx.theme().primary
} else {
cx.theme().border
})
.bg(if is_active {
cx.theme().tab_active
} else {
cx.theme().muted
})
.cursor_pointer()
.hover(|this| this.bg(cx.theme().secondary))
.on_mouse_down(
MouseButton::Left,
cx.listener(move |this, _, _, cx| {
this.connect_saved_session(
connect_id.clone(),
cx,
)
}),
)
.tooltip({
let tooltip_text = format!("{} {}", name, session.user);
move |window, cx| {
gpui_component::tooltip::Tooltip::new(tooltip_text.clone()).build(window, cx)
}
})
.child(
div()
.text_size(rems(0.833))
.font_weight(FontWeight::BOLD)
.text_color(if is_active {
cx.theme().primary
} else {
cx.theme().foreground
})
.child(abbrev),
)
},
)),
)
.child(
div()
.absolute()
.top_0()
.bottom_0()
.left_0()
.right_0()
.child(
gpui_component::scroll::Scrollbar::new(
&self.collapsed_saved_scroll_handle,
)
.id("collapsed-saved-scrollbar")
.axis(gpui_component::scroll::ScrollbarAxis::Vertical)
.scrollbar_show(
gpui_component::scroll::ScrollbarShow::Scrolling,
),
),
),
)
}
fn render_tab_bar(&self, cx: &mut Context<Self>) -> impl IntoElement {
let active_tab_index = self
.active_tab
@@ -2187,16 +2341,6 @@ impl Render for Ashell {
}
}
let sidebar_area = resizable_panel()
.size(px(self
.config
.workspace_panels()
.and_then(|s| s.first().copied())
.unwrap_or(SIDEBAR_WIDTH)))
.size_range(px(240.)..px(520.))
.flex_none()
.child(self.sidebar(cx));
let monitoring_contents = v_flex()
.size_full()
.when(self.config.monitoring_position() == "Bottom", |this| {
@@ -2236,19 +2380,57 @@ impl Render for Ashell {
)
.into_any_element();
let main_area = resizable_panel().child(
v_flex()
let workspace = if self.sidebar_collapsed {
h_flex()
.size_full()
.relative()
.overflow_hidden()
.child(self.render_tab_bar(cx))
.child(body_panel),
);
.child(
div()
.flex_none()
.w(px(COLLAPSED_SIDEBAR_WIDTH))
.h_full()
.child(self.render_collapsed_sidebar(cx)),
)
.child(
div()
.flex_1()
.h_full()
.min_w(px(0.))
.child(
v_flex()
.size_full()
.relative()
.overflow_hidden()
.child(self.render_tab_bar(cx))
.child(body_panel),
),
)
.into_any_element()
} else {
let sidebar_area = resizable_panel()
.size(px(self
.config
.workspace_panels()
.and_then(|s| s.first().copied())
.unwrap_or(SIDEBAR_WIDTH)))
.size_range(px(240.)..px(520.))
.flex_none()
.child(self.sidebar(cx));
let workspace = h_resizable("ashell-workspace")
.with_state(&self.workspace_panels)
.child(sidebar_area)
.child(main_area);
let main_area = resizable_panel().child(
v_flex()
.size_full()
.relative()
.overflow_hidden()
.child(self.render_tab_bar(cx))
.child(body_panel),
);
h_resizable("ashell-workspace")
.with_state(&self.workspace_panels)
.child(sidebar_area)
.child(main_area)
.into_any_element()
};
div()
.id("ashell-root")
@@ -2259,6 +2441,10 @@ impl Render for Ashell {
.on_action(cx.listener(|this, _: &crate::OpenSettings, window, cx| this.show_settings_dialog(window, cx)))
.on_action(cx.listener(|this, _: &crate::OpenSession, window, cx| this.show_selector_dialog(window, cx)))
.on_action(cx.listener(|this, _: &crate::NewSsh, window, cx| this.show_ssh_dialog(window, cx)))
.on_action(cx.listener(|this, _: &crate::ToggleSidebar, _, cx| {
this.sidebar_collapsed = !this.sidebar_collapsed;
cx.notify();
}))
.on_action(cx.listener(|this, _: &crate::ToggleSftpZoom, window, cx| {
this.toggle_sftp_minimized(window, cx);
}))
+1
View File
@@ -17,6 +17,7 @@ gpui::actions!(ashell_terminal, [TerminalTabKey, TerminalBacktabKey]);
pub(crate) use app::keybinding_recorder::{
ClosePane, FocusPaneDown, FocusPaneLeft, FocusPaneRight, FocusPaneUp, NewSsh, OpenSession,
OpenSettings, SplitPaneDown, SplitPaneLeft, SplitPaneRight, SplitPaneUp, ToggleSftpZoom,
ToggleSidebar,
};
pub(crate) use app::{