Skip to main content

ui/components/resizable/
mod.rs

1//! [`ResizablePanel`]/[`ResizableHandle`]: the reusable divider-drag
2//! primitives shared by the standalone [`group::ResizablePanelGroup`] and by
3//! [`crate::PaneGroup`]'s recursive split rendering.
4//!
5//! Divider drag reuses the pointer-delta + clamp pattern from column resize
6//! (`redistributable_columns.rs` / `data_table.rs`).
7
8mod group;
9
10use gpui::{AnyElement, Axis, Div, Stateful};
11use smallvec::SmallVec;
12
13pub use group::{ResizablePanelGroup, ResizablePreview};
14
15use crate::prelude::*;
16
17/// A panel inside a [`ResizablePanelGroup`] (or, via [`Self::axis`], a
18/// [`crate::PaneGroup`] split).
19#[derive(IntoElement)]
20pub struct ResizablePanel {
21    children: SmallVec<[AnyElement; 2]>,
22    fraction: f32,
23    axis: Axis,
24}
25
26impl ResizablePanel {
27    /// Defaults to [`Axis::Horizontal`] — every existing caller
28    /// (`ResizablePanelGroup`) keeps its current row-of-panels behavior
29    /// unless it opts into [`Self::axis`].
30    pub fn new() -> Self {
31        Self {
32            children: SmallVec::new(),
33            fraction: 1.0,
34            axis: Axis::Horizontal,
35        }
36    }
37
38    pub(crate) fn fraction(mut self, fraction: f32) -> Self {
39        self.fraction = fraction;
40        self
41    }
42
43    /// Lays this panel out along `axis`: sized by width (`Horizontal`) or
44    /// height (`Vertical`).
45    pub fn axis(mut self, axis: Axis) -> Self {
46        self.axis = axis;
47        self
48    }
49}
50
51impl Default for ResizablePanel {
52    fn default() -> Self {
53        Self::new()
54    }
55}
56
57impl ParentElement for ResizablePanel {
58    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
59        self.children.extend(elements);
60    }
61}
62
63impl RenderOnce for ResizablePanel {
64    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
65        let is_horizontal = self.axis == Axis::Horizontal;
66        div()
67            .when(is_horizontal, |this| this.h_full())
68            .when(!is_horizontal, |this| this.w_full())
69            .overflow_hidden()
70            .when(self.fraction >= 1.0, |this| this.flex_grow())
71            .when(self.fraction < 1.0 && is_horizontal, |this| {
72                this.flex_shrink_0().w(relative(self.fraction))
73            })
74            .when(self.fraction < 1.0 && !is_horizontal, |this| {
75                this.flex_shrink_0().h(relative(self.fraction))
76            })
77            .bg(semantic::surface(cx))
78            .children(self.children)
79    }
80}
81
82/// Draggable divider between two [`ResizablePanel`]s: a wide invisible hit
83/// area (with the axis-appropriate resize cursor) centered on a thin
84/// visible line.
85#[derive(IntoElement)]
86pub struct ResizableHandle {
87    div: Stateful<Div>,
88    axis: Axis,
89}
90
91impl ResizableHandle {
92    /// Defaults to [`Axis::Horizontal`] (vertical line, `col-resize`
93    /// cursor) — call [`Self::axis`] for a horizontal divider.
94    pub fn new(id: impl Into<ElementId>) -> Self {
95        Self {
96            div: div().id(id),
97            axis: Axis::Horizontal,
98        }
99    }
100
101    /// Orients this divider along `axis`: `Horizontal` renders a vertical
102    /// line with a `col-resize` cursor (splits panels side by side);
103    /// `Vertical` renders a horizontal line with a `row-resize` cursor
104    /// (splits panels top/bottom).
105    pub fn axis(mut self, axis: Axis) -> Self {
106        self.axis = axis;
107        self
108    }
109}
110
111impl InteractiveElement for ResizableHandle {
112    fn interactivity(&mut self) -> &mut gpui::Interactivity {
113        self.div.interactivity()
114    }
115}
116
117impl StatefulInteractiveElement for ResizableHandle {}
118
119impl RenderOnce for ResizableHandle {
120    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
121        let is_horizontal = self.axis == Axis::Horizontal;
122        let line = div()
123            .when(is_horizontal, |this| this.w(px(1.)).h_full())
124            .when(!is_horizontal, |this| this.h(px(1.)).w_full())
125            .bg(semantic::border(cx));
126
127        // Flush panels: negative margins collapse the handle's net layout size
128        // to ~1px (the visible line) so adjacent panels sit edge-to-edge, while
129        // the handle itself stays a 7px grab target (resize on hover) centered
130        // on the seam.
131        self.div
132            .relative()
133            .flex_shrink_0()
134            .flex()
135            .items_center()
136            .justify_center()
137            .when(is_horizontal, |this| {
138                this.w(px(7.)).mx(px(-3.)).h_full().cursor_col_resize()
139            })
140            .when(!is_horizontal, |this| {
141                this.h(px(7.)).my(px(-3.)).w_full().cursor_row_resize()
142            })
143            .child(line)
144    }
145}