Skip to main content

rgpui_component/resizable/
mod.rs

1use std::ops::Range;
2
3use rgpui::{
4    Along, App, Axis, Bounds, Context, ElementId, EventEmitter, IsZero, Pixels, Window, px,
5};
6
7mod panel;
8mod resize_handle;
9pub use panel::*;
10pub(crate) use resize_handle::*;
11
12pub(crate) const PANEL_MIN_SIZE: Pixels = px(100.);
13
14/// Create a [`ResizablePanelGroup`] with horizontal resizing
15pub fn h_resizable(id: impl Into<ElementId>) -> ResizablePanelGroup {
16    ResizablePanelGroup::new(id).axis(Axis::Horizontal)
17}
18
19/// Create a [`ResizablePanelGroup`] with vertical resizing
20pub fn v_resizable(id: impl Into<ElementId>) -> ResizablePanelGroup {
21    ResizablePanelGroup::new(id).axis(Axis::Vertical)
22}
23
24/// Create a [`ResizablePanel`].
25pub fn resizable_panel() -> ResizablePanel {
26    ResizablePanel::new()
27}
28
29/// State for a [`ResizablePanel`]
30#[derive(Debug, Clone)]
31pub struct ResizableState {
32    /// The `axis` will sync to actual axis of the ResizablePanelGroup in use.
33    axis: Axis,
34    panels: Vec<ResizablePanelState>,
35    sizes: Vec<Pixels>,
36    pub(crate) resizing_panel_ix: Option<usize>,
37    bounds: Bounds<Pixels>,
38}
39
40impl Default for ResizableState {
41    fn default() -> Self {
42        Self {
43            axis: Axis::Horizontal,
44            panels: vec![],
45            sizes: vec![],
46            resizing_panel_ix: None,
47            bounds: Bounds::default(),
48        }
49    }
50}
51
52impl ResizableState {
53    /// Get the size of the panels.
54    pub fn sizes(&self) -> &Vec<Pixels> {
55        &self.sizes
56    }
57
58    /// Programmatically resize the panel at `ix` to `size`, redistributing
59    /// space among siblings using the same logic as a drag.
60    ///
61    /// Sizes are clamped to the panel's `size_range` and to the container.
62    /// Emits `ResizablePanelEvent::Resized` so subscribers (e.g. preference
63    /// persistence) see the change just as if the user had dragged a handle.
64    ///
65    /// Out-of-range indices are a no-op. For the last panel, space is taken
66    /// from the previous sibling (the last panel has no handle of its own).
67    pub fn resize_panel(
68        &mut self,
69        ix: usize,
70        size: Pixels,
71        window: &mut Window,
72        cx: &mut Context<Self>,
73    ) {
74        if ix >= self.sizes.len() {
75            return;
76        }
77        if ix + 1 < self.sizes.len() {
78            self.resize_panel_at_handle(ix, size, window, cx);
79        } else if ix > 0 {
80            // Last panel: drive its size by resizing the previous sibling so
81            // the freed space lands here.
82            let delta = self.sizes[ix] - size;
83            let prev = self.sizes[ix - 1];
84            self.resize_panel_at_handle(ix - 1, prev + delta, window, cx);
85        }
86        self.done_resizing(cx);
87    }
88
89    pub(crate) fn insert_panel(
90        &mut self,
91        size: Option<Pixels>,
92        ix: Option<usize>,
93        cx: &mut Context<Self>,
94    ) {
95        let panel_state = ResizablePanelState {
96            size,
97            ..Default::default()
98        };
99
100        let size = size.unwrap_or(PANEL_MIN_SIZE);
101
102        // We make sure that the size always sums up to the container size
103        // by reducing the size of all other panels first.
104        let container_size = self.container_size().max(px(1.));
105        let total_leftover_size = (container_size - size).max(px(1.));
106
107        for (i, panel) in self.panels.iter_mut().enumerate() {
108            let ratio = self.sizes[i] / container_size;
109            self.sizes[i] = total_leftover_size * ratio;
110            panel.size = Some(self.sizes[i]);
111        }
112
113        if let Some(ix) = ix {
114            self.panels.insert(ix, panel_state);
115            self.sizes.insert(ix, size);
116        } else {
117            self.panels.push(panel_state);
118            self.sizes.push(size);
119        };
120
121        cx.notify();
122    }
123
124    pub(crate) fn sync_panels_count(
125        &mut self,
126        axis: Axis,
127        panels_count: usize,
128        cx: &mut Context<Self>,
129    ) {
130        let mut changed = self.axis != axis;
131        self.axis = axis;
132
133        if panels_count > self.panels.len() {
134            let diff = panels_count - self.panels.len();
135            self.panels
136                .extend(vec![ResizablePanelState::default(); diff]);
137            self.sizes.extend(vec![PANEL_MIN_SIZE; diff]);
138            changed = true;
139        }
140
141        if panels_count < self.panels.len() {
142            self.panels.truncate(panels_count);
143            self.sizes.truncate(panels_count);
144            changed = true;
145        }
146
147        if changed {
148            // We need to make sure the total size is in line with the container size.
149            self.adjust_to_container_size(cx);
150        }
151    }
152
153    pub(crate) fn update_panel_size(
154        &mut self,
155        panel_ix: usize,
156        bounds: Bounds<Pixels>,
157        size_range: Range<Pixels>,
158        cx: &mut Context<Self>,
159    ) {
160        let size = bounds.size.along(self.axis);
161        // This check is only necessary to stop the very first panel from resizing on its own
162        // it needs to be passed when the panel is freshly created so we get the initial size,
163        // but its also fine when it sometimes passes later.
164        if self.sizes[panel_ix].as_f32() == PANEL_MIN_SIZE.as_f32() {
165            self.sizes[panel_ix] = size;
166            self.panels[panel_ix].size = Some(size);
167        }
168        self.panels[panel_ix].bounds = bounds;
169        self.panels[panel_ix].size_range = size_range;
170        cx.notify();
171    }
172
173    pub(crate) fn remove_panel(&mut self, panel_ix: usize, cx: &mut Context<Self>) {
174        self.panels.remove(panel_ix);
175        self.sizes.remove(panel_ix);
176        if let Some(resizing_panel_ix) = self.resizing_panel_ix {
177            if resizing_panel_ix > panel_ix {
178                self.resizing_panel_ix = Some(resizing_panel_ix - 1);
179            }
180        }
181        self.adjust_to_container_size(cx);
182    }
183
184    pub(crate) fn replace_panel(
185        &mut self,
186        panel_ix: usize,
187        panel: ResizablePanelState,
188        cx: &mut Context<Self>,
189    ) {
190        let old_size = self.sizes[panel_ix];
191
192        self.panels[panel_ix] = panel;
193        self.sizes[panel_ix] = old_size;
194        self.adjust_to_container_size(cx);
195    }
196
197    pub(crate) fn clear(&mut self) {
198        self.panels.clear();
199        self.sizes.clear();
200    }
201
202    #[inline]
203    pub(crate) fn container_size(&self) -> Pixels {
204        self.bounds.size.along(self.axis)
205    }
206
207    pub(crate) fn done_resizing(&mut self, cx: &mut Context<Self>) {
208        self.resizing_panel_ix = None;
209        cx.emit(ResizablePanelEvent::Resized);
210    }
211
212    fn panel_size_range(&self, ix: usize) -> Range<Pixels> {
213        let Some(panel) = self.panels.get(ix) else {
214            return PANEL_MIN_SIZE..Pixels::MAX;
215        };
216
217        panel.size_range.clone()
218    }
219
220    fn sync_real_panel_sizes(&mut self, _: &App) {
221        for (i, panel) in self.panels.iter().enumerate() {
222            self.sizes[i] = panel.bounds.size.along(self.axis);
223        }
224    }
225
226    /// Resize the panel at `ix` by treating `ix` as the drag-handle position
227    /// (the handle that sits between panel `ix` and panel `ix + 1`). Returns
228    /// early on the last panel since there is no handle below it.
229    ///
230    /// This is the worker behind drag interactions and the public
231    /// [`Self::resize_panel`] API.
232    fn resize_panel_at_handle(
233        &mut self,
234        ix: usize,
235        size: Pixels,
236        _: &mut Window,
237        cx: &mut Context<Self>,
238    ) {
239        let old_sizes = self.sizes.clone();
240
241        let mut ix = ix;
242        // Only resize the left panels.
243        if ix >= old_sizes.len() - 1 {
244            return;
245        }
246        let container_size = self.container_size();
247        self.sync_real_panel_sizes(cx);
248
249        let move_changed = size - old_sizes[ix];
250        if move_changed == px(0.) {
251            return;
252        }
253
254        let size_range = self.panel_size_range(ix);
255        let new_size = size.clamp(size_range.start, size_range.end);
256        let is_expand = move_changed > px(0.);
257
258        let main_ix = ix;
259        let mut new_sizes = old_sizes.clone();
260
261        if is_expand {
262            let mut changed = new_size - old_sizes[ix];
263            new_sizes[ix] = new_size;
264
265            while changed > px(0.) && ix < old_sizes.len() - 1 {
266                ix += 1;
267                let size_range = self.panel_size_range(ix);
268                let available_size = (new_sizes[ix] - size_range.start).max(px(0.));
269                let to_reduce = changed.min(available_size);
270                new_sizes[ix] -= to_reduce;
271                changed -= to_reduce;
272            }
273        } else {
274            let mut changed = new_size - size;
275            new_sizes[ix] = new_size;
276
277            while changed > px(0.) && ix > 0 {
278                ix -= 1;
279                let size_range = self.panel_size_range(ix);
280                let available_size = (new_sizes[ix] - size_range.start).max(px(0.));
281                let to_reduce = changed.min(available_size);
282                changed -= to_reduce;
283                new_sizes[ix] -= to_reduce;
284            }
285
286            new_sizes[main_ix + 1] += old_sizes[main_ix] - size - changed;
287        }
288
289        // If total size exceeds container size, adjust the main panel
290        let total_size: Pixels = new_sizes.iter().map(|s| s.as_f32()).sum::<f32>().into();
291        if total_size > container_size {
292            let overflow = total_size - container_size;
293            new_sizes[main_ix] = (new_sizes[main_ix] - overflow).max(size_range.start);
294        }
295
296        for (i, _) in old_sizes.iter().enumerate() {
297            let size = new_sizes[i];
298            self.panels[i].size = Some(size);
299        }
300        self.sizes = new_sizes;
301        cx.notify();
302    }
303
304    /// Adjust panel sizes according to the container size.
305    ///
306    /// When the container size changes, the panels should take up the same percentage as they did before.
307    fn adjust_to_container_size(&mut self, cx: &mut Context<Self>) {
308        if self.container_size().is_zero() {
309            return;
310        }
311
312        let container_size = self.container_size();
313        let total_size = px(self.sizes.iter().map(|s| s.as_f32()).sum::<f32>());
314
315        for i in 0..self.panels.len() {
316            let size = self.sizes[i];
317            let ratio = size / total_size;
318            let new_size = container_size * ratio;
319
320            self.sizes[i] = new_size;
321            self.panels[i].size = Some(new_size);
322        }
323        cx.notify();
324    }
325}
326
327impl EventEmitter<ResizablePanelEvent> for ResizableState {}
328
329#[derive(Debug, Clone, Default)]
330pub(crate) struct ResizablePanelState {
331    pub size: Option<Pixels>,
332    pub size_range: Range<Pixels>,
333    bounds: Bounds<Pixels>,
334}