use std::rc::Rc; use gpui::{ canvas, div, prelude::FluentBuilder, px, AnyElement, AnyView, Axis, Bounds, DragMoveEvent, EntityId, InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels, Render, StatefulInteractiveElement, Styled, View, ViewContext, VisualContext as _, WindowContext, }; use crate::{h_flex, theme::ActiveTheme, v_flex, AxisExt}; const HANDLE_PADDING: Pixels = px(4.); #[derive(Clone, Render)] pub struct DragPanel(pub (EntityId, usize, Axis)); #[derive(Clone)] pub struct ResizablePanelGroup { panels: Vec>, sizes: Vec, axis: Axis, handle_size: Pixels, size: Pixels, resizing_panel_ix: Option, } impl ResizablePanelGroup { pub(super) fn new() -> Self { Self { axis: Axis::Horizontal, sizes: Vec::new(), panels: Vec::new(), handle_size: px(1.), size: px(20.), resizing_panel_ix: None, } } /// Set the axis of the resizable panel group, default is horizontal. pub fn axis(mut self, axis: Axis) -> Self { self.axis = axis; self } pub(crate) fn set_axis(&mut self, axis: Axis, cx: &mut ViewContext) { self.axis = axis; cx.notify(); } /// Set the size of the resize handle, default is 3px. /// /// The handle size will inherit the parent group handle size, if you insert a group into another group. pub fn handle_size(mut self, size: Pixels) -> Self { self.handle_size = size; self } /// Add a resizable panel to the group. pub fn child(mut self, panel: ResizablePanel, cx: &mut WindowContext) -> Self { self.add_child(panel, cx); self } /// Add a ResizablePanelGroup as a child to the group. pub fn group(self, group: ResizablePanelGroup, cx: &mut WindowContext) -> Self { let mut group: ResizablePanelGroup = group; group.handle_size = self.handle_size; let size = group.size; let panel = ResizablePanel::new() .content_view(cx.new_view(|_| group).into()) .size(size); self.child(panel, cx) } /// Set size of the resizable panel group /// /// - When the axis is horizontal, the size is the height of the group. /// - When the axis is vertical, the size is the width of the group. pub fn size(mut self, size: Pixels) -> Self { self.size = size; self } pub fn add_child(&mut self, panel: ResizablePanel, cx: &mut WindowContext) { let mut panel = panel; panel.axis = self.axis; self.sizes.push(panel.size); self.panels.push(cx.new_view(|_| panel)); } pub fn insert_child(&mut self, panel: ResizablePanel, ix: usize, cx: &mut ViewContext) { let mut panel = panel; panel.axis = self.axis; self.sizes.insert(ix, panel.size); self.panels.insert(ix, cx.new_view(|_| panel)); cx.notify() } /// Replace a child panel with a new panel at the given index. pub(crate) fn replace_child( &mut self, panel: ResizablePanel, ix: usize, cx: &mut ViewContext, ) { let mut panel = panel; panel.axis = self.axis; self.sizes[ix] = panel.size; self.panels[ix] = cx.new_view(|_| panel); cx.notify() } pub fn remove_child(&mut self, ix: usize, cx: &mut ViewContext) { self.sizes.remove(ix); self.panels.remove(ix); cx.notify() } pub(crate) fn remove_all_children(&mut self, cx: &mut ViewContext) { self.sizes.clear(); self.panels.clear(); cx.notify() } fn render_resize_handle(&self, ix: usize, cx: &mut ViewContext) -> impl IntoElement { let axis = self.axis; let neg_offset = -HANDLE_PADDING + px(1.); div() .id(("resizable-handle", ix)) .occlude() .absolute() .flex_shrink_0() .when(self.axis.is_horizontal(), |this| { this.cursor_col_resize() .top_0() .right(neg_offset) .h_full() .w(px(1.)) .px(HANDLE_PADDING) }) .when(self.axis.is_vertical(), |this| { this.cursor_row_resize() .bottom(neg_offset) .left_0() .w_full() .h(px(1.)) .py(HANDLE_PADDING) }) .child( div() .bg(cx.theme().border) .when(self.axis.is_horizontal(), |this| { this.h_full().w(self.handle_size) }) .when(self.axis.is_vertical(), |this| { this.w_full().h(self.handle_size) }), ) .on_drag_move(cx.listener( move |view, e: &DragMoveEvent, cx| match e.drag(cx) { DragPanel((entity_id, ix, axis)) => { if cx.entity_id() != *entity_id { return; } let ix = *ix; view.resizing_panel_ix = Some(ix); let panel = view .panels .get(ix) .expect("BUG: invalid panel index") .read(cx); view.sync_real_panel_sizes(cx); match axis { Axis::Horizontal => { view.resize_panels(ix, e.event.position.x - panel.bounds.left(), cx) } Axis::Vertical => { view.resize_panels(ix, e.event.position.y - panel.bounds.top(), cx); } } } }, )) .on_mouse_up_out( MouseButton::Left, cx.listener(|view, _, _| { if view.resizing_panel_ix.is_none() { return; } view.resizing_panel_ix = None; }), ) .on_drag(DragPanel((cx.entity_id(), ix, axis)), |drag_panel, cx| { cx.stop_propagation(); cx.new_view(|_| drag_panel.clone()) }) } fn sync_real_panel_sizes(&mut self, cx: &WindowContext) { for (i, panel) in self.panels.iter_mut().enumerate() { if self.axis.is_horizontal() { self.sizes[i] = panel.read(cx).bounds.size.width; } else { self.sizes[i] = panel.read(cx).bounds.size.height; } } } /// The `ix`` is the index of the panel to resize, /// and the `size` is the new size for the panel. fn resize_panels(&mut self, ix: usize, size: Pixels, cx: &mut ViewContext) { // Only resize the left panels. if ix == self.panels.len() - 1 { return; } let size = size.floor(); // 1. The `size` is the new size for the `ix` offset panel will be. // 2. Limit `size` with the panel min and max size. // 3. Get the `ix` panel changed size. // 4. If the changed size is less than 1px, do nothing. // 5. Update the next panel size with it old size minus the changed size. // 6. When the old_size is small than the min_size, get the overflow size and then reduce other panels size with the overflow size. let old_size = self.sizes[ix]; let new_size = self.panels[ix].read(cx).limit_size(size); if new_size < size { return; } let changed_size = (new_size - old_size).floor(); // If change size is less than 1px, do nothing. if changed_size > px(-1.0) && changed_size < px(1.0) { return; } let next_size = self.sizes[ix + 1] - changed_size; let next_new_size = self.panels[ix + 1].read(cx).limit_size(next_size); let overflow_size = next_new_size - next_size; if overflow_size != px(0.) { return; } self.sizes[ix] = new_size; self.panels[ix].update(cx, |this, _| this.size = new_size); self.sizes[ix + 1] = next_new_size; for (i, panel) in self.panels.iter_mut().enumerate() { let size = self.sizes[i]; panel.update(cx, |this, _| this.size = size); } } } impl Render for ResizablePanelGroup { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let container = if self.axis.is_horizontal() { h_flex() } else { v_flex() }; container .size_full() .flex_grow() .flex_shrink() // .map(|this| { // use crate::StyledExt as _; // match self.axis { // Axis::Horizontal => this.debug_red(), // Axis::Vertical => this.debug_blue(), // } // }) .children(self.panels.iter().enumerate().map(|(ix, panel)| { if ix < self.panels.len() - 1 { let handle = self.render_resize_handle(ix, cx); panel.update(cx, |view, _| { view.resize_handle = Some(handle.into_any_element()) }); } panel.clone() })) } } pub struct ResizablePanel { size: Pixels, max_size: Option, min_size: Option, axis: Axis, content_builder: Option AnyElement>>, content_view: Option, /// The bounds of the resizable panel, when render the bounds will be updated. bounds: Bounds, resize_handle: Option, } impl ResizablePanel { pub(super) fn new() -> Self { Self { size: px(20.), axis: Axis::Horizontal, max_size: None, min_size: None, content_builder: None, content_view: None, bounds: Bounds::default(), resize_handle: None, } } pub fn content(mut self, content: F) -> Self where F: Fn(&mut WindowContext) -> AnyElement + 'static, { self.content_builder = Some(Rc::new(content)); self } pub fn content_view(mut self, content: AnyView) -> Self { self.content_view = Some(content); self } pub fn size(mut self, size: Pixels) -> Self { self.size = size; self } pub fn max_size(mut self, max_size: Pixels) -> Self { self.max_size = Some(max_size); self } pub fn min_size(mut self, min_size: Pixels) -> Self { self.min_size = Some(min_size); self } fn limit_size(&self, size: Pixels) -> Pixels { if let Some(max_size) = self.max_size { if size > max_size { return max_size; } } if let Some(min_size) = self.min_size { if size < min_size { return min_size; } } size } } impl FluentBuilder for ResizablePanel {} impl Render for ResizablePanel { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let view = cx.view().clone(); let size = self.limit_size(self.size); div() .relative() .size_full() .flex_grow() .flex_shrink() .when(self.axis.is_vertical(), |this| this.h(size)) .when(self.axis.is_horizontal(), |this| this.w(size)) .child({ canvas( move |bounds, cx| view.update(cx, |r, _| r.bounds = bounds), |_, _, _| {}, ) .absolute() .size_full() }) .when_some(self.content_builder.clone(), |this, c| this.child(c(cx))) .when_some(self.content_view.clone(), |this, c| this.child(c)) .when_some(self.resize_handle.take(), |this, c| this.child(c)) } }