diff --git a/crates/ely_app/src/shell/chrome/sidebar.rs b/crates/ely_app/src/shell/chrome/sidebar.rs index e40d3ab..e466a22 100644 --- a/crates/ely_app/src/shell/chrome/sidebar.rs +++ b/crates/ely_app/src/shell/chrome/sidebar.rs @@ -2,9 +2,9 @@ use ely_browser_core::BrowserSnapshot; use ely_design_system::{colors, spacing}; use ely_domain::BrowserTab; use gpui::{ - AnyElement, BoxShadow, Context, InteractiveElement, IntoElement, ParentElement, SharedString, - StatefulInteractiveElement, Styled, div, hsla, linear_color_stop, linear_gradient, point, - prelude::FluentBuilder, px, rgb, rgba, + AnyElement, BoxShadow, Context, InteractiveElement, IntoElement, MouseButton, + ParentElement, SharedString, StatefulInteractiveElement, Styled, div, hsla, + linear_color_stop, linear_gradient, point, prelude::FluentBuilder, px, rgb, rgba, }; use gpui_component::{IconName, StyledExt, scroll::ScrollableElement}; @@ -22,6 +22,7 @@ impl ElyShell { div() .w(px(sidebar_width)) .h_full() + .relative() .flex() .flex_col() .rounded(px(spacing::RADIUS_CARD)) @@ -60,6 +61,7 @@ impl ElyShell { .child(self.render_new_tab_row(cx)), ) .child(self.render_sidebar_footer(snapshot, cx)) + .child(render_sidebar_resize_handle(cx)) .into_any_element() } @@ -419,6 +421,31 @@ const UNREAD_BADGE_BG: u32 = 0x281e140f; /// never participates in hit testing. const HIGHLIGHT_BORDER: u32 = 0xffffff80; +/// Sidebar resize affordance pinned to the panel's right edge. The +/// strip itself is invisible until hovered; on mouse-down it tells the +/// shell to capture the drag origin so window-level mouse-move can +/// compute the live width. +fn render_sidebar_resize_handle(cx: &mut Context) -> AnyElement { + div() + .id(SharedString::from("sidebar-resize-handle")) + .absolute() + .top_0() + .bottom_0() + .right(px(-2.0)) + .w(px(6.0)) + .cursor_col_resize() + .hover(|style| style.bg(rgba(RESIZE_HANDLE_HOVER_BG))) + .on_mouse_down( + MouseButton::Left, + cx.listener(|shell, event: &gpui::MouseDownEvent, _, cx| { + shell.begin_sidebar_resize(f32::from(event.position.x), cx); + }), + ) + .into_any_element() +} + +const RESIZE_HANDLE_HOVER_BG: u32 = 0xffffff66; + /// Maps appearance translucency_pct to a panel rgba u32 tinted by the /// active wallpaper theme. /// diff --git a/crates/ely_app/src/shell/mod.rs b/crates/ely_app/src/shell/mod.rs index 5c06b4e..58c960c 100644 --- a/crates/ely_app/src/shell/mod.rs +++ b/crates/ely_app/src/shell/mod.rs @@ -68,6 +68,10 @@ pub struct ElyShell { pub(crate) workspace_picker_open: bool, pub(crate) sidebar_hover_expanded: bool, pub(crate) command_selected_index: usize, + /// Live state for sidebar-resize drag. While the user holds the + /// resize handle: `(mouse_x_at_drag_start, sidebar_width_px_at_drag_start)`. + /// `None` whenever no drag is in flight. + pub(crate) sidebar_resize_origin: Option<(f32, u16)>, download_action_error: Option, download_clear_confirmation: bool, download_security_confirmation: Option, @@ -182,6 +186,7 @@ impl ElyShell { workspace_picker_open: false, sidebar_hover_expanded: false, command_selected_index: 0, + sidebar_resize_origin: None, download_action_error: None, download_clear_confirmation: false, download_security_confirmation: None, diff --git a/crates/ely_app/src/shell/render.rs b/crates/ely_app/src/shell/render.rs index 64e5505..3703113 100644 --- a/crates/ely_app/src/shell/render.rs +++ b/crates/ely_app/src/shell/render.rs @@ -2,9 +2,9 @@ use ely_browser_core::BrowserSnapshot; use ely_design_system::{colors, spacing}; use ely_domain::{BrowserTab, DEFAULT_SIDEBAR_WIDTH_PX, HIDDEN_SIDEBAR_WIDTH_PX}; use gpui::{ - AnyElement, Context, InteractiveElement, IntoElement, KeyDownEvent, MouseMoveEvent, - ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Window, div, - prelude::FluentBuilder, px, rgb, rgba, + AnyElement, Context, InteractiveElement, IntoElement, KeyDownEvent, MouseButton, + MouseMoveEvent, MouseUpEvent, ParentElement, Render, SharedString, + StatefulInteractiveElement, Styled, Window, div, prelude::FluentBuilder, px, rgb, rgba, }; use super::chrome::command_match::visible_command_rows; @@ -68,6 +68,7 @@ impl ElyShell { .on_action(cx.listener(Self::on_zoom_in)) .on_action(cx.listener(Self::on_zoom_out)) .on_mouse_move(cx.listener(Self::on_window_mouse_move)) + .on_mouse_up(MouseButton::Left, cx.listener(Self::on_window_mouse_up)) .capture_key_down(cx.listener(Self::on_command_overlay_key_down)) .font_family(SANS_FAMILY) .text_color(rgb(colors::INK)) @@ -214,6 +215,15 @@ impl ElyShell { ) { let cursor_x = f32::from(event.position.x); + if let Some((origin_x, origin_width)) = self.sidebar_resize_origin { + let delta = cursor_x - origin_x; + let candidate = (f32::from(origin_width) + delta) + .clamp(MIN_RESIZE_WIDTH_PX, MAX_RESIZE_WIDTH_PX) + .round() as u16; + self.set_active_sidebar_width(candidate, cx); + return; + } + if cursor_x >= REVEAL_THRESHOLD_PX && cursor_x <= COLLAPSE_THRESHOLD_PX { return; } @@ -235,6 +245,36 @@ impl ElyShell { } } + pub(super) fn begin_sidebar_resize( + &mut self, + cursor_x: f32, + cx: &mut Context, + ) { + let ShellState::Ready(core) = &self.state else { + return; + }; + let Some(width) = core.active_space_sidebar_width() else { + return; + }; + self.sidebar_resize_origin = Some((cursor_x, width)); + cx.notify(); + } + + pub(super) fn end_sidebar_resize(&mut self, cx: &mut Context) { + if self.sidebar_resize_origin.take().is_some() { + cx.notify(); + } + } + + fn on_window_mouse_up( + &mut self, + _event: &MouseUpEvent, + _window: &mut Window, + cx: &mut Context, + ) { + self.end_sidebar_resize(cx); + } + fn render_hidden_sidebar_rail(&mut self, cx: &mut Context) -> AnyElement { div() .id(SharedString::from("hidden-sidebar-rail")) @@ -256,6 +296,16 @@ impl ElyShell { /// never participates in hit testing. const MAIN_PANE_HIGHLIGHT_BORDER: u32 = 0xffffff80; +/// Lower clamp for the live sidebar-resize drag. Stays a touch above +/// the per-space DEFAULT so a casual flick doesn't snap the user to a +/// nearly-collapsed pane that's awkward to recover from. The toggle +/// button still owns the discrete collapse-to-rail behavior. +pub(super) const MIN_RESIZE_WIDTH_PX: f32 = 220.0; + +/// Upper clamp — wider than this and the main pane disappears on +/// smaller windows. Tuned to fit comfortably on a 1280-wide window. +pub(super) const MAX_RESIZE_WIDTH_PX: f32 = 480.0; + /// Cursor x within this px from the left edge auto-reveals the hidden sidebar. /// Sized so the user only triggers the reveal when they actually approach the /// rail, not when they hover the page content.