From 43ab4be548b35a7e85eaefda565f9fba23338281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 8 May 2026 08:36:58 -0400 Subject: [PATCH] Add sidebar collapse shortcut --- crates/ely_app/src/main.rs | 2 + crates/ely_app/src/shell/mod.rs | 1 + crates/ely_app/src/shell/render.rs | 34 ++--- crates/ely_app/src/shell/sidebar.rs | 215 ++++++++++++++++++++++++++++ crates/ely_app/src/shortcuts.rs | 23 ++- crates/ely_domain/src/lib.rs | 2 +- crates/ely_domain/src/space.rs | 1 + 7 files changed, 260 insertions(+), 18 deletions(-) create mode 100644 crates/ely_app/src/shell/sidebar.rs diff --git a/crates/ely_app/src/main.rs b/crates/ely_app/src/main.rs index 0efe59c..d8d7c02 100644 --- a/crates/ely_app/src/main.rs +++ b/crates/ely_app/src/main.rs @@ -28,6 +28,7 @@ actions!( SplitRight, ToggleFavoriteTab, TogglePinnedTab, + ToggleSidebar, ] ); @@ -52,6 +53,7 @@ fn main() { MenuItem::action("Split Right", SplitRight), MenuItem::separator(), MenuItem::action("Command Mode", FocusCommandMode), + MenuItem::action("Toggle Sidebar", ToggleSidebar), MenuItem::separator(), MenuItem::action("Close Tab", CloseCurrentTab), MenuItem::separator(), diff --git a/crates/ely_app/src/shell/mod.rs b/crates/ely_app/src/shell/mod.rs index 1f45ebb..2c15338 100644 --- a/crates/ely_app/src/shell/mod.rs +++ b/crates/ely_app/src/shell/mod.rs @@ -6,6 +6,7 @@ mod notes; mod plugins; mod reading_list; mod render; +mod sidebar; mod site_permissions; mod splits; mod tab_groups; diff --git a/crates/ely_app/src/shell/render.rs b/crates/ely_app/src/shell/render.rs index 2935b90..2d16dc7 100644 --- a/crates/ely_app/src/shell/render.rs +++ b/crates/ely_app/src/shell/render.rs @@ -11,6 +11,7 @@ use gpui_component::{ input::Input, }; +use super::sidebar::{collapsed_sidebar_active, render_command_bar_identity}; use super::{ElyShell, ShellState}; impl Render for ElyShell { @@ -36,6 +37,7 @@ impl ElyShell { Ok(sidebar_width) => sidebar_width, Err(message) => return render_error(message), }; + let sidebar_collapsed = collapsed_sidebar_active(sidebar_width); div() .size_full() @@ -54,17 +56,24 @@ impl ElyShell { .on_action(cx.listener(Self::on_split_right)) .on_action(cx.listener(Self::on_toggle_favorite_tab)) .on_action(cx.listener(Self::on_toggle_pinned_tab)) + .on_action(cx.listener(Self::on_toggle_sidebar)) .bg(rgb(ELY_THEME.canvas)) .text_color(rgb(ELY_THEME.ink)) .flex() .flex_col() - .child(self.render_command_bar(&snapshot, &active_tab, sidebar_width, cx)) + .child(self.render_command_bar( + &snapshot, + &active_tab, + sidebar_width, + sidebar_collapsed, + cx, + )) .child( div() .flex() .flex_1() .overflow_hidden() - .child(self.render_sidebar(&snapshot, sidebar_width, cx)) + .child(self.render_sidebar(&snapshot, sidebar_width, sidebar_collapsed, cx)) .child(self.render_content_area(&snapshot, &active_tab, cx)), ) .into_any_element() @@ -75,6 +84,7 @@ impl ElyShell { snapshot: &BrowserSnapshot, active_tab: &BrowserTab, sidebar_width: f32, + sidebar_collapsed: bool, cx: &mut Context, ) -> AnyElement { let favorite_icon = @@ -91,20 +101,7 @@ impl ElyShell { .items_center() .border_b_1() .border_color(rgb(colors::HAIRLINE)) - .child( - div() - .w(px(sidebar_width - spacing::XL)) - .flex() - .items_center() - .gap_2() - .child(div().text_size(px(18.0)).font_semibold().child("ELY Browser")) - .child( - div() - .text_xs() - .text_color(rgb(colors::MUTED)) - .child(snapshot.active_space_name.clone()), - ), - ) + .child(render_command_bar_identity(snapshot, sidebar_width, sidebar_collapsed)) .child( div() .flex_1() @@ -149,8 +146,13 @@ impl ElyShell { &mut self, snapshot: &BrowserSnapshot, sidebar_width: f32, + sidebar_collapsed: bool, cx: &mut Context, ) -> AnyElement { + if sidebar_collapsed { + return self.render_compact_sidebar(snapshot, sidebar_width, cx); + } + div() .w(px(sidebar_width)) .h_full() diff --git a/crates/ely_app/src/shell/sidebar.rs b/crates/ely_app/src/shell/sidebar.rs new file mode 100644 index 0000000..7d3a2db --- /dev/null +++ b/crates/ely_app/src/shell/sidebar.rs @@ -0,0 +1,215 @@ +use ely_browser_core::BrowserSnapshot; +use ely_design_system::{colors, spacing}; +use ely_domain::{ + ArchivedTab, BrowserTab, COLLAPSED_SIDEBAR_WIDTH_PX, DEFAULT_SIDEBAR_WIDTH_PX, Space, +}; +use gpui::{AnyElement, Context, IntoElement, ParentElement, Styled, Window, div, px, rgb}; +use gpui_component::{ + IconName, Selectable, Sizable, StyledExt, + button::{Button, ButtonVariants}, +}; + +use super::{ElyShell, ShellState}; +use crate::ToggleSidebar; + +impl ElyShell { + pub(super) fn on_toggle_sidebar( + &mut self, + _: &ToggleSidebar, + _: &mut Window, + cx: &mut Context, + ) { + self.toggle_sidebar_width(cx); + } + + pub(super) fn render_compact_sidebar( + &mut self, + snapshot: &BrowserSnapshot, + sidebar_width: f32, + cx: &mut Context, + ) -> AnyElement { + div() + .w(px(sidebar_width)) + .h_full() + .flex() + .flex_col() + .items_center() + .gap_2() + .p_2() + .border_r_1() + .border_color(rgb(colors::HAIRLINE)) + .bg(rgb(colors::CANVAS)) + .children(snapshot.favorites.iter().enumerate().map(|(index, tab)| { + self.render_compact_tab_button( + ("compact-favorite", index), + tab, + tab.id() == &snapshot.active_tab_id, + IconName::Star, + cx, + ) + })) + .children(snapshot.pinned_tabs.iter().enumerate().map(|(index, tab)| { + self.render_compact_tab_button( + ("compact-pinned", index), + tab, + tab.id() == &snapshot.active_tab_id, + IconName::Asterisk, + cx, + ) + })) + .children(snapshot.spaces.iter().enumerate().map(|(index, space)| { + self.render_compact_space_button( + index, + space, + space.id() == &snapshot.active_space_id, + cx, + ) + })) + .children(snapshot.tabs.iter().enumerate().map(|(index, tab)| { + self.render_compact_tab_button( + ("compact-tab", index), + tab, + tab.id() == &snapshot.active_tab_id, + IconName::Globe, + cx, + ) + })) + .children(snapshot.archived_tabs.iter().rev().enumerate().map( + |(index, archived_tab)| { + self.render_compact_archived_button(index, archived_tab, cx) + }, + )) + .into_any_element() + } + + fn toggle_sidebar_width(&mut self, cx: &mut Context) { + let ShellState::Ready(core) = &mut self.state else { + return; + }; + let Ok(snapshot) = core.snapshot() else { + return; + }; + let Some(active_space) = + snapshot.spaces.iter().find(|space| space.id() == &snapshot.active_space_id) + else { + return; + }; + + let next_width = if active_space.sidebar_width_px() <= COLLAPSED_SIDEBAR_WIDTH_PX { + DEFAULT_SIDEBAR_WIDTH_PX + } else { + COLLAPSED_SIDEBAR_WIDTH_PX + }; + + if core.set_space_sidebar_width(&snapshot.active_space_id, next_width).is_ok() { + cx.notify(); + } + } + + fn render_compact_space_button( + &mut self, + index: usize, + space: &Space, + active: bool, + cx: &mut Context, + ) -> AnyElement { + let space_id = space.id().clone(); + Button::new(("compact-space", index)) + .ghost() + .small() + .selected(active) + .label(space.icon().to_string()) + .tooltip(space.name().to_string()) + .on_click(cx.listener(move |shell, _, window, cx| { + shell.select_space(&space_id, window, cx); + })) + .into_any_element() + } + + fn render_compact_tab_button( + &mut self, + id: (&'static str, usize), + tab: &BrowserTab, + active: bool, + icon: IconName, + cx: &mut Context, + ) -> AnyElement { + let tab_id = tab.id().clone(); + Button::new(id) + .ghost() + .small() + .selected(active) + .icon(icon) + .tooltip(tab.title().to_string()) + .on_click(cx.listener(move |shell, _, window, cx| { + shell.select_tab(&tab_id, window, cx); + })) + .into_any_element() + } + + fn render_compact_archived_button( + &mut self, + index: usize, + archived_tab: &ArchivedTab, + cx: &mut Context, + ) -> AnyElement { + let tab = archived_tab.tab(); + let tab_id = tab.id().clone(); + Button::new(("compact-archive", index)) + .ghost() + .small() + .icon(IconName::Undo2) + .tooltip(tab.title().to_string()) + .on_click(cx.listener(move |shell, _, window, cx| { + shell.restore_archived_tab(&tab_id, window, cx); + })) + .into_any_element() + } +} + +pub(super) fn render_command_bar_identity( + snapshot: &BrowserSnapshot, + sidebar_width: f32, + sidebar_collapsed: bool, +) -> AnyElement { + let width = sidebar_width - spacing::XL; + if sidebar_collapsed { + return div() + .w(px(width)) + .flex() + .items_center() + .justify_center() + .child( + div() + .size(px(28.0)) + .rounded_md() + .bg(rgb(colors::PRIMARY)) + .text_color(rgb(colors::CANVAS)) + .text_xs() + .font_semibold() + .flex() + .items_center() + .justify_center() + .child("ELY"), + ) + .into_any_element(); + } + + div() + .w(px(width)) + .flex() + .items_center() + .gap_2() + .child(div().text_size(px(18.0)).font_semibold().child("ELY Browser")) + .child( + div() + .text_xs() + .text_color(rgb(colors::MUTED)) + .child(snapshot.active_space_name.clone()), + ) + .into_any_element() +} + +pub(super) fn collapsed_sidebar_active(sidebar_width: f32) -> bool { + sidebar_width <= f32::from(COLLAPSED_SIDEBAR_WIDTH_PX) +} diff --git a/crates/ely_app/src/shortcuts.rs b/crates/ely_app/src/shortcuts.rs index 9f70c3b..f0ea04a 100644 --- a/crates/ely_app/src/shortcuts.rs +++ b/crates/ely_app/src/shortcuts.rs @@ -5,7 +5,7 @@ use gpui::{App, KeyBinding}; use crate::{ CloseCurrentTab, FocusAddressBar, FocusCommandMode, OpenDownloads, OpenHistory, OpenNewTab, OpenSettings, OpenTaskManager, Quit, RestoreClosedTab, SelectNextTab, SelectPreviousTab, - SplitRight, ToggleFavoriteTab, + SplitRight, ToggleFavoriteTab, ToggleSidebar, }; #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] @@ -33,6 +33,7 @@ pub(crate) enum ShortcutAction { SelectNextTab, SelectPreviousTab, SplitRight, + ToggleSidebar, ToggleFavoriteTab, OpenDownloads, OpenHistory, @@ -52,6 +53,7 @@ impl ShortcutAction { Self::SelectNextTab => "Next Tab", Self::SelectPreviousTab => "Previous Tab", Self::SplitRight => "Split Right", + Self::ToggleSidebar => "Toggle Sidebar", Self::ToggleFavoriteTab => "Toggle Favorite", Self::OpenDownloads => "Open Downloads", Self::OpenHistory => "Open History", @@ -70,6 +72,7 @@ impl ShortcutAction { | Self::SelectNextTab | Self::SelectPreviousTab | Self::SplitRight + | Self::ToggleSidebar | Self::ToggleFavoriteTab => "Tabs", Self::OpenDownloads | Self::OpenHistory => "Library", Self::OpenSettings | Self::OpenTaskManager => "System", @@ -87,6 +90,7 @@ impl ShortcutAction { Self::SelectNextTab => None, Self::SelectPreviousTab => None, Self::SplitRight => Some(">split-right"), + Self::ToggleSidebar => None, Self::ToggleFavoriteTab => Some(">favorite"), Self::OpenDownloads => Some(">open-downloads"), Self::OpenHistory => Some(">open-history"), @@ -126,6 +130,7 @@ pub(crate) const SHORTCUT_ACTIONS: &[ShortcutAction] = &[ ShortcutAction::SelectNextTab, ShortcutAction::SelectPreviousTab, ShortcutAction::SplitRight, + ShortcutAction::ToggleSidebar, ShortcutAction::ToggleFavoriteTab, ShortcutAction::OpenDownloads, ShortcutAction::OpenHistory, @@ -139,6 +144,8 @@ pub(crate) const SHORTCUT_BINDINGS: &[ShortcutBinding] = &[ shortcut(ShortcutAction::OpenNewTab, ShortcutPlatform::WindowsLinux, "ctrl-t"), shortcut(ShortcutAction::SplitRight, ShortcutPlatform::Macos, "cmd-\\"), shortcut(ShortcutAction::SplitRight, ShortcutPlatform::WindowsLinux, "ctrl-\\"), + shortcut(ShortcutAction::ToggleSidebar, ShortcutPlatform::Macos, "cmd-b"), + shortcut(ShortcutAction::ToggleSidebar, ShortcutPlatform::WindowsLinux, "ctrl-b"), shortcut(ShortcutAction::OpenDownloads, ShortcutPlatform::Macos, "cmd-shift-j"), shortcut(ShortcutAction::OpenDownloads, ShortcutPlatform::WindowsLinux, "ctrl-shift-j"), shortcut(ShortcutAction::OpenHistory, ShortcutPlatform::Macos, "cmd-y"), @@ -240,6 +247,7 @@ impl ShortcutBinding { ShortcutAction::ToggleFavoriteTab => { KeyBinding::new(self.keystroke, ToggleFavoriteTab, None) } + ShortcutAction::ToggleSidebar => KeyBinding::new(self.keystroke, ToggleSidebar, None), } } } @@ -289,6 +297,19 @@ mod tests { assert_eq!(bindings, vec!["Cmd + ,".to_string(), "Ctrl + ,".to_string()]); } + #[test] + fn toggle_sidebar_shortcut_has_platform_bindings() { + let bindings = bindings_for_action(ShortcutAction::ToggleSidebar, ShortcutPlatform::Macos) + .chain(bindings_for_action( + ShortcutAction::ToggleSidebar, + ShortcutPlatform::WindowsLinux, + )) + .map(|binding| binding.display_keystroke()) + .collect::>(); + + assert_eq!(bindings, vec!["Cmd + B".to_string(), "Ctrl + B".to_string()]); + } + #[test] fn every_declared_action_has_a_binding() { for action in SHORTCUT_ACTIONS { diff --git a/crates/ely_domain/src/lib.rs b/crates/ely_domain/src/lib.rs index 5a7ce01..983cd25 100644 --- a/crates/ely_domain/src/lib.rs +++ b/crates/ely_domain/src/lib.rs @@ -49,7 +49,7 @@ pub use site_permission::{ SiteOrigin, SitePermissionAuditAction, SitePermissionAuditEvent, SitePermissionDecision, SitePermissionEntry, SitePermissionFeature, }; -pub use space::{ArchivePolicy, DEFAULT_SIDEBAR_WIDTH_PX, Space}; +pub use space::{ArchivePolicy, COLLAPSED_SIDEBAR_WIDTH_PX, DEFAULT_SIDEBAR_WIDTH_PX, Space}; pub use split::{MAX_SPLIT_PANES, SplitAxis, SplitLayout, SplitPane}; pub use sync::{ SyncConnectionState, SyncObjectKind, SyncObjectPolicy, SyncObjectState, SyncObjectStatus, diff --git a/crates/ely_domain/src/space.rs b/crates/ely_domain/src/space.rs index c802bca..49e703e 100644 --- a/crates/ely_domain/src/space.rs +++ b/crates/ely_domain/src/space.rs @@ -3,6 +3,7 @@ use std::time::{Duration, SystemTime}; use crate::{ProfileId, SpaceId}; pub const DEFAULT_SIDEBAR_WIDTH_PX: u16 = 280; +pub const COLLAPSED_SIDEBAR_WIDTH_PX: u16 = 56; #[derive(Clone, Debug, Eq, PartialEq)] pub enum ArchivePolicy {