panel: Improve resize handle to show highlight color on dragging. (#622)

https://github.com/user-attachments/assets/aabbd73b-3c11-43e2-a4f2-121d46fba1d7
This commit is contained in:
Jason Lee 2025-02-13 16:07:00 +08:00 committed by GitHub
parent 437f1b2098
commit 83375147fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 206 additions and 102 deletions

View file

@ -1,18 +1,17 @@
//! Dock is a fixed container that places at left, bottom, right of the Windows. //! 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::{ use gpui::{
div, prelude::FluentBuilder as _, px, App, AppContext, Axis, Context, Element, Empty, Entity, div, prelude::FluentBuilder as _, px, App, AppContext, Axis, Context, Element, Empty, Entity,
InteractiveElement as _, IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render, Style,
Point, Render, StatefulInteractiveElement, Style, StyleRefinement, Styled as _, WeakEntity, StyleRefinement, Styled as _, WeakEntity, Window,
Window,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
resizable::{HANDLE_PADDING, HANDLE_SIZE, PANEL_MIN_SIZE}, resizable::{resize_handle, PANEL_MIN_SIZE},
ActiveTheme as _, AxisExt as _, StyledExt, StyledExt,
}; };
use super::{DockArea, DockItem, PanelView, TabPanel}; use super::{DockArea, DockItem, PanelView, TabPanel};
@ -278,51 +277,16 @@ impl Dock {
fn render_resize_handle(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render_resize_handle(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let axis = self.placement.axis(); let axis = self.placement.axis();
let neg_offset = -HANDLE_PADDING;
let view = cx.entity().clone(); let view = cx.entity().clone();
div() resize_handle("resize-handle", axis)
.id("resize-handle") .placement(self.placement)
.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)),
)
.on_drag(ResizePanel {}, move |info, _, _, cx| { .on_drag(ResizePanel {}, move |info, _, _, cx| {
cx.stop_propagation(); cx.stop_propagation();
view.update(cx, |view, _| { view.update(cx, |view, _| {
view.is_resizing = true; view.is_resizing = true;
}); });
cx.new(|_| info.clone()) cx.new(|_| info.deref().clone())
}) })
} }
fn resize(&mut self, mouse_position: Point<Pixels>, _: &mut Window, cx: &mut Context<Self>) { fn resize(&mut self, mouse_position: Point<Pixels>, _: &mut Window, cx: &mut Context<Self>) {

View file

@ -1,10 +1,9 @@
use std::rc::Rc; use std::{ops::Deref, rc::Rc};
use gpui::{ use gpui::{
canvas, div, prelude::FluentBuilder, px, relative, Along, AnyElement, AnyView, App, AppContext, canvas, div, prelude::FluentBuilder, px, relative, Along, AnyElement, AnyView, App, AppContext,
Axis, Bounds, Context, Element, Empty, Entity, EntityId, EventEmitter, IntoElement, IsZero, Axis, Bounds, Context, Element, Empty, Entity, EntityId, EventEmitter, IntoElement, IsZero,
MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Render, StatefulInteractiveElement as _, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Render, Style, Styled, WeakEntity, Window,
Style, Styled, WeakEntity, Window,
}; };
use crate::{h_flex, v_flex, AxisExt}; use crate::{h_flex, v_flex, AxisExt};
@ -174,7 +173,7 @@ impl ResizablePanelGroup {
view.update(cx, |view, _| { view.update(cx, |view, _| {
view.resizing_panel_ix = Some(ix); view.resizing_panel_ix = Some(ix);
}); });
cx.new(|_| drag_panel.clone()) cx.new(|_| drag_panel.deref().clone())
}, },
) )
} }

View file

