From 90c09aca15a865a4581ddd15da47ca57aca8cfdb Mon Sep 17 00:00:00 2001 From: Eric Holk Date: Thu, 12 Mar 2026 13:57:32 -0700 Subject: [PATCH] wip: adding flex content drag handle Co-authored-by: Max Brunsfeld --- crates/workspace/src/dock.rs | 177 ++++++++++++------------------ crates/workspace/src/workspace.rs | 173 ++++++++++++++++++++++------- 2 files changed, 203 insertions(+), 147 deletions(-) diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 0c655415e750f4fab4ff009156515660e50d5b56..f77b6df3c3851cff416c0f65eb051a890d66aa22 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -7,7 +7,7 @@ use client::proto; use gpui::{ Action, AnyElement, AnyView, App, Axis, Context, Corner, Entity, EntityId, EventEmitter, FocusHandle, Focusable, IntoElement, KeyContext, MouseButton, MouseDownEvent, MouseUpEvent, - ParentElement, Render, SharedString, StyleRefinement, Styled, Subscription, WeakEntity, Window, + ParentElement, Render, SharedString, Styled, Subscription, WeakEntity, Window, deferred, div, px, }; use settings::SettingsStore; @@ -257,6 +257,12 @@ pub enum DockPosition { Right, } +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum DockPart { + Fixed, + Flexible, +} + impl From for DockPosition { fn from(value: settings::DockPosition) -> Self { match value { @@ -392,7 +398,7 @@ impl Dock { self.is_open } - fn resizable(&self, cx: &App) -> bool { + pub fn resizable(&self, cx: &App) -> bool { !(self.zoom_layer_open || self.modal_layer.read(cx).has_active_modal()) } @@ -797,10 +803,9 @@ impl Dock { } } - fn dispatch_context() -> KeyContext { + pub fn dispatch_context() -> KeyContext { let mut dispatch_context = KeyContext::new_with_defaults(); dispatch_context.add("Dock"); - dispatch_context } @@ -814,110 +819,68 @@ impl Dock { } } -impl Render for Dock { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let dispatch_context = Self::dispatch_context(); - if let Some(entry) = self - .visible_entry() - .filter(|entry| entry.panel.has_panel_content(window, cx)) - { - let size = entry.panel.size(window, cx); - - let panel_content = entry - .panel - .to_any() - .cached(StyleRefinement::default().v_flex().size_full()); - - let position = self.position; - let create_resize_handle = || { - let handle = div() - .id("resize-handle") - .on_drag(DraggedDock { position }, |dock, _, _, cx| { - cx.stop_propagation(); - cx.new(|_| dock.clone()) - }) - .on_mouse_down( - MouseButton::Left, - cx.listener(|_, _: &MouseDownEvent, _, cx| { - cx.stop_propagation(); - }), - ) - .on_mouse_up( - MouseButton::Left, - cx.listener(|dock, e: &MouseUpEvent, window, cx| { - if e.click_count == 2 { - dock.resize_active_panel(None, window, cx); - dock.workspace - .update(cx, |workspace, cx| { - workspace.serialize_workspace(window, cx); - }) - .ok(); - cx.stop_propagation(); - } - }), - ) - .occlude(); - match self.position() { - DockPosition::Left => deferred( - handle - .absolute() - .right(-RESIZE_HANDLE_SIZE / 2.) - .top(px(0.)) - .h_full() - .w(RESIZE_HANDLE_SIZE) - .cursor_col_resize(), - ), - DockPosition::Bottom => deferred( - handle - .absolute() - .top(-RESIZE_HANDLE_SIZE / 2.) - .left(px(0.)) - .w_full() - .h(RESIZE_HANDLE_SIZE) - .cursor_row_resize(), - ), - DockPosition::Right => deferred( - handle - .absolute() - .top(px(0.)) - .left(-RESIZE_HANDLE_SIZE / 2.) - .h_full() - .w(RESIZE_HANDLE_SIZE) - .cursor_col_resize(), - ), +pub fn create_resize_handle( + position: DockPosition, + part: DockPart, + cx: &mut Context, +) -> gpui::Deferred { + let handle = div() + .id(match part { + DockPart::Fixed => "resize-handle", + DockPart::Flexible => "flexible-resize-handle", + }) + .on_drag(DraggedDock { position, part }, |dock, _, _, cx| { + cx.stop_propagation(); + cx.new(|_| dock.clone()) + }) + .on_mouse_down( + MouseButton::Left, + cx.listener(|_, _: &MouseDownEvent, _, cx| { + cx.stop_propagation(); + }), + ) + .on_mouse_up( + MouseButton::Left, + cx.listener(move |workspace, e: &MouseUpEvent, window, cx| { + if e.click_count == 2 { + let dock = workspace.dock_at_position(position); + dock.update(cx, |dock, cx| { + dock.resize_active_panel(None, window, cx); + }); + workspace.serialize_workspace(window, cx); + cx.stop_propagation(); } - }; - - div() - .key_context(dispatch_context) - .track_focus(&self.focus_handle(cx)) - .flex() - .bg(cx.theme().colors().panel_background) - .border_color(cx.theme().colors().border) - .overflow_hidden() - .map(|this| match self.position().axis() { - Axis::Horizontal => this - .h_full() - .flex_row() - .child(div().w(size).h_full().child(panel_content)), - Axis::Vertical => this - .w_full() - .flex_col() - .child(div().h(size).w_full().child(panel_content)), - }) - .map(|this| match self.position() { - DockPosition::Left => this.border_r_1(), - DockPosition::Right => this.border_l_1(), - DockPosition::Bottom => this.border_t_1(), - }) - .when(self.resizable(cx), |this| { - this.child(create_resize_handle()) - }) - } else { - div() - .key_context(dispatch_context) - .track_focus(&self.focus_handle(cx)) - } + }), + ) + .occlude(); + match position { + DockPosition::Left => deferred( + handle + .absolute() + .right(-RESIZE_HANDLE_SIZE / 2.) + .top(px(0.)) + .h_full() + .w(RESIZE_HANDLE_SIZE) + .cursor_col_resize(), + ), + DockPosition::Bottom => deferred( + handle + .absolute() + .top(-RESIZE_HANDLE_SIZE / 2.) + .left(px(0.)) + .w_full() + .h(RESIZE_HANDLE_SIZE) + .cursor_row_resize(), + ), + DockPosition::Right => deferred( + handle + .absolute() + .top(px(0.)) + .left(-RESIZE_HANDLE_SIZE / 2.) + .h_full() + .w(RESIZE_HANDLE_SIZE) + .cursor_col_resize(), + ), } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b46dfa59f3322eda173541ec039fb438fd03fe91..da9436c1c5a49b0f9bf1aa0dbd840e9022c4ae5a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -51,12 +51,13 @@ use futures::{ future::{Shared, try_join_all}, }; use gpui::{ - Action, AnyElement, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds, - Context, CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, - Focusable, Global, HitboxBehavior, Hsla, KeyContext, Keystroke, ManagedView, MouseButton, - PathPromptOptions, Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription, - SystemWindowTabController, Task, Tiling, WeakEntity, WindowBounds, WindowHandle, WindowId, - WindowOptions, actions, canvas, point, relative, size, transparent_black, + Action, AnyElement, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Axis, + Bounds, Context, CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, + FocusHandle, Focusable, Global, HitboxBehavior, Hsla, KeyContext, Keystroke, ManagedView, + MouseButton, PathPromptOptions, Point, PromptLevel, Render, ResizeEdge, Size, Stateful, + StyleRefinement, Subscription, SystemWindowTabController, Task, Tiling, WeakEntity, + WindowBounds, WindowHandle, WindowId, WindowOptions, actions, canvas, point, relative, size, + transparent_black, }; pub use history_manager::*; pub use item::{ @@ -148,7 +149,7 @@ pub use workspace_settings::{ }; use zed_actions::{Spawn, feedback::FileBugReport}; -use crate::{item::ItemBufferKind, notifications::NotificationId}; +use crate::{dock::DockPart, item::ItemBufferKind, notifications::NotificationId}; use crate::{ persistence::{ SerializedAxis, @@ -6973,7 +6974,7 @@ impl Workspace { position: DockPosition, dock: &Entity, window: &mut Window, - cx: &mut App, + cx: &mut Context, ) -> impl Iterator { let mut results = [None, None]; @@ -6987,12 +6988,57 @@ impl Workspace { leader_border_for_pane(follower_states, &pane, window, cx) }); + let fixed_content = { + let dispatch_context = Dock::dispatch_context(); + if let Some(panel) = dock + .read(cx) + .visible_panel() + .filter(|panel| panel.has_panel_content(window, cx)) + { + let size = panel.size(window, cx); + + let panel_content = panel + .to_any() + .cached(StyleRefinement::default().v_flex().size_full()); + + div() + .key_context(dispatch_context) + .track_focus(&self.focus_handle(cx)) + .flex() + .bg(cx.theme().colors().panel_background) + .border_color(cx.theme().colors().border) + .overflow_hidden() + .map(|this| match position.axis() { + Axis::Horizontal => this + .h_full() + .flex_row() + .child(div().w(size).h_full().child(panel_content)), + Axis::Vertical => this + .w_full() + .flex_col() + .child(div().h(size).w_full().child(panel_content)), + }) + .map(|this| match position { + DockPosition::Left => this.border_r_1(), + DockPosition::Right => this.border_l_1(), + DockPosition::Bottom => this.border_t_1(), + }) + .when(dock.read(cx).resizable(cx), |this| { + this.child(dock::create_resize_handle(position, DockPart::Fixed, cx)) + }) + } else { + div() + .key_context(dispatch_context) + .track_focus(&self.focus_handle(cx)) + } + }; + results[0] = Some( div() .flex() .flex_none() .overflow_hidden() - .child(dock.clone()) + .child(fixed_content) .children(leader_border) .into_any_element(), ); @@ -7008,7 +7054,13 @@ impl Workspace { .flex_1() .min_w(px(10.)) .flex_grow() + .when(position == DockPosition::Right, |this| { + this.child(dock::create_resize_handle(position, DockPart::Flexible, cx)) + }) .child(flex_content) + .when(position == DockPosition::Left, |this| { + this.child(dock::create_resize_handle(position, DockPart::Flexible, cx)) + }) .into_any_element() }); @@ -7246,6 +7298,11 @@ impl Workspace { } } +fn render_resize_handle() -> impl IntoElement { + // FIXME: actually render the handle and wire it up. + div().debug_bg_magenta() +} + pub trait AnyActiveCall { fn entity(&self) -> AnyEntity; fn is_in_room(&self, _: &App) -> bool; @@ -7590,6 +7647,7 @@ impl Focusable for Workspace { #[derive(Clone)] struct DraggedDock { position: DockPosition, + part: DockPart, } impl Render for DraggedDock { @@ -7723,39 +7781,74 @@ impl Render for Workspace { .when(self.zoomed.is_none(), |this| { this.on_drag_move(cx.listener( move |workspace, e: &DragMoveEvent, window, cx| { - if workspace.previous_dock_drag_coordinates - != Some(e.event.position) - { - workspace.previous_dock_drag_coordinates = - Some(e.event.position); - - match e.drag(cx).position { - DockPosition::Left => { - workspace.resize_left_dock( - e.event.position.x - - workspace.bounds.left(), - window, - cx, - ); + let drag = e.drag(cx); + match drag.part { + DockPart::Fixed => { + if workspace.previous_dock_drag_coordinates + != Some(e.event.position) + { + workspace.previous_dock_drag_coordinates = + Some(e.event.position); + + match e.drag(cx).position { + DockPosition::Left => { + workspace.resize_left_dock( + e.event.position.x + - workspace.bounds.left(), + window, + cx, + ); + } + DockPosition::Right => { + workspace.resize_right_dock( + workspace.bounds.right() + - e.event.position.x, + window, + cx, + ); + } + DockPosition::Bottom => { + workspace.resize_bottom_dock( + workspace.bounds.bottom() + - e.event.position.y, + window, + cx, + ); + } + }; + workspace.serialize_workspace(window, cx); } - DockPosition::Right => { - workspace.resize_right_dock( - workspace.bounds.right() - - e.event.position.x, - window, - cx, - ); - } - DockPosition::Bottom => { - workspace.resize_bottom_dock( - workspace.bounds.bottom() - - e.event.position.y, - window, - cx, - ); + } + DockPart::Flexible => { + match drag.position { + DockPosition::Left => { + let fixed_width = workspace + .left_dock + .read(cx) + .active_panel() + .map_or(px(0.0), |p| { + p.size(window, cx) + }); + let start_x = + workspace.bounds.left() + fixed_width; + let new_width = + e.event.position.x - start_x; + + dbg!(&e.event.position); + dbg!(&workspace.bounds); + dbg!(&fixed_width); + dbg!(&start_x); + dbg!(new_width); + // + } + DockPosition::Right => { + // + } + DockPosition::Bottom => unreachable!( + "bottom dock cannot have flex content" + ), } - }; - workspace.serialize_workspace(window, cx); + } } }, ))