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:
parent
ad5e480e63
commit
7b5efd19bb
5 changed files with 197 additions and 127 deletions
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue