diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index 4eca6f3a30..7ba1e85100 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -71,7 +71,7 @@ pub struct Window { pub(crate) hovered_region_ids: Vec, pub(crate) clicked_region_ids: Vec, pub(crate) clicked_region: Option<(MouseRegionId, MouseButton)>, - text_layout_cache: TextLayoutCache, + text_layout_cache: Arc, refreshing: bool, } @@ -107,7 +107,7 @@ impl Window { cursor_regions: Default::default(), mouse_regions: Default::default(), event_handlers: Default::default(), - text_layout_cache: TextLayoutCache::new(cx.font_system.clone()), + text_layout_cache: Arc::new(TextLayoutCache::new(cx.font_system.clone())), last_mouse_moved_event: None, last_mouse_position: Vector2F::zero(), pressed_buttons: Default::default(), @@ -303,7 +303,7 @@ impl<'a> WindowContext<'a> { self.window.refreshing } - pub fn text_layout_cache(&self) -> &TextLayoutCache { + pub fn text_layout_cache(&self) -> &Arc { &self.window.text_layout_cache } diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index 6f89375df0..323b3d9f89 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -5,7 +5,7 @@ use crate::{ use anyhow::Result; use gpui::{ geometry::{vector::Vector2F, Size}, - text_layout::LineLayout, + text_layout::Line, LayoutId, }; use parking_lot::Mutex; @@ -32,7 +32,7 @@ impl Element for Text { _view: &mut V, cx: &mut ViewContext, ) -> Result<(LayoutId, Self::PaintState)> { - let fonts = cx.platform().fonts(); + let layout_cache = cx.text_layout_cache().clone(); let text_style = cx.text_style(); let line_height = cx.font_cache().line_height(text_style.font_size); let text = self.text.clone(); @@ -41,14 +41,14 @@ impl Element for Text { let layout_id = cx.add_measured_layout_node(Default::default(), { let paint_state = paint_state.clone(); move |_params| { - let line_layout = fonts.layout_line( + let line_layout = layout_cache.layout_str( text.as_ref(), text_style.font_size, &[(text.len(), text_style.to_run())], ); let size = Size { - width: line_layout.width, + width: line_layout.width(), height: line_height, }; @@ -85,13 +85,9 @@ impl Element for Text { line_height = paint_state.line_height; } - let text_style = cx.text_style(); - let line = - gpui::text_layout::Line::new(line_layout, &[(self.text.len(), text_style.to_run())]); - // TODO: We haven't added visible bounds to the new element system yet, so this is a placeholder. let visible_bounds = bounds; - line.paint(bounds.origin(), visible_bounds, line_height, cx.legacy_cx); + line_layout.paint(bounds.origin(), visible_bounds, line_height, cx.legacy_cx); } } @@ -104,6 +100,6 @@ impl IntoElement for Text { } pub struct TextLayout { - line_layout: Arc, + line_layout: Arc, line_height: f32, } diff --git a/crates/storybook/src/stories/components.rs b/crates/storybook/src/stories/components.rs index 345fcfa309..85d5ce088f 100644 --- a/crates/storybook/src/stories/components.rs +++ b/crates/storybook/src/stories/components.rs @@ -6,13 +6,17 @@ pub mod collab_panel; pub mod context_menu; pub mod facepile; pub mod keybinding; +pub mod language_selector; +pub mod multi_buffer; pub mod palette; pub mod panel; pub mod project_panel; +pub mod recent_projects; pub mod status_bar; pub mod tab; pub mod tab_bar; pub mod terminal; +pub mod theme_selector; pub mod title_bar; pub mod toolbar; pub mod traffic_lights; diff --git a/crates/storybook/src/stories/components/language_selector.rs b/crates/storybook/src/stories/components/language_selector.rs new file mode 100644 index 0000000000..c6dbd13d3f --- /dev/null +++ b/crates/storybook/src/stories/components/language_selector.rs @@ -0,0 +1,16 @@ +use ui::prelude::*; +use ui::LanguageSelector; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct LanguageSelectorStory {} + +impl LanguageSelectorStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + Story::container(cx) + .child(Story::title_for::<_, LanguageSelector>(cx)) + .child(Story::label(cx, "Default")) + .child(LanguageSelector::new()) + } +} diff --git a/crates/storybook/src/stories/components/multi_buffer.rs b/crates/storybook/src/stories/components/multi_buffer.rs new file mode 100644 index 0000000000..cd760c54dc --- /dev/null +++ b/crates/storybook/src/stories/components/multi_buffer.rs @@ -0,0 +1,24 @@ +use ui::prelude::*; +use ui::{hello_world_rust_buffer_example, MultiBuffer}; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct MultiBufferStory {} + +impl MultiBufferStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + Story::container(cx) + .child(Story::title_for::<_, MultiBuffer>(cx)) + .child(Story::label(cx, "Default")) + .child(MultiBuffer::new(vec![ + hello_world_rust_buffer_example(&theme), + hello_world_rust_buffer_example(&theme), + hello_world_rust_buffer_example(&theme), + hello_world_rust_buffer_example(&theme), + hello_world_rust_buffer_example(&theme), + ])) + } +} diff --git a/crates/storybook/src/stories/components/recent_projects.rs b/crates/storybook/src/stories/components/recent_projects.rs new file mode 100644 index 0000000000..f924654695 --- /dev/null +++ b/crates/storybook/src/stories/components/recent_projects.rs @@ -0,0 +1,16 @@ +use ui::prelude::*; +use ui::RecentProjects; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct RecentProjectsStory {} + +impl RecentProjectsStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + Story::container(cx) + .child(Story::title_for::<_, RecentProjects>(cx)) + .child(Story::label(cx, "Default")) + .child(RecentProjects::new()) + } +} diff --git a/crates/storybook/src/stories/components/theme_selector.rs b/crates/storybook/src/stories/components/theme_selector.rs new file mode 100644 index 0000000000..43e2a704e7 --- /dev/null +++ b/crates/storybook/src/stories/components/theme_selector.rs @@ -0,0 +1,16 @@ +use ui::prelude::*; +use ui::ThemeSelector; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct ThemeSelectorStory {} + +impl ThemeSelectorStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + Story::container(cx) + .child(Story::title_for::<_, ThemeSelector>(cx)) + .child(Story::label(cx, "Default")) + .child(ThemeSelector::new()) + } +} diff --git a/crates/storybook/src/story_selector.rs b/crates/storybook/src/story_selector.rs index acc965aa0a..6b0a9ac78d 100644 --- a/crates/storybook/src/story_selector.rs +++ b/crates/storybook/src/story_selector.rs @@ -42,13 +42,17 @@ pub enum ComponentStory { CollabPanel, Facepile, Keybinding, + LanguageSelector, + MultiBuffer, Palette, Panel, ProjectPanel, + RecentProjects, StatusBar, Tab, TabBar, Terminal, + ThemeSelector, TitleBar, Toolbar, TrafficLights, @@ -69,15 +73,25 @@ impl ComponentStory { Self::CollabPanel => components::collab_panel::CollabPanelStory::default().into_any(), Self::Facepile => components::facepile::FacepileStory::default().into_any(), Self::Keybinding => components::keybinding::KeybindingStory::default().into_any(), + Self::LanguageSelector => { + components::language_selector::LanguageSelectorStory::default().into_any() + } + Self::MultiBuffer => components::multi_buffer::MultiBufferStory::default().into_any(), Self::Palette => components::palette::PaletteStory::default().into_any(), Self::Panel => components::panel::PanelStory::default().into_any(), Self::ProjectPanel => { components::project_panel::ProjectPanelStory::default().into_any() } + Self::RecentProjects => { + components::recent_projects::RecentProjectsStory::default().into_any() + } Self::StatusBar => components::status_bar::StatusBarStory::default().into_any(), Self::Tab => components::tab::TabStory::default().into_any(), Self::TabBar => components::tab_bar::TabBarStory::default().into_any(), Self::Terminal => components::terminal::TerminalStory::default().into_any(), + Self::ThemeSelector => { + components::theme_selector::ThemeSelectorStory::default().into_any() + } Self::TitleBar => components::title_bar::TitleBarStory::default().into_any(), Self::Toolbar => components::toolbar::ToolbarStory::default().into_any(), Self::TrafficLights => { diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 0af13040f7..65b0218565 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -9,17 +9,22 @@ mod editor_pane; mod facepile; mod icon_button; mod keybinding; +mod language_selector; mod list; +mod multi_buffer; mod palette; mod panel; mod panes; mod player_stack; mod project_panel; +mod recent_projects; mod status_bar; mod tab; mod tab_bar; mod terminal; +mod theme_selector; mod title_bar; +mod toast; mod toolbar; mod traffic_lights; mod workspace; @@ -35,17 +40,22 @@ pub use editor_pane::*; pub use facepile::*; pub use icon_button::*; pub use keybinding::*; +pub use language_selector::*; pub use list::*; +pub use multi_buffer::*; pub use palette::*; pub use panel::*; pub use panes::*; pub use player_stack::*; pub use project_panel::*; +pub use recent_projects::*; pub use status_bar::*; pub use tab::*; pub use tab_bar::*; pub use terminal::*; +pub use theme_selector::*; pub use title_bar::*; +pub use toast::*; pub use toolbar::*; pub use traffic_lights::*; pub use workspace::*; diff --git a/crates/ui/src/components/language_selector.rs b/crates/ui/src/components/language_selector.rs new file mode 100644 index 0000000000..124d7f13ee --- /dev/null +++ b/crates/ui/src/components/language_selector.rs @@ -0,0 +1,36 @@ +use crate::prelude::*; +use crate::{OrderMethod, Palette, PaletteItem}; + +#[derive(Element)] +pub struct LanguageSelector { + scroll_state: ScrollState, +} + +impl LanguageSelector { + pub fn new() -> Self { + Self { + scroll_state: ScrollState::default(), + } + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + div().child( + Palette::new(self.scroll_state.clone()) + .items(vec![ + PaletteItem::new("C"), + PaletteItem::new("C++"), + PaletteItem::new("CSS"), + PaletteItem::new("Elixir"), + PaletteItem::new("Elm"), + PaletteItem::new("ERB"), + PaletteItem::new("Rust (current)"), + PaletteItem::new("Scheme"), + PaletteItem::new("TOML"), + PaletteItem::new("TypeScript"), + ]) + .placeholder("Select a language...") + .empty_string("No matches") + .default_order(OrderMethod::Ascending), + ) + } +} diff --git a/crates/ui/src/components/multi_buffer.rs b/crates/ui/src/components/multi_buffer.rs new file mode 100644 index 0000000000..d38603457a --- /dev/null +++ b/crates/ui/src/components/multi_buffer.rs @@ -0,0 +1,42 @@ +use std::marker::PhantomData; + +use crate::prelude::*; +use crate::{v_stack, Buffer, Icon, IconButton, Label, LabelSize}; + +#[derive(Element)] +pub struct MultiBuffer { + view_type: PhantomData, + buffers: Vec, +} + +impl MultiBuffer { + pub fn new(buffers: Vec) -> Self { + Self { + view_type: PhantomData, + buffers, + } + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + v_stack() + .w_full() + .h_full() + .flex_1() + .children(self.buffers.clone().into_iter().map(|buffer| { + v_stack() + .child( + div() + .flex() + .items_center() + .justify_between() + .p_4() + .fill(theme.lowest.base.default.background) + .child(Label::new("main.rs").size(LabelSize::Small)) + .child(IconButton::new(Icon::ArrowUpRight)), + ) + .child(buffer) + })) + } +} diff --git a/crates/ui/src/components/palette.rs b/crates/ui/src/components/palette.rs index 430ab8be63..16001e50c1 100644 --- a/crates/ui/src/components/palette.rs +++ b/crates/ui/src/components/palette.rs @@ -93,19 +93,17 @@ impl Palette { .fill(theme.lowest.base.hovered.background) .active() .fill(theme.lowest.base.pressed.background) - .child( - PaletteItem::new(item.label) - .keybinding(item.keybinding.clone()), - ) + .child(item.clone()) })), ), ) } } -#[derive(Element)] +#[derive(Element, Clone)] pub struct PaletteItem { pub label: &'static str, + pub sublabel: Option<&'static str>, pub keybinding: Option, } @@ -113,6 +111,7 @@ impl PaletteItem { pub fn new(label: &'static str) -> Self { Self { label, + sublabel: None, keybinding: None, } } @@ -122,6 +121,11 @@ impl PaletteItem { self } + pub fn sublabel>>(mut self, sublabel: L) -> Self { + self.sublabel = sublabel.into(); + self + } + pub fn keybinding(mut self, keybinding: K) -> Self where K: Into>, @@ -138,7 +142,11 @@ impl PaletteItem { .flex_row() .grow() .justify_between() - .child(Label::new(self.label)) + .child( + v_stack() + .child(Label::new(self.label)) + .children(self.sublabel.map(|sublabel| Label::new(sublabel))), + ) .children(self.keybinding.clone()) } } diff --git a/crates/ui/src/components/recent_projects.rs b/crates/ui/src/components/recent_projects.rs new file mode 100644 index 0000000000..6aca6631b9 --- /dev/null +++ b/crates/ui/src/components/recent_projects.rs @@ -0,0 +1,32 @@ +use crate::prelude::*; +use crate::{OrderMethod, Palette, PaletteItem}; + +#[derive(Element)] +pub struct RecentProjects { + scroll_state: ScrollState, +} + +impl RecentProjects { + pub fn new() -> Self { + Self { + scroll_state: ScrollState::default(), + } + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + div().child( + Palette::new(self.scroll_state.clone()) + .items(vec![ + PaletteItem::new("zed").sublabel("~/projects/zed"), + PaletteItem::new("saga").sublabel("~/projects/saga"), + PaletteItem::new("journal").sublabel("~/journal"), + PaletteItem::new("dotfiles").sublabel("~/dotfiles"), + PaletteItem::new("zed.dev").sublabel("~/projects/zed.dev"), + PaletteItem::new("laminar").sublabel("~/projects/laminar"), + ]) + .placeholder("Recent Projects...") + .empty_string("No matches") + .default_order(OrderMethod::Ascending), + ) + } +} diff --git a/crates/ui/src/components/theme_selector.rs b/crates/ui/src/components/theme_selector.rs new file mode 100644 index 0000000000..e6f5237afe --- /dev/null +++ b/crates/ui/src/components/theme_selector.rs @@ -0,0 +1,37 @@ +use crate::prelude::*; +use crate::{OrderMethod, Palette, PaletteItem}; + +#[derive(Element)] +pub struct ThemeSelector { + scroll_state: ScrollState, +} + +impl ThemeSelector { + pub fn new() -> Self { + Self { + scroll_state: ScrollState::default(), + } + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + div().child( + Palette::new(self.scroll_state.clone()) + .items(vec![ + PaletteItem::new("One Dark"), + PaletteItem::new("Rosé Pine"), + PaletteItem::new("Rosé Pine Moon"), + PaletteItem::new("Sandcastle"), + PaletteItem::new("Solarized Dark"), + PaletteItem::new("Summercamp"), + PaletteItem::new("Atelier Cave Light"), + PaletteItem::new("Atelier Dune Light"), + PaletteItem::new("Atelier Estuary Light"), + PaletteItem::new("Atelier Forest Light"), + PaletteItem::new("Atelier Heath Light"), + ]) + .placeholder("Select Theme...") + .empty_string("No matches") + .default_order(OrderMethod::Ascending), + ) + } +} diff --git a/crates/ui/src/components/toast.rs b/crates/ui/src/components/toast.rs new file mode 100644 index 0000000000..c299cdd6bc --- /dev/null +++ b/crates/ui/src/components/toast.rs @@ -0,0 +1,66 @@ +use crate::prelude::*; + +#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] +pub enum ToastOrigin { + #[default] + Bottom, + BottomRight, +} + +#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] +pub enum ToastVariant { + #[default] + Toast, + Status, +} + +/// A toast is a small, temporary window that appears to show a message to the user +/// or indicate a required action. +/// +/// Toasts should not persist on the screen for more than a few seconds unless +/// they are actively showing the a process in progress. +/// +/// Only one toast may be visible at a time. +#[derive(Element)] +pub struct Toast { + origin: ToastOrigin, + children: HackyChildren, + payload: HackyChildrenPayload, +} + +impl Toast { + pub fn new( + origin: ToastOrigin, + children: HackyChildren, + payload: HackyChildrenPayload, + ) -> Self { + Self { + origin, + children, + payload, + } + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let color = ThemeColor::new(cx); + + let mut div = div(); + + if self.origin == ToastOrigin::Bottom { + div = div.right_1_2(); + } else { + div = div.right_4(); + } + + div.absolute() + .bottom_4() + .flex() + .py_2() + .px_1p5() + .min_w_40() + .rounded_md() + .fill(color.elevated_surface) + .max_w_64() + .children_any((self.children)(cx, self.payload.as_ref())) + } +} diff --git a/crates/ui/src/components/workspace.rs b/crates/ui/src/components/workspace.rs index b609546f7f..b3d375bd64 100644 --- a/crates/ui/src/components/workspace.rs +++ b/crates/ui/src/components/workspace.rs @@ -82,6 +82,7 @@ impl WorkspaceElement { ); div() + .relative() .size_full() .flex() .flex_col() @@ -169,5 +170,17 @@ impl WorkspaceElement { ), ) .child(StatusBar::new()) + // An example of a toast is below + // Currently because of stacking order this gets obscured by other elements + + // .child(Toast::new( + // ToastOrigin::Bottom, + // |_, payload| { + // let theme = payload.downcast_ref::>().unwrap(); + + // vec![Label::new("label").into_any()] + // }, + // Box::new(theme.clone()), + // )) } } diff --git a/crates/ui/src/elements/icon.rs b/crates/ui/src/elements/icon.rs index 6d4053a4ae..26bf7dab22 100644 --- a/crates/ui/src/elements/icon.rs +++ b/crates/ui/src/elements/icon.rs @@ -60,6 +60,7 @@ pub enum Icon { ChevronUp, Close, ExclamationTriangle, + ExternalLink, File, FileGeneric, FileDoc, @@ -109,6 +110,7 @@ impl Icon { Icon::ChevronUp => "icons/chevron_up.svg", Icon::Close => "icons/x.svg", Icon::ExclamationTriangle => "icons/warning.svg", + Icon::ExternalLink => "icons/external_link.svg", Icon::File => "icons/file.svg", Icon::FileGeneric => "icons/file_icons/file.svg", Icon::FileDoc => "icons/file_icons/book.svg", diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index b19b2becd1..e0da3579e2 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -29,6 +29,26 @@ impl SystemColor { } } +#[derive(Clone, Copy)] +pub struct ThemeColor { + pub border: Hsla, + pub border_variant: Hsla, + /// The background color of an elevated surface, like a modal, tooltip or toast. + pub elevated_surface: Hsla, +} + +impl ThemeColor { + pub fn new(cx: &WindowContext) -> Self { + let theme = theme(cx); + + Self { + border: theme.lowest.base.default.border, + border_variant: theme.lowest.variant.default.border, + elevated_surface: theme.middle.base.default.background, + } + } +} + #[derive(Default, PartialEq, EnumIter, Clone, Copy)] pub enum HighlightColor { #[default]