Update dock layout

This commit is contained in:
Jason Lee 2024-07-04 01:05:32 +08:00
parent 19930e46db
commit f78d50b5e9
13 changed files with 279 additions and 159 deletions

View file

@ -4,7 +4,7 @@ use story::{
ButtonStory, CheckboxStory, DropdownStory, ImageStory, InputStory, ListStory, PickerStory,
PopoverStory, StoryContainer, SwitchStory, TooltipStory,
};
use workspace::{TitleBar, Workspace};
use workspace::{dock::DockPosition, TitleBar, Workspace};
use std::sync::Arc;
use ui::{
@ -40,7 +40,7 @@ impl StoryWorkspace {
})
.detach();
StoryContainer::open(
StoryContainer::add_pane(
"Buttons",
"Displays a button or a component that looks like a button.",
ButtonStory::view(cx).into(),
@ -49,25 +49,23 @@ impl StoryWorkspace {
)
.detach();
StoryContainer::open(
"Inputs",
"Displays a text input field.",
StoryContainer::add_panel(
InputStory::view(cx).into(),
workspace.clone(),
DockPosition::Right,
px(500.0),
cx,
)
.detach();
);
StoryContainer::open(
"Checkbox",
"A control that allows the user to toggle between checked and not checked.",
StoryContainer::add_panel(
CheckboxStory::view(cx).into(),
workspace.clone(),
DockPosition::Bottom,
px(300.),
cx,
)
.detach();
);
StoryContainer::open(
StoryContainer::add_pane(
"Switch",
"A control that allows the user to toggle between two states.",
SwitchStory::view(cx).into(),
@ -76,7 +74,7 @@ impl StoryWorkspace {
)
.detach();
StoryContainer::open(
StoryContainer::add_pane(
"Dropdowns",
"Displays a list of options for the user to pick from—triggered by a button.",
DropdownStory::new(cx).into(),
@ -85,7 +83,7 @@ impl StoryWorkspace {
)
.detach();
StoryContainer::open(
StoryContainer::add_pane(
"Picker",
"Picker is a component that allows the user to select an item from a list of options.",
PickerStory::view(cx).into(),
@ -94,7 +92,7 @@ impl StoryWorkspace {
)
.detach();
StoryContainer::open(
StoryContainer::add_pane(
"Popover",
"Displays rich content in a portal, triggered by a button.",
PopoverStory::view(cx).into(),
@ -103,24 +101,24 @@ impl StoryWorkspace {
)
.detach();
StoryContainer::open(
StoryContainer::add_pane(
"Tooltip",
"A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.",
"Displays a short message when users hover over an element.",
TooltipStory::view(cx).into(),
workspace.clone(),
cx,
).detach();
StoryContainer::open(
"List",
"A complex list example, includes use Picker, Scrollbar, and more.",
ListStory::view(cx).into(),
workspace.clone(),
cx,
)
.detach();
StoryContainer::open(
StoryContainer::add_panel(
ListStory::view(cx).into(),
workspace.clone(),
DockPosition::Left,
px(360.),
cx,
);
StoryContainer::add_pane(
"Image",
"Render SVG image and Chart",
ImageStory::view(cx).into(),
@ -136,7 +134,7 @@ impl StoryWorkspace {
app_state: Arc<AppState>,
cx: &mut AppContext,
) -> Task<anyhow::Result<WindowHandle<Self>>> {
let window_bounds = Bounds::centered(None, size(px(1200.0), px(900.0)), cx);
let window_bounds = Bounds::centered(None, size(px(1600.0), px(1200.0)), cx);
cx.spawn(|mut cx| async move {
let options = WindowOptions {

View file

@ -33,6 +33,7 @@ impl CheckboxStory {
impl Render for CheckboxStory {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.p_4()
.gap_6()
.child(
v_flex().items_start().justify_start().gap_6().child(

View file

@ -83,6 +83,7 @@ impl Render for InputStory {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.size_full()
.p_4()
.justify_start()
.gap_3()
.child(

View file

@ -21,9 +21,10 @@ pub use switch_story::SwitchStory;
pub use tooltip_story::TooltipStory;
use gpui::{
div, prelude::FluentBuilder as _, px, AnyElement, AnyView, AppContext, Div, EventEmitter,
FocusableView, IntoElement, ParentElement, Render, SharedString, Styled as _, Task, View,
ViewContext, VisualContext, WindowContext,
div, prelude::FluentBuilder as _, px, AnyElement, AnyView, AppContext, Div, Element,
EventEmitter, FocusableView, InteractiveElement, IntoElement, ParentElement, Pixels, Render,
ScrollHandle, SharedString, StatefulInteractiveElement, Styled as _, Task, View, ViewContext,
VisualContext, WindowContext,
};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
@ -32,7 +33,7 @@ use workspace::{
};
use anyhow::Result;
use ui::{divider::Divider, h_flex, label::Label, v_flex};
use ui::{divider::Divider, h_flex, label::Label, v_flex, StyledExt};
pub fn story_case(
name: &'static str,
@ -61,6 +62,7 @@ pub fn section(title: impl Into<SharedString>, cx: &WindowContext) -> Div {
pub struct StoryContainer {
focus_handle: gpui::FocusHandle,
scroll_handle: gpui::ScrollHandle,
name: SharedString,
description: SharedString,
position: DockPosition,
@ -76,48 +78,6 @@ impl FocusableView for StoryContainer {
}
}
impl EventEmitter<PanelEvent> for StoryContainer {}
impl Panel for StoryContainer {
fn persistent_name() -> &'static str {
"story-container"
}
fn position(&self, _: &WindowContext) -> workspace::dock::DockPosition {
self.position
}
fn can_position(&self, _: workspace::dock::DockPosition, _: &WindowContext) -> bool {
true
}
fn set_position(&mut self, position: workspace::dock::DockPosition, _: &mut WindowContext) {
self.position = position;
}
fn size(&self, _: &WindowContext) -> gpui::Pixels {
match self.position {
DockPosition::Left | DockPosition::Right => self.width.unwrap_or(px(360.)),
DockPosition::Bottom => self.height.unwrap_or(px(360.)),
}
}
fn set_size(&mut self, size: Option<gpui::Pixels>, _: &mut WindowContext) {
match self.position {
DockPosition::Left | DockPosition::Right => self.width = size,
DockPosition::Bottom => self.height = size,
}
}
fn set_active(&mut self, active: bool, _: &mut WindowContext) {
self.active = active;
}
fn starts_open(&self, _cx: &WindowContext) -> bool {
true
}
}
#[derive(Debug)]
pub enum ContainerEvent {
Close,
@ -172,6 +132,7 @@ impl StoryContainer {
Self {
focus_handle,
scroll_handle: ScrollHandle::new(),
name: name.into(),
description: description.into(),
width: None,
@ -182,7 +143,7 @@ impl StoryContainer {
}
}
pub fn open(
pub fn add_pane(
name: impl Into<SharedString>,
description: impl Into<SharedString>,
story: AnyView,
@ -203,6 +164,19 @@ impl StoryContainer {
})
}
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
@ -226,21 +200,112 @@ impl StoryContainer {
impl Render for StoryContainer {
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
div()
.id("story-container")
.overflow_y_scroll()
.track_scroll(&self.scroll_handle)
.size_full()
.gap_6()
.p_2()
.child(
div()
.flex()
.flex_col()
.gap_4()
.child(Label::new(self.name.clone()).text_size(px(24.0)))
.child(Label::new(self.description.clone()).text_size(px(16.0))),
v_flex()
.size_full()
.gap_6()
.p_4()
.child(
div()
.flex()
.flex_col()
.gap_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())
.when_some(self.story.clone(), |this, story| {
this.child(v_flex().size_full().child(story))
}),
)
.child(Divider::horizontal())
.when_some(self.story.clone(), |this, story| {
this.child(v_flex().size_full().child(story))
})
}
}
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>) {
dbg!("================ set_size", size);
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, _cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.id("my-panel")
.track_focus(&self.focus_handle)
.size_full()
.child(self.view.clone())
}
}

View file

@ -2,9 +2,9 @@ use core::time;
use fake::Fake;
use gpui::{
actions, div, prelude::FluentBuilder as _, px, ElementId, InteractiveElement, IntoElement,
ParentElement, Render, RenderOnce, Styled, Timer, View, ViewContext, VisualContext,
WindowContext,
actions, div, prelude::FluentBuilder as _, px, ElementId, FocusHandle, FocusableView,
InteractiveElement, IntoElement, ParentElement, Render, RenderOnce, Styled, Timer, View,
ViewContext, VisualContext, WindowContext,
};
use ui::{
@ -175,22 +175,11 @@ impl CompanyListDelegate {
}
pub struct ListStory {
focus_handle: FocusHandle,
company_list: View<Picker<CompanyListDelegate>>,
selected_company: Option<Company>,
}
fn random_company() -> Company {
let last_done = (0.0..999.0).fake::<f64>();
let prev_close = last_done * (-0.1..0.1).fake::<f64>();
Company {
name: fake::faker::company::en::CompanyName().fake(),
industry: fake::faker::company::en::Industry().fake(),
description: fake::faker::lorem::en::Paragraph(3..5).fake(),
last_done,
prev_close,
}
}
impl ListStory {
pub fn view(cx: &mut WindowContext) -> View<Self> {
cx.new_view(|cx| Self::new(cx))
@ -235,6 +224,7 @@ impl ListStory {
.detach();
Self {
focus_handle: cx.focus_handle(),
company_list,
selected_company: None,
}
@ -248,48 +238,68 @@ impl ListStory {
}
}
fn random_company() -> Company {
let last_done = (0.0..999.0).fake::<f64>();
let prev_close = last_done * (-0.1..0.1).fake::<f64>();
Company {
name: fake::faker::company::en::CompanyName().fake(),
industry: fake::faker::company::en::Industry().fake(),
description: fake::faker::lorem::en::Paragraph(3..5).fake(),
last_done,
prev_close,
}
}
impl FocusableView for ListStory {
fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for ListStory {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
h_flex()
.size_full()
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::selected_company))
.size_full()
.gap_4()
.mb_4()
.child(
v_flex()
.h_full()
.w_full()
.border_1()
.border_color(cx.theme().border)
.rounded_md()
.w(px(400.))
.occlude()
.child(self.company_list.clone()),
)
.child(
div()
.flex_1()
.size_full()
.border_1()
.border_color(cx.theme().border)
.py_1()
.px_4()
.rounded_md()
.when_some(self.selected_company.clone(), |this, company| {
this.child(
div()
.flex_1()
.gap_2()
.child(
h_flex()
.items_start()
.justify_between()
.child(div().text_3xl().mb_6().child(company.name.clone()))
.child(format!("{:.2}", company.last_done)),
)
.child(company.industry.clone())
.child(company.description.clone()),
)
}),
)
// .child(
// div()
// .invisible()
// .flex_1()
// .size_full()
// .border_1()
// .border_color(cx.theme().border)
// .py_1()
// .px_4()
// .rounded_md()
// .when_some(self.selected_company.clone(), |this, company| {
// this.child(
// div()
// .flex_1()
// .gap_2()
// .child(
// h_flex()
// .items_start()
// .justify_between()
// .child(div().text_3xl().mb_6().child(company.name.clone()))
// .child(format!("{:.2}", company.last_done)),
// )
// .child(company.industry.clone())
// .child(company.description.clone()),
// )
// }),
// )
}
}

View file

@ -66,6 +66,7 @@ impl Render for PopoverStory {
let form = self.form.clone();
v_flex()
.p_4()
.size_full()
.gap_6()
.child(

View file

@ -27,7 +27,7 @@ impl TooltipStory {
impl Render for TooltipStory {
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement {
v_flex()
.w(px(360.))
.p_4()
.gap_5()
.child(
div()

View file

@ -1,4 +1,4 @@
use crate::theme::ActiveTheme;
use crate::theme::{hsl, ActiveTheme};
use gpui::{hsla, point, px, BoxShadow, Styled, WindowContext};
use smallvec::{smallvec, SmallVec};
@ -76,6 +76,26 @@ pub trait StyledExt: Styled + Sized {
fn elevation_3(self, cx: &WindowContext) -> Self {
elevated(self, cx, ElevationIndex::ModalSurface)
}
/// Render a border with a width of 1px, color blue
fn debug_blue(self) -> Self {
self.border_1().border_color(gpui::blue())
}
/// Render a border with a width of 1px, color yellow
fn debug_yellow(self) -> Self {
self.border_1().border_color(gpui::yellow())
}
/// Render a border with a width of 1px, color green
fn debug_green(self) -> Self {
self.border_1().border_color(gpui::green())
}
/// Render a border with a width of 1px, color pink
fn debug_pink(self) -> Self {
self.border_1().border_color(hsl(300., 100., 47.))
}
}
impl<E: Styled> StyledExt for E {}

View file

@ -20,7 +20,7 @@ pub struct Tab {
impl Tab {
pub fn new(id: impl Into<ElementId>, label: impl Into<AnyElement>) -> Self {
Self {
base: div().id(id.into()),
base: div().id(id.into()).gap_1().py_1().px_3(),
label: label.into(),
disabled: false,
selected: false,
@ -57,6 +57,12 @@ impl InteractiveElement for Tab {
impl StatefulInteractiveElement for Tab {}
impl Styled for Tab {
fn style(&mut self) -> &mut gpui::StyleRefinement {
self.base.style()
}
}
impl RenderOnce for Tab {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let (text_color, bg_color) = match (self.selected, self.disabled) {
@ -68,14 +74,10 @@ impl RenderOnce for Tab {
self.base
.flex()
.items_center()
.gap_2()
.h_full()
.flex_shrink_0()
.overflow_scroll()
.cursor_pointer()
.py_1()
.px_3()
.min_w_16()
.text_color(text_color)
.bg(bg_color)
.when(self.selected, |this| this.rounded(px(6.)))

View file

@ -1,11 +1,11 @@
use crate::stock::h_flex;
use crate::theme::ActiveTheme;
use gpui::prelude::FluentBuilder as _;
use gpui::InteractiveElement;
use gpui::{
div, AnyElement, Div, IntoElement, ParentElement, RenderOnce, ScrollHandle, SharedString,
StatefulInteractiveElement as _, WindowContext,
StatefulInteractiveElement as _, Styled, WindowContext,
};
use gpui::{InteractiveElement, Styled as _};
use smallvec::SmallVec;
#[derive(IntoElement)]
@ -19,7 +19,7 @@ pub struct TabBar {
impl TabBar {
pub fn new(id: impl Into<SharedString>) -> Self {
Self {
base: div(),
base: div().h_10().p_1(),
id: id.into(),
children: SmallVec::new(),
scroll_handle: None,
@ -39,6 +39,12 @@ impl ParentElement for TabBar {
}
}
impl Styled for TabBar {
fn style(&mut self) -> &mut gpui::StyleRefinement {
self.base.style()
}
}
impl RenderOnce for TabBar {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let theme = cx.theme();
@ -50,8 +56,6 @@ impl RenderOnce for TabBar {
.flex_none()
.items_center()
.w_full()
.h_10()
.p_1()
.bg(theme.muted)
.text_color(theme.muted_foreground)
.relative()

View file

@ -170,7 +170,7 @@ impl Colors {
ring: hsl(240.0, 5.9, 10.0),
selection: hsl(211.0, 97.0, 85.0),
scrollbar: Hsla::transparent_black(),
scrollbar_thumb: hsl(240.0, 5.9, 90.0),
scrollbar_thumb: hsl(240.0, 5.9, 90.0).opacity(0.7),
panel: hsl(0.0, 0.0, 100.0),
drop_target: hsl(240.0, 65., 44.0).opacity(0.15),
}
@ -229,9 +229,9 @@ impl Colors {
ring: hsl(240.0, 4.9, 83.9),
selection: hsl(211.0, 97.0, 85.0),
scrollbar: Hsla::transparent_black(),
scrollbar_thumb: hsl(240.0, 3.7, 15.9),
scrollbar_thumb: hsl(240.0, 3.7, 15.9).opacity(0.7),
panel: hsl(299.0, 2., 9.),
drop_target: hsl(240.0, 65., 29.0).opacity(0.3),
drop_target: hsl(240.0, 89., 67.0).opacity(0.8),
}
}
}

View file

@ -45,15 +45,17 @@ pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
/// Return the position of the panel.
fn position(&self, cx: &WindowContext) -> DockPosition;
/// Return true if the panel can be positioned at the given position.
fn can_position(&self, position: DockPosition, cx: &WindowContext) -> bool;
fn can_position(&self, position: DockPosition) -> bool {
true
}
/// Set the position of the panel.
fn set_position(&mut self, position: DockPosition, cx: &mut WindowContext);
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {}
/// Return the size of the panel.
fn size(&self, cx: &WindowContext) -> Pixels;
/// Set the size of the panel.
fn set_size(&mut self, size: Option<Pixels>, cx: &mut WindowContext);
fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {}
/// Set the active state of the panel.
fn set_active(&mut self, active: bool, cx: &mut WindowContext);
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {}
fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
None
}
@ -62,7 +64,7 @@ pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
}
fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {}
fn starts_open(&self, _cx: &WindowContext) -> bool {
false
true
}
}
@ -99,7 +101,7 @@ where
}
fn can_position(&self, position: DockPosition, cx: &WindowContext) -> bool {
self.read(cx).can_position(position, cx)
self.read(cx).can_position(position)
}
fn set_position(&self, position: DockPosition, cx: &mut WindowContext) {

View file

@ -183,6 +183,13 @@ impl Pane {
cx.notify();
}
pub fn set_should_display_tab_bar<F>(&mut self, f: F)
where
F: 'static + Fn(&ViewContext<Pane>) -> bool,
{
self.should_display_tab_bar = Rc::new(f);
}
pub fn set_custom_drop_handle<F>(&mut self, cx: &mut ViewContext<Self>, handle: F)
where
F: 'static + Fn(&mut Pane, &dyn Any, &mut ViewContext<Pane>) -> ControlFlow<(), ()>,
@ -651,24 +658,30 @@ impl Pane {
},
cx,
);
// let indicator = render_item_indicator(item.boxed_clone(), cx);
let item_id = item.item_id();
let _is_first_item = ix == 0;
let _is_last_item = ix == self.items.len() - 1;
let _position_relative_to_active_item = ix.cmp(&self.active_item_index);
Tab::new(ix, label)
.group("tab")
.px(px(5.))
.prefix(div().size_2().into_any_element())
.gap_0p5()
.suffix(
div()
.id("close-tab")
.p(px(1.5))
.rounded_md()
.child(Icon::new(IconName::Close).size_3())
.p(px(0.5))
.rounded_lg()
.invisible()
.child(Icon::new(IconName::Close).size_2())
.hover(|this| this.bg(cx.theme().accent.darken(0.1)))
.active(|this| this.bg(cx.theme().accent.darken(0.2)))
.on_click(cx.listener(move |pane, _, cx| {
pane.close_item_by_id(item_id, cx).detach_and_log_err(cx);
}))
.group_hover("tab", |this| this.visible())
.into_any(),
)
.selected(is_active)
@ -685,7 +698,11 @@ impl Pane {
},
|tab, cx| cx.new_view(|_| tab.clone()),
)
.drag_over::<DraggedTab>(|tab, _, cx| tab.bg(cx.theme().drop_target))
.drag_over::<DraggedTab>(|tab, _, cx| {
tab.border_l_3()
.rounded_l_none()
.border_color(cx.theme().drop_target)
})
.drag_over::<DraggedSelection>(|tab, _, cx| tab.bg(cx.theme().drop_target))
.when_some(self.can_drop_predicate.clone(), |this, p| {
this.can_drop(move |a, cx| p(a, cx))
@ -722,6 +739,7 @@ impl Pane {
TabBar::new("tab-bar")
.track_scroll(self.tab_bar_scroll_handle.clone())
.gap(px(2.))
.when(self.has_focus(cx), |tab_bar| {
tab_bar.child({
let render_tab_buttons = self.render_tab_bar_buttons.clone();
@ -739,8 +757,6 @@ impl Pane {
div()
.id("tab-bar-drop-target")
.min_w_6()
// HACK: This empty child is currently necessary to force the drop target to appear
// despite us setting a min width above.
.child("")
.h_full()
.flex_grow()