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:
parent
20b9f9c0f2
commit
6f1be83b6e
17 changed files with 913 additions and 158 deletions
1
assets/icons/panel-bottom-open.svg
Normal file
1
assets/icons/panel-bottom-open.svg
Normal 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 |
1
assets/icons/panel-bottom.svg
Normal file
1
assets/icons/panel-bottom.svg
Normal 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 |
1
assets/icons/panel-left-open.svg
Normal file
1
assets/icons/panel-left-open.svg
Normal 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 |
1
assets/icons/panel-left.svg
Normal file
1
assets/icons/panel-left.svg
Normal 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 |
1
assets/icons/panel-right-open.svg
Normal file
1
assets/icons/panel-right-open.svg
Normal 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 |
1
assets/icons/panel-right.svg
Normal file
1
assets/icons/panel-right.svg
Normal 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 |
|
|
@ -11,7 +11,7 @@ use story::{
|
|||
use ui::{
|
||||
button::{Button, ButtonStyled as _},
|
||||
color_picker::{ColorPicker, ColorPickerEvent},
|
||||
dock::{DockArea, DockEvent, DockItem, DockItemState},
|
||||
dock::{DockArea, DockEvent, DockItem, DockItemState, PanelView},
|
||||
h_flex,
|
||||
popup_menu::PopupMenuExt,
|
||||
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 {
|
||||
DockEvent::LayoutChanged => this.save_layout(dock_area, cx),
|
||||
|
|
@ -173,7 +189,6 @@ impl StoryWorkspace {
|
|||
Arc::new(StoryContainer::panel::<DropdownStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<ModalStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<PopupStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<ListStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<SwitchStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<ProgressStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<TableStory>(cx)),
|
||||
|
|
|
|||
|
|
@ -38,8 +38,7 @@ pub use webview_story::WebViewStory;
|
|||
use gpui::{
|
||||
actions, div, prelude::FluentBuilder as _, px, AnyElement, AnyView, AppContext, Div,
|
||||
EventEmitter, FocusableView, Hsla, InteractiveElement, IntoElement, ParentElement, Render,
|
||||
SharedString, StatefulInteractiveElement, Styled as _, View, ViewContext, VisualContext,
|
||||
WindowContext,
|
||||
SharedString, Styled as _, View, ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
|
||||
use ui::{
|
||||
|
|
@ -313,7 +312,6 @@ impl Render for StoryContainer {
|
|||
v_flex()
|
||||
.id("story-container")
|
||||
.size_full()
|
||||
.overflow_scroll()
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(cx.listener(Self::on_action_panel_info))
|
||||
.when(self.description.len() > 0, |this| {
|
||||
|
|
@ -328,14 +326,7 @@ impl Render for StoryContainer {
|
|||
)
|
||||
})
|
||||
.when_some(self.story.clone(), |this, story| {
|
||||
this.child(
|
||||
v_flex()
|
||||
.id("story-children")
|
||||
.overflow_scroll()
|
||||
.size_full()
|
||||
.p_4()
|
||||
.child(story),
|
||||
)
|
||||
this.child(v_flex().id("story-children").size_full().p_4().child(story))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,3 +103,11 @@ Dock:
|
|||
en: Zoom Out
|
||||
zh-CN: 缩小
|
||||
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
310
crates/ui/src/dock/dock.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
mod dock;
|
||||
mod invalid_panel;
|
||||
mod panel;
|
||||
mod stack_panel;
|
||||
|
|
@ -5,10 +6,11 @@ mod tab_panel;
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use dock::*;
|
||||
use gpui::{
|
||||
actions, div, prelude::FluentBuilder, AnyElement, AnyView, AppContext, Axis, EventEmitter,
|
||||
InteractiveElement as _, IntoElement, ParentElement as _, Pixels, Render, SharedString, Styled,
|
||||
View, ViewContext, VisualContext, WeakView, WindowContext,
|
||||
actions, canvas, div, prelude::FluentBuilder, AnyElement, AnyView, AppContext, Axis, Bounds,
|
||||
EventEmitter, InteractiveElement as _, IntoElement, ParentElement as _, Pixels, Render,
|
||||
SharedString, Styled, View, ViewContext, VisualContext, WeakView, WindowContext,
|
||||
};
|
||||
pub use panel::*;
|
||||
pub use stack_panel::*;
|
||||
|
|
@ -31,7 +33,18 @@ pub enum DockEvent {
|
|||
/// The main area of the dock.
|
||||
pub struct DockArea {
|
||||
id: SharedString,
|
||||
pub(crate) bounds: Bounds<Pixels>,
|
||||
|
||||
/// The center view of the dockarea.
|
||||
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>,
|
||||
}
|
||||
|
||||
|
|
@ -166,7 +179,7 @@ impl DockItem {
|
|||
}
|
||||
|
||||
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 dock_item = DockItem::Split {
|
||||
axis: Axis::Horizontal,
|
||||
|
|
@ -177,13 +190,18 @@ impl DockArea {
|
|||
|
||||
Self {
|
||||
id: id.into(),
|
||||
bounds: Bounds::default(),
|
||||
items: dock_item,
|
||||
zoom_view: None,
|
||||
left_dock: None,
|
||||
right_dock: None,
|
||||
bottom_dock: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>) {
|
||||
self.subscribe_item(&item, cx);
|
||||
self.items = item;
|
||||
|
|
@ -191,6 +209,90 @@ impl DockArea {
|
|||
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.
|
||||
///
|
||||
/// See also `DockItemState::to_item` for the load DockItem from DockItemState.
|
||||
|
|
@ -275,16 +377,58 @@ impl DockArea {
|
|||
impl EventEmitter<DockEvent> for DockArea {}
|
||||
impl Render for DockArea {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
// println!("Rendering dock area");
|
||||
let view = cx.view().clone();
|
||||
|
||||
div()
|
||||
.id("dock-area")
|
||||
.relative()
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
canvas(
|
||||
move |bounds, cx| view.update(cx, |r, _| r.bounds = bounds),
|
||||
|_, _, _| {},
|
||||
)
|
||||
.absolute()
|
||||
.size_full(),
|
||||
)
|
||||
.map(|this| {
|
||||
if let Some(zoom_view) = self.zoom_view.clone() {
|
||||
this.child(zoom_view)
|
||||
} 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))
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@ use crate::{
|
|||
ResizablePanelGroup,
|
||||
},
|
||||
theme::ActiveTheme,
|
||||
Placement,
|
||||
AxisExt, Placement,
|
||||
};
|
||||
|
||||
use super::{DockArea, DockItemState, Panel, PanelEvent, PanelView, TabPanel};
|
||||
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,
|
||||
VisualContext, WeakView,
|
||||
};
|
||||
|
|
@ -277,6 +277,39 @@ impl StackPanel {
|
|||
.update(cx, |view, cx| view.set_axis(axis, cx));
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder, rems, AnchorCorner, AppContext, DefiniteLength, DismissEvent,
|
||||
DragMoveEvent, Empty, EventEmitter, FocusHandle, FocusableView, InteractiveElement as _,
|
||||
IntoElement, ParentElement, Pixels, Render, ScrollHandle, StatefulInteractiveElement, Styled,
|
||||
View, ViewContext, VisualContext as _, WeakView, WindowContext,
|
||||
div, prelude::FluentBuilder, px, rems, AnchorCorner, AppContext, DefiniteLength, DismissEvent,
|
||||
DragMoveEvent, Empty, Entity, EventEmitter, FocusHandle, FocusableView,
|
||||
InteractiveElement as _, IntoElement, ParentElement, Pixels, Render, ScrollHandle,
|
||||
SharedString, StatefulInteractiveElement, Styled, View, ViewContext, VisualContext as _,
|
||||
WeakView, WindowContext,
|
||||
};
|
||||
use rust_i18n::t;
|
||||
|
||||
|
|
@ -15,11 +16,12 @@ use crate::{
|
|||
popup_menu::{PopupMenu, PopupMenuExt},
|
||||
tab::{Tab, TabBar},
|
||||
theme::ActiveTheme,
|
||||
v_flex, AxisExt, IconName, Placement, Selectable, Sizable,
|
||||
v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt,
|
||||
};
|
||||
|
||||
use super::{
|
||||
ClosePanel, DockArea, DockItemState, Panel, PanelEvent, PanelView, StackPanel, ToggleZoom,
|
||||
ClosePanel, DockArea, DockItemState, DockPlacement, Panel, PanelEvent, PanelView, StackPanel,
|
||||
ToggleZoom,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
@ -57,12 +59,18 @@ impl Render for DragPanel {
|
|||
pub struct TabPanel {
|
||||
focus_handle: FocusHandle,
|
||||
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>>,
|
||||
pub(crate) panels: Vec<Arc<dyn PanelView>>,
|
||||
pub(crate) active_ix: usize,
|
||||
tab_bar_scroll_handle: ScrollHandle,
|
||||
|
||||
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
|
||||
will_split_placement: Option<Placement>,
|
||||
|
|
@ -80,12 +88,20 @@ impl Panel for TabPanel {
|
|||
}
|
||||
|
||||
fn closeable(&self, cx: &WindowContext) -> bool {
|
||||
if !self.closeable {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.active_panel()
|
||||
.map(|panel| panel.closeable(cx))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn zoomable(&self, cx: &WindowContext) -> bool {
|
||||
if !self.zoomable {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.active_panel()
|
||||
.map(|panel| panel.zoomable(cx))
|
||||
.unwrap_or(false)
|
||||
|
|
@ -130,6 +146,8 @@ impl TabPanel {
|
|||
tab_bar_scroll_handle: ScrollHandle::new(),
|
||||
will_split_placement: None,
|
||||
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 {
|
||||
let closeable = self.closeable(cx);
|
||||
let zoomable = self.zoomable(cx);
|
||||
|
|
@ -253,6 +276,8 @@ impl TabPanel {
|
|||
let view = cx.view().clone();
|
||||
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()
|
||||
.gap_2()
|
||||
.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 {
|
||||
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 {
|
||||
let panel = self.panels.get(0).unwrap();
|
||||
let title_style = panel.title_style(cx);
|
||||
|
|
@ -304,31 +448,59 @@ impl TabPanel {
|
|||
.justify_between()
|
||||
.items_center()
|
||||
.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| {
|
||||
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(
|
||||
div()
|
||||
.id("tab")
|
||||
.py_2()
|
||||
.px_3()
|
||||
.flex_1()
|
||||
.debug_red()
|
||||
.min_w_16()
|
||||
.overflow_hidden()
|
||||
.text_ellipsis()
|
||||
.whitespace_nowrap()
|
||||
.debug_green()
|
||||
.child(panel.title(cx))
|
||||
.on_drag(
|
||||
DragPanel {
|
||||
panel: panel.clone(),
|
||||
tab_panel: view,
|
||||
},
|
||||
|drag, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.new_view(|_| drag.clone())
|
||||
},
|
||||
),
|
||||
.when(self.can_split(), |this| {
|
||||
this.on_drag(
|
||||
DragPanel {
|
||||
panel: panel.clone(),
|
||||
tab_panel: view,
|
||||
},
|
||||
|drag, cx| {
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
@ -336,6 +508,25 @@ impl TabPanel {
|
|||
|
||||
TabBar::new("tab-bar")
|
||||
.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)| {
|
||||
let active = ix == self.active_ix;
|
||||
Tab::new(("tab", ix), panel.title(cx))
|
||||
|
|
@ -344,20 +535,24 @@ impl TabPanel {
|
|||
.on_click(cx.listener(move |view, _, cx| {
|
||||
view.set_active_ix(ix, cx);
|
||||
}))
|
||||
.on_drag(DragPanel::new(panel.clone(), view.clone()), |drag, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.new_view(|_| drag.clone())
|
||||
.when(self.can_split(), |this| {
|
||||
this.on_drag(DragPanel::new(panel.clone(), view.clone()), |drag, cx| {
|
||||
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(
|
||||
// empty space to allow move to last tab right
|
||||
|
|
@ -366,18 +561,20 @@ impl TabPanel {
|
|||
.h_full()
|
||||
.flex_grow()
|
||||
.min_w_16()
|
||||
.drag_over::<DragPanel>(|this, _, cx| this.bg(cx.theme().drop_target))
|
||||
.on_drop(cx.listener(move |this, drag: &DragPanel, cx| {
|
||||
this.will_split_placement = None;
|
||||
.when(self.can_split(), |this| {
|
||||
this.drag_over::<DragPanel>(|this, _, cx| this.bg(cx.theme().drop_target))
|
||||
.on_drop(cx.listener(move |this, drag: &DragPanel, cx| {
|
||||
this.will_split_placement = None;
|
||||
|
||||
let ix = if drag.tab_panel == view {
|
||||
Some(tabs_count - 1)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let ix = if drag.tab_panel == view {
|
||||
Some(tabs_count - 1)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
this.on_drop(drag, ix, cx)
|
||||
})),
|
||||
this.on_drop(drag, ix, cx)
|
||||
}))
|
||||
}),
|
||||
)
|
||||
.suffix(
|
||||
h_flex()
|
||||
|
|
@ -389,8 +586,10 @@ impl TabPanel {
|
|||
.h_full()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().tab_bar)
|
||||
.px_3()
|
||||
.child(self.render_menu_button(cx)),
|
||||
.px_2()
|
||||
.gap_1()
|
||||
.child(self.render_menu_button(cx))
|
||||
.when_some(right_dock_button, |this, btn| this.child(btn)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
|
@ -405,33 +604,39 @@ impl TabPanel {
|
|||
.overflow_x_hidden()
|
||||
.flex_1()
|
||||
.child(panel.view())
|
||||
.on_drag_move(cx.listener(Self::on_panel_drag_move))
|
||||
.child(
|
||||
div()
|
||||
.invisible()
|
||||
.absolute()
|
||||
.bg(cx.theme().drop_target)
|
||||
.map(|this| match self.will_split_placement {
|
||||
Some(placement) => {
|
||||
let size = DefiniteLength::Fraction(0.35);
|
||||
match placement {
|
||||
Placement::Left => this.left_0().top_0().bottom_0().w(size),
|
||||
Placement::Right => {
|
||||
this.right_0().top_0().bottom_0().w(size)
|
||||
.when(self.can_split(), |this| {
|
||||
this.on_drag_move(cx.listener(Self::on_panel_drag_move))
|
||||
.child(
|
||||
div()
|
||||
.invisible()
|
||||
.absolute()
|
||||
.bg(cx.theme().drop_target)
|
||||
.map(|this| match self.will_split_placement {
|
||||
Some(placement) => {
|
||||
let size = DefiniteLength::Fraction(0.35);
|
||||
match placement {
|
||||
Placement::Left => {
|
||||
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),
|
||||
Placement::Bottom => {
|
||||
this.bottom_0().left_0().right_0().h(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
})),
|
||||
)
|
||||
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()
|
||||
})
|
||||
.unwrap_or(Empty {}.into_any_element())
|
||||
|
|
@ -634,6 +839,7 @@ impl EventEmitter<PanelEvent> for TabPanel {}
|
|||
impl Render for TabPanel {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl gpui::IntoElement {
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
v_flex()
|
||||
.id("tab-panel")
|
||||
.track_focus(&focus_handle)
|
||||
|
|
|
|||
|
|
@ -43,6 +43,12 @@ pub enum IconName {
|
|||
Minus,
|
||||
Moon,
|
||||
Palette,
|
||||
PanelBottom,
|
||||
PanelBottomOpen,
|
||||
PanelLeft,
|
||||
PanelLeftOpen,
|
||||
PanelRight,
|
||||
PanelRightOpen,
|
||||
Plus,
|
||||
Search,
|
||||
SortAscending,
|
||||
|
|
@ -95,6 +101,12 @@ impl IconName {
|
|||
IconName::Minus => "icons/minus.svg",
|
||||
IconName::Moon => "icons/moon.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::Search => "icons/search.svg",
|
||||
IconName::SortAscending => "icons/sort-ascending.svg",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
use gpui::{Axis, ViewContext};
|
||||
|
||||
mod panel;
|
||||
mod resize_handle;
|
||||
pub use panel::*;
|
||||
pub(crate) use resize_handle::*;
|
||||
|
||||
pub fn h_resizable(cx: &mut ViewContext<ResizablePanelGroup>) -> ResizablePanelGroup {
|
||||
ResizablePanelGroup::new(cx).axis(Axis::Horizontal)
|
||||
|
|
|
|||
|
|
@ -2,15 +2,16 @@ use std::rc::Rc;
|
|||
|
||||
use gpui::{
|
||||
canvas, div, prelude::FluentBuilder, px, Along, AnyElement, AnyView, Axis, Bounds, Element,
|
||||
Entity, EntityId, EventEmitter, InteractiveElement as _, IntoElement, MouseMoveEvent,
|
||||
MouseUpEvent, ParentElement, Pixels, Render, StatefulInteractiveElement, Style, Styled, View,
|
||||
ViewContext, VisualContext as _, WindowContext,
|
||||
Entity, EntityId, EventEmitter, IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement,
|
||||
Pixels, Render, StatefulInteractiveElement as _, Style, Styled, View, ViewContext,
|
||||
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.);
|
||||
const HANDLE_PADDING: Pixels = px(4.);
|
||||
use super::resize_handle;
|
||||
|
||||
pub(crate) const PANEL_MIN_SIZE: Pixels = px(100.);
|
||||
|
||||
pub enum ResizablePanelEvent {
|
||||
Resized,
|
||||
|
|
@ -24,7 +25,6 @@ pub struct ResizablePanelGroup {
|
|||
panels: Vec<View<ResizablePanel>>,
|
||||
sizes: Vec<Pixels>,
|
||||
axis: Axis,
|
||||
handle_size: Pixels,
|
||||
size: Option<Pixels>,
|
||||
bounds: Bounds<Pixels>,
|
||||
resizing_panel_ix: Option<usize>,
|
||||
|
|
@ -36,7 +36,6 @@ impl ResizablePanelGroup {
|
|||
axis: Axis::Horizontal,
|
||||
sizes: Vec::new(),
|
||||
panels: Vec::new(),
|
||||
handle_size: px(1.),
|
||||
size: None,
|
||||
bounds: Bounds::default(),
|
||||
resizing_panel_ix: None,
|
||||
|
|
@ -59,14 +58,6 @@ impl ResizablePanelGroup {
|
|||
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 ViewContext<Self>) -> Self {
|
||||
self.add_child(panel, cx);
|
||||
|
|
@ -75,8 +66,7 @@ impl ResizablePanelGroup {
|
|||
|
||||
/// Add a ResizablePanelGroup as a child to the group.
|
||||
pub fn group(self, group: ResizablePanelGroup, cx: &mut ViewContext<Self>) -> Self {
|
||||
let mut group: ResizablePanelGroup = group;
|
||||
group.handle_size = self.handle_size;
|
||||
let group: ResizablePanelGroup = group;
|
||||
let size = group.size;
|
||||
let panel = ResizablePanel::new()
|
||||
.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 {
|
||||
let axis = self.axis;
|
||||
let neg_offset = -HANDLE_PADDING;
|
||||
let view = cx.view().clone();
|
||||
|
||||
div()
|
||||
.id(("resizable-handle", ix))
|
||||
.occlude()
|
||||
.absolute()
|
||||
.flex_shrink_0()
|
||||
.when(self.axis.is_horizontal(), |this| {
|
||||
this.cursor_col_resize()
|
||||
.top_0()
|
||||
.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())
|
||||
},
|
||||
)
|
||||
resize_handle(("resizable-handle", ix), self.axis).on_drag(
|
||||
DragPanel((cx.entity_id(), ix, self.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>) {
|
||||
|
|
|
|||
72
crates/ui/src/resizable/resize_handle.rs
Normal file
72
crates/ui/src/resizable/resize_handle.rs
Normal 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)),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue