gpui_component/resizable/
mod.rs

1use std::ops::Range;
2
3use gpui::{
4    px, Along, App, AppContext, Axis, Bounds, Context, ElementId, Entity, EventEmitter, Pixels,
5    Window,
6};
7
8use crate::PixelsExt;
9
10mod panel;
11mod resize_handle;
12pub use panel::*;
13pub(crate) use resize_handle::*;
14
15pub(crate) const PANEL_MIN_SIZE: Pixels = px(100.);
16
17/// Create a [`ResizablePanelGroup`] with horizontal resizing
18pub fn h_resizable(id: impl Into<ElementId>, state: Entity<ResizableState>) -> ResizablePanelGroup {
19    ResizablePanelGroup::new(id, state).axis(Axis::Horizontal)
20}
21
22/// Create a [`ResizablePanelGroup`] with vertical resizing
23pub fn v_resizable(id: impl Into<ElementId>, state: Entity<ResizableState>) -> ResizablePanelGroup {
24    ResizablePanelGroup::new(id, state).axis(Axis::Vertical)
25}
26
27/// Create a [`ResizablePanel`].
28pub fn resizable_panel() -> ResizablePanel {
29    ResizablePanel::new()
30}
31
32#[derive(Debug, Clone)]
33/// State for a [`ResizablePanel`]
34pub struct ResizableState {
35    /// The `axis` will sync to actual axis of the ResizablePanelGroup in use.
36    axis: Axis,
37    panels: Vec<ResizablePanelState>,
38    sizes: Vec<Pixels>,
39    pub(crate) resizing_panel_ix: Option<usize>,
40    bounds: Bounds<Pixels>,
41}
42
43impl ResizableState {
44    pub fn new(cx: &mut App) -> Entity<Self> {
45        cx.new(|_| Self {
46            axis: Axis::Horizontal,
47            panels: vec![],
48            sizes: vec![],
49            resizing_panel_ix: None,
50            bounds: Bounds::default(),
51        })
52    }
53
54    pub fn insert_panel(
55        &mut self,
56        size: Option<Pixels>,
57        ix: Option<usize>,
58        cx: &mut Context<Self>,
59    ) {
60        let panel_state = ResizablePanelState {
61            size,
62            ..Default::default()
63        };
64
65        if let Some(ix) = ix {
66            self.panels.insert(ix, panel_state);
67            self.sizes.insert(ix, size.unwrap_or(PANEL_MIN_SIZE));
68        } else {
69            self.panels.push(panel_state);
70            self.sizes.push(size.unwrap_or(PANEL_MIN_SIZE));
71        };
72        cx.notify();
73    }
74
75    pub(crate) fn sync_panels_count(&mut self, axis: Axis, panels_count: usize) {
76        self.axis = axis;
77        if panels_count > self.panels.len() {
78            let diff = panels_count - self.panels.len();
79            self.panels
80                .extend(vec![ResizablePanelState::default(); diff]);
81            self.sizes.extend(vec![PANEL_MIN_SIZE; diff]);
82        }
83    }
84
85    pub(crate) fn update_panel_size(
86        &mut self,
87        panel_ix: usize,
88        bounds: Bounds<Pixels>,
89        size_range: Range<Pixels>,
90        cx: &mut Context<Self>,
91    ) {
92        let size = bounds.size.along(self.axis);
93        self.sizes[panel_ix] = size;
94        self.panels[panel_ix].size = Some(size);
95        self.panels[panel_ix].bounds = bounds;
96        self.panels[panel_ix].size_range = size_range;
97        cx.notify();
98    }
99
100    pub(crate) fn remove_panel(&mut self, panel_ix: usize, cx: &mut Context<Self>) {
101        self.panels.remove(panel_ix);
102        self.sizes.remove(panel_ix);
103        if let Some(resizing_panel_ix) = self.resizing_panel_ix {
104            if resizing_panel_ix > panel_ix {
105                self.resizing_panel_ix = Some(resizing_panel_ix - 1);
106            }
107        }
108        cx.notify();
109    }
110
111    pub(crate) fn replace_panel(
112        &mut self,
113        panel_ix: usize,
114        panel: ResizablePanelState,
115        cx: &mut Context<Self>,
116    ) {
117        let old_size = self.sizes[panel_ix];
118
119        self.panels[panel_ix] = panel;
120        self.sizes[panel_ix] = old_size;
121        cx.notify();
122    }
123
124    pub(crate) fn clear(&mut self) {
125        self.panels.clear();
126        self.sizes.clear();
127    }
128
129    /// Get the size of the panels.
130    pub fn sizes(&self) -> &Vec<Pixels> {
131        &self.sizes
132    }
133
134    pub(crate) fn total_size(&self) -> Pixels {
135        self.sizes.iter().map(|s| s.as_f32()).sum::<f32>().into()
136    }
137
138    pub(crate) fn done_resizing(&mut self, cx: &mut Context<Self>) {
139        self.resizing_panel_ix = None;
140        cx.emit(ResizablePanelEvent::Resized);
141    }
142
143    fn panel_size_range(&self, ix: usize) -> Range<Pixels> {
144        let Some(panel) = self.panels.get(ix) else {
145            return PANEL_MIN_SIZE..Pixels::MAX;
146        };
147
148        panel.size_range.clone()
149    }
150
151    fn sync_real_panel_sizes(&mut self, _: &App) {
152        for (i, panel) in self.panels.iter().enumerate() {
153            self.sizes[i] = panel.bounds.size.along(self.axis).floor();
154        }
155    }
156
157    /// The `ix`` is the index of the panel to resize,
158    /// and the `size` is the new size for the panel.
159    fn resize_panel(&mut self, ix: usize, size: Pixels, _: &mut Window, cx: &mut Context<Self>) {
160        let old_sizes = self.sizes.clone();
161
162        let mut ix = ix;
163        // Only resize the left panels.
164        if ix >= old_sizes.len() - 1 {
165            return;
166        }
167        let size = size.floor();
168        let container_size = self.bounds.size.along(self.axis);
169        self.sync_real_panel_sizes(cx);
170
171        let move_changed = size - old_sizes[ix];
172        if move_changed == px(0.) {
173            return;
174        }
175
176        let size_range = self.panel_size_range(ix);
177        let new_size = size.clamp(size_range.start, size_range.end);
178        let is_expand = move_changed > px(0.);
179
180        let main_ix = ix;
181        let mut new_sizes = old_sizes.clone();
182
183        if is_expand {
184            let mut changed = new_size - old_sizes[ix];
185            new_sizes[ix] = new_size;
186
187            while changed > px(0.) && ix < old_sizes.len() - 1 {
188                ix += 1;
189                let size_range = self.panel_size_range(ix);
190                let available_size = (new_sizes[ix] - size_range.start).max(px(0.));
191                let to_reduce = changed.min(available_size);
192                new_sizes[ix] -= to_reduce;
193                changed -= to_reduce;
194            }
195        } else {
196            let mut changed = new_size - size;
197            new_sizes[ix + 1] += old_sizes[ix] - new_size;
198            new_sizes[ix] = new_size;
199
200            while changed > px(0.) && ix > 0 {
201                ix -= 1;
202                let size_range = self.panel_size_range(ix);
203                let available_size = (new_sizes[ix] - size_range.start).max(px(0.));
204                let to_reduce = changed.min(available_size);
205                changed -= to_reduce;
206                new_sizes[ix] -= to_reduce;
207            }
208        }
209
210        // If total size exceeds container size, adjust the main panel
211        let total_size: Pixels = new_sizes.iter().map(|s| s.as_f32()).sum::<f32>().into();
212        if total_size > container_size {
213            let overflow = total_size - container_size;
214            new_sizes[main_ix] = (new_sizes[main_ix] - overflow).max(size_range.start);
215        }
216
217        for (i, _) in old_sizes.iter().enumerate() {
218            let size = new_sizes[i];
219            self.panels[i].size = Some(size);
220        }
221
222        self.sizes = new_sizes;
223        cx.notify();
224    }
225}
226
227impl EventEmitter<ResizablePanelEvent> for ResizableState {}
228
229#[derive(Debug, Clone, Default)]
230pub(crate) struct ResizablePanelState {
231    pub size: Option<Pixels>,
232    pub size_range: Range<Pixels>,
233    bounds: Bounds<Pixels>,
234}