use std::ops::Range; use gpui::{ px, Along, App, Axis, Bounds, Context, ElementId, EventEmitter, IsZero, Pixels, Window, }; mod panel; mod resize_handle; pub use panel::*; pub(crate) use resize_handle::*; pub(crate) const PANEL_MIN_SIZE: Pixels = px(100.); /// Create a [`ResizablePanelGroup`] with horizontal resizing pub fn h_resizable(id: impl Into) -> ResizablePanelGroup { ResizablePanelGroup::new(id).axis(Axis::Horizontal) } /// Create a [`ResizablePanelGroup`] with vertical resizing pub fn v_resizable(id: impl Into) -> ResizablePanelGroup { ResizablePanelGroup::new(id).axis(Axis::Vertical) } /// Create a [`ResizablePanel`]. pub fn resizable_panel() -> ResizablePanel { ResizablePanel::new() } /// State for a [`ResizablePanel`] #[derive(Debug, Clone)] pub struct ResizableState { /// The `axis` will sync to actual axis of the ResizablePanelGroup in use. axis: Axis, panels: Vec, sizes: Vec, pub(crate) resizing_panel_ix: Option, bounds: Bounds, } impl Default for ResizableState { fn default() -> Self { Self { axis: Axis::Horizontal, panels: vec![], sizes: vec![], resizing_panel_ix: None, bounds: Bounds::default(), } } } impl ResizableState { /// Get the size of the panels. pub fn sizes(&self) -> &Vec { &self.sizes } pub(crate) fn insert_panel( &mut self, size: Option, ix: Option, cx: &mut Context, ) { let panel_state = ResizablePanelState { size, ..Default::default() }; let size = size.unwrap_or(PANEL_MIN_SIZE); // We make sure that the size always sums up to the container size // by reducing the size of all other panels first. let container_size = self.container_size().max(px(1.)); let total_leftover_size = (container_size - size).max(px(1.)); for (i, panel) in self.panels.iter_mut().enumerate() { let ratio = self.sizes[i] / container_size; self.sizes[i] = total_leftover_size * ratio; panel.size = Some(self.sizes[i]); } if let Some(ix) = ix { self.panels.insert(ix, panel_state); self.sizes.insert(ix, size); } else { self.panels.push(panel_state); self.sizes.push(size); }; cx.notify(); } pub(crate) fn sync_panels_count( &mut self, axis: Axis, panels_count: usize, cx: &mut Context, ) { let mut changed = self.axis != axis; self.axis = axis; if panels_count > self.panels.len() { let diff = panels_count - self.panels.len(); self.panels .extend(vec![ResizablePanelState::default(); diff]); self.sizes.extend(vec![PANEL_MIN_SIZE; diff]); changed = true; } if panels_count < self.panels.len() { self.panels.truncate(panels_count); self.sizes.truncate(panels_count); changed = true; } if changed { // We need to make sure the total size is in line with the container size. self.adjust_to_container_size(cx); } } pub(crate) fn update_panel_size( &mut self, panel_ix: usize, bounds: Bounds, size_range: Range, cx: &mut Context, ) { let size = bounds.size.along(self.axis); // This check is only necessary to stop the very first panel from resizing on its own // it needs to be passed when the panel is freshly created so we get the initial size, // but its also fine when it sometimes passes later. if self.sizes[panel_ix].to_f64() == PANEL_MIN_SIZE.to_f64() { self.sizes[panel_ix] = size; self.panels[panel_ix].size = Some(size); } self.panels[panel_ix].bounds = bounds; self.panels[panel_ix].size_range = size_range; cx.notify(); } pub(crate) fn remove_panel(&mut self, panel_ix: usize, cx: &mut Context) { self.panels.remove(panel_ix); self.sizes.remove(panel_ix); if let Some(resizing_panel_ix) = self.resizing_panel_ix { if resizing_panel_ix > panel_ix { self.resizing_panel_ix = Some(resizing_panel_ix - 1); } } self.adjust_to_container_size(cx); } pub(crate) fn replace_panel( &mut self, panel_ix: usize, panel: ResizablePanelState, cx: &mut Context, ) { let old_size = self.sizes[panel_ix]; self.panels[panel_ix] = panel; self.sizes[panel_ix] = old_size; self.adjust_to_container_size(cx); } pub(crate) fn clear(&mut self) { self.panels.clear(); self.sizes.clear(); } #[inline] pub(crate) fn container_size(&self) -> Pixels { self.bounds.size.along(self.axis) } pub(crate) fn done_resizing(&mut self, cx: &mut Context) { self.resizing_panel_ix = None; cx.emit(ResizablePanelEvent::Resized); } fn panel_size_range(&self, ix: usize) -> Range { let Some(panel) = self.panels.get(ix) else { return PANEL_MIN_SIZE..Pixels::MAX; }; panel.size_range.clone() } fn sync_real_panel_sizes(&mut self, _: &App) { for (i, panel) in self.panels.iter().enumerate() { self.sizes[i] = panel.bounds.size.along(self.axis); } } /// The `ix`` is the index of the panel to resize, /// and the `size` is the new size for the panel. fn resize_panel(&mut self, ix: usize, size: Pixels, _: &mut Window, cx: &mut Context) { let old_sizes = self.sizes.clone(); let mut ix = ix; // Only resize the left panels. if ix >= old_sizes.len() - 1 { return; } let container_size = self.container_size(); self.sync_real_panel_sizes(cx); let move_changed = size - old_sizes[ix]; if move_changed == px(0.) { return; } let size_range = self.panel_size_range(ix); let new_size = size.clamp(size_range.start, size_range.end); let is_expand = move_changed > px(0.); let main_ix = ix; let mut new_sizes = old_sizes.clone(); if is_expand { let mut changed = new_size - old_sizes[ix]; new_sizes[ix] = new_size; while changed > px(0.) && ix < old_sizes.len() - 1 { ix += 1; let size_range = self.panel_size_range(ix); let available_size = (new_sizes[ix] - size_range.start).max(px(0.)); let to_reduce = changed.min(available_size); new_sizes[ix] -= to_reduce; changed -= to_reduce; } } else { let mut changed = new_size - size; new_sizes[ix] = new_size; while changed > px(0.) && ix > 0 { ix -= 1; let size_range = self.panel_size_range(ix); let available_size = (new_sizes[ix] - size_range.start).max(px(0.)); let to_reduce = changed.min(available_size); changed -= to_reduce; new_sizes[ix] -= to_reduce; } new_sizes[main_ix + 1] += old_sizes[main_ix] - size - changed; } let total_size: Pixels = new_sizes.iter().map(|s| s.to_f64()).sum::().into(); // If total size exceeds container size, adjust the main panel if total_size > container_size { let overflow = total_size - container_size; new_sizes[main_ix] = (new_sizes[main_ix] - overflow).max(size_range.start); } for (i, _) in old_sizes.iter().enumerate() { let size = new_sizes[i]; self.panels[i].size = Some(size); } self.sizes = new_sizes; cx.notify(); } /// Adjust panel sizes according to the container size. /// /// When the container size changes, the panels should take up the same percentage as they did before. fn adjust_to_container_size(&mut self, cx: &mut Context) { if self.container_size().is_zero() { return; } let container_size = self.container_size(); let total_size = px(self.sizes.iter().map(f32::from).sum::()); for i in 0..self.panels.len() { let size = self.sizes[i]; let ratio = size / total_size; let new_size = container_size * ratio; self.sizes[i] = new_size; self.panels[i].size = Some(new_size); } cx.notify(); } } impl EventEmitter for ResizableState {} #[derive(Debug, Clone, Default)] pub(crate) struct ResizablePanelState { pub size: Option, pub size_range: Range, bounds: Bounds, }