//! The resizable group: stacked panes with drag dividers, outside the dock. use gpui::prelude::FluentBuilder as _; use gpui::{ AnyElement, App, AppContext as _, Axis, Bounds, Context, ElementId, Entity, InteractiveElement as _, IntoElement, ParentElement as _, Pixels, RenderOnce, SharedString, StyleRefinement, Styled, Window, canvas, div, px, }; use gpui_native_utils::StyledExt as _; use crate::resize_handle::ResizeHandle; /// The size a pane starts at before the group has measured its extent. const DEFAULT_PANE_SIZE: f32 = 200.0; /// The floor a pane keeps while dragging unless the caller lowers it. const DEFAULT_PANE_MIN: f32 = 48.0; type DebugSelector = std::rc::Rc String>; /// The retained pane sizes of one resizable group. pub struct ResizableState { /// Sizes of every pane except the remainder pane, which fills what's left. sizes: Vec, /// The group's main-axis extent, captured at paint for drag clamping. measured: Option, } impl ResizableState { /// Creates state whose panes start at the default size. pub fn new(cx: &mut App) -> Entity { cx.new(|_| Self { sizes: Vec::new(), measured: None, }) } /// The stored size of each fixed pane; the remainder pane isn't listed. pub fn sizes(&self) -> &[Pixels] { &self.sizes } /// Restores pane sizes, e.g. from a persisted layout. pub fn set_sizes(&mut self, sizes: I, cx: &mut Context) where I: IntoIterator, { self.sizes = sizes.into_iter().collect(); cx.notify(); } /// Records the group's measured extent, notifying only when it changes. fn set_measured(&mut self, extent: Pixels, cx: &mut Context) { if self.measured != Some(extent) { self.measured = Some(extent); cx.notify(); } } /// The equal split of the measured extent. fn equal_split(&self, pane_count: usize) -> Pixels { match self.measured { Some(extent) => extent / pane_count as f32, None => px(DEFAULT_PANE_SIZE), } } /// The size pane `ix` renders at: its stored size, or the equal split. fn pane_size(&self, ix: usize, pane_count: usize) -> Pixels { self.sizes .get(ix) .copied() .unwrap_or_else(|| self.equal_split(pane_count)) } /// Applies a drag at divider `ix` (between pane ix and ix+1). fn resize(&mut self, ix: usize, delta: Pixels, mins: &[Pixels], cx: &mut Context) { let Some(&floor) = mins.get(ix) else { return; }; let pane_count = mins.len(); let mut size = self.pane_size(ix, pane_count) + delta; size = size.max(floor); if let Some(extent) = self.measured { let floors = mins .iter() .enumerate() .filter(|(j, _)| *j != ix) .fold(px(0.), |sum, (_, min)| sum + *min); let ceiling = extent - floors; size = size.min(ceiling); } while self.sizes.len() <= ix { self.sizes.push(px(DEFAULT_PANE_SIZE)); } self.sizes[ix] = size; cx.notify(); } /// Restores pane `ix` to the equal split, as a double-click does. fn reset_pane(&mut self, ix: usize, pane_count: usize, cx: &mut Context) { let split = self.equal_split(pane_count); while self.sizes.len() <= ix { self.sizes.push(px(DEFAULT_PANE_SIZE)); } self.sizes[ix] = split; cx.notify(); } } /// One pane of a resizable group: its content and its drag floor. pub struct ResizablePane { content: AnyElement, min: Pixels, } impl ResizablePane { /// The floor a pane keeps while dragging unless the caller lowers it. pub const DEFAULT_MIN: Pixels = px(DEFAULT_PANE_MIN); /// Wraps this element as a pane. pub fn new(content: E) -> Self where E: IntoElement, { Self { content: content.into_any_element(), min: Self::DEFAULT_MIN, } } /// Sets the floor this pane keeps while dragging. pub fn min(mut self, min: Pixels) -> Self { self.min = min; self } } /// A group of panes stacked along one axis, separated by drag dividers. #[derive(IntoElement)] pub struct Resizable { id: ElementId, state: Entity, axis: Axis, panes: Vec, debug_selector: Option, style: StyleRefinement, } impl Resizable { /// Creates a group stacking its panes vertically, bound to `state`. pub fn new(id: I, state: Entity) -> Self where I: Into, { Self { id: id.into(), state, axis: Axis::Vertical, panes: Vec::new(), debug_selector: None, style: StyleRefinement::default(), } } /// Sets the axis the panes stack along. pub fn axis(mut self, axis: Axis) -> Self { self.axis = axis; self } /// Adds a pane with the default drag floor. pub fn pane(mut self, content: E) -> Self where E: IntoElement, { self.panes.push(ResizablePane::new(content)); self } /// Adds a pane that can't be dragged below `min`. pub fn pane_with_min(mut self, content: E, min: Pixels) -> Self where E: IntoElement, { self.panes.push(ResizablePane::new(content).min(min)); self } /// Sets the debug-bounds selector of each pane, suffixed with its index. pub fn debug_selector(mut self, selector: F) -> Self where F: Fn() -> String + 'static, { self.debug_selector = Some(std::rc::Rc::new(selector)); self } } impl Styled for Resizable { fn style(&mut self) -> &mut StyleRefinement { &mut self.style } } impl RenderOnce for Resizable { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let axis = self.axis; let state = self.state.clone(); let pane_count = self.panes.len(); let mins: Vec = self.panes.iter().map(|pane| pane.min).collect(); let debug_selector = self.debug_selector; let sizes: Vec = (0..pane_count) .map(|ix| state.read(cx).pane_size(ix, pane_count)) .collect(); let mut group = div() .flex() .overflow_hidden() .when(axis == Axis::Vertical, |group| group.flex_col()) .when(axis == Axis::Horizontal, |group| group.flex_row()) .refine_style(&self.style); for (ix, pane) in self.panes.into_iter().enumerate() { let remainder = ix + 1 == pane_count; let content = pane.content; let mut pane_element = if remainder { div() .flex_1() .overflow_hidden() .when(axis == Axis::Vertical, |p| p.min_h(pane.min)) .when(axis == Axis::Horizontal, |p| p.min_w(pane.min)) } else { div() .flex_none() .overflow_hidden() .when(axis == Axis::Vertical, |p| p.h(sizes[ix]).w_full()) .when(axis == Axis::Horizontal, |p| p.w(sizes[ix]).h_full()) }; if let Some(selector) = &debug_selector { let prefix = selector(); pane_element = pane_element.debug_selector(move || format!("{prefix}-{ix}")); } group = group.child(pane_element.child(content)); if !remainder { let divider = ResizeHandle::new(SharedString::from(format!("{}-divider-{ix}", self.id))) .axis(axis) .on_resize({ let state = state.clone(); let mins = mins.clone(); move |delta, _, cx| { state.update(cx, |state, cx| state.resize(ix, delta, &mins, cx)); } }) .on_reset({ let state = state.clone(); let mins = mins.clone(); move |_, cx| { state.update(cx, |state, cx| state.reset_pane(ix, mins.len(), cx)); } }); group = group.child(divider); } } if pane_count > 0 { group = group.child( canvas( { let state = state.clone(); move |bounds: Bounds, _, cx: &mut App| { let extent = match axis { Axis::Vertical => bounds.size.height, Axis::Horizontal => bounds.size.width, }; state.update(cx, |state, cx| state.set_measured(extent, cx)); } }, |_, _, _, _| {}, ) .absolute() .inset_0(), ); } group } }