dock: Add left, bottom, right fixed dock and support toggle. (#291)

- Improve TabPanel to support control closable.

## Demo 


https://github.com/user-attachments/assets/dcf8cde3-61ee-42f6-be93-76f10fc8cf34
This commit is contained in:
Jason Lee 2024-10-02 17:42:52 +08:00 committed by GitHub
parent 20b9f9c0f2
commit 6f1be83b6e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 913 additions and 158 deletions

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-bottom-open"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 15h18"/><path d="m9 10 3-3 3 3"/></svg>

After

Width:  |  Height:  |  Size: 322 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-bottom"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 15h18"/></svg>

After

Width:  |  Height:  |  Size: 292 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-left-open"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="m14 9 3 3-3 3"/></svg>

After

Width:  |  Height:  |  Size: 319 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-left"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/></svg>

After

Width:  |  Height:  |  Size: 289 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-right-open"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/><path d="m10 15-3-3 3-3"/></svg>

After

Width:  |  Height:  |  Size: 322 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-right"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/></svg>

After

Width:  |  Height:  |  Size: 291 B

View file

@ -11,7 +11,7 @@ use story::{
use ui::{ use ui::{
button::{Button, ButtonStyled as _}, button::{Button, ButtonStyled as _},
color_picker::{ColorPicker, ColorPickerEvent}, color_picker::{ColorPicker, ColorPickerEvent},
dock::{DockArea, DockEvent, DockItem, DockItemState}, dock::{DockArea, DockEvent, DockItem, DockItemState, PanelView},
h_flex, h_flex,
popup_menu::PopupMenuExt, popup_menu::PopupMenuExt,
theme::{ActiveTheme, Colorize as _, Theme}, theme::{ActiveTheme, Colorize as _, Theme},
@ -61,7 +61,23 @@ impl StoryWorkspace {
} }
}; };
dock_area.update(cx, |view, cx| view.set_root(dock_item, cx)); let left_panels: Vec<Arc<dyn PanelView>> =
vec![Arc::new(StoryContainer::panel::<ListStory>(cx))];
let bottom_panels: Vec<Arc<dyn PanelView>> = vec![
Arc::new(StoryContainer::panel::<TextStory>(cx)),
Arc::new(StoryContainer::panel::<IconStory>(cx)),
];
let right_panels: Vec<Arc<dyn PanelView>> =
vec![Arc::new(StoryContainer::panel::<ImageStory>(cx))];
dock_area.update(cx, |view, cx| {
view.set_root(dock_item, cx);
view.set_left_dock(left_panels, Some(px(350.)), cx);
view.set_bottom_dock(bottom_panels, Some(px(200.)), cx);
view.set_right_dock(right_panels, Some(px(320.)), cx);
});
cx.subscribe(&dock_area, |this, dock_area, ev: &DockEvent, cx| match ev { cx.subscribe(&dock_area, |this, dock_area, ev: &DockEvent, cx| match ev {
DockEvent::LayoutChanged => this.save_layout(dock_area, cx), DockEvent::LayoutChanged => this.save_layout(dock_area, cx),
@ -173,7 +189,6 @@ impl StoryWorkspace {
Arc::new(StoryContainer::panel::<DropdownStory>(cx)), Arc::new(StoryContainer::panel::<DropdownStory>(cx)),
Arc::new(StoryContainer::panel::<ModalStory>(cx)), Arc::new(StoryContainer::panel::<ModalStory>(cx)),
Arc::new(StoryContainer::panel::<PopupStory>(cx)), Arc::new(StoryContainer::panel::<PopupStory>(cx)),
Arc::new(StoryContainer::panel::<ListStory>(cx)),
Arc::new(StoryContainer::panel::<SwitchStory>(cx)), Arc::new(StoryContainer::panel::<SwitchStory>(cx)),
Arc::new(StoryContainer::panel::<ProgressStory>(cx)), Arc::new(StoryContainer::panel::<ProgressStory>(cx)),
Arc::new(StoryContainer::panel::<TableStory>(cx)), Arc::new(StoryContainer::panel::<TableStory>(cx)),

View file

@ -38,8 +38,7 @@ pub use webview_story::WebViewStory;
use gpui::{ use gpui::{
actions, div, prelude::FluentBuilder as _, px, AnyElement, AnyView, AppContext, Div, actions, div, prelude::FluentBuilder as _, px, AnyElement, AnyView, AppContext, Div,
EventEmitter, FocusableView, Hsla, InteractiveElement, IntoElement, ParentElement, Render, EventEmitter, FocusableView, Hsla, InteractiveElement, IntoElement, ParentElement, Render,
SharedString, StatefulInteractiveElement, Styled as _, View, ViewContext, VisualContext, SharedString, Styled as _, View, ViewContext, VisualContext, WindowContext,
WindowContext,
}; };
use ui::{ use ui::{
@ -313,7 +312,6 @@ impl Render for StoryContainer {
v_flex() v_flex()
.id("story-container") .id("story-container")
.size_full() .size_full()
.overflow_scroll()
.track_focus(&self.focus_handle) .track_focus(&self.focus_handle)
.on_action(cx.listener(Self::on_action_panel_info)) .on_action(cx.listener(Self::on_action_panel_info))
.when(self.description.len() > 0, |this| { .when(self.description.len() > 0, |this| {
@ -328,14 +326,7 @@ impl Render for StoryContainer {
) )
}) })
.when_some(self.story.clone(), |this, story| { .when_some(self.story.clone(), |this, story| {
this.child( this.child(v_flex().id("story-children").size_full().p_4().child(story))
v_flex()
.id("story-children")
.overflow_scroll()
.size_full()
.p_4()
.child(story),
)
}) })
} }
} }

View file

@ -103,3 +103,11 @@ Dock:
en: Zoom Out en: Zoom Out
zh-CN: 缩小 zh-CN: 缩小
zh-HK: 縮小 zh-HK: 縮小
Collapse:
en: Collapse
zh-CN: 隐藏
zh-HK: 隱藏
Expand:
en: Expand
zh-CN: 展开
zh-HK: 展開

310
crates/ui/src/dock/dock.rs Normal file
View file

@ -0,0 +1,310 @@
//! Dock is a fixed container that places at left, bottom, right of the Windows.
use std::sync::Arc;
use gpui::{
div, prelude::FluentBuilder as _, px, Axis, Element, InteractiveElement as _, IntoElement,
MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render,
StatefulInteractiveElement, Style, Styled as _, View, ViewContext, VisualContext as _,
WeakView,
};
use crate::{
resizable::{HANDLE_PADDING, HANDLE_SIZE, PANEL_MIN_SIZE},
theme::ActiveTheme as _,
AxisExt as _, StyledExt,
};
use super::{DockArea, PanelView, TabPanel};
#[derive(Clone, Render)]
struct ResizePanel;
#[derive(Debug, Clone, Copy)]
pub enum DockPlacement {
Left,
Bottom,
Right,
}
impl DockPlacement {
fn axis(&self) -> Axis {
match self {
Self::Left | Self::Right => Axis::Horizontal,
Self::Bottom => Axis::Vertical,
}
}
pub fn is_left(&self) -> bool {
matches!(self, Self::Left)
}
pub fn is_bottom(&self) -> bool {
matches!(self, Self::Bottom)
}
pub fn is_right(&self) -> bool {
matches!(self, Self::Right)
}
}
/// The Dock is a fixed container that places at left, bottom, right of the Windows.
///
/// This is unlike Panel, it can't be move or add any other panel.
pub struct Dock {
placement: DockPlacement,
dock_area: WeakView<DockArea>,
pub(crate) panel: View<TabPanel>,
/// The size is means the width or height of the Dock, if the placement is left or right, the size is width, otherwise the size is height.
size: Pixels,
open: bool,
resizeable: bool,
is_resizing: bool,
}
impl Dock {
fn new(
dock_area: WeakView<DockArea>,
placement: DockPlacement,
cx: &mut ViewContext<Self>,
) -> Self {
let panel = cx.new_view(|cx| {
let mut tab = TabPanel::new(None, dock_area.clone(), cx);
tab.closeable = false;
tab.zoomable = false;
tab
});
Self {
placement,
dock_area,
panel,
open: true,
resizeable: true,
size: px(200.0),
is_resizing: false,
}
}
pub fn left(dock_area: WeakView<DockArea>, cx: &mut ViewContext<Self>) -> Self {
Self::new(dock_area, DockPlacement::Left, cx)
}
pub fn bottom(dock_area: WeakView<DockArea>, cx: &mut ViewContext<Self>) -> Self {
Self::new(dock_area, DockPlacement::Bottom, cx)
}
pub fn right(dock_area: WeakView<DockArea>, cx: &mut ViewContext<Self>) -> Self {
Self::new(dock_area, DockPlacement::Right, cx)
}
pub fn set_panels(&mut self, panels: Vec<Arc<dyn PanelView>>, cx: &mut ViewContext<Self>) {
self.panel.update(cx, |tab_panel, _| {
tab_panel.panels = panels;
tab_panel.active_ix = 0;
});
cx.notify();
}
/// Set the Dock to be resizeable, default: true
pub fn resizeable(mut self, resizeable: bool) -> Self {
self.resizeable = resizeable;
self
}
pub fn is_open(&self) -> bool {
self.open
}
pub fn toggle_open(&mut self, cx: &mut ViewContext<Self>) {
self.open = !self.open;
cx.notify();
}
/// Returns the size of the Dock, the size is means the width or height of
/// the Dock, if the placement is left or right, the size is width,
/// otherwise the size is height.
pub fn size(&self) -> Pixels {
self.size
}
/// Set the size of the Dock.
pub fn set_size(&mut self, size: Pixels, cx: &mut ViewContext<Self>) {
self.size = size.max(PANEL_MIN_SIZE);
cx.notify();
}
/// Set the open state of the Dock.
pub fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) {
self.open = open;
cx.notify();
}
fn render_resize_handle(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let axis = self.placement.axis();
let neg_offset = -HANDLE_PADDING;
let view = cx.view().clone();
div()
.id("resize-handle")
.occlude()
.absolute()
.flex_shrink_0()
.when(self.placement.is_left(), |this| {
// FIXME: Improve this to let the scroll bar have px(HANDLE_PADDING)
this.cursor_col_resize()
.top_0()
.right(px(1.))
.h_full()
.w(HANDLE_SIZE)
.pl(HANDLE_PADDING)
})
.when(self.placement.is_right(), |this| {
this.cursor_col_resize()
.top_0()
.left(neg_offset)
.h_full()
.w(HANDLE_SIZE)
.px(HANDLE_PADDING)
})
.when(self.placement.is_bottom(), |this| {
this.cursor_row_resize()
.top(neg_offset)
.left_0()
.w_full()
.h(HANDLE_SIZE)
.py(HANDLE_PADDING)
})
.child(
div()
.bg(cx.theme().border)
.when(axis.is_horizontal(), |this| this.h_full().w(HANDLE_SIZE))
.when(axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE)),
)
.on_drag(ResizePanel {}, move |info, cx| {
cx.stop_propagation();
view.update(cx, |view, _| {
view.is_resizing = true;
});
cx.new_view(|_| info.clone())
})
}
fn resize(&mut self, mouse_position: Point<Pixels>, cx: &mut ViewContext<Self>) {
if !self.is_resizing {
return;
}
let area_bounds = self
.dock_area
.upgrade()
.expect("DockArea is missing")
.read(cx)
.bounds;
let size = match self.placement {
DockPlacement::Left => mouse_position.x - area_bounds.left(),
DockPlacement::Right => area_bounds.right() - mouse_position.x,
DockPlacement::Bottom => area_bounds.bottom() - mouse_position.y,
};
self.size = size.max(PANEL_MIN_SIZE);
cx.notify();
}
fn done_resizing(&mut self, _: &mut ViewContext<Self>) {
self.is_resizing = false;
}
}
impl Render for Dock {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl gpui::IntoElement {
if !self.open && !self.placement.is_bottom() {
return div();
}
div()
.relative()
.overflow_hidden()
.map(|this| match self.placement {
DockPlacement::Left | DockPlacement::Right => this.h_flex().h_full().w(self.size),
DockPlacement::Bottom => this.w_full().h(self.size),
})
// Bottom Dock should keep the title bar, then user can click the Toggle button
.when(!self.open && self.placement.is_bottom(), |this| {
this.h(px(30.))
})
.child(self.panel.clone())
.child(self.render_resize_handle(cx))
.child(DockElement {
view: cx.view().clone(),
})
}
}
struct DockElement {
view: View<Dock>,
}
impl IntoElement for DockElement {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for DockElement {
type RequestLayoutState = ();
type PrepaintState = ();
fn id(&self) -> Option<gpui::ElementId> {
None
}
fn request_layout(
&mut self,
_: Option<&gpui::GlobalElementId>,
cx: &mut gpui::WindowContext,
) -> (gpui::LayoutId, Self::RequestLayoutState) {
(cx.request_layout(Style::default(), None), ())
}
fn prepaint(
&mut self,
_: Option<&gpui::GlobalElementId>,
_: gpui::Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
_: &mut gpui::WindowContext,
) -> Self::PrepaintState {
()
}
fn paint(
&mut self,
_: Option<&gpui::GlobalElementId>,
_: gpui::Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
cx: &mut gpui::WindowContext,
) {
cx.on_mouse_event({
let view = self.view.clone();
move |e: &MouseMoveEvent, phase, cx| {
if phase.bubble() {
view.update(cx, |view, cx| view.resize(e.position, 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, cx| view.done_resizing(cx));
}
}
})
}
}

View file

@ -1,3 +1,4 @@
mod dock;
mod invalid_panel; mod invalid_panel;
mod panel; mod panel;
mod stack_panel; mod stack_panel;
@ -5,10 +6,11 @@ mod tab_panel;
use std::sync::Arc; use std::sync::Arc;
pub use dock::*;
use gpui::{ use gpui::{
actions, div, prelude::FluentBuilder, AnyElement, AnyView, AppContext, Axis, EventEmitter, actions, canvas, div, prelude::FluentBuilder, AnyElement, AnyView, AppContext, Axis, Bounds,
InteractiveElement as _, IntoElement, ParentElement as _, Pixels, Render, SharedString, Styled, EventEmitter, InteractiveElement as _, IntoElement, ParentElement as _, Pixels, Render,
View, ViewContext, VisualContext, WeakView, WindowContext, SharedString, Styled, View, ViewContext, VisualContext, WeakView, WindowContext,
}; };
pub use panel::*; pub use panel::*;
pub use stack_panel::*; pub use stack_panel::*;
@ -31,7 +33,18 @@ pub enum DockEvent {
/// The main area of the dock. /// The main area of the dock.
pub struct DockArea { pub struct DockArea {
id: SharedString, id: SharedString,
pub(crate) bounds: Bounds<Pixels>,
/// The center view of the dockarea.
items: DockItem, items: DockItem,
/// The left dock of the dockarea.
left_dock: Option<View<Dock>>,
/// The bottom dock of the dockarea.
bottom_dock: Option<View<Dock>>,
/// The right dock of the dockarea.
right_dock: Option<View<Dock>>,
/// The top zoom view of the dockarea, if any.
zoom_view: Option<AnyView>, zoom_view: Option<AnyView>,
} }
@ -166,7 +179,7 @@ impl DockItem {
} }
impl DockArea { impl DockArea {
pub fn new(id: impl Into<SharedString>, cx: &mut WindowContext) -> Self { pub fn new(id: impl Into<SharedString>, cx: &mut ViewContext<Self>) -> Self {
let stack_panel = cx.new_view(|cx| StackPanel::new(Axis::Horizontal, cx)); let stack_panel = cx.new_view(|cx| StackPanel::new(Axis::Horizontal, cx));
let dock_item = DockItem::Split { let dock_item = DockItem::Split {
axis: Axis::Horizontal, axis: Axis::Horizontal,
@ -177,13 +190,18 @@ impl DockArea {
Self { Self {
id: id.into(), id: id.into(),
bounds: Bounds::default(),
items: dock_item, items: dock_item,
zoom_view: None, zoom_view: None,
left_dock: None,
right_dock: None,
bottom_dock: None,
} }
} }
/// The the DockItem as the root of the dock area. /// The the DockItem as the root of the dock area.
#[must_use] ///
/// This is used to render at the Center of the DockArea.
pub fn set_root(&mut self, item: DockItem, cx: &mut ViewContext<Self>) { pub fn set_root(&mut self, item: DockItem, cx: &mut ViewContext<Self>) {
self.subscribe_item(&item, cx); self.subscribe_item(&item, cx);
self.items = item; self.items = item;
@ -191,6 +209,90 @@ impl DockArea {
cx.notify(); cx.notify();
} }
pub fn set_left_dock(
&mut self,
panels: Vec<Arc<dyn PanelView>>,
size: Option<Pixels>,
cx: &mut ViewContext<Self>,
) {
let weak_self = cx.view().downgrade();
self.left_dock = Some(cx.new_view(|cx| {
let mut dock = Dock::left(weak_self.clone(), cx);
if let Some(size) = size {
dock.set_size(size, cx);
}
dock.set_panels(panels, cx);
dock
}))
}
pub fn set_bottom_dock(
&mut self,
panels: Vec<Arc<dyn PanelView>>,
size: Option<Pixels>,
cx: &mut ViewContext<Self>,
) {
let weak_self = cx.view().downgrade();
self.bottom_dock = Some(cx.new_view(|cx| {
let mut dock = Dock::bottom(weak_self.clone(), cx);
if let Some(size) = size {
dock.set_size(size, cx);
}
dock.set_panels(panels, cx);
dock
}))
}
pub fn set_right_dock(
&mut self,
panels: Vec<Arc<dyn PanelView>>,
size: Option<Pixels>,
cx: &mut ViewContext<Self>,
) {
let weak_self = cx.view().downgrade();
self.right_dock = Some(cx.new_view(|cx| {
let mut dock = Dock::right(weak_self.clone(), cx);
if let Some(size) = size {
dock.set_size(size, cx);
}
dock.set_panels(panels, cx);
dock
}))
}
pub fn is_dock_open(&self, placement: DockPlacement, cx: &AppContext) -> bool {
match placement {
DockPlacement::Left => self
.left_dock
.as_ref()
.and_then(|dock| Some(dock.read(cx).is_open()))
.unwrap_or(false),
DockPlacement::Bottom => self
.bottom_dock
.as_ref()
.and_then(|dock| Some(dock.read(cx).is_open()))
.unwrap_or(false),
DockPlacement::Right => self
.right_dock
.as_ref()
.and_then(|dock| Some(dock.read(cx).is_open()))
.unwrap_or(false),
}
}
pub fn toggle_dock(&self, placement: DockPlacement, cx: &mut ViewContext<Self>) {
let dock = match placement {
DockPlacement::Left => &self.left_dock,
DockPlacement::Bottom => &self.bottom_dock,
DockPlacement::Right => &self.right_dock,
};
if let Some(dock) = dock {
dock.update(cx, |view, cx| {
view.toggle_open(cx);
})
}
}
/// Dump the dock panels layout to DockItemState. /// Dump the dock panels layout to DockItemState.
/// ///
/// See also `DockItemState::to_item` for the load DockItem from DockItemState. /// See also `DockItemState::to_item` for the load DockItem from DockItemState.
@ -275,16 +377,58 @@ impl DockArea {
impl EventEmitter<DockEvent> for DockArea {} impl EventEmitter<DockEvent> for DockArea {}
impl Render for DockArea { impl Render for DockArea {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
// println!("Rendering dock area"); let view = cx.view().clone();
div() div()
.id("dock-area") .id("dock-area")
.relative()
.size_full() .size_full()
.overflow_hidden() .overflow_hidden()
.child(
canvas(
move |bounds, cx| view.update(cx, |r, _| r.bounds = bounds),
|_, _, _| {},
)
.absolute()
.size_full(),
)
.map(|this| { .map(|this| {
if let Some(zoom_view) = self.zoom_view.clone() { if let Some(zoom_view) = self.zoom_view.clone() {
this.child(zoom_view) this.child(zoom_view)
} else { } else {
this.child(self.render_items(cx)) this.child(
div()
.flex()
.flex_row()
.h_full()
// Left dock
.when_some(self.left_dock.clone(), |this, dock| {
this.child(div().flex().flex_none().child(dock))
})
// Center
.child(
div()
.flex()
.flex_1()
.flex_col()
.overflow_hidden()
// Top center
.child(
div()
.flex_1()
.overflow_hidden()
.child(self.render_items(cx)),
)
// Bottom Dock
.when_some(self.bottom_dock.clone(), |this, dock| {
this.child(dock)
}),
)
// Right Dock
.when_some(self.right_dock.clone(), |this, dock| {
this.child(div().flex().flex_none().child(dock))
}),
)
} }
}) })
} }

View file

@ -8,12 +8,12 @@ use crate::{
ResizablePanelGroup, ResizablePanelGroup,
}, },
theme::ActiveTheme, theme::ActiveTheme,
Placement, AxisExt, Placement,
}; };
use super::{DockArea, DockItemState, Panel, PanelEvent, PanelView, TabPanel}; use super::{DockArea, DockItemState, Panel, PanelEvent, PanelView, TabPanel};
use gpui::{ use gpui::{
prelude::FluentBuilder as _, AppContext, Axis, DismissEvent, EventEmitter, FocusHandle, prelude::FluentBuilder as _, AppContext, 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,
}; };
@ -277,6 +277,39 @@ impl StackPanel {
.update(cx, |view, cx| view.set_axis(axis, cx)); .update(cx, |view, cx| view.set_axis(axis, cx));
cx.notify(); cx.notify();
} }
/// Check if the given panel is at the first top left in the stack.
pub(super) fn is_top_left_panel(&self, panel: View<TabPanel>, cx: &AppContext) -> bool {
let first_panel = self.panels.first();
if let Some(view) = first_panel {
if let Ok(view) = view.view().downcast::<TabPanel>() {
return view.entity_id() == panel.entity_id();
} else if let Ok(view) = view.view().downcast::<Self>() {
return view.read(cx).is_top_left_panel(panel, cx);
}
}
false
}
/// Check if the given panel is at the first top right in the stack.
pub(super) fn is_top_right_panel(&self, panel: View<TabPanel>, cx: &AppContext) -> bool {
let first_panel = if self.axis.is_vertical() {
self.panels.first()
} else {
self.panels.last()
};
if let Some(view) = first_panel {
if let Ok(view) = view.view().downcast::<TabPanel>() {
return view.entity_id() == panel.entity_id();
} else if let Ok(view) = view.view().downcast::<Self>() {
return view.read(cx).is_top_right_panel(panel, cx);
}
}
false
}
} }
impl FocusableView for StackPanel { impl FocusableView for StackPanel {

View file

@ -1,10 +1,11 @@
use std::sync::Arc; use std::sync::Arc;
use gpui::{ use gpui::{
div, prelude::FluentBuilder, rems, AnchorCorner, AppContext, DefiniteLength, DismissEvent, div, prelude::FluentBuilder, px, rems, AnchorCorner, AppContext, DefiniteLength, DismissEvent,
DragMoveEvent, Empty, EventEmitter, FocusHandle, FocusableView, InteractiveElement as _, DragMoveEvent, Empty, Entity, EventEmitter, FocusHandle, FocusableView,
IntoElement, ParentElement, Pixels, Render, ScrollHandle, StatefulInteractiveElement, Styled, InteractiveElement as _, IntoElement, ParentElement, Pixels, Render, ScrollHandle,
View, ViewContext, VisualContext as _, WeakView, WindowContext, SharedString, StatefulInteractiveElement, Styled, View, ViewContext, VisualContext as _,
WeakView, WindowContext,
}; };
use rust_i18n::t; use rust_i18n::t;
@ -15,11 +16,12 @@ use crate::{
popup_menu::{PopupMenu, PopupMenuExt}, popup_menu::{PopupMenu, PopupMenuExt},
tab::{Tab, TabBar}, tab::{Tab, TabBar},
theme::ActiveTheme, theme::ActiveTheme,
v_flex, AxisExt, IconName, Placement, Selectable, Sizable, v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt,
}; };
use super::{ use super::{
ClosePanel, DockArea, DockItemState, Panel, PanelEvent, PanelView, StackPanel, ToggleZoom, ClosePanel, DockArea, DockItemState, DockPlacement, Panel, PanelEvent, PanelView, StackPanel,
ToggleZoom,
}; };
#[derive(Clone)] #[derive(Clone)]
@ -57,12 +59,18 @@ impl Render for DragPanel {
pub struct TabPanel { pub struct TabPanel {
focus_handle: FocusHandle, focus_handle: FocusHandle,
dock_area: WeakView<DockArea>, dock_area: WeakView<DockArea>,
/// The stock_panel can be None, if is None, that means the panels can't be split or move
stack_panel: Option<View<StackPanel>>, stack_panel: Option<View<StackPanel>>,
pub(crate) panels: Vec<Arc<dyn PanelView>>, pub(crate) panels: Vec<Arc<dyn PanelView>>,
pub(crate) active_ix: usize, pub(crate) active_ix: usize,
tab_bar_scroll_handle: ScrollHandle, tab_bar_scroll_handle: ScrollHandle,
is_zoomed: bool, is_zoomed: bool,
/// If this is true, the Panel closeable will follow the active panel's closeable,
/// otherwise this TabPanel will not able to close
pub(crate) closeable: bool,
/// If this is true, the Panel zoomable will follow the active panel's zoomable,
/// otherwise this TabPanel will not able to zoom
pub(crate) zoomable: bool,
/// When drag move, will get the placement of the panel to be split /// When drag move, will get the placement of the panel to be split
will_split_placement: Option<Placement>, will_split_placement: Option<Placement>,
@ -80,12 +88,20 @@ impl Panel for TabPanel {
} }
fn closeable(&self, cx: &WindowContext) -> bool { fn closeable(&self, cx: &WindowContext) -> bool {
if !self.closeable {
return false;
}
self.active_panel() self.active_panel()
.map(|panel| panel.closeable(cx)) .map(|panel| panel.closeable(cx))
.unwrap_or(false) .unwrap_or(false)
} }
fn zoomable(&self, cx: &WindowContext) -> bool { fn zoomable(&self, cx: &WindowContext) -> bool {
if !self.zoomable {
return false;
}
self.active_panel() self.active_panel()
.map(|panel| panel.zoomable(cx)) .map(|panel| panel.zoomable(cx))
.unwrap_or(false) .unwrap_or(false)
@ -130,6 +146,8 @@ impl TabPanel {
tab_bar_scroll_handle: ScrollHandle::new(), tab_bar_scroll_handle: ScrollHandle::new(),
will_split_placement: None, will_split_placement: None,
is_zoomed: false, is_zoomed: false,
closeable: true,
zoomable: true,
} }
} }
@ -245,6 +263,11 @@ impl TabPanel {
} }
} }
/// Return true if the panel can be split or move
fn can_split(&self) -> bool {
self.stack_panel.is_some()
}
fn render_menu_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render_menu_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let closeable = self.closeable(cx); let closeable = self.closeable(cx);
let zoomable = self.zoomable(cx); let zoomable = self.zoomable(cx);
@ -253,6 +276,8 @@ impl TabPanel {
let view = cx.view().clone(); let view = cx.view().clone();
let build_popup_menu = move |this, cx: &WindowContext| view.read(cx).popup_menu(this, cx); let build_popup_menu = move |this, cx: &WindowContext| view.read(cx).popup_menu(this, cx);
// TODO: Do not show MenuButton if there is no menu items
h_flex() h_flex()
.gap_2() .gap_2()
.occlude() .occlude()
@ -293,9 +318,128 @@ impl TabPanel {
) )
} }
fn render_dock_toggle_button(
&self,
placement: DockPlacement,
cx: &mut ViewContext<Self>,
) -> Option<impl IntoElement> {
let dock_area = self.dock_area.upgrade().expect("BUG: DockArea is missing");
if self.is_zoomed {
return None;
}
let mut self_is_left_dock = false;
let mut self_is_right_dock = false;
let mut self_is_bottom_dock = false;
if let Some(left_view) = &dock_area.read(cx).left_dock {
if left_view.read(cx).panel.entity_id() == cx.view().entity_id() {
self_is_left_dock = true;
}
}
if let Some(right_view) = &dock_area.read(cx).right_dock {
if right_view.read(cx).panel.entity_id() == cx.view().entity_id() {
self_is_right_dock = true;
}
}
if let Some(bottom_view) = &dock_area.read(cx).bottom_dock {
if bottom_view.read(cx).panel.entity_id() == cx.view().entity_id() {
self_is_bottom_dock = true;
}
}
// Check the dock origin vs self.bounds.origin, if they are in the same line, then render the ToggleButton
match placement {
DockPlacement::Left => {
if self_is_left_dock || self_is_right_dock || self_is_bottom_dock {
return None;
}
if let Some(parent) = self.stack_panel.as_ref() {
if !parent.read(cx).is_top_left_panel(cx.view().clone(), cx) {
return None;
}
}
}
DockPlacement::Right => {
if self_is_left_dock || self_is_right_dock || self_is_bottom_dock {
return None;
}
if let Some(parent) = self.stack_panel.as_ref() {
if !parent.read(cx).is_top_right_panel(cx.view().clone(), cx) {
return None;
}
}
}
DockPlacement::Bottom => {
if !self_is_bottom_dock {
return None;
}
}
}
let is_left_dock_open = dock_area
.read(cx)
.is_dock_open(super::DockPlacement::Left, cx);
let is_right_dock_open = dock_area
.read(cx)
.is_dock_open(super::DockPlacement::Right, cx);
let is_bottom_dock_open = dock_area
.read(cx)
.is_dock_open(super::DockPlacement::Bottom, cx);
let (icon, is_open) = match placement {
DockPlacement::Left => {
if is_left_dock_open {
(IconName::PanelLeft, true)
} else {
(IconName::PanelLeftOpen, false)
}
}
DockPlacement::Right => {
if is_right_dock_open {
(IconName::PanelRight, true)
} else {
(IconName::PanelRightOpen, false)
}
}
DockPlacement::Bottom => {
if is_bottom_dock_open {
(IconName::PanelBottom, true)
} else {
(IconName::PanelBottomOpen, false)
}
}
};
Some(
Button::new(SharedString::from(format!("toggle-dock:{:?}", placement)))
.icon(icon)
.xsmall()
.ghost()
.tooltip(match is_open {
true => t!("Dock.Collapse"),
false => t!("Dock.Expand"),
})
.on_click(cx.listener({
let dock_area = dock_area.clone();
move |_, _, cx| {
dock_area.update(cx, |dock_area, cx| {
dock_area.toggle_dock(placement, cx);
});
}
})),
)
}
fn render_tabs(&self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render_tabs(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let view = cx.view().clone(); let view = cx.view().clone();
let left_dock_button = self.render_dock_toggle_button(DockPlacement::Left, cx);
let bottom_dock_button = self.render_dock_toggle_button(DockPlacement::Bottom, cx);
let right_dock_button = self.render_dock_toggle_button(DockPlacement::Right, cx);
if self.panels.len() == 1 { if self.panels.len() == 1 {
let panel = self.panels.get(0).unwrap(); let panel = self.panels.get(0).unwrap();
let title_style = panel.title_style(cx); let title_style = panel.title_style(cx);
@ -304,31 +448,59 @@ impl TabPanel {
.justify_between() .justify_between()
.items_center() .items_center()
.line_height(rems(1.0)) .line_height(rems(1.0))
.pr_3() .h(px(30.))
.py_2()
.px_3()
.when(left_dock_button.is_some(), |this| this.pl_2())
.when(right_dock_button.is_some(), |this| this.pr_2())
.when_some(title_style, |this, theme| { .when_some(title_style, |this, theme| {
this.bg(theme.background).text_color(theme.foreground) this.bg(theme.background).text_color(theme.foreground)
}) })
.when(
left_dock_button.is_some() || bottom_dock_button.is_some(),
|this| {
this.child(
h_flex()
.flex_shrink_0()
.mr_1()
.gap_1()
.children(left_dock_button)
.children(bottom_dock_button),
)
},
)
.child( .child(
div() div()
.id("tab") .id("tab")
.py_2() .flex_1()
.px_3() .debug_red()
.min_w_16() .min_w_16()
.overflow_hidden() .overflow_hidden()
.text_ellipsis() .text_ellipsis()
.whitespace_nowrap()
.debug_green()
.child(panel.title(cx)) .child(panel.title(cx))
.on_drag( .when(self.can_split(), |this| {
DragPanel { this.on_drag(
panel: panel.clone(), DragPanel {
tab_panel: view, panel: panel.clone(),
}, tab_panel: view,
|drag, cx| { },
cx.stop_propagation(); |drag, cx| {
cx.new_view(|_| drag.clone()) cx.stop_propagation();
}, cx.new_view(|_| drag.clone())
), },
)
}),
)
.child(
h_flex()
.flex_shrink_0()
.ml_1()
.gap_1()
.child(self.render_menu_button(cx))
.children(right_dock_button),
) )
.child(self.render_menu_button(cx))
.into_any_element(); .into_any_element();
} }
@ -336,6 +508,25 @@ impl TabPanel {
TabBar::new("tab-bar") TabBar::new("tab-bar")
.track_scroll(self.tab_bar_scroll_handle.clone()) .track_scroll(self.tab_bar_scroll_handle.clone())
.when(
left_dock_button.is_some() || bottom_dock_button.is_some(),
|this| {
this.prefix(
h_flex()
.items_center()
.top_0()
.right_0()
.border_r_1()
.border_b_1()
.h_full()
.border_color(cx.theme().border)
.bg(cx.theme().tab_bar)
.px_2()
.children(left_dock_button)
.children(bottom_dock_button),
)
},
)
.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))
@ -344,20 +535,24 @@ impl TabPanel {
.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);
})) }))
.on_drag(DragPanel::new(panel.clone(), view.clone()), |drag, cx| { .when(self.can_split(), |this| {
cx.stop_propagation(); this.on_drag(DragPanel::new(panel.clone(), view.clone()), |drag, cx| {
cx.new_view(|_| drag.clone()) cx.stop_propagation();
cx.new_view(|_| drag.clone())
})
.drag_over::<DragPanel>(|this, _, cx| {
this.rounded_l_none()
.border_l_2()
.border_r_0()
.border_color(cx.theme().drag_border)
})
.on_drop(cx.listener(
move |this, drag: &DragPanel, cx| {
this.will_split_placement = None;
this.on_drop(drag, Some(ix), cx)
},
))
}) })
.drag_over::<DragPanel>(|this, _, cx| {
this.rounded_l_none()
.border_l_2()
.border_r_0()
.border_color(cx.theme().drag_border)
})
.on_drop(cx.listener(move |this, drag: &DragPanel, cx| {
this.will_split_placement = None;
this.on_drop(drag, Some(ix), cx)
}))
})) }))
.child( .child(
// empty space to allow move to last tab right // empty space to allow move to last tab right
@ -366,18 +561,20 @@ impl TabPanel {
.h_full() .h_full()
.flex_grow() .flex_grow()
.min_w_16() .min_w_16()
.drag_over::<DragPanel>(|this, _, cx| this.bg(cx.theme().drop_target)) .when(self.can_split(), |this| {
.on_drop(cx.listener(move |this, drag: &DragPanel, cx| { this.drag_over::<DragPanel>(|this, _, cx| this.bg(cx.theme().drop_target))
this.will_split_placement = None; .on_drop(cx.listener(move |this, drag: &DragPanel, cx| {
this.will_split_placement = None;
let ix = if drag.tab_panel == view { let ix = if drag.tab_panel == view {
Some(tabs_count - 1) Some(tabs_count - 1)
} else { } else {
None None
}; };
this.on_drop(drag, ix, cx) this.on_drop(drag, ix, cx)
})), }))
}),
) )
.suffix( .suffix(
h_flex() h_flex()
@ -389,8 +586,10 @@ impl TabPanel {
.h_full() .h_full()
.border_color(cx.theme().border) .border_color(cx.theme().border)
.bg(cx.theme().tab_bar) .bg(cx.theme().tab_bar)
.px_3() .px_2()
.child(self.render_menu_button(cx)), .gap_1()
.child(self.render_menu_button(cx))
.when_some(right_dock_button, |this, btn| this.child(btn)),
) )
.into_any_element() .into_any_element()
} }
@ -405,33 +604,39 @@ impl TabPanel {
.overflow_x_hidden() .overflow_x_hidden()
.flex_1() .flex_1()
.child(panel.view()) .child(panel.view())
.on_drag_move(cx.listener(Self::on_panel_drag_move)) .when(self.can_split(), |this| {
.child( this.on_drag_move(cx.listener(Self::on_panel_drag_move))
div() .child(
.invisible() div()
.absolute() .invisible()
.bg(cx.theme().drop_target) .absolute()
.map(|this| match self.will_split_placement { .bg(cx.theme().drop_target)
Some(placement) => { .map(|this| match self.will_split_placement {
let size = DefiniteLength::Fraction(0.35); Some(placement) => {
match placement { let size = DefiniteLength::Fraction(0.35);
Placement::Left => this.left_0().top_0().bottom_0().w(size), match placement {
Placement::Right => { Placement::Left => {
this.right_0().top_0().bottom_0().w(size) this.left_0().top_0().bottom_0().w(size)
}
Placement::Right => {
this.right_0().top_0().bottom_0().w(size)
}
Placement::Top => {
this.top_0().left_0().right_0().h(size)
}
Placement::Bottom => {
this.bottom_0().left_0().right_0().h(size)
}
}
} }
Placement::Top => this.top_0().left_0().right_0().h(size), None => this.top_0().left_0().size_full(),
Placement::Bottom => { })
this.bottom_0().left_0().right_0().h(size) .group_drag_over::<DragPanel>("", |this| this.visible())
} .on_drop(cx.listener(|this, drag: &DragPanel, cx| {
} this.on_drop(drag, None, cx)
} })),
None => this.top_0().left_0().size_full(), )
}) })
.group_drag_over::<DragPanel>("", |this| this.visible())
.on_drop(cx.listener(|this, drag: &DragPanel, cx| {
this.on_drop(drag, None, cx)
})),
)
.into_any_element() .into_any_element()
}) })
.unwrap_or(Empty {}.into_any_element()) .unwrap_or(Empty {}.into_any_element())
@ -634,6 +839,7 @@ impl EventEmitter<PanelEvent> for TabPanel {}
impl Render for TabPanel { impl Render for TabPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl gpui::IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl gpui::IntoElement {
let focus_handle = self.focus_handle(cx); let focus_handle = self.focus_handle(cx);
v_flex() v_flex()
.id("tab-panel") .id("tab-panel")
.track_focus(&focus_handle) .track_focus(&focus_handle)

View file

@ -43,6 +43,12 @@ pub enum IconName {
Minus, Minus,
Moon, Moon,
Palette, Palette,
PanelBottom,
PanelBottomOpen,
PanelLeft,
PanelLeftOpen,
PanelRight,
PanelRightOpen,
Plus, Plus,
Search, Search,
SortAscending, SortAscending,
@ -95,6 +101,12 @@ impl IconName {
IconName::Minus => "icons/minus.svg", IconName::Minus => "icons/minus.svg",
IconName::Moon => "icons/moon.svg", IconName::Moon => "icons/moon.svg",
IconName::Palette => "icons/palette.svg", IconName::Palette => "icons/palette.svg",
IconName::PanelBottom => "icons/panel-bottom.svg",
IconName::PanelBottomOpen => "icons/panel-bottom-open.svg",
IconName::PanelLeft => "icons/panel-left.svg",
IconName::PanelLeftOpen => "icons/panel-left-open.svg",
IconName::PanelRight => "icons/panel-right.svg",
IconName::PanelRightOpen => "icons/panel-right-open.svg",
IconName::Plus => "icons/plus.svg", IconName::Plus => "icons/plus.svg",
IconName::Search => "icons/search.svg", IconName::Search => "icons/search.svg",
IconName::SortAscending => "icons/sort-ascending.svg", IconName::SortAscending => "icons/sort-ascending.svg",

View file

@ -1,7 +1,9 @@
use gpui::{Axis, ViewContext}; use gpui::{Axis, ViewContext};
mod panel; mod panel;
mod resize_handle;
pub use panel::*; pub use panel::*;
pub(crate) use resize_handle::*;
pub fn h_resizable(cx: &mut ViewContext<ResizablePanelGroup>) -> ResizablePanelGroup { pub fn h_resizable(cx: &mut ViewContext<ResizablePanelGroup>) -> ResizablePanelGroup {
ResizablePanelGroup::new(cx).axis(Axis::Horizontal) ResizablePanelGroup::new(cx).axis(Axis::Horizontal)

View file

@ -2,15 +2,16 @@ use std::rc::Rc;
use gpui::{ use gpui::{
canvas, div, prelude::FluentBuilder, px, Along, AnyElement, AnyView, Axis, Bounds, Element, canvas, div, prelude::FluentBuilder, px, Along, AnyElement, AnyView, Axis, Bounds, Element,
Entity, EntityId, EventEmitter, InteractiveElement as _, IntoElement, MouseMoveEvent, Entity, EntityId, EventEmitter, IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement,
MouseUpEvent, ParentElement, Pixels, Render, StatefulInteractiveElement, Style, Styled, View, Pixels, Render, StatefulInteractiveElement as _, Style, Styled, View, ViewContext,
ViewContext, VisualContext as _, WindowContext, VisualContext as _, WindowContext,
}; };
use crate::{h_flex, theme::ActiveTheme, v_flex, AxisExt}; use crate::{h_flex, v_flex, AxisExt};
const PANEL_MIN_SIZE: Pixels = px(100.); use super::resize_handle;
const HANDLE_PADDING: Pixels = px(4.);
pub(crate) const PANEL_MIN_SIZE: Pixels = px(100.);
pub enum ResizablePanelEvent { pub enum ResizablePanelEvent {
Resized, Resized,
@ -24,7 +25,6 @@ pub struct ResizablePanelGroup {
panels: Vec<View<ResizablePanel>>, panels: Vec<View<ResizablePanel>>,
sizes: Vec<Pixels>, sizes: Vec<Pixels>,
axis: Axis, axis: Axis,
handle_size: Pixels,
size: Option<Pixels>, size: Option<Pixels>,
bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
resizing_panel_ix: Option<usize>, resizing_panel_ix: Option<usize>,
@ -36,7 +36,6 @@ impl ResizablePanelGroup {
axis: Axis::Horizontal, axis: Axis::Horizontal,
sizes: Vec::new(), sizes: Vec::new(),
panels: Vec::new(), panels: Vec::new(),
handle_size: px(1.),
size: None, size: None,
bounds: Bounds::default(), bounds: Bounds::default(),
resizing_panel_ix: None, resizing_panel_ix: None,
@ -59,14 +58,6 @@ impl ResizablePanelGroup {
cx.notify(); 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. /// Add a resizable panel to the group.
pub fn child(mut self, panel: ResizablePanel, cx: &mut ViewContext<Self>) -> Self { pub fn child(mut self, panel: ResizablePanel, cx: &mut ViewContext<Self>) -> Self {
self.add_child(panel, cx); self.add_child(panel, cx);
@ -75,8 +66,7 @@ impl ResizablePanelGroup {
/// Add a ResizablePanelGroup as a child to the group. /// Add a ResizablePanelGroup as a child to the group.
pub fn group(self, group: ResizablePanelGroup, cx: &mut ViewContext<Self>) -> Self { pub fn group(self, group: ResizablePanelGroup, cx: &mut ViewContext<Self>) -> Self {
let mut group: ResizablePanelGroup = group; let group: ResizablePanelGroup = group;
group.handle_size = self.handle_size;
let size = group.size; let size = group.size;
let panel = ResizablePanel::new() let panel = ResizablePanel::new()
.content_view(cx.new_view(|_| group).into()) .content_view(cx.new_view(|_| group).into())
@ -150,52 +140,18 @@ impl ResizablePanelGroup {
} }
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 neg_offset = -HANDLE_PADDING;
let view = cx.view().clone(); let view = cx.view().clone();
resize_handle(("resizable-handle", ix), self.axis).on_drag(
div() DragPanel((cx.entity_id(), ix, self.axis)),
.id(("resizable-handle", ix)) move |drag_panel, cx| {
.occlude() cx.stop_propagation();
.absolute() // Set current resizing panel ix
.flex_shrink_0() view.update(cx, |view, _| {
.when(self.axis.is_horizontal(), |this| { view.resizing_panel_ix = Some(ix);
this.cursor_col_resize() });
.top_0() cx.new_view(|_| drag_panel.clone())
.left(neg_offset) },
.h_full() )
.w(px(1.))
.px(HANDLE_PADDING)
})
.when(self.axis.is_vertical(), |this| {
this.cursor_row_resize()
.top(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(
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);
});
cx.new_view(|_| drag_panel.clone())
},
)
} }
fn done_resizing(&mut self, cx: &mut ViewContext<Self>) { fn done_resizing(&mut self, cx: &mut ViewContext<Self>) {

View file

@ -0,0 +1,72 @@
use gpui::{
div, prelude::FluentBuilder as _, px, Axis, Div, ElementId, InteractiveElement, IntoElement,
ParentElement as _, Pixels, RenderOnce, Stateful, StatefulInteractiveElement, Styled as _,
WindowContext,
};
use crate::{theme::ActiveTheme as _, AxisExt as _};
pub(crate) const HANDLE_PADDING: Pixels = px(4.);
pub(crate) const HANDLE_SIZE: Pixels = px(1.);
#[derive(IntoElement)]
pub(crate) struct ResizeHandle {
base: Stateful<Div>,
axis: Axis,
}
impl ResizeHandle {
fn new(id: impl Into<ElementId>, axis: Axis) -> Self {
Self {
base: div().id(id.into()),
axis,
}
}
}
/// Create a resize handle for a resizable panel.
pub(crate) fn resize_handle(id: impl Into<ElementId>, axis: Axis) -> ResizeHandle {
ResizeHandle::new(id, axis)
}
impl InteractiveElement for ResizeHandle {
fn interactivity(&mut self) -> &mut gpui::Interactivity {
self.base.interactivity()
}
}
impl StatefulInteractiveElement for ResizeHandle {}
impl RenderOnce for ResizeHandle {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let neg_offset = -HANDLE_PADDING;
self.base
.occlude()
.absolute()
.flex_shrink_0()
.when(self.axis.is_horizontal(), |this| {
this.cursor_col_resize()
.top_0()
.left(neg_offset)
.h_full()
.w(HANDLE_SIZE)
.px(HANDLE_PADDING)
})
.when(self.axis.is_vertical(), |this| {
this.cursor_row_resize()
.top(neg_offset)
.left_0()
.w_full()
.h(HANDLE_SIZE)
.py(HANDLE_PADDING)
})
.child(
div()
.bg(cx.theme().border)
.when(self.axis.is_horizontal(), |this| {
this.h_full().w(HANDLE_SIZE)
})
.when(self.axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE)),
)
}
}