//! The drag divider: a thin handle that resizes the adjacent region. use std::rc::Rc; use gpui::prelude::FluentBuilder as _; use gpui::{ App, Axis, Bounds, CursorStyle, DispatchPhase, ElementId, HitboxBehavior, InteractiveElement as _, IntoElement, KeyDownEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Window, canvas, deferred, div, fill, point, px, }; use gpui_native_theme::{ActiveTheme as _, RESIZE_KEYBOARD_STEP, Size}; use gpui_native_utils::{focus_handle, focus_ring}; /// How far one keyboard arrow step moves the seam. const KEYBOARD_STEP: Pixels = RESIZE_KEYBOARD_STEP; type ResizeHandler = Rc; type ResetHandler = Rc; /// The pointer drag state, keyed to the handle's element id. #[derive(Clone, Copy)] struct DragState { dragging: bool, last: Point, } /// A thin drag divider that resizes the adjacent region. #[derive(IntoElement)] pub struct ResizeHandle { id: ElementId, /// The axis the drag moves along: a vertical seam drags horizontally. axis: Axis, disabled: bool, a11y_label: Option, on_resize: Option, on_reset: Option, } impl ResizeHandle { /// Creates a horizontal resize handle (a vertical seam). pub fn new(id: I) -> Self where I: Into, { Self { id: id.into(), axis: Axis::Horizontal, disabled: false, a11y_label: None, on_resize: None, on_reset: None, } } /// Sets the axis the drag moves along. pub fn axis(mut self, axis: Axis) -> Self { self.axis = axis; self } /// Renders the handle inert and dimmed. pub fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } /// Overrides the accessibility label, which defaults to "Resize". pub fn accessibility_label(mut self, label: L) -> Self where L: Into, { self.a11y_label = Some(label.into()); self } /// Runs for every pointer movement while the seam is dragged. pub fn on_resize(mut self, handler: F) -> Self where F: Fn(Pixels, &mut Window, &mut App) + 'static, { self.on_resize = Some(Rc::new(handler)); self } /// Runs when the seam is double-clicked, to restore the default size. pub fn on_reset(mut self, handler: F) -> Self where F: Fn(&mut Window, &mut App) + 'static, { self.on_reset = Some(Rc::new(handler)); self } } impl RenderOnce for ResizeHandle { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); let border = theme.colors.border; let ring = theme.colors.ring; let thickness = theme.sizes.resolve(Size::default()).dense_padding_x; // Half the handle overlaps each pane so the hairline lands on their shared boundary, // leaving the handle no layout footprint of its own. let straddle = -thickness / 2.0; let label = self .a11y_label .clone() .unwrap_or_else(|| SharedString::from("Resize")); let drag = window.with_element_namespace(self.id.clone(), |window| { window .use_keyed_state("resize-drag", cx, |_, _| DragState { dragging: false, last: point(px(0.), px(0.)), }) .clone() }); let focus = focus_handle(&self.id, window, cx); let dragging = drag.read(cx).dragging; let axis = self.axis; let disabled = self.disabled; let cursor = match axis { Axis::Horizontal => CursorStyle::ResizeLeftRight, Axis::Vertical => CursorStyle::ResizeUpDown, }; let on_reset = self.on_reset; deferred( div() .id(self.id.clone()) .debug_selector(|| "resize-handle".to_string()) .role(Role::Splitter) .aria_label(label) .flex_none() .when(axis == Axis::Horizontal, |handle| { handle.w(thickness).h_full().mx(straddle) }) .when(axis == Axis::Vertical, |handle| { handle.w_full().h(thickness).my(straddle) }) .when(self.disabled, |handle| handle.opacity(0.5)) .when(!self.disabled, |handle| { handle .track_focus(&focus) .focusable() .tab_stop(true) .focus_visible(|style| style.shadow(focus_ring(ring))) .on_mouse_down(MouseButton::Left, { let drag = drag.clone(); let focus = focus.clone(); move |event: &MouseDownEvent, window, cx| { window.focus(&focus, cx); cx.stop_propagation(); if event.click_count >= 2 { drag.update(cx, |drag, cx| { drag.dragging = false; cx.notify(); }); if let Some(on_reset) = on_reset.clone() { on_reset(window, cx); } return; } drag.update(cx, |drag, cx| { drag.dragging = true; drag.last = event.position; cx.notify(); }); } }) .on_key_down({ let on_resize = self.on_resize.clone(); move |event: &KeyDownEvent, window, cx| { let delta = match (axis, event.keystroke.key.as_str()) { (Axis::Horizontal, "left") => Some(-KEYBOARD_STEP), (Axis::Horizontal, "right") => Some(KEYBOARD_STEP), (Axis::Vertical, "up") => Some(-KEYBOARD_STEP), (Axis::Vertical, "down") => Some(KEYBOARD_STEP), _ => None, }; if let Some(delta) = delta { cx.stop_propagation(); if let Some(on_resize) = on_resize.clone() { on_resize(delta, window, cx); } } } }) }) .child( canvas( { move |bounds, window, _| { let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); (hitbox, bounds) } }, { let drag = drag.clone(); let on_resize = self.on_resize.clone(); move |_, (hitbox, bounds), window, _cx| { let hovered = hitbox.is_hovered(window); if !disabled { window.set_cursor_style(cursor, &hitbox); } // The hairline highlights while hovered or dragged. let line_color = if !disabled && (dragging || hovered) { ring } else { border }; let line = match axis { Axis::Horizontal => { let x = bounds.center().x - px(0.5); Bounds::from_corners( point(x, bounds.top()), point(x + px(1.), bounds.bottom()), ) } Axis::Vertical => { let y = bounds.center().y - px(0.5); Bounds::from_corners( point(bounds.left(), y), point(bounds.right(), y + px(1.)), ) } }; window.paint_quad(fill(line, line_color)); window.on_mouse_event({ let drag = drag.clone(); let on_resize = on_resize.clone(); move |event: &MouseMoveEvent, phase, window, cx| { if phase != DispatchPhase::Bubble { return; } let (dragging, last) = { let drag = drag.read(cx); (drag.dragging, drag.last) }; if !dragging { return; } if event.pressed_button != Some(MouseButton::Left) { drag.update(cx, |drag, cx| { drag.dragging = false; cx.notify(); }); return; } let delta = match axis { Axis::Horizontal => event.position.x - last.x, Axis::Vertical => event.position.y - last.y, }; drag.update(cx, |drag, _| drag.last = event.position); if let Some(on_resize) = on_resize.clone() { on_resize(delta, window, cx); } } }); window.on_mouse_event({ let drag = drag.clone(); move |event: &MouseUpEvent, phase, _, cx| { if phase != DispatchPhase::Bubble || event.button != MouseButton::Left { return; } if drag.read(cx).dragging { drag.update(cx, |drag, cx| { drag.dragging = false; cx.notify(); }); } } }); } }, ) .absolute() .inset_0(), ), ) } }