use std::cell::Cell; use std::rc::Rc; use gpui::prelude::FluentBuilder as _; use gpui::{ div, px, AnyElement, App, Axis, Element, ElementId, Entity, GlobalElementId, InteractiveElement, IntoElement, MouseDownEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render, StatefulInteractiveElement, Styled as _, Window, }; use theme::ActiveTheme; use ui::AxisExt; use crate::dock::DockPlacement; pub(crate) const HANDLE_PADDING: Pixels = px(4.); pub(crate) const HANDLE_SIZE: Pixels = px(1.); /// Create a resize handle for a resizable panel. pub(crate) fn resize_handle( id: impl Into, axis: Axis, ) -> ResizeHandle { ResizeHandle::new(id, axis) } #[allow(clippy::type_complexity)] 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 } #[allow(dead_code)] pub(crate) fn placement(mut self, placement: DockPlacement) -> Self { self.placement = Some(placement); self } } #[derive(Default, Debug, Clone)] struct ResizeHandleState { active: Cell, } impl ResizeHandleState { fn set_active(&self, active: bool) { self.active.set(active); } fn is_active(&self) -> bool { self.active.get() } } impl IntoElement for ResizeHandle { type Element = ResizeHandle; fn into_element(self) -> Self::Element { self } } impl Element for ResizeHandle { type PrepaintState = (); type RequestLayoutState = AnyElement; fn id(&self) -> Option { Some(self.id.clone()) } fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { None } fn request_layout( &mut self, id: Option<&GlobalElementId>, _: Option<&gpui::InspectorElementId>, window: &mut Window, cx: &mut App, ) -> (gpui::LayoutId, Self::RequestLayoutState) { let neg_offset = -HANDLE_PADDING; let axis = self.axis; window.with_element_state(id.unwrap(), |state, window| { let state = state.unwrap_or(ResizeHandleState::default()); let bg_color = if state.is_active() { cx.theme().border_variant } 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(bg_color)) .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>, _: Option<&gpui::InspectorElementId>, _: 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>, _: Option<&gpui::InspectorElementId>, 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_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) }); } }