gpui_component/resizable/
mod.rs

1use std::ops::Range;
2
3use gpui::{px, Along, App, Axis, Bounds, Context, ElementId, EventEmitter, Pixels, Window};
4
5use crate::PixelsExt;
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    pub(crate) fn insert_panel(
59        &mut self,
60        size: Option<Pixels>,
61        ix: Option<usize>,
62        cx: &mut Context<Self>,
63    ) {
64        let panel_state = ResizablePanelState {
65            size,
66            ..Default::default()
67        };
68
69        if let Some(ix) = ix {
70            self.panels.insert(ix, panel_state);
71            self.sizes.insert(ix, size.unwrap_or(PANEL_MIN_SIZE));
72        } else {
73            self.panels.push(panel_state);
74            self.sizes.push(size.unwrap_or(PANEL_MIN_SIZE));
75        };
76        cx.notify();
77    }
78
79    pub(crate) fn sync_panels_count(&mut self, axis: Axis, panels_count: usize) {
80        self.axis = axis;
81        if panels_count > self.panels.len() {
82            let diff = panels_count - self.panels.len();
83            self.panels
84                .extend(vec![ResizablePanelState::default(); diff]);
85            self.sizes.extend(vec![PANEL_MIN_SIZE; diff]);
86        }
87    }
88
89    pub(crate) fn update_panel_size(
90        &mut self,
91        panel_ix: usize,
92        bounds: Bounds<Pixels>,
93        size_range: Range<Pixels>,
94        cx: &mut Context<Self>,
95    ) {
96        let size = bounds.size.along(self.axis);
97        self.sizes[panel_ix] = size;
98        self.panels[panel_ix].size = Some(size);
99        self.panels[panel_ix].bounds = bounds;
100        self.panels[panel_ix].size_range = size_range;
101        cx.notify();
102    }
103
104    pub(crate) fn remove_panel(&mut self, panel_ix: usize, cx: &mut Context<Self>) {
105        self.panels.remove(panel_ix);
106        self.sizes.remove(panel_ix);
107        if let Some(resizing_panel_ix) = self.resizing_panel_ix {
108            if resizing_panel_ix > panel_ix {
109                self.resizing_panel_ix = Some(resizing_panel_ix - 1);
110            }
111        }
112        cx.notify();
113    }
114
115    pub(crate) fn replace_panel(
116        &mut self,
117        panel_ix: usize,
118        panel: ResizablePanelState,
119        cx: &mut Context<Self>,
120    ) {
121        let old_size = self.sizes[panel_ix];
122
123        self.panels[panel_ix] = panel;
124        self.sizes[panel_ix] = old_size;
125        cx.notify();
126    }
127
128    pub(crate) fn clear(&mut self) {
129        self.panels.clear();
130        self.sizes.clear();
131    }
132
133    pub(crate) fn total_size(&self) -> Pixels {
134        self.sizes.iter().map(|s| s.as_f32()).sum::<f32>().into()
135    }
136
137    pub(crate) fn done_resizing(&mut self, cx: &mut Context<Self>) {
138        self.resizing_panel_ix = None;
139        cx.emit(ResizablePanelEvent::Resized);
140    }
141
142    fn panel_size_range(&self, ix: usize) -> Range<Pixels> {
143        let Some(panel) = self.panels.get(ix) else {
144            return PANEL_MIN_SIZE..Pixels::MAX;
145        };
146
147        panel.size_range.clone()
148    }
149
150    fn sync_real_panel_sizes(&mut self, _: &App) {
151        for (i, panel) in self.panels.iter().enumerate() {
152            self.sizes[i] = panel.bounds.size.along(self.axis).floor();
153        }
154    }
155
156    /// The `ix`` is the index of the panel to resize,
157    /// and the `size` is the new size for the panel.
158    fn resize_panel(&mut self, ix: usize, size: Pixels, _: &mut Window, cx: &mut Context<Self>) {
159        let old_sizes = self.sizes.clone();
160
161        let mut ix = ix;
162        // Only resize the left panels.
163        if ix >= old_sizes.len() - 1 {
164            return;
165        }
166        let size = size.floor();
167        let container_size = self.bounds.size.along(self.axis);
168        self.sync_real_panel_sizes(cx);
169
170        let move_changed = size - old_sizes[ix];
171        if move_changed == px(0.) {
172            return;
173        }
174
175        let size_range = self.panel_size_range(ix);
176        let new_size = size.clamp(size_range.start, size_range.end);
177        let is_expand = move_changed > px(0.);
178
179        let main_ix = ix;
180        let mut new_sizes = old_sizes.clone();
181
182        if is_expand {
183            let mut changed = new_size - old_sizes[ix];
184            new_sizes[ix] = new_size;
185
186            while changed > px(0.) && ix < old_sizes.len() - 1 {
187                ix += 1;
188                let size_range = self.panel_size_range(ix);
189                let available_size = (new_sizes[ix] - size_range.start).max(px(0.));
190                let to_reduce = changed.min(available_size);
191                new_sizes[ix] -= to_reduce;
192                changed -= to_reduce;
193            }
194        } else {
195            let mut changed = new_size - size;
196            new_sizes[ix + 1] += old_sizes[ix] - new_size;
197            new_sizes[ix] = new_size;
198
199            while changed > px(0.) && ix > 0 {
200                ix -= 1;
201                let size_range = self.panel_size_range(ix);
202                let available_size = (new_sizes[ix] - size_range.start).max(px(0.));
203                let to_reduce = changed.min(available_size);
204                changed -= to_reduce;
205                new_sizes[ix] -= to_reduce;
206            }
207        }
208
209        // If total size exceeds container size, adjust the main panel
210        let total_size: Pixels = new_sizes.iter().map(|s| s.as_f32()).sum::<f32>().into();
211        if total_size > container_size {
212            let overflow = total_size - container_size;
213            new_sizes[main_ix] = (new_sizes[main_ix] - overflow).max(size_range.start);
214        }
215
216        for (i, _) in old_sizes.iter().enumerate() {
217            let size = new_sizes[i];
218            self.panels[i].size = Some(size);
219        }
220
221        self.sizes = new_sizes;
222        cx.notify();
223    }
224}
225
226impl EventEmitter<ResizablePanelEvent> for ResizableState {}
227
228#[derive(Debug, Clone, Default)]
229pub(crate) struct ResizablePanelState {
230    pub size: Option<Pixels>,
231    pub size_range: Range<Pixels>,
232    bounds: Bounds<Pixels>,
233}