New Dock (#172)
https://github.com/user-attachments/assets/c8c55faa-8f17-4fa2-9f62-5fdd598087ef - [x] Move to insert panel to tabs middle. - [x] Zoom panel - [x] Tab popup menu (Close Panel, Split, Zoom in/Zoom out) - [x] TabBar scrollable - [ ] TabBar nav history
This commit is contained in:
parent
7e9d79ca1b
commit
c5c8e46ac7
17 changed files with 1198 additions and 291 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -4915,7 +4915,6 @@ dependencies = [
|
|||
"regex",
|
||||
"serde",
|
||||
"ui",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@ use story::{
|
|||
ModalStory, PopupStory, ProgressStory, ResizableStory, ScrollableStory, StoryContainer,
|
||||
SwitchStory, TableStory, TextStory, TooltipStory,
|
||||
};
|
||||
use workspace::{TitleBar, Workspace};
|
||||
use workspace::TitleBar;
|
||||
|
||||
use std::sync::Arc;
|
||||
use ui::{
|
||||
button::Button,
|
||||
dock::{DockArea, StackPanel, TabPanel},
|
||||
drawer::Drawer,
|
||||
h_flex,
|
||||
modal::Modal,
|
||||
|
|
@ -38,26 +39,48 @@ pub fn init(_app_state: Arc<AppState>, cx: &mut AppContext) {
|
|||
}
|
||||
|
||||
pub struct StoryWorkspace {
|
||||
workspace: View<Workspace>,
|
||||
locale_selector: View<LocaleSelector>,
|
||||
// stack_panel: View<StackPanel>,
|
||||
dock_area: View<DockArea>,
|
||||
}
|
||||
|
||||
impl StoryWorkspace {
|
||||
pub fn new(
|
||||
_app_state: Arc<AppState>,
|
||||
workspace: View<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
pub fn new(_app_state: Arc<AppState>, cx: &mut ViewContext<Self>) -> Self {
|
||||
cx.observe_window_appearance(|_workspace, cx| {
|
||||
Theme::sync_system_appearance(cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
let stack_panel = cx.new_view(|cx| StackPanel::new(Axis::Horizontal, cx));
|
||||
let dock_area = cx.new_view(|cx| DockArea::new(stack_panel.clone(), cx));
|
||||
let weak_dock_area = dock_area.downgrade();
|
||||
|
||||
let tab_panel = cx.new_view(|cx| TabPanel::new(weak_dock_area.clone(), cx));
|
||||
let right_tab_panel = cx.new_view(|cx| TabPanel::new(weak_dock_area.clone(), cx));
|
||||
let right_tab_panel1 = cx.new_view(|cx| TabPanel::new(weak_dock_area.clone(), cx));
|
||||
|
||||
stack_panel.update(cx, |view, cx| {
|
||||
view.add_panel(tab_panel.clone(), None, weak_dock_area.clone(), cx);
|
||||
|
||||
let stock_panel1 = cx.new_view(|cx| StackPanel::new(Axis::Vertical, cx));
|
||||
view.add_panel(
|
||||
stock_panel1.clone(),
|
||||
Some(px(400.)),
|
||||
weak_dock_area.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
stock_panel1.update(cx, |view, cx| {
|
||||
view.add_panel(right_tab_panel.clone(), None, weak_dock_area.clone(), cx);
|
||||
view.add_panel(right_tab_panel1.clone(), None, weak_dock_area.clone(), cx);
|
||||
})
|
||||
});
|
||||
|
||||
StoryContainer::add_pane(
|
||||
"Buttons",
|
||||
"Displays a button or a component that looks like a button.",
|
||||
ButtonStory::view(cx).into(),
|
||||
workspace.clone(),
|
||||
tab_panel.clone(),
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
|
|
@ -66,7 +89,7 @@ impl StoryWorkspace {
|
|||
"Input",
|
||||
"A control that allows the user to input text.",
|
||||
InputStory::view(cx).into(),
|
||||
workspace.clone(),
|
||||
tab_panel.clone(),
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
|
|
@ -75,7 +98,7 @@ impl StoryWorkspace {
|
|||
"Text",
|
||||
"Links, paragraphs, checkboxes, and more.",
|
||||
TextStory::view(cx).into(),
|
||||
workspace.clone(),
|
||||
tab_panel.clone(),
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
|
|
@ -84,7 +107,7 @@ impl StoryWorkspace {
|
|||
"Switch",
|
||||
"A control that allows the user to toggle between two states.",
|
||||
SwitchStory::view(cx).into(),
|
||||
workspace.clone(),
|
||||
tab_panel.clone(),
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
|
|
@ -93,7 +116,7 @@ impl StoryWorkspace {
|
|||
"Dropdowns",
|
||||
"Displays a list of options for the user to pick from—triggered by a button.",
|
||||
DropdownStory::new(cx).into(),
|
||||
workspace.clone(),
|
||||
tab_panel.clone(),
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
|
|
@ -102,7 +125,7 @@ impl StoryWorkspace {
|
|||
"Modal",
|
||||
"Modal & Drawer use examples",
|
||||
ModalStory::view(cx).into(),
|
||||
workspace.clone(),
|
||||
tab_panel.clone(),
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
|
|
@ -111,7 +134,7 @@ impl StoryWorkspace {
|
|||
"Popup",
|
||||
"A popup displays content on top of the main page.",
|
||||
PopupStory::view(cx).into(),
|
||||
workspace.clone(),
|
||||
tab_panel.clone(),
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
|
|
@ -120,7 +143,7 @@ impl StoryWorkspace {
|
|||
"Tooltip",
|
||||
"Displays a short message when users hover over an element.",
|
||||
TooltipStory::view(cx).into(),
|
||||
workspace.clone(),
|
||||
tab_panel.clone(),
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
|
|
@ -129,7 +152,7 @@ impl StoryWorkspace {
|
|||
"List",
|
||||
"A list displays a series of items.",
|
||||
ListStory::view(cx).into(),
|
||||
workspace.clone(),
|
||||
tab_panel.clone(),
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
|
|
@ -138,7 +161,7 @@ impl StoryWorkspace {
|
|||
"Icon",
|
||||
"Icon use examples",
|
||||
IconStory::view(cx).into(),
|
||||
workspace.clone(),
|
||||
tab_panel.clone(),
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
|
|
@ -147,14 +170,14 @@ impl StoryWorkspace {
|
|||
"Image",
|
||||
"Render SVG image and Chart",
|
||||
ImageStory::view(cx).into(),
|
||||
workspace.clone(),
|
||||
right_tab_panel1.clone(),
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
|
||||
// StoryContainer::add_panel(
|
||||
// WebViewStory::view(cx).into(),
|
||||
// workspace.clone(),
|
||||
// stack_panel.clone(),
|
||||
// DockPosition::Right,
|
||||
// px(450.),
|
||||
// cx,
|
||||
|
|
@ -164,7 +187,7 @@ impl StoryWorkspace {
|
|||
"Table",
|
||||
"Powerful table and datagrids built.",
|
||||
TableStory::view(cx).into(),
|
||||
workspace.clone(),
|
||||
tab_panel.clone(),
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
|
|
@ -173,7 +196,7 @@ impl StoryWorkspace {
|
|||
"Progress",
|
||||
"Displays an indicator showing the completion progress of a task, typically displayed as a progress bar.",
|
||||
ProgressStory::view(cx).into(),
|
||||
workspace.clone(),
|
||||
tab_panel.clone(),
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
|
|
@ -182,7 +205,7 @@ impl StoryWorkspace {
|
|||
"Resizable",
|
||||
"Accessible resizable panel groups and layouts with keyboard support.",
|
||||
ResizableStory::view(cx).into(),
|
||||
workspace.clone(),
|
||||
tab_panel.clone(),
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
|
|
@ -191,7 +214,7 @@ impl StoryWorkspace {
|
|||
"Scrollable",
|
||||
"A scrollable area with scroll bar.",
|
||||
ScrollableStory::view(cx).into(),
|
||||
workspace.clone(),
|
||||
tab_panel.clone(),
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
|
|
@ -200,14 +223,15 @@ impl StoryWorkspace {
|
|||
"Calendar",
|
||||
"A calendar component.",
|
||||
CalendarStory::view(cx).into(),
|
||||
workspace.clone(),
|
||||
right_tab_panel.clone(),
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
|
||||
let locale_selector = cx.new_view(LocaleSelector::new);
|
||||
|
||||
Self {
|
||||
workspace,
|
||||
dock_area,
|
||||
locale_selector,
|
||||
}
|
||||
}
|
||||
|
|
@ -235,8 +259,7 @@ impl StoryWorkspace {
|
|||
};
|
||||
|
||||
let window = cx.open_window(options, |cx| {
|
||||
let workspace = cx.new_view(|cx| Workspace::new(None, cx));
|
||||
let story_view = cx.new_view(|cx| Self::new(app_state.clone(), workspace, cx));
|
||||
let story_view = cx.new_view(|cx| Self::new(app_state.clone(), cx));
|
||||
cx.new_view(|cx| Root::new(story_view.into(), cx))
|
||||
})?;
|
||||
|
||||
|
|
@ -366,7 +389,7 @@ impl Render for StoryWorkspace {
|
|||
),
|
||||
),
|
||||
)
|
||||
.child(self.workspace.clone())
|
||||
.child(self.dock_area.clone())
|
||||
.when(!has_active_modal, |this| {
|
||||
this.when_some(active_drawer, |this, builder| {
|
||||
let drawer = Drawer::new(cx);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ ui.workspace = true
|
|||
gpui.workspace = true
|
||||
fake = "2.9.2"
|
||||
anyhow = "1"
|
||||
workspace.workspace = true
|
||||
charts-rs = "0.3"
|
||||
regex = "1"
|
||||
chrono = "0.4"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ mod text_story;
|
|||
mod tooltip_story;
|
||||
mod webview_story;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use button_story::ButtonStory;
|
||||
pub use calendar_story::CalendarStory;
|
||||
pub use dropdown_story::DropdownStory;
|
||||
|
|
@ -35,18 +37,19 @@ pub use tooltip_story::TooltipStory;
|
|||
pub use webview_story::WebViewStory;
|
||||
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder as _, px, AnyElement, AnyView, AppContext, Div, EventEmitter,
|
||||
FocusableView, InteractiveElement, IntoElement, ParentElement, Pixels, Render, SharedString,
|
||||
div, prelude::FluentBuilder as _, px, AnyView, AppContext, Div, EventEmitter, FocusableView,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled as _, Task, View, ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
item::{Item, ItemEvent},
|
||||
Workspace, WorkspaceId,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use ui::{divider::Divider, h_flex, label::Label, v_flex};
|
||||
use ui::{
|
||||
divider::Divider,
|
||||
dock::{Panel, PanelEvent, TabPanel},
|
||||
h_flex,
|
||||
label::Label,
|
||||
v_flex,
|
||||
};
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
input_story::init(cx);
|
||||
|
|
@ -75,10 +78,8 @@ pub struct StoryContainer {
|
|||
focus_handle: gpui::FocusHandle,
|
||||
name: SharedString,
|
||||
description: SharedString,
|
||||
position: DockPosition,
|
||||
width: Option<gpui::Pixels>,
|
||||
height: Option<gpui::Pixels>,
|
||||
active: bool,
|
||||
story: Option<AnyView>,
|
||||
}
|
||||
|
||||
|
|
@ -93,43 +94,6 @@ pub enum ContainerEvent {
|
|||
Close,
|
||||
}
|
||||
|
||||
impl Item for StoryContainer {
|
||||
type Event = ContainerEvent;
|
||||
|
||||
fn tab_content(
|
||||
&self,
|
||||
_params: workspace::item::TabContentParams,
|
||||
_cx: &WindowContext,
|
||||
) -> AnyElement {
|
||||
Label::new(self.name.clone()).into_any_element()
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, _cx: &mut ViewContext<Self>) {
|
||||
self.active = false;
|
||||
}
|
||||
|
||||
fn workspace_deactivated(&mut self, _cx: &mut ViewContext<Self>) {
|
||||
self.active = false;
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_: Option<WorkspaceId>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<View<Self>> {
|
||||
Some(cx.new_view(|cx| {
|
||||
Self::new(self.name.clone(), self.description.clone(), cx)
|
||||
.story(self.story.clone().unwrap())
|
||||
}))
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
|
||||
match event {
|
||||
ContainerEvent::Close => f(ItemEvent::CloseItem),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ContainerEvent> for StoryContainer {}
|
||||
|
||||
impl StoryContainer {
|
||||
|
|
@ -146,8 +110,6 @@ impl StoryContainer {
|
|||
description: description.into(),
|
||||
width: None,
|
||||
height: None,
|
||||
position: DockPosition::Left,
|
||||
active: false,
|
||||
story: None,
|
||||
}
|
||||
}
|
||||
|
|
@ -156,36 +118,21 @@ impl StoryContainer {
|
|||
name: impl Into<SharedString>,
|
||||
description: impl Into<SharedString>,
|
||||
story: AnyView,
|
||||
workspace: View<Workspace>,
|
||||
tab_panel: View<TabPanel>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
let pane = workspace.read(cx).active_pane().clone();
|
||||
let name = name.into();
|
||||
let description = description.into();
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
tab_panel.update(&mut cx, |panel, cx| {
|
||||
let view = cx.new_view(|cx| Self::new(name, description, cx).story(story));
|
||||
|
||||
pane.add_item(Box::new(view.clone()), true, true, None, cx);
|
||||
panel.add_panel(Arc::new(view.clone()), cx);
|
||||
view
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_panel(
|
||||
story: AnyView,
|
||||
workspace: View<Workspace>,
|
||||
position: DockPosition,
|
||||
size: gpui::Pixels,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let panel = cx.new_view(|cx| MyPanel::new(story, position, size, cx));
|
||||
workspace.add_panel(panel, cx)
|
||||
});
|
||||
}
|
||||
|
||||
pub fn width(mut self, width: gpui::Pixels) -> Self {
|
||||
self.width = Some(width);
|
||||
self
|
||||
|
|
@ -196,17 +143,19 @@ impl StoryContainer {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn position(mut self, position: DockPosition) -> Self {
|
||||
self.position = position;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn story(mut self, story: AnyView) -> Self {
|
||||
self.story = Some(story);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for StoryContainer {
|
||||
fn title(&self, _cx: &WindowContext) -> SharedString {
|
||||
self.name.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for StoryContainer {}
|
||||
impl Render for StoryContainer {
|
||||
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
|
|
@ -219,7 +168,6 @@ impl Render for StoryContainer {
|
|||
.flex_col()
|
||||
.gap_4()
|
||||
.p_4()
|
||||
.child(Label::new(self.name.clone()).text_size(px(24.0)))
|
||||
.child(Label::new(self.description.clone()).text_size(px(16.0)))
|
||||
.child(Divider::horizontal().label("This is a divider")),
|
||||
)
|
||||
|
|
@ -235,82 +183,3 @@ impl Render for StoryContainer {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct MyPanel {
|
||||
focus_handle: gpui::FocusHandle,
|
||||
view: AnyView,
|
||||
_position: DockPosition,
|
||||
width: Option<Pixels>,
|
||||
height: Option<Pixels>,
|
||||
}
|
||||
|
||||
impl MyPanel {
|
||||
fn new(view: AnyView, position: DockPosition, size: Pixels, cx: &mut WindowContext) -> Self {
|
||||
let mut this = Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
view,
|
||||
_position: position,
|
||||
width: None,
|
||||
height: None,
|
||||
};
|
||||
this.update_size(size);
|
||||
this
|
||||
}
|
||||
|
||||
fn update_size(&mut self, size: Pixels) {
|
||||
match self._position {
|
||||
DockPosition::Bottom => self.height = Some(size),
|
||||
DockPosition::Left | DockPosition::Right => self.width = Some(size),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for MyPanel {
|
||||
fn persistent_name() -> &'static str {
|
||||
"my-panel"
|
||||
}
|
||||
|
||||
fn can_position(&self, position: DockPosition) -> bool {
|
||||
match self._position {
|
||||
DockPosition::Bottom => matches!(position, DockPosition::Bottom),
|
||||
DockPosition::Left | DockPosition::Right => {
|
||||
matches!(position, DockPosition::Left | DockPosition::Right)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn position(&self, _cx: &WindowContext) -> DockPosition {
|
||||
self._position
|
||||
}
|
||||
|
||||
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
|
||||
self._position = position;
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
fn size(&self, _cx: &WindowContext) -> gpui::Pixels {
|
||||
match self._position {
|
||||
DockPosition::Bottom => self.height.unwrap_or(px(100.)),
|
||||
DockPosition::Left | DockPosition::Right => self.width.unwrap_or(px(100.)),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_size(&mut self, size: Option<gpui::Pixels>, cx: &mut ViewContext<Self>) {
|
||||
if let Some(size) = size {
|
||||
self.update_size(size)
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for MyPanel {}
|
||||
impl FocusableView for MyPanel {
|
||||
fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
impl Render for MyPanel {
|
||||
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div().id("my-panel").size_full().child(self.view.clone())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ impl ResizableStory {
|
|||
resizable_panel()
|
||||
.size(px(300.))
|
||||
.min_size(px(100.))
|
||||
.grow()
|
||||
.content(|cx| panel_box("Right (Grow)", cx)),
|
||||
cx,
|
||||
),
|
||||
|
|
@ -63,7 +62,6 @@ impl ResizableStory {
|
|||
.size(px(150.))
|
||||
.max_size(px(550.))
|
||||
.min_size(px(100.))
|
||||
.grow()
|
||||
.content(|cx| panel_box("Center (Grow)", cx)),
|
||||
cx,
|
||||
)
|
||||
|
|
@ -90,7 +88,6 @@ impl ResizableStory {
|
|||
.size(px(400.))
|
||||
.max_size(px(550.))
|
||||
.min_size(px(100.))
|
||||
.grow()
|
||||
.content(|cx| panel_box("Right (Grow)", cx)),
|
||||
cx,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -86,3 +86,16 @@ Dropdown:
|
|||
en: "Please select"
|
||||
zh-CN: "请选择"
|
||||
zh-HK: "請選擇"
|
||||
Dock:
|
||||
Unnamed:
|
||||
en: Unnamed
|
||||
zh-CN: 未命名
|
||||
zh-HK: 未命名
|
||||
Zoom In:
|
||||
en: Zoom In
|
||||
zh-CN: 放大
|
||||
zh-HK: 放大
|
||||
Zoom Out:
|
||||
en: Zoom Out
|
||||
zh-CN: 缩小
|
||||
zh-HK: 縮小
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ use crate::{
|
|||
Disableable, Icon, Selectable, Sizable, Size,
|
||||
};
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder as _, px, AnyElement, ClickEvent, Div, ElementId, FocusHandle,
|
||||
Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, RenderOnce,
|
||||
SharedString, StatefulInteractiveElement as _, Styled, WindowContext,
|
||||
div, prelude::FluentBuilder as _, px, relative, AnyElement, ClickEvent, Div, ElementId,
|
||||
FocusHandle, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels,
|
||||
RenderOnce, SharedString, StatefulInteractiveElement as _, Styled, WindowContext,
|
||||
};
|
||||
|
||||
pub enum ButtonRounded {
|
||||
|
|
@ -372,6 +372,7 @@ impl RenderOnce for Button {
|
|||
Size::Small => this.text_sm(),
|
||||
_ => this.text_base(),
|
||||
})
|
||||
.line_height(relative(1.))
|
||||
.when(!self.loading, |this| {
|
||||
this.when_some(self.icon, |this, icon| {
|
||||
this.child(icon.with_size(icon_size))
|
||||
|
|
@ -457,7 +458,7 @@ impl ButtonStyle {
|
|||
ButtonStyle::Primary => cx.theme().primary_hover,
|
||||
ButtonStyle::Secondary | ButtonStyle::Outline => cx.theme().secondary_hover,
|
||||
ButtonStyle::Danger => cx.theme().destructive_hover,
|
||||
ButtonStyle::Ghost => cx.theme().secondary,
|
||||
ButtonStyle::Ghost => cx.theme().secondary_hover,
|
||||
ButtonStyle::Link => cx.theme().transparent,
|
||||
ButtonStyle::Text => cx.theme().transparent,
|
||||
ButtonStyle::Custom(colors) => colors.hover,
|
||||
|
|
|
|||
62
crates/ui/src/dock/mod.rs
Normal file
62
crates/ui/src/dock/mod.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
mod panel;
|
||||
mod stack_panel;
|
||||
mod tab_panel;
|
||||
|
||||
use gpui::{
|
||||
actions, div, prelude::FluentBuilder, AnyView, InteractiveElement as _, IntoElement,
|
||||
ParentElement as _, Render, Styled, View, ViewContext,
|
||||
};
|
||||
pub use panel::*;
|
||||
pub use stack_panel::*;
|
||||
pub use tab_panel::*;
|
||||
|
||||
use crate::theme::ActiveTheme;
|
||||
|
||||
actions!(dock, [ToggleZoom]);
|
||||
|
||||
/// The main area of the dock.
|
||||
pub struct DockArea {
|
||||
root: View<StackPanel>,
|
||||
zoom_view: Option<AnyView>,
|
||||
}
|
||||
|
||||
impl DockArea {
|
||||
pub fn new(root: View<StackPanel>, _cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
root,
|
||||
zoom_view: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggles the zoom view.
|
||||
pub fn toggle_zoom<P: Panel>(&mut self, panel: View<P>, cx: &mut ViewContext<Self>) {
|
||||
if self.zoom_view.is_some() {
|
||||
self.zoom_view = None;
|
||||
} else {
|
||||
self.zoom_view = Some(panel.into());
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for DockArea {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.id("dock-area")
|
||||
.flex()
|
||||
.flex_grow()
|
||||
.flex_shrink()
|
||||
.overflow_hidden()
|
||||
.map(|this| match self.zoom_view.clone() {
|
||||
Some(view) => this.bg(cx.theme().tab_bar).p_3().child(
|
||||
div()
|
||||
.size_full()
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.shadow_lg()
|
||||
.child(view),
|
||||
),
|
||||
None => this.child(self.root.clone()),
|
||||
})
|
||||
}
|
||||
}
|
||||
53
crates/ui/src/dock/panel.rs
Normal file
53
crates/ui/src/dock/panel.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
use gpui::{AnyView, EventEmitter, FocusableView, SharedString, View, WindowContext};
|
||||
use rust_i18n::t;
|
||||
|
||||
use super::PanelEvent;
|
||||
|
||||
pub trait Panel: EventEmitter<PanelEvent> + FocusableView {
|
||||
/// The title of the panel, default is `None`.
|
||||
fn title(&self, _cx: &WindowContext) -> SharedString {
|
||||
t!("Dock.Unnamed").into()
|
||||
}
|
||||
|
||||
/// Whether the panel can be closed, default is `true`.
|
||||
fn closeable(&self, _cx: &WindowContext) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PanelView: 'static + Send + Sync {
|
||||
/// The title of the panel, default is `None`.
|
||||
fn title(&self, _cx: &WindowContext) -> SharedString {
|
||||
t!("Dock.Unnamed").into()
|
||||
}
|
||||
|
||||
fn view(&self) -> AnyView;
|
||||
}
|
||||
|
||||
impl<T: Panel> PanelView for View<T> {
|
||||
fn title(&self, cx: &WindowContext) -> SharedString {
|
||||
self.read(cx).title(cx)
|
||||
}
|
||||
|
||||
fn view(&self) -> AnyView {
|
||||
self.clone().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&dyn PanelView> for AnyView {
|
||||
fn from(handle: &dyn PanelView) -> Self {
|
||||
handle.view()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Panel> From<&dyn PanelView> for View<T> {
|
||||
fn from(value: &dyn PanelView) -> Self {
|
||||
value.view().downcast::<T>().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for dyn PanelView {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.view() == other.view()
|
||||
}
|
||||
}
|
||||
272
crates/ui/src/dock/stack_panel.rs
Normal file
272
crates/ui/src/dock/stack_panel.rs
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
resizable::{h_resizable, resizable_panel, v_resizable, ResizablePanel, ResizablePanelGroup},
|
||||
theme::ActiveTheme,
|
||||
Placement,
|
||||
};
|
||||
|
||||
use super::{DockArea, Panel, PanelEvent, PanelView, TabPanel};
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder as _, px, Axis, DismissEvent, Entity, EventEmitter, FocusHandle,
|
||||
FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, View, ViewContext,
|
||||
VisualContext, WeakView,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
pub struct StackPanel {
|
||||
pub(super) parent: Option<View<StackPanel>>,
|
||||
pub(super) axis: Axis,
|
||||
focus_handle: FocusHandle,
|
||||
panels: SmallVec<[Arc<dyn PanelView>; 2]>,
|
||||
panel_group: View<ResizablePanelGroup>,
|
||||
}
|
||||
|
||||
impl Panel for StackPanel {}
|
||||
|
||||
impl StackPanel {
|
||||
pub fn new(axis: Axis, cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
axis,
|
||||
parent: None,
|
||||
focus_handle: cx.focus_handle(),
|
||||
panels: SmallVec::new(),
|
||||
panel_group: cx.new_view(|_| {
|
||||
if axis == Axis::Horizontal {
|
||||
h_resizable()
|
||||
} else {
|
||||
v_resizable()
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// The first level of the stack panel is root, will not have a parent.
|
||||
fn is_root(&self) -> bool {
|
||||
self.parent.is_none()
|
||||
}
|
||||
|
||||
pub(super) fn panels_len(&self) -> usize {
|
||||
self.panels.len()
|
||||
}
|
||||
|
||||
/// Return the index of the panel.
|
||||
pub fn index_of_panel<P>(&self, panel: View<P>) -> Option<usize>
|
||||
where
|
||||
P: Panel,
|
||||
{
|
||||
let entity_id = panel.entity_id();
|
||||
self.panels
|
||||
.iter()
|
||||
.position(|p| p.view().entity_id() == entity_id)
|
||||
}
|
||||
|
||||
/// Add a panel at the end of the stack.
|
||||
pub fn add_panel<P>(
|
||||
&mut self,
|
||||
panel: View<P>,
|
||||
size: Option<Pixels>,
|
||||
dock_area: WeakView<DockArea>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) where
|
||||
P: Panel,
|
||||
{
|
||||
self.insert_panel(panel, self.panels.len(), size, dock_area, cx);
|
||||
}
|
||||
|
||||
pub fn add_panel_at<P>(
|
||||
&mut self,
|
||||
panel: View<P>,
|
||||
ix: usize,
|
||||
placement: Placement,
|
||||
dock_area: WeakView<DockArea>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) where
|
||||
P: Panel,
|
||||
{
|
||||
match placement {
|
||||
Placement::Top | Placement::Left => self.insert_panel_before(panel, ix, dock_area, cx),
|
||||
Placement::Right | Placement::Bottom => {
|
||||
self.insert_panel_after(panel, ix, dock_area, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a panel at the index.
|
||||
pub fn insert_panel_before<P>(
|
||||
&mut self,
|
||||
panel: View<P>,
|
||||
ix: usize,
|
||||
dock_area: WeakView<DockArea>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) where
|
||||
P: Panel,
|
||||
{
|
||||
self.insert_panel(panel, ix, None, dock_area, cx);
|
||||
}
|
||||
|
||||
/// Insert a panel after the index.
|
||||
pub fn insert_panel_after<P>(
|
||||
&mut self,
|
||||
panel: View<P>,
|
||||
ix: usize,
|
||||
dock_area: WeakView<DockArea>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) where
|
||||
P: Panel,
|
||||
{
|
||||
self.insert_panel(panel, ix + 1, None, dock_area, cx);
|
||||
}
|
||||
|
||||
fn new_resizable_panel<P>(panel: View<P>, size: Option<Pixels>) -> ResizablePanel
|
||||
where
|
||||
P: Panel,
|
||||
{
|
||||
resizable_panel()
|
||||
.content_view(panel.view())
|
||||
.min_size(px(100.))
|
||||
.when_some(size, |this, size| this.size(size))
|
||||
}
|
||||
|
||||
fn insert_panel<P>(
|
||||
&mut self,
|
||||
panel: View<P>,
|
||||
ix: usize,
|
||||
size: Option<Pixels>,
|
||||
dock_area: WeakView<DockArea>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) where
|
||||
P: Panel,
|
||||
{
|
||||
// If the panel is already in the stack, return.
|
||||
if let Some(_) = self.index_of_panel(panel.clone()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dock_area = dock_area.clone();
|
||||
cx.subscribe(&panel, move |_, panel, event, cx| match event {
|
||||
PanelEvent::ZoomIn | PanelEvent::ZoomOut => {
|
||||
if let Some(dock) = dock_area.upgrade() {
|
||||
dock.update(cx, |dock, cx| {
|
||||
dock.toggle_zoom(panel.clone(), cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.spawn(|view, mut cx| {
|
||||
let panel = panel.clone();
|
||||
async move {
|
||||
if let Some(view) = view.upgrade() {
|
||||
cx.update(|cx| {
|
||||
// If the panel is a TabPanel, set its parent to this.
|
||||
if let Ok(tab_panel) = panel.view().downcast::<TabPanel>() {
|
||||
tab_panel.update(cx, |tab_panel, _| tab_panel.set_parent(view.clone()));
|
||||
} else if let Ok(stack_panel) = panel.view().downcast::<Self>() {
|
||||
stack_panel.update(cx, |stack_panel, _| {
|
||||
stack_panel.parent = Some(view.clone())
|
||||
});
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
self.panels.insert(ix, Arc::new(panel.clone()));
|
||||
self.panel_group.update(cx, |view, cx| {
|
||||
view.insert_child(Self::new_resizable_panel(panel, size), ix, cx)
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Remove panel from the stack.
|
||||
pub fn remove_panel<P>(&mut self, panel: View<P>, cx: &mut ViewContext<Self>)
|
||||
where
|
||||
P: Panel,
|
||||
{
|
||||
if let Some(ix) = self.index_of_panel(panel) {
|
||||
self.panels.remove(ix);
|
||||
self.panel_group.update(cx, |view, cx| {
|
||||
view.remove_child(ix, cx);
|
||||
});
|
||||
|
||||
self.remove_self_if_empty(cx);
|
||||
} else {
|
||||
println!("Panel not found in stack panel.");
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace the old panel with the new panel at same index.
|
||||
pub(super) fn replace_panel<P>(
|
||||
&mut self,
|
||||
old_panel: View<P>,
|
||||
new_panel: View<StackPanel>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) where
|
||||
P: Panel,
|
||||
{
|
||||
if let Some(ix) = self.index_of_panel(old_panel) {
|
||||
self.panels[ix] = Arc::new(new_panel.clone());
|
||||
self.panel_group.update(cx, |view, cx| {
|
||||
view.replace_child(Self::new_resizable_panel(new_panel.clone(), None), ix, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// If children is empty, remove self from parent view.
|
||||
pub(crate) fn remove_self_if_empty(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if self.is_root() {
|
||||
return;
|
||||
}
|
||||
|
||||
if !self.panels.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let view = cx.view().clone();
|
||||
if let Some(parent) = self.parent.as_ref() {
|
||||
parent.update(cx, |parent, cx| {
|
||||
parent.remove_panel(view, cx);
|
||||
});
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Remove all panels from the stack.
|
||||
pub(super) fn remove_all_panels(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.panels.clear();
|
||||
self.panel_group
|
||||
.update(cx, |view, cx| view.remove_all_children(cx));
|
||||
}
|
||||
|
||||
/// Change the axis of the stack panel.
|
||||
pub(super) fn set_axis(&mut self, axis: Axis, cx: &mut ViewContext<Self>) {
|
||||
self.axis = axis;
|
||||
self.panel_group
|
||||
.update(cx, |view, cx| view.set_axis(axis, cx));
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for StackPanel {
|
||||
fn focus_handle(&self, _cx: &gpui::AppContext) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
impl EventEmitter<PanelEvent> for StackPanel {}
|
||||
impl EventEmitter<DismissEvent> for StackPanel {}
|
||||
impl Render for StackPanel {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.bg(cx.theme().tab_bar)
|
||||
.child(self.panel_group.clone())
|
||||
}
|
||||
}
|
||||
502
crates/ui/src/dock/tab_panel.rs
Normal file
502
crates/ui/src/dock/tab_panel.rs
Normal file
|
|
@ -0,0 +1,502 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder, rems, AnchorCorner, AppContext, DefiniteLength, DismissEvent,
|
||||
DragMoveEvent, Empty, EventEmitter, FocusHandle, FocusableView, InteractiveElement as _,
|
||||
IntoElement, ParentElement, Render, ScrollHandle, StatefulInteractiveElement, Styled, View,
|
||||
ViewContext, VisualContext as _, WeakView,
|
||||
};
|
||||
use rust_i18n::t;
|
||||
|
||||
use crate::{
|
||||
button::Button,
|
||||
h_flex,
|
||||
popup_menu::PopupMenuExt,
|
||||
tab::{Tab, TabBar},
|
||||
theme::ActiveTheme,
|
||||
v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt,
|
||||
};
|
||||
|
||||
use super::{DockArea, Panel, PanelView, StackPanel, ToggleZoom};
|
||||
|
||||
pub enum PanelEvent {
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct DragPanel {
|
||||
pub(crate) panel: Arc<dyn PanelView>,
|
||||
pub(crate) tab_panel: View<TabPanel>,
|
||||
}
|
||||
|
||||
impl DragPanel {
|
||||
pub(crate) fn new(panel: Arc<dyn PanelView>, tab_panel: View<TabPanel>) -> Self {
|
||||
Self { panel, tab_panel }
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for DragPanel {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.cursor_grab()
|
||||
.py_1()
|
||||
.px_3()
|
||||
.w_24()
|
||||
.overflow_hidden()
|
||||
.whitespace_nowrap()
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.rounded_md()
|
||||
.bg(cx.theme().tab_active)
|
||||
.opacity(0.75)
|
||||
.child(self.panel.title(cx))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TabPanel {
|
||||
focus_handle: FocusHandle,
|
||||
dock_area: WeakView<DockArea>,
|
||||
stack_panel: Option<View<StackPanel>>,
|
||||
panels: Vec<Arc<dyn PanelView>>,
|
||||
active_ix: usize,
|
||||
tab_bar_scroll_handle: ScrollHandle,
|
||||
|
||||
is_zoomed: bool,
|
||||
|
||||
/// When drag move, will get the placement of the panel to be split
|
||||
will_split_placement: Option<Placement>,
|
||||
}
|
||||
|
||||
impl TabPanel {
|
||||
pub fn new(dock_area: WeakView<DockArea>, cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
dock_area,
|
||||
stack_panel: None,
|
||||
panels: Vec::new(),
|
||||
active_ix: 0,
|
||||
tab_bar_scroll_handle: ScrollHandle::new(),
|
||||
will_split_placement: None,
|
||||
is_zoomed: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set_parent(&mut self, parent: View<StackPanel>) {
|
||||
self.stack_panel = Some(parent);
|
||||
}
|
||||
|
||||
/// Return current active_panel View
|
||||
pub fn active_panel(&self) -> Option<Arc<dyn PanelView>> {
|
||||
self.panels.get(self.active_ix).cloned()
|
||||
}
|
||||
|
||||
fn set_active_ix(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
|
||||
self.active_ix = ix;
|
||||
self.tab_bar_scroll_handle.scroll_to_item(ix);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Add a panel to the end of the tabs
|
||||
pub fn add_panel(&mut self, panel: Arc<dyn PanelView>, cx: &mut ViewContext<Self>) {
|
||||
if self
|
||||
.panels
|
||||
.iter()
|
||||
.any(|p| p.view().entity_id() == panel.view().entity_id())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.panels.push(panel);
|
||||
// set the active panel to the new panel
|
||||
self.set_active_ix(self.panels.len() - 1, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn insert_panel_at(
|
||||
&mut self,
|
||||
panel: Arc<dyn PanelView>,
|
||||
ix: usize,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if self
|
||||
.panels
|
||||
.iter()
|
||||
.any(|p| p.view().entity_id() == panel.view().entity_id())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.panels.insert(ix, panel);
|
||||
self.set_active_ix(ix, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Remove a panel from the tab panel
|
||||
pub fn remove_panel(&mut self, panel: Arc<dyn PanelView>, cx: &mut ViewContext<Self>) {
|
||||
self.detach_panel(panel, cx);
|
||||
self.remove_self_if_empty(cx)
|
||||
}
|
||||
|
||||
fn detach_panel(&mut self, panel: Arc<dyn PanelView>, cx: &mut ViewContext<Self>) {
|
||||
let panel_view = panel.view();
|
||||
self.panels.retain(|p| p.view() != panel_view);
|
||||
if self.active_ix >= self.panels.len() {
|
||||
self.set_active_ix(self.panels.len().saturating_sub(1), cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check to remove self from the parent StackPanel, if there is no panel left
|
||||
fn remove_self_if_empty(&self, cx: &mut ViewContext<Self>) {
|
||||
if !self.panels.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let tab_view = cx.view().clone();
|
||||
if let Some(stack_panel) = self.stack_panel.as_ref() {
|
||||
stack_panel.update(cx, |view, cx| {
|
||||
view.remove_panel(tab_view, cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn render_menu_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let is_zoomed = self.is_zoomed;
|
||||
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.occlude()
|
||||
.items_center()
|
||||
.when(self.is_zoomed, |this| {
|
||||
this.child(
|
||||
Button::new("zoom", cx)
|
||||
.icon(IconName::Minimize)
|
||||
.xsmall()
|
||||
.ghost()
|
||||
.on_click(
|
||||
cx.listener(|view, _, cx| view.on_action_toggle_zoom(&ToggleZoom, cx)),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Button::new("menu", cx)
|
||||
.icon(IconName::Ellipsis)
|
||||
.xsmall()
|
||||
.ghost()
|
||||
.popup_menu(move |this, _| {
|
||||
this.menu(
|
||||
if is_zoomed {
|
||||
t!("Dock.Zoom Out")
|
||||
} else {
|
||||
t!("Dock.Zoom In")
|
||||
},
|
||||
Box::new(ToggleZoom),
|
||||
)
|
||||
})
|
||||
.anchor(AnchorCorner::TopRight),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_tabs(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let view = cx.view().clone();
|
||||
|
||||
if self.panels.len() == 1 {
|
||||
let panel = self.panels.get(0).unwrap();
|
||||
|
||||
return h_flex()
|
||||
.id("tab")
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.py_2()
|
||||
.px_3()
|
||||
.line_height(rems(1.0))
|
||||
.child(panel.title(cx))
|
||||
.child(self.render_menu_button(cx))
|
||||
.on_drag(
|
||||
DragPanel {
|
||||
panel: panel.clone(),
|
||||
tab_panel: view,
|
||||
},
|
||||
|drag, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.new_view(|_| drag.clone())
|
||||
},
|
||||
)
|
||||
.into_any_element();
|
||||
}
|
||||
|
||||
let tabs_count = self.panels.len();
|
||||
|
||||
TabBar::new("tab-bar")
|
||||
.track_scroll(self.tab_bar_scroll_handle.clone())
|
||||
.children(self.panels.iter().enumerate().map(|(ix, panel)| {
|
||||
let active = ix == self.active_ix;
|
||||
Tab::new(("tab", ix), panel.title(cx))
|
||||
.selected(active)
|
||||
.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())
|
||||
})
|
||||
.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
|
||||
div()
|
||||
.id("tab-bar-empty-space")
|
||||
.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;
|
||||
|
||||
let ix = if drag.tab_panel == view {
|
||||
Some(tabs_count - 1)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
this.on_drop(drag, ix, cx)
|
||||
})),
|
||||
)
|
||||
.suffix(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.top_0()
|
||||
.right_0()
|
||||
.border_l_1()
|
||||
.h_full()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().tab_bar)
|
||||
.px_3()
|
||||
.child(self.render_menu_button(cx)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_active_panel(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
self.active_panel()
|
||||
.map(|panel| {
|
||||
div()
|
||||
.id("tab-content")
|
||||
.group("")
|
||||
.overflow_y_scroll()
|
||||
.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.25);
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
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())
|
||||
}
|
||||
|
||||
/// Calculate the split direction based on the current mouse position
|
||||
fn on_panel_drag_move(&mut self, drag: &DragMoveEvent<DragPanel>, cx: &mut ViewContext<Self>) {
|
||||
let bounds = drag.bounds;
|
||||
let position = drag.event.position;
|
||||
|
||||
// Check the mouse position to determine the split direction
|
||||
if position.x < bounds.left() + bounds.size.width * 0.25 {
|
||||
self.will_split_placement = Some(Placement::Left);
|
||||
} else if position.x > bounds.left() + bounds.size.width * 0.75 {
|
||||
self.will_split_placement = Some(Placement::Right);
|
||||
} else if position.y < bounds.top() + bounds.size.height * 0.25 {
|
||||
self.will_split_placement = Some(Placement::Top);
|
||||
} else if position.y > bounds.top() + bounds.size.height * 0.75 {
|
||||
self.will_split_placement = Some(Placement::Bottom);
|
||||
} else {
|
||||
// center to merge into the current tab
|
||||
self.will_split_placement = None;
|
||||
}
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
fn on_drop(&mut self, drag: &DragPanel, ix: Option<usize>, cx: &mut ViewContext<Self>) {
|
||||
let panel = drag.panel.clone();
|
||||
let is_same_tab = drag.tab_panel == *cx.view();
|
||||
|
||||
// If target is same tab, and it is only one panel, do nothing.
|
||||
if is_same_tab && ix.is_none() {
|
||||
if self.will_split_placement.is_none() {
|
||||
return;
|
||||
} else {
|
||||
if self.panels.len() == 1 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Here is looks like remove_panel on a same item, but it differnece.
|
||||
//
|
||||
// We must to split it to remove_panel, unless it will be crash by error:
|
||||
// Cannot update ui::dock::tab_panel::TabPanel while it is already being updated
|
||||
if is_same_tab {
|
||||
self.detach_panel(panel.clone(), cx);
|
||||
} else {
|
||||
let _ = drag.tab_panel.update(cx, |view, cx| {
|
||||
view.detach_panel(panel.clone(), cx);
|
||||
view.remove_self_if_empty(cx);
|
||||
});
|
||||
}
|
||||
|
||||
// Insert into new tabs
|
||||
if let Some(placement) = self.will_split_placement {
|
||||
self.split_panel(panel, placement, cx);
|
||||
} else {
|
||||
if let Some(ix) = ix {
|
||||
self.insert_panel_at(panel, ix, cx)
|
||||
} else {
|
||||
self.add_panel(panel, cx)
|
||||
}
|
||||
}
|
||||
|
||||
self.remove_self_if_empty(cx);
|
||||
}
|
||||
|
||||
/// Add panel with split placement
|
||||
fn split_panel(
|
||||
&self,
|
||||
panel: Arc<dyn PanelView>,
|
||||
placement: Placement,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let dock_area = self.dock_area.clone();
|
||||
// wrap the panel in a TabPanel
|
||||
let new_tab_panel = cx.new_view(|cx| Self::new(dock_area.clone(), cx));
|
||||
new_tab_panel.update(cx, |view, cx| {
|
||||
view.add_panel(panel, cx);
|
||||
});
|
||||
|
||||
let stack_panel = self.stack_panel.as_ref().unwrap();
|
||||
let parent_axis = stack_panel.read(cx).axis;
|
||||
let ix = stack_panel
|
||||
.read(cx)
|
||||
.index_of_panel(cx.view().clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
if parent_axis.is_vertical() && placement.is_vertical() {
|
||||
stack_panel.update(cx, |view, cx| {
|
||||
view.add_panel_at(new_tab_panel, ix, placement, dock_area.clone(), cx);
|
||||
});
|
||||
} else if parent_axis.is_horizontal() && placement.is_horizontal() {
|
||||
stack_panel.update(cx, |view, cx| {
|
||||
view.add_panel_at(new_tab_panel, ix, placement, dock_area.clone(), cx);
|
||||
});
|
||||
} else {
|
||||
// 1. Create new StackPanel with new axis
|
||||
// 2. Move cx.view() from parent StackPanel to the new StackPanel
|
||||
// 3. Add the new TabPanel to the new StackPanel at the correct index
|
||||
// 4. Add new StackPanel to the parent StackPanel at the correct index
|
||||
let tab_panel = cx.view().clone();
|
||||
|
||||
// Try to use the old stack panel, not just create a new one, to avoid too many nested stack panels
|
||||
let new_stack_panel = if stack_panel.read(cx).panels_len() <= 1 {
|
||||
stack_panel.update(cx, |view, cx| {
|
||||
view.remove_all_panels(cx);
|
||||
view.set_axis(placement.axis(), cx);
|
||||
});
|
||||
stack_panel.clone()
|
||||
} else {
|
||||
cx.new_view(|cx| {
|
||||
let mut panel = StackPanel::new(placement.axis(), cx);
|
||||
panel.parent = Some(stack_panel.clone());
|
||||
panel
|
||||
})
|
||||
};
|
||||
|
||||
new_stack_panel.update(cx, |view, cx| match placement {
|
||||
Placement::Left | Placement::Top => {
|
||||
view.add_panel(new_tab_panel, None, dock_area.clone(), cx);
|
||||
view.add_panel(tab_panel.clone(), None, dock_area.clone(), cx);
|
||||
}
|
||||
Placement::Right | Placement::Bottom => {
|
||||
view.add_panel(tab_panel.clone(), None, dock_area.clone(), cx);
|
||||
view.add_panel(new_tab_panel, None, dock_area.clone(), cx);
|
||||
}
|
||||
});
|
||||
|
||||
if *stack_panel != new_stack_panel {
|
||||
stack_panel.update(cx, |view, cx| {
|
||||
view.replace_panel(tab_panel.clone(), new_stack_panel.clone(), cx);
|
||||
});
|
||||
}
|
||||
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
cx.update(|cx| tab_panel.update(cx, |view, cx| view.remove_self_if_empty(cx)))
|
||||
})
|
||||
.detach()
|
||||
}
|
||||
}
|
||||
|
||||
fn on_action_toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
|
||||
self.is_zoomed = !self.is_zoomed;
|
||||
if self.is_zoomed {
|
||||
cx.emit(PanelEvent::ZoomIn)
|
||||
} else {
|
||||
cx.emit(PanelEvent::ZoomOut)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for TabPanel {}
|
||||
impl FocusableView for TabPanel {
|
||||
fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle {
|
||||
// FIXME: Delegate to the active panel
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
impl EventEmitter<DismissEvent> for TabPanel {}
|
||||
impl EventEmitter<PanelEvent> for TabPanel {}
|
||||
impl Render for TabPanel {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl gpui::IntoElement {
|
||||
v_flex()
|
||||
.id("tab-panel")
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(cx.listener(Self::on_action_toggle_zoom))
|
||||
.size_full()
|
||||
.flex_grow()
|
||||
.flex_shrink()
|
||||
.flex_none()
|
||||
.overflow_hidden()
|
||||
.bg(cx.theme().background)
|
||||
.child(self.render_tabs(cx))
|
||||
.child(self.render_active_panel(cx))
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ pub mod checkbox;
|
|||
pub mod clipboard;
|
||||
pub mod context_menu;
|
||||
pub mod divider;
|
||||
pub mod dock;
|
||||
pub mod drawer;
|
||||
pub mod dropdown;
|
||||
pub mod history;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
canvas, div, prelude::FluentBuilder as _, px, AnyElement, AnyView, Axis, Bounds, DragMoveEvent,
|
||||
EntityId, InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels, Render,
|
||||
StatefulInteractiveElement, Styled, View, ViewContext, VisualContext as _, WindowContext,
|
||||
canvas, deferred, div, prelude::FluentBuilder, px, AnyElement, AnyView, Axis, Bounds,
|
||||
DragMoveEvent, EntityId, InteractiveElement as _, IntoElement, MouseButton, ParentElement,
|
||||
Pixels, Render, StatefulInteractiveElement, Styled, View, ViewContext, VisualContext as _,
|
||||
WindowContext,
|
||||
};
|
||||
|
||||
use crate::{h_flex, theme::ActiveTheme, v_flex, AxisExt};
|
||||
|
||||
const HANDLE_PADDING: Pixels = px(4.);
|
||||
|
||||
#[derive(Clone, Render)]
|
||||
pub struct DragPanel(pub (EntityId, usize, Axis));
|
||||
|
||||
|
|
@ -27,7 +30,7 @@ impl ResizablePanelGroup {
|
|||
axis: Axis::Horizontal,
|
||||
sizes: Vec::new(),
|
||||
panels: Vec::new(),
|
||||
handle_size: px(3.),
|
||||
handle_size: px(1.),
|
||||
size: px(20.),
|
||||
resizing_panel_ix: None,
|
||||
}
|
||||
|
|
@ -39,6 +42,11 @@ impl ResizablePanelGroup {
|
|||
self
|
||||
}
|
||||
|
||||
pub(crate) fn set_axis(&mut self, axis: Axis, cx: &mut ViewContext<Self>) {
|
||||
self.axis = axis;
|
||||
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.
|
||||
|
|
@ -49,10 +57,7 @@ impl ResizablePanelGroup {
|
|||
|
||||
/// Add a resizable panel to the group.
|
||||
pub fn child(mut self, panel: ResizablePanel, cx: &mut WindowContext) -> Self {
|
||||
let mut panel = panel;
|
||||
panel.axis = self.axis;
|
||||
self.sizes.push(panel.size);
|
||||
self.panels.push(cx.new_view(|_| panel));
|
||||
self.add_child(panel, cx);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -76,63 +81,133 @@ impl ResizablePanelGroup {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn add_child(&mut self, panel: ResizablePanel, cx: &mut WindowContext) {
|
||||
let mut panel = panel;
|
||||
panel.axis = self.axis;
|
||||
self.sizes.push(panel.size);
|
||||
self.panels.push(cx.new_view(|_| panel));
|
||||
}
|
||||
|
||||
pub fn insert_child(&mut self, panel: ResizablePanel, ix: usize, cx: &mut ViewContext<Self>) {
|
||||
let mut panel = panel;
|
||||
panel.axis = self.axis;
|
||||
self.sizes.insert(ix, panel.size);
|
||||
self.panels.insert(ix, cx.new_view(|_| panel));
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
/// Replace a child panel with a new panel at the given index.
|
||||
pub(crate) fn replace_child(
|
||||
&mut self,
|
||||
panel: ResizablePanel,
|
||||
ix: usize,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let mut panel = panel;
|
||||
panel.axis = self.axis;
|
||||
self.sizes[ix] = panel.size;
|
||||
self.panels[ix] = cx.new_view(|_| panel);
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
pub fn remove_child(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
|
||||
self.sizes.remove(ix);
|
||||
self.panels.remove(ix);
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
pub(crate) fn remove_all_children(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.sizes.clear();
|
||||
self.panels.clear();
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
fn render_resize_handle(&self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let axis = self.axis;
|
||||
let handle_size = self.handle_size;
|
||||
let is_resizing = self.resizing_panel_ix == Some(ix);
|
||||
|
||||
div()
|
||||
.id(("resizable-handle", ix))
|
||||
.occlude()
|
||||
.hover(|this| this.bg(cx.theme().drag_border))
|
||||
.when(is_resizing, |this| this.bg(cx.theme().drag_border))
|
||||
.when(self.axis.is_horizontal(), |this| {
|
||||
this.cursor_col_resize().top_0().h_full().w(handle_size)
|
||||
})
|
||||
.when(self.axis.is_vertical(), |this| {
|
||||
this.cursor_row_resize().left_0().w_full().h(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 {
|
||||
let neg_offset = -HANDLE_PADDING;
|
||||
|
||||
deferred(
|
||||
div()
|
||||
.id(("resizable-handle", ix))
|
||||
.occlude()
|
||||
.absolute()
|
||||
.flex_shrink_0()
|
||||
.when(self.axis.is_horizontal(), |this| {
|
||||
this.cursor_col_resize()
|
||||
.top_0()
|
||||
.right(neg_offset)
|
||||
.h_full()
|
||||
.w(px(0.))
|
||||
.px(HANDLE_PADDING)
|
||||
})
|
||||
.when(self.axis.is_vertical(), |this| {
|
||||
this.cursor_row_resize()
|
||||
.bottom(neg_offset)
|
||||
.left_0()
|
||||
.w_full()
|
||||
.h(px(0.))
|
||||
.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_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;
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
.on_mouse_up_out(
|
||||
MouseButton::Left,
|
||||
cx.listener(|view, _, _| {
|
||||
if view.resizing_panel_ix.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
let ix = *ix;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
))
|
||||
.on_mouse_up_out(
|
||||
MouseButton::Left,
|
||||
cx.listener(|view, _, _| {
|
||||
if view.resizing_panel_ix.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
view.resizing_panel_ix = None;
|
||||
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())
|
||||
}),
|
||||
)
|
||||
.on_drag(DragPanel((cx.entity_id(), ix, axis)), |drag_panel, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.new_view(|_| drag_panel.clone())
|
||||
})
|
||||
)
|
||||
.with_priority(0)
|
||||
}
|
||||
|
||||
fn sync_real_panel_sizes(&mut self, cx: &WindowContext) {
|
||||
|
|
@ -154,23 +229,35 @@ impl ResizablePanelGroup {
|
|||
}
|
||||
let size = size.floor();
|
||||
|
||||
// 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.
|
||||
|
||||
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;
|
||||
let changed_size = (new_size - old_size).floor();
|
||||
|
||||
// If change size is less than 1px, do nothing.
|
||||
if changed_size > px(-1.0) && changed_size < px(1.0) {
|
||||
return;
|
||||
}
|
||||
self.sizes[ix] = new_size;
|
||||
|
||||
let next_size = self.sizes[ix + 1];
|
||||
self.sizes[ix + 1] = self.panels[ix + 1]
|
||||
.read(cx)
|
||||
.limit_size(next_size - changed_size);
|
||||
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;
|
||||
}
|
||||
|
||||
self.sizes[ix] = new_size;
|
||||
self.panels[ix].update(cx, |this, _| this.size = new_size);
|
||||
self.sizes[ix + 1] = next_new_size;
|
||||
|
||||
for (i, panel) in self.panels.iter_mut().enumerate() {
|
||||
let size = self.sizes[i];
|
||||
|
|
@ -181,21 +268,33 @@ impl ResizablePanelGroup {
|
|||
|
||||
impl Render for ResizablePanelGroup {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let mut children: Vec<AnyElement> = vec![];
|
||||
for (ix, panel) in self.panels.iter().enumerate() {
|
||||
children.push(panel.clone().into_any_element());
|
||||
if ix < self.panels.len() - 1 {
|
||||
children.push(self.render_resize_handle(ix, cx).into_any_element());
|
||||
}
|
||||
}
|
||||
|
||||
let container = if self.axis.is_horizontal() {
|
||||
h_flex()
|
||||
} else {
|
||||
v_flex()
|
||||
};
|
||||
|
||||
container.size_full().children(children)
|
||||
container
|
||||
.size_full()
|
||||
.flex_grow()
|
||||
.flex_shrink()
|
||||
// .map(|this| {
|
||||
// use crate::StyledExt as _;
|
||||
// match self.axis {
|
||||
// Axis::Horizontal => this.debug_red(),
|
||||
// Axis::Vertical => this.debug_blue(),
|
||||
// }
|
||||
// })
|
||||
.children(self.panels.iter().enumerate().map(|(ix, panel)| {
|
||||
if ix < self.panels.len() - 1 {
|
||||
let handle = self.render_resize_handle(ix, cx);
|
||||
panel.update(cx, |view, _| {
|
||||
view.resize_handle = Some(handle.into_any_element())
|
||||
});
|
||||
}
|
||||
|
||||
panel.clone()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -208,8 +307,7 @@ pub struct ResizablePanel {
|
|||
content_view: Option<AnyView>,
|
||||
/// The bounds of the resizable panel, when render the bounds will be updated.
|
||||
bounds: Bounds<Pixels>,
|
||||
|
||||
grow: bool,
|
||||
resize_handle: Option<AnyElement>,
|
||||
}
|
||||
|
||||
impl ResizablePanel {
|
||||
|
|
@ -222,7 +320,7 @@ impl ResizablePanel {
|
|||
content_builder: None,
|
||||
content_view: None,
|
||||
bounds: Bounds::default(),
|
||||
grow: false,
|
||||
resize_handle: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -269,23 +367,20 @@ impl ResizablePanel {
|
|||
|
||||
size
|
||||
}
|
||||
|
||||
/// Set the panel to grow to fill the remaining space.
|
||||
pub fn grow(mut self) -> Self {
|
||||
self.grow = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.relative()
|
||||
.when(self.grow, |this| this.flex_grow())
|
||||
.size_full()
|
||||
.flex_grow()
|
||||
.flex_shrink()
|
||||
.when(self.axis.is_vertical(), |this| this.h(size))
|
||||
.when(self.axis.is_horizontal(), |this| this.w(size))
|
||||
.child({
|
||||
|
|
@ -298,5 +393,6 @@ impl Render for ResizablePanel {
|
|||
})
|
||||
.when_some(self.content_builder.clone(), |this, c| this.child(c(cx)))
|
||||
.when_some(self.content_view.clone(), |this, c| this.child(c))
|
||||
.when_some(self.resize_handle.take(), |this, c| this.child(c))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
use crate::{
|
||||
scroll::{Scrollable, ScrollbarAxis},
|
||||
theme::{ActiveTheme, Colorize},
|
||||
|
|
@ -331,18 +333,36 @@ pub enum Placement {
|
|||
Right,
|
||||
}
|
||||
|
||||
impl Display for Placement {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Placement::Top => write!(f, "Top"),
|
||||
Placement::Bottom => write!(f, "Bottom"),
|
||||
Placement::Left => write!(f, "Left"),
|
||||
Placement::Right => write!(f, "Right"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Placement {
|
||||
pub fn is_horizontal(&self) -> bool {
|
||||
match self {
|
||||
Placement::Top | Placement::Bottom => true,
|
||||
Placement::Left | Placement::Right => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_vertical(&self) -> bool {
|
||||
match self {
|
||||
Placement::Left | Placement::Right => true,
|
||||
Placement::Top | Placement::Bottom => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn axis(&self) -> Axis {
|
||||
match self {
|
||||
Placement::Top | Placement::Bottom => Axis::Vertical,
|
||||
Placement::Left | Placement::Right => Axis::Horizontal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ pub struct Tab {
|
|||
}
|
||||
|
||||
impl Tab {
|
||||
pub fn new(id: impl Into<ElementId>, label: impl Into<AnyElement>) -> Self {
|
||||
pub fn new(id: impl Into<ElementId>, label: impl IntoElement) -> Self {
|
||||
Self {
|
||||
base: div().id(id.into()).gap_1().py_1p5().px_3().h_8(),
|
||||
label: label.into(),
|
||||
label: label.into_any_element(),
|
||||
disabled: false,
|
||||
selected: false,
|
||||
prefix: None,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use crate::h_flex;
|
|||
use crate::theme::ActiveTheme;
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, AnyElement, Div, IntoElement, ParentElement, RenderOnce, ScrollHandle, SharedString,
|
||||
div, AnyElement, Div, ElementId, IntoElement, ParentElement, RenderOnce, ScrollHandle,
|
||||
StatefulInteractiveElement as _, Styled, WindowContext,
|
||||
};
|
||||
use gpui::{px, InteractiveElement};
|
||||
|
|
@ -11,7 +11,7 @@ use smallvec::SmallVec;
|
|||
#[derive(IntoElement)]
|
||||
pub struct TabBar {
|
||||
base: Div,
|
||||
id: SharedString,
|
||||
id: ElementId,
|
||||
scroll_handle: ScrollHandle,
|
||||
prefix: Option<AnyElement>,
|
||||
suffix: Option<AnyElement>,
|
||||
|
|
@ -19,7 +19,7 @@ pub struct TabBar {
|
|||
}
|
||||
|
||||
impl TabBar {
|
||||
pub fn new(id: impl Into<SharedString>) -> Self {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
base: div().h_8().px(px(-1.)),
|
||||
id: id.into(),
|
||||
|
|
@ -37,14 +37,14 @@ impl TabBar {
|
|||
}
|
||||
|
||||
/// Set the prefix element of the TabBar
|
||||
pub fn prefix(mut self, prefix: impl Into<AnyElement>) -> Self {
|
||||
self.prefix = Some(prefix.into());
|
||||
pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
|
||||
self.prefix = Some(prefix.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the suffix element of the TabBar
|
||||
pub fn suffix(mut self, suffix: impl Into<AnyElement>) -> Self {
|
||||
self.suffix = Some(suffix.into());
|
||||
pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
|
||||
self.suffix = Some(suffix.into_any_element());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -71,9 +71,9 @@ impl RenderOnce for TabBar {
|
|||
.flex()
|
||||
.flex_none()
|
||||
.items_center()
|
||||
.bg(theme.tab_bar)
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(theme.tab_bar)
|
||||
.text_color(theme.tab_foreground)
|
||||
.when_some(self.prefix, |this, prefix| this.child(prefix))
|
||||
// The child will append to this level
|
||||
|
|
|
|||
|
|
@ -179,9 +179,9 @@ impl Colors {
|
|||
primary_active: hsl(223.0, 5.9, 45.0),
|
||||
primary_foreground: hsl(223.0, 0.0, 98.0),
|
||||
secondary: hsl(240.0, 4.8, 95.9),
|
||||
secondary_hover: hsl(240.0, 4.8, 99.),
|
||||
secondary_active: hsl(240.0, 5.9, 94.0),
|
||||
secondary_foreground: hsl(240.0, 59.0, 10.0),
|
||||
secondary_hover: hsl(240.0, 5.8, 10.).opacity(0.05),
|
||||
secondary_active: hsl(240.0, 5.9, 10.).opacity(0.1),
|
||||
secondary_foreground: hsl(240.0, 59.0, 10.),
|
||||
destructive: hsl(0.0, 84.2, 60.2),
|
||||
destructive_hover: hsl(0.0, 84.2, 65.0),
|
||||
destructive_active: hsl(0.0, 84.2, 47.0),
|
||||
|
|
@ -220,8 +220,8 @@ impl Colors {
|
|||
primary_active: hsl(223.0, 0.0, 60.0),
|
||||
primary_foreground: hsl(223.0, 5.9, 10.0),
|
||||
secondary: hsl(240.0, 3.7, 15.9),
|
||||
secondary_hover: hsl(240.0, 3.7, 20.9),
|
||||
secondary_active: hsl(240.0, 3.7, 8.9),
|
||||
secondary_hover: hsl(240.0, 3.7, 20.9).opacity(0.5),
|
||||
secondary_active: hsl(240.0, 3.7, 20.9).opacity(0.8),
|
||||
secondary_foreground: hsl(0.0, 0.0, 98.0),
|
||||
destructive: hsl(0.0, 62.8, 30.6),
|
||||
destructive_hover: hsl(0.0, 62.8, 35.6),
|
||||
|
|
@ -365,7 +365,7 @@ impl From<Colors> for Theme {
|
|||
panel: colors.panel,
|
||||
selection: colors.selection,
|
||||
drag_border: crate::blue_500(),
|
||||
drop_target: hsl(240.0, 65., 44.0).opacity(0.15),
|
||||
drop_target: hsl(220.0, 65., 44.0).opacity(0.15),
|
||||
tab_bar: colors.tab_bar,
|
||||
tab: gpui::transparent_black(),
|
||||
tab_active: colors.background,
|
||||
|
|
|
|||
Loading…
Reference in a new issue