Better Panel resize. (#178)

- Resize panels when windows is resize to smaller #175.

https://github.com/user-attachments/assets/b2c50949-7310-4d3d-8276-5bf15cf3ba9f
This commit is contained in:
Jason Lee 2024-08-27 18:38:37 +08:00 committed by GitHub
parent ad5e480e63
commit 7b5efd19bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 197 additions and 127 deletions

View file

@ -30,28 +30,25 @@ impl ResizableStory {
} }
let group1 = cx.new_view(|cx| { let group1 = cx.new_view(|cx| {
v_resizable() v_resizable(cx)
.group( .group(
h_resizable() h_resizable(cx)
.size(px(150.)) .size(px(150.))
.child( .child(
resizable_panel() resizable_panel()
.size(px(300.)) .size(px(300.))
.min_size(px(120.))
.content(|cx| panel_box("Left 1 (Min 120px)", cx)), .content(|cx| panel_box("Left 1 (Min 120px)", cx)),
cx, cx,
) )
.child( .child(
resizable_panel() resizable_panel()
.size(px(400.)) .size(px(400.))
.min_size(px(100.))
.content(|cx| panel_box("Center 1", cx)), .content(|cx| panel_box("Center 1", cx)),
cx, cx,
) )
.child( .child(
resizable_panel() resizable_panel()
.size(px(300.)) .size(px(300.))
.min_size(px(100.))
.content(|cx| panel_box("Right (Grow)", cx)), .content(|cx| panel_box("Right (Grow)", cx)),
cx, cx,
), ),
@ -60,34 +57,28 @@ impl ResizableStory {
.child( .child(
resizable_panel() resizable_panel()
.size(px(150.)) .size(px(150.))
.max_size(px(550.))
.min_size(px(100.))
.content(|cx| panel_box("Center (Grow)", cx)), .content(|cx| panel_box("Center (Grow)", cx)),
cx, cx,
) )
.child( .child(
resizable_panel() resizable_panel()
.size(px(210.)) .size(px(210.))
.min_size(px(100.))
.content(|cx| panel_box("Bottom", cx)), .content(|cx| panel_box("Bottom", cx)),
cx, cx,
) )
}); });
let group2 = cx.new_view(|cx| { let group2 = cx.new_view(|cx| {
h_resizable() h_resizable(cx)
.child( .child(
resizable_panel() resizable_panel()
.size(px(300.)) .size(px(300.))
.min_size(px(100.))
.content(|cx| panel_box("Left 2", cx)), .content(|cx| panel_box("Left 2", cx)),
cx, cx,
) )
.child( .child(
resizable_panel() resizable_panel()
.size(px(400.)) .size(px(400.))
.max_size(px(550.))
.min_size(px(100.))
.content(|cx| panel_box("Right (Grow)", cx)), .content(|cx| panel_box("Right (Grow)", cx)),
cx, cx,
) )

View file

@ -9,7 +9,7 @@ use crate::{
use super::{DockArea, Panel, PanelEvent, PanelView, TabPanel}; use super::{DockArea, Panel, PanelEvent, PanelView, TabPanel};
use gpui::{ use gpui::{
prelude::FluentBuilder as _, px, Axis, DismissEvent, Entity, EventEmitter, FocusHandle, prelude::FluentBuilder as _, Axis, DismissEvent, Entity, EventEmitter, FocusHandle,
FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, View, ViewContext, FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, View, ViewContext,
VisualContext, WeakView, VisualContext, WeakView,
}; };
@ -32,11 +32,11 @@ impl StackPanel {
parent: None, parent: None,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
panels: SmallVec::new(), panels: SmallVec::new(),
panel_group: cx.new_view(|_| { panel_group: cx.new_view(|cx| {
if axis == Axis::Horizontal { if axis == Axis::Horizontal {
h_resizable() h_resizable(cx)
} else { } else {
v_resizable() v_resizable(cx)
} }
}), }),
} }
@ -125,7 +125,6 @@ impl StackPanel {
{ {
resizable_panel() resizable_panel()
.content_view(panel.view()) .content_view(panel.view())
.min_size(px(100.))
.when_some(size, |this, size| this.size(size)) .when_some(size, |this, size| this.size(size))
} }

View file

@ -39,6 +39,7 @@ impl DragPanel {
impl Render for DragPanel { impl Render for DragPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div() div()
.id("drag-panel")
.cursor_grab() .cursor_grab()
.py_1() .py_1()
.px_3() .px_3()
@ -214,6 +215,8 @@ impl TabPanel {
.py_2() .py_2()
.px_3() .px_3()
.min_w_16() .min_w_16()
.overflow_hidden()
.whitespace_nowrap()
.child(panel.title(cx)) .child(panel.title(cx))
.on_drag( .on_drag(
DragPanel { DragPanel {
@ -237,6 +240,7 @@ impl TabPanel {
.children(self.panels.iter().enumerate().map(|(ix, panel)| { .children(self.panels.iter().enumerate().map(|(ix, panel)| {
let active = ix == self.active_ix; let active = ix == self.active_ix;
Tab::new(("tab", ix), panel.title(cx)) Tab::new(("tab", ix), panel.title(cx))
.py_2()
.selected(active) .selected(active)
.on_click(cx.listener(move |view, _, cx| { .on_click(cx.listener(move |view, _, cx| {
view.set_active_ix(ix, cx); view.set_active_ix(ix, cx);

View file

@ -1,14 +1,14 @@
use gpui::Axis; use gpui::{Axis, ViewContext};
mod panel; mod panel;
pub use panel::*; pub use panel::*;
pub fn h_resizable() -> ResizablePanelGroup { pub fn h_resizable(cx: &mut ViewContext<ResizablePanelGroup>) -> ResizablePanelGroup {
ResizablePanelGroup::new().axis(Axis::Horizontal) ResizablePanelGroup::new(cx).axis(Axis::Horizontal)
} }
pub fn v_resizable() -> ResizablePanelGroup { pub fn v_resizable(cx: &mut ViewContext<ResizablePanelGroup>) -> ResizablePanelGroup {
ResizablePanelGroup::new().axis(Axis::Vertical) ResizablePanelGroup::new(cx).axis(Axis::Vertical)
} }
pub fn resizable_panel() -> ResizablePanel { pub fn resizable_panel() -> ResizablePanel {

View file

@ -1,9 +1,10 @@
use std::rc::Rc; use std::rc::Rc;
use gpui::{ use gpui::{
canvas, div, prelude::FluentBuilder, px, AnyElement, AnyView, Axis, Bounds, DragMoveEvent, canvas, div, prelude::FluentBuilder, px, Along, AnyElement, AnyView, Axis, Bounds, Element,
EntityId, InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels, Render, EntityId, InteractiveElement as _, IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement,
StatefulInteractiveElement, Styled, View, ViewContext, VisualContext as _, WindowContext, Pixels, Render, Size, StatefulInteractiveElement, Style, Styled, View, ViewContext,
VisualContext as _, WindowContext,
}; };
use crate::{h_flex, theme::ActiveTheme, v_flex, AxisExt}; use crate::{h_flex, theme::ActiveTheme, v_flex, AxisExt};
@ -20,18 +21,25 @@ pub struct ResizablePanelGroup {
axis: Axis, axis: Axis,
handle_size: Pixels, handle_size: Pixels,
size: Pixels, size: Pixels,
bounds: Bounds<Pixels>,
resizing_panel_ix: Option<usize>, resizing_panel_ix: Option<usize>,
last_window_size: Size<Pixels>,
} }
impl ResizablePanelGroup { impl ResizablePanelGroup {
pub(super) fn new() -> Self { pub(super) fn new(cx: &mut ViewContext<Self>) -> Self {
// Observe the window size to update the bounds of the resizable panel group.
cx.observe_window_bounds(Self::on_window_resize).detach();
Self { Self {
axis: Axis::Horizontal, axis: Axis::Horizontal,
sizes: Vec::new(), sizes: Vec::new(),
panels: Vec::new(), panels: Vec::new(),
handle_size: px(1.), handle_size: px(1.),
size: px(20.), size: px(20.),
bounds: Bounds::default(),
resizing_panel_ix: None, resizing_panel_ix: None,
last_window_size: cx.bounds().size,
} }
} }
@ -121,10 +129,26 @@ impl ResizablePanelGroup {
cx.notify() cx.notify()
} }
fn on_window_resize(&mut self, cx: &mut ViewContext<Self>) {
let changed_size = cx.bounds().size - self.last_window_size;
self.last_window_size = cx.bounds().size;
let changed = changed_size.along(self.axis);
// Avg the change in size across all panels.
// The minimum size limited in ResizablePanel.
let avg_change = changed / self.panels.len() as f32;
for (ix, panel) in self.panels.iter().enumerate() {
self.sizes[ix] += avg_change;
panel.update(cx, |this, _| this.size = self.sizes[ix]);
}
cx.notify();
}
fn render_resize_handle(&self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render_resize_handle(&self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
let axis = self.axis; let axis = self.axis;
let neg_offset = -HANDLE_PADDING + px(1.); let neg_offset = -HANDLE_PADDING + px(1.);
let view = cx.view().clone();
div() div()
.id(("resizable-handle", ix)) .id(("resizable-handle", ix))
@ -157,98 +181,78 @@ impl ResizablePanelGroup {
this.w_full().h(self.handle_size) this.w_full().h(self.handle_size)
}), }),
) )
.on_drag_move(cx.listener( .on_drag(
move |view, e: &DragMoveEvent<DragPanel>, cx| match e.drag(cx) { DragPanel((cx.entity_id(), ix, axis)),
DragPanel((entity_id, ix, axis)) => { move |drag_panel, cx| {
if cx.entity_id() != *entity_id { cx.stop_propagation();
return; // Set current resizing panel ix
} view.update(cx, |view, _| {
let ix = *ix;
view.resizing_panel_ix = Some(ix); view.resizing_panel_ix = Some(ix);
let panel = view });
.panels cx.new_view(|_| drag_panel.clone())
.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) { fn sync_real_panel_sizes(&mut self, cx: &WindowContext) {
for (i, panel) in self.panels.iter_mut().enumerate() { for (i, panel) in self.panels.iter().enumerate() {
if self.axis.is_horizontal() { self.sizes[i] = panel.read(cx).bounds.size.along(self.axis)
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, /// The `ix`` is the index of the panel to resize,
/// and the `size` is the new size for the panel. /// and the `size` is the new size for the panel.
fn resize_panels(&mut self, ix: usize, size: Pixels, cx: &mut ViewContext<Self>) { fn resize_panels(&mut self, ix: usize, size: Pixels, cx: &mut ViewContext<Self>) {
let mut ix = ix;
// Only resize the left panels. // Only resize the left panels.
if ix == self.panels.len() - 1 { if ix >= self.panels.len() - 1 {
return; return;
} }
let size = size.floor(); let size = size.floor();
let container_size = self.bounds.size.along(self.axis);
// 1. The `size` is the new size for the `ix` offset panel will be. self.sync_real_panel_sizes(cx);
// 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 mut changed = size - self.sizes[ix];
let new_size = self.panels[ix].read(cx).limit_size(size); let is_expand = changed > px(0.);
if new_size < size {
return;
}
let changed_size = (new_size - old_size).floor();
// If change size is less than 1px, do nothing. let main_ix = ix;
if changed_size > px(-1.0) && changed_size < px(1.0) { let mut new_sizes = self.sizes.clone();
return;
if is_expand {
new_sizes[ix] = size;
// Now to expand logic is correct.
while changed > px(0.) && ix < self.panels.len() - 1 {
ix += 1;
let available_size = (new_sizes[ix] - PANEL_MIN_SIZE).max(px(0.));
let to_reduce = changed.min(available_size);
new_sizes[ix] -= to_reduce;
changed -= to_reduce;
}
} else {
let new_size = size.max(PANEL_MIN_SIZE);
new_sizes[ix] = new_size;
changed = size - PANEL_MIN_SIZE;
new_sizes[ix + 1] += self.sizes[ix] - new_size;
while changed < px(0.) && ix > 0 {
ix -= 1;
let available_size = self.sizes[ix] - PANEL_MIN_SIZE;
let to_increase = (changed).min(available_size);
new_sizes[ix] += to_increase;
changed += to_increase;
}
} }
let next_size = self.sizes[ix + 1] - changed_size; // If total size exceeds container size, adjust the main panel
let next_new_size = self.panels[ix + 1].read(cx).limit_size(next_size); let total_size: Pixels = new_sizes.iter().map(|s| s.0).sum::<f32>().into();
let overflow_size = next_new_size - next_size; if total_size > container_size {
if overflow_size != px(0.) { let overflow = total_size - container_size;
return; new_sizes[main_ix] = (new_sizes[main_ix] - overflow).max(PANEL_MIN_SIZE);
} }
self.sizes[ix] = new_size; self.sizes = new_sizes;
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() { for (i, panel) in self.panels.iter_mut().enumerate() {
let size = self.sizes[i]; let size = self.sizes[i];
panel.update(cx, |this, _| this.size = size); panel.update(cx, |this, _| this.size = size);
@ -258,6 +262,7 @@ impl ResizablePanelGroup {
impl Render for ResizablePanelGroup { impl Render for ResizablePanelGroup {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let view = cx.view().clone();
let container = if self.axis.is_horizontal() { let container = if self.axis.is_horizontal() {
h_flex() h_flex()
} else { } else {
@ -285,13 +290,25 @@ impl Render for ResizablePanelGroup {
panel.clone() panel.clone()
})) }))
.child({
canvas(
move |bounds, cx| view.update(cx, |r, _| r.bounds = bounds),
|_, _, _| {},
)
.absolute()
.size_full()
})
.child(ResizePanelGroupElement {
view: cx.view().clone(),
axis: self.axis,
})
} }
} }
const PANEL_MIN_SIZE: Pixels = px(100.);
pub struct ResizablePanel { pub struct ResizablePanel {
size: Pixels, size: Pixels,
max_size: Option<Pixels>,
min_size: Option<Pixels>,
axis: Axis, axis: Axis,
content_builder: Option<Rc<dyn Fn(&mut WindowContext) -> AnyElement>>, content_builder: Option<Rc<dyn Fn(&mut WindowContext) -> AnyElement>>,
content_view: Option<AnyView>, content_view: Option<AnyView>,
@ -305,8 +322,6 @@ impl ResizablePanel {
Self { Self {
size: px(20.), size: px(20.),
axis: Axis::Horizontal, axis: Axis::Horizontal,
max_size: None,
min_size: None,
content_builder: None, content_builder: None,
content_view: None, content_view: None,
bounds: Bounds::default(), bounds: Bounds::default(),
@ -331,32 +346,6 @@ impl ResizablePanel {
self.size = size; self.size = size;
self 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 FluentBuilder for ResizablePanel {}
@ -364,7 +353,7 @@ impl FluentBuilder for ResizablePanel {}
impl Render for ResizablePanel { impl Render for ResizablePanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let view = cx.view().clone(); let view = cx.view().clone();
let size = self.limit_size(self.size); let size = self.size.max(PANEL_MIN_SIZE);
div() div()
.relative() .relative()
@ -386,3 +375,90 @@ impl Render for ResizablePanel {
.when_some(self.resize_handle.take(), |this, c| this.child(c)) .when_some(self.resize_handle.take(), |this, c| this.child(c))
} }
} }
struct ResizePanelGroupElement {
axis: Axis,
view: View<ResizablePanelGroup>,
}
impl IntoElement for ResizePanelGroupElement {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for ResizePanelGroupElement {
type RequestLayoutState = ();
type PrepaintState = ();
fn id(&self) -> Option<gpui::ElementId> {
None
}
fn request_layout(
&mut self,
_: Option<&gpui::GlobalElementId>,
cx: &mut WindowContext,
) -> (gpui::LayoutId, Self::RequestLayoutState) {
(cx.request_layout(Style::default(), None), ())
}
fn prepaint(
&mut self,
_: Option<&gpui::GlobalElementId>,
_: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
_: &mut WindowContext,
) -> Self::PrepaintState {
()
}
fn paint(
&mut self,
_: Option<&gpui::GlobalElementId>,
_: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
cx: &mut WindowContext,
) {
cx.on_mouse_event({
let view = self.view.clone();
let axis = self.axis;
let current_ix = view.read(cx).resizing_panel_ix;
move |e: &MouseMoveEvent, phase, cx| {
if phase.bubble() {
if let Some(ix) = current_ix {
view.update(cx, |view, cx| {
let panel = view
.panels
.get(ix)
.expect("BUG: invalid panel index")
.read(cx);
match axis {
Axis::Horizontal => {
view.resize_panels(ix, e.position.x - panel.bounds.left(), cx)
}
Axis::Vertical => {
view.resize_panels(ix, e.position.y - panel.bounds.top(), cx);
}
}
})
}
}
}
});
// When any mouse up, stop dragging
cx.on_mouse_event({
let view = self.view.clone();
move |_: &MouseUpEvent, phase, cx| {
if phase.bubble() {
view.update(cx, |view, _| view.resizing_panel_ix = None);
}
}
})
}
}