@ -1,72 +1,213 @@
use std::{cell::RefCell, rc::Rc};
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, px, App, Axis, Div, ElementId, InteractiveElement, div, prelude::FluentBuilder as _, px, AnyElement, App, Axis, Element, ElementId, Entity,
IntoElement, ParentElement as _, Pixels, RenderOnce, Stateful, StatefulInteractiveElement, GlobalElementId, InteractiveElement, IntoElement, MouseDownEvent, MouseUpEvent,
Styled as _, Window, 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_PADDING: Pixels = px(4.);
pub(crate) const HANDLE_SIZE: Pixels = px(1.); pub(crate) const HANDLE_SIZE: Pixels = px(1.);
#[derive(IntoElement)]
pub(crate) struct ResizeHandle {
base: Stateful<Div>,
axis: Axis,
}
impl ResizeHandle {
fn new(id: impl Into<ElementId>, axis: Axis) -> Self {
Self {
base: div().id(id.into()),
axis,
}
}
}
/// Create a resize handle for a resizable panel. /// Create a resize handle for a resizable panel.
pub(crate) fn resize_handle(id: impl Into<ElementId>, axis: Axis) -> ResizeHandle { pub(crate) fn resize_handle<T: 'static, E: 'static + Render>(
id: impl Into<ElementId>,
axis: Axis,
) -> ResizeHandle<T, E> {
ResizeHandle::new(id, axis) ResizeHandle::new(id, axis)
} }
impl InteractiveElement for ResizeHandle { pub(crate) struct ResizeHandle<T: 'static, E: 'static + Render> {
fn interactivity(&mut self) -> &mut gpui::Interactivity { id: ElementId,
self.base.interactivity() axis: Axis,
drag_value: Option<Rc<T>>,
placement: Option<DockPlacement>,
on_drag: Option<Rc<dyn Fn(&Point<Pixels>, &mut Window, &mut App) -> Entity<E>>>,
}
impl<T: 'static, E: 'static + Render> ResizeHandle<T, E> {
fn new(id: impl Into<ElementId>, 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<T>, &Point<Pixels>, &mut Window, &mut App) -> Entity<E> + '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 { #[derive(Default, Debug, Clone)]
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { struct ResizeHandleState {
active: Rc<RefCell<bool>>,
}
impl ResizeHandleState {
fn set_active(&self, active: bool) {
*self.active.borrow_mut() = active;
}
fn is_active(&self) -> bool {
*self.active.borrow()
}
}
impl<T: 'static, E: 'static + Render> IntoElement for ResizeHandle<T, E> {
type Element = ResizeHandle<T, E>;
fn into_element(self) -> Self::Element {
self
}
}
impl<T: 'static, E: 'static + Render> Element for ResizeHandle<T, E> {
type RequestLayoutState = AnyElement;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
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 neg_offset = -HANDLE_PADDING;
let axis = self.axis;
self.base window.with_element_state(id.unwrap(), |state, window| {
.occlude() let state = state.unwrap_or(ResizeHandleState::default());
.absolute()
.flex_shrink_0() let bg_color = if state.is_active() {
.when(self.axis.is_horizontal(), |this| { cx.theme().drag_border
this.cursor_col_resize() } else {
.top_0() cx.theme().border
.left(neg_offset) };
.h_full()
.w(HANDLE_SIZE) let mut el = div()
.px(HANDLE_PADDING) .id(self.id.clone())
}) .occlude()
.when(self.axis.is_vertical(), |this| { .absolute()
this.cursor_row_resize() .flex_shrink_0()
.top(neg_offset) .group("handle")
.left_0() .when_some(self.on_drag.clone(), |this, on_drag| {
.w_full() this.on_drag(
.h(HANDLE_SIZE) self.drag_value.clone().unwrap(),
.py(HANDLE_PADDING) move |_, position, window, cx| on_drag(&position, window, cx),
}) )
.child( })
div() .map(|this| match self.placement {
.bg(cx.theme().border) Some(DockPlacement::Left) => {
.when(self.axis.is_horizontal(), |this| { // Special for Left Dock
this.h_full().w(HANDLE_SIZE) // FIXME: Improve this to let the scroll bar have px(HANDLE_PADDING)
}) this.cursor_col_resize()
.when(self.axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE)), .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<Pixels>,
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<Pixels>,
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<ResizeHandleState>, 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)
});
} }
} }