From 83375147feb6d961280ca80b5ab34a58a324e5cc Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 13 Feb 2025 16:07:00 +0800 Subject: [PATCH] panel: Improve resize handle to show highlight color on dragging. (#622) https://github.com/user-attachments/assets/aabbd73b-3c11-43e2-a4f2-121d46fba1d7 --- crates/ui/src/dock/dock.rs | 52 +---- crates/ui/src/resizable/panel.rs | 7 +- crates/ui/src/resizable/resize_handle.rs | 249 ++++++++++++++++++----- 3 files changed, 206 insertions(+), 102 deletions(-) diff --git a/crates/ui/src/dock/dock.rs b/crates/ui/src/dock/dock.rs index 7f01502c..5ba8190e 100644 --- a/crates/ui/src/dock/dock.rs +++ b/crates/ui/src/dock/dock.rs @@ -1,18 +1,17 @@ //! Dock is a fixed container that places at left, bottom, right of the Windows. -use std::sync::Arc; +use std::{ops::Deref, sync::Arc}; use gpui::{ div, prelude::FluentBuilder as _, px, App, AppContext, Axis, Context, Element, Empty, Entity, - InteractiveElement as _, IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, - Point, Render, StatefulInteractiveElement, Style, StyleRefinement, Styled as _, WeakEntity, - Window, + IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render, Style, + StyleRefinement, Styled as _, WeakEntity, Window, }; use serde::{Deserialize, Serialize}; use crate::{ - resizable::{HANDLE_PADDING, HANDLE_SIZE, PANEL_MIN_SIZE}, - ActiveTheme as _, AxisExt as _, StyledExt, + resizable::{resize_handle, PANEL_MIN_SIZE}, + StyledExt, }; use super::{DockArea, DockItem, PanelView, TabPanel}; @@ -278,51 +277,16 @@ impl Dock { fn render_resize_handle(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let axis = self.placement.axis(); - let neg_offset = -HANDLE_PADDING; let view = cx.entity().clone(); - div() - .id("resize-handle") - .occlude() - .absolute() - .flex_shrink_0() - .when(self.placement.is_left(), |this| { - // FIXME: Improve this to let the scroll bar have px(HANDLE_PADDING) - this.cursor_col_resize() - .top_0() - .right(px(1.)) - .h_full() - .w(HANDLE_SIZE) - .pl(HANDLE_PADDING) - }) - .when(self.placement.is_right(), |this| { - this.cursor_col_resize() - .top_0() - .left(neg_offset) - .h_full() - .w(HANDLE_SIZE) - .px(HANDLE_PADDING) - }) - .when(self.placement.is_bottom(), |this| { - this.cursor_row_resize() - .top(neg_offset) - .left_0() - .w_full() - .h(HANDLE_SIZE) - .py(HANDLE_PADDING) - }) - .child( - div() - .bg(cx.theme().border) - .when(axis.is_horizontal(), |this| this.h_full().w(HANDLE_SIZE)) - .when(axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE)), - ) + resize_handle("resize-handle", axis) + .placement(self.placement) .on_drag(ResizePanel {}, move |info, _, _, cx| { cx.stop_propagation(); view.update(cx, |view, _| { view.is_resizing = true; }); - cx.new(|_| info.clone()) + cx.new(|_| info.deref().clone()) }) } fn resize(&mut self, mouse_position: Point, _: &mut Window, cx: &mut Context) { diff --git a/crates/ui/src/resizable/panel.rs b/crates/ui/src/resizable/panel.rs index 138ff062..4c35a07f 100644 --- a/crates/ui/src/resizable/panel.rs +++ b/crates/ui/src/resizable/panel.rs @@ -1,10 +1,9 @@ -use std::rc::Rc; +use std::{ops::Deref, rc::Rc}; use gpui::{ canvas, div, prelude::FluentBuilder, px, relative, Along, AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Element, Empty, Entity, EntityId, EventEmitter, IntoElement, IsZero, - MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Render, StatefulInteractiveElement as _, - Style, Styled, WeakEntity, Window, + MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Render, Style, Styled, WeakEntity, Window, }; use crate::{h_flex, v_flex, AxisExt}; @@ -174,7 +173,7 @@ impl ResizablePanelGroup { view.update(cx, |view, _| { view.resizing_panel_ix = Some(ix); }); - cx.new(|_| drag_panel.clone()) + cx.new(|_| drag_panel.deref().clone()) }, ) } diff --git a/crates/ui/src/resizable/resize_handle.rs b/crates/ui/src/resizable/resize_handle.rs index 157f0c12..da049120 100644 --- a/crates/ui/src/resizable/resize_handle.rs +++ b/crates/ui/src/resizable/resize_handle.rs @@ -1,72 +1,213 @@ +use std::{cell::RefCell, rc::Rc}; + use gpui::{ - div, prelude::FluentBuilder as _, px, App, Axis, Div, ElementId, InteractiveElement, - IntoElement, ParentElement as _, Pixels, RenderOnce, Stateful, StatefulInteractiveElement, - Styled as _, Window, + div, prelude::FluentBuilder as _, px, AnyElement, App, Axis, Element, ElementId, Entity, + GlobalElementId, InteractiveElement, IntoElement, MouseDownEvent, MouseUpEvent, + ParentElement as _, Pixels, Point, Render, StatefulInteractiveElement, Styled as _, Window, }; -use crate::{ActiveTheme as _, AxisExt as _}; +use crate::{dock::DockPlacement, ActiveTheme as _, AxisExt as _}; pub(crate) const HANDLE_PADDING: Pixels = px(4.); pub(crate) const HANDLE_SIZE: Pixels = px(1.); -#[derive(IntoElement)] -pub(crate) struct ResizeHandle { - base: Stateful
, - axis: Axis, -} - -impl ResizeHandle { - fn new(id: impl Into, axis: Axis) -> Self { - Self { - base: div().id(id.into()), - axis, - } - } -} - /// Create a resize handle for a resizable panel. -pub(crate) fn resize_handle(id: impl Into, axis: Axis) -> ResizeHandle { +pub(crate) fn resize_handle( + id: impl Into, + axis: Axis, +) -> ResizeHandle { ResizeHandle::new(id, axis) } -impl InteractiveElement for ResizeHandle { - fn interactivity(&mut self) -> &mut gpui::Interactivity { - self.base.interactivity() +pub(crate) struct ResizeHandle { + id: ElementId, + axis: Axis, + drag_value: Option>, + placement: Option, + on_drag: Option, &mut Window, &mut App) -> Entity>>, +} + +impl ResizeHandle { + fn new(id: impl Into, axis: Axis) -> Self { + let id = id.into(); + Self { + id: id.clone(), + on_drag: None, + drag_value: None, + placement: None, + axis, + } + } + + pub(crate) fn on_drag( + mut self, + value: T, + f: impl Fn(Rc, &Point, &mut Window, &mut App) -> Entity + 'static, + ) -> Self { + let value = Rc::new(value); + self.drag_value = Some(value.clone()); + self.on_drag = Some(Rc::new(move |p, window, cx| { + f(value.clone(), p, window, cx) + })); + self + } + + pub(crate) fn placement(mut self, placement: DockPlacement) -> Self { + self.placement = Some(placement); + self } } -impl StatefulInteractiveElement for ResizeHandle {} -impl RenderOnce for ResizeHandle { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { +#[derive(Default, Debug, Clone)] +struct ResizeHandleState { + active: Rc>, +} + +impl ResizeHandleState { + fn set_active(&self, active: bool) { + *self.active.borrow_mut() = active; + } + + fn is_active(&self) -> bool { + *self.active.borrow() + } +} + +impl IntoElement for ResizeHandle { + type Element = ResizeHandle; + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for ResizeHandle { + type RequestLayoutState = AnyElement; + type PrepaintState = (); + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn request_layout( + &mut self, + id: Option<&GlobalElementId>, + window: &mut Window, + cx: &mut App, + ) -> (gpui::LayoutId, Self::RequestLayoutState) { let neg_offset = -HANDLE_PADDING; + let axis = self.axis; - self.base - .occlude() - .absolute() - .flex_shrink_0() - .when(self.axis.is_horizontal(), |this| { - this.cursor_col_resize() - .top_0() - .left(neg_offset) - .h_full() - .w(HANDLE_SIZE) - .px(HANDLE_PADDING) - }) - .when(self.axis.is_vertical(), |this| { - this.cursor_row_resize() - .top(neg_offset) - .left_0() - .w_full() - .h(HANDLE_SIZE) - .py(HANDLE_PADDING) - }) - .child( - div() - .bg(cx.theme().border) - .when(self.axis.is_horizontal(), |this| { - this.h_full().w(HANDLE_SIZE) - }) - .when(self.axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE)), - ) + window.with_element_state(id.unwrap(), |state, window| { + let state = state.unwrap_or(ResizeHandleState::default()); + + let bg_color = if state.is_active() { + cx.theme().drag_border + } else { + cx.theme().border + }; + + let mut el = div() + .id(self.id.clone()) + .occlude() + .absolute() + .flex_shrink_0() + .group("handle") + .when_some(self.on_drag.clone(), |this, on_drag| { + this.on_drag( + self.drag_value.clone().unwrap(), + move |_, position, window, cx| on_drag(&position, window, cx), + ) + }) + .map(|this| match self.placement { + Some(DockPlacement::Left) => { + // Special for Left Dock + // FIXME: Improve this to let the scroll bar have px(HANDLE_PADDING) + this.cursor_col_resize() + .top_0() + .right(px(1.)) + .h_full() + .w(HANDLE_SIZE) + .pl(HANDLE_PADDING) + } + _ => this + .when(axis.is_horizontal(), |this| { + this.cursor_col_resize() + .top_0() + .left(neg_offset) + .h_full() + .w(HANDLE_SIZE) + .px(HANDLE_PADDING) + }) + .when(axis.is_vertical(), |this| { + this.cursor_row_resize() + .top(neg_offset) + .left_0() + .w_full() + .h(HANDLE_SIZE) + .py(HANDLE_PADDING) + }), + }) + .child( + div() + .bg(bg_color) + .group_hover("handle", |this| this.bg(cx.theme().drag_border)) + .when(axis.is_horizontal(), |this| this.h_full().w(HANDLE_SIZE)) + .when(axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE)), + ) + .into_any_element(); + + let layout_id = el.request_layout(window, cx); + + ((layout_id, el), state) + }) + } + + fn prepaint( + &mut self, + _: Option<&GlobalElementId>, + _: gpui::Bounds, + request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Self::PrepaintState { + request_layout.prepaint(window, cx); + } + + fn paint( + &mut self, + id: Option<&GlobalElementId>, + bounds: gpui::Bounds, + request_layout: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + request_layout.paint(window, cx); + + window.with_element_state(id.unwrap(), |state: Option, window| { + let state = state.unwrap_or(ResizeHandleState::default()); + + window.on_mouse_event({ + let state = state.clone(); + move |ev: &MouseDownEvent, phase, window, _| { + if bounds.contains(&ev.position) && phase.bubble() { + state.set_active(true); + window.refresh(); + } + } + }); + + window.on_mouse_event({ + let state = state.clone(); + move |_: &MouseUpEvent, _, window, _| { + if state.is_active() { + state.set_active(false); + window.refresh(); + } + } + }); + + ((), state) + }); } }