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

View file

@ -9,7 +9,7 @@ use crate::{
use super::{DockArea, Panel, PanelEvent, PanelView, TabPanel};
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,
VisualContext, WeakView,
};
@ -32,11 +32,11 @@ impl StackPanel {
parent: None,
focus_handle: cx.focus_handle(),
panels: SmallVec::new(),
panel_group: cx.new_view(|_| {
panel_group: cx.new_view(|cx| {
if axis == Axis::Horizontal {
h_resizable()
h_resizable(cx)
} else {
v_resizable()
v_resizable(cx)
}
}),
}
@ -125,7 +125,6 @@ impl StackPanel {
{
resizable_panel()
.content_view(panel.view())
.min_size(px(100.))
.when_some(size, |this, size| this.size(size))
}

View file

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

View file

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

View file

@ -1,9 +1,10 @@
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,
canvas, div, prelude::FluentBuilder, px, Along, AnyElement, AnyView, Axis, Bounds, Element,
EntityId, InteractiveElement as _, IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement,
Pixels, Render, Size, StatefulInteractiveElement, Style, Styled, View, ViewContext,
VisualContext as _, WindowContext,
};
use crate::{h_flex, theme::ActiveTheme, v_flex, AxisExt};
@ -20,18 +21,25 @@ pub struct ResizablePanelGroup {
axis: Axis,
handle_size: Pixels,
size: Pixels,
bounds: Bounds<Pixels>,
resizing_panel_ix: Option<usize>,
last_window_size: Size<Pixels>,
}
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 {
axis: Axis::Horizontal,
sizes: Vec::new(),
panels: Vec::new(),
handle_size: px(1.),
size: px(20.),
bounds: Bounds::default(),
resizing_panel_ix: None,
last_window_size: cx.bounds().size,
}
}
@ -121,10 +129,26 @@ impl ResizablePanelGroup {
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 {
let axis = self.axis;
let neg_offset = -HANDLE_PADDING + px(1.);
let view = cx.view().clone();
div()
.id(("resizable-handle", ix))
@ -157,98 +181,78 @@ impl ResizablePanelGroup {
this.w_full().h(self.handle_size)
}),
)
.on_drag_move(cx.listener(
move |view, e: &DragMoveEvent<DragPanel>, cx| match e.drag(cx) {
DragPanel((entity_id, ix, axis)) => {
if cx.entity_id() != *entity_id {
return;
}
let ix = *ix;
.on_drag(
DragPanel((cx.entity_id(), ix, axis)),
move |drag_panel, cx| {
cx.stop_propagation();
// Set current resizing panel ix
view.update(cx, |view, _| {
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);
}
}
}
});
cx.new_view(|_| drag_panel.clone())
},
))
.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;
}
for (i, panel) in self.panels.iter().enumerate() {
self.sizes[i] = panel.read(cx).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_panels(&mut self, ix: usize, size: Pixels, cx: &mut ViewContext<Self>) {
let mut ix = ix;
// Only resize the left panels.
if ix == self.panels.len() - 1 {
if ix >= self.panels.len() - 1 {
return;
}
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.
// 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.
self.sync_real_panel_sizes(cx);
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();
let mut changed = size - self.sizes[ix];
let is_expand = changed > px(0.);
// If change size is less than 1px, do nothing.
if changed_size > px(-1.0) && changed_size < px(1.0) {
return;
let main_ix = ix;
let mut new_sizes = self.sizes.clone();
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;
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;
// If total size exceeds container size, adjust the main panel
let total_size: Pixels = new_sizes.iter().map(|s| s.0).sum::<f32>().into();
if total_size > container_size {
let overflow = total_size - container_size;
new_sizes[main_ix] = (new_sizes[main_ix] - overflow).max(PANEL_MIN_SIZE);
}
self.sizes[ix] = new_size;
self.panels[ix].update(cx, |this, _| this.size = new_size);
self.sizes[ix + 1] = next_new_size;
self.sizes = new_sizes;
for (i, panel) in self.panels.iter_mut().enumerate() {
let size = self.sizes[i];
panel.update(cx, |this, _| this.size = size);
@ -258,6 +262,7 @@ impl ResizablePanelGroup {
impl Render for ResizablePanelGroup {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let view = cx.view().clone();
let container = if self.axis.is_horizontal() {
h_flex()
} else {
@ -285,13 +290,25 @@ impl Render for ResizablePanelGroup {
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 {
size: Pixels,
max_size: Option<Pixels>,
min_size: Option<Pixels>,
axis: Axis,
content_builder: Option<Rc<dyn Fn(&mut WindowContext) -> AnyElement>>,
content_view: Option<AnyView>,
@ -305,8 +322,6 @@ impl ResizablePanel {
Self {
size: px(20.),
axis: Axis::Horizontal,
max_size: None,
min_size: None,
content_builder: None,
content_view: None,
bounds: Bounds::default(),
@ -331,32 +346,6 @@ impl ResizablePanel {
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 {}
@ -364,7 +353,7 @@ impl FluentBuilder for ResizablePanel {}
impl Render for ResizablePanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let view = cx.view().clone();
let size = self.limit_size(self.size);
let size = self.size.max(PANEL_MIN_SIZE);
div()
.relative()
@ -386,3 +375,90 @@ impl Render for ResizablePanel {
.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);
}
}
})
}
}