gpui_component/resizable/
mod.rs

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