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:
Jason Lee 2024-08-23 18:01:29 +08:00 committed by GitHub
parent 7e9d79ca1b
commit c5c8e46ac7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1198 additions and 291 deletions

1
Cargo.lock generated
View file

@ -4915,7 +4915,6 @@ dependencies = [
"regex",
"serde",
"ui",
"workspace",
]
[[package]]

View file

@ -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);

View file

@ -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"

View file

@ -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())
}
}

View file

@ -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,
)

View file

@ -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: 縮小

View file

@ -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
View 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()),
})
}
}

View 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()
}
}

View 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())
}
}

View 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))
}
}

View file

@ -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;

View file

@ -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))
}
}

View file

@ -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,
}
}
}

View file

@ -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,

View file

@ -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

View file

@ -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,