Mainline GPUI2 UI work (#3099)

Marshall Bowers , Nate Butler , and Antonio Scandurra created

This PR mainlines the current state of new GPUI2-based UI from the
`gpui2-ui` branch.

Included in this is a performance improvement to make use of the
`TextLayoutCache` when calling `layout` for `Text` elements.

Release Notes:

- N/A

---------

Co-authored-by: Nate Butler <iamnbutler@gmail.com>
Co-authored-by: Antonio Scandurra <me@as-cii.com>

Change summary

crates/gpui/src/app/window.rs                                |  6 
crates/gpui2/src/elements/text.rs                            | 16 
crates/storybook/src/stories/components.rs                   |  4 
crates/storybook/src/stories/components/language_selector.rs | 16 +
crates/storybook/src/stories/components/multi_buffer.rs      | 24 ++
crates/storybook/src/stories/components/recent_projects.rs   | 16 +
crates/storybook/src/stories/components/theme_selector.rs    | 16 +
crates/storybook/src/story_selector.rs                       | 14 +
crates/ui/src/components.rs                                  | 10 
crates/ui/src/components/language_selector.rs                | 36 +++
crates/ui/src/components/multi_buffer.rs                     | 42 +++
crates/ui/src/components/palette.rs                          | 20 +
crates/ui/src/components/recent_projects.rs                  | 32 ++
crates/ui/src/components/theme_selector.rs                   | 37 +++
crates/ui/src/components/toast.rs                            | 66 ++++++
crates/ui/src/components/workspace.rs                        | 13 +
crates/ui/src/elements/icon.rs                               |  2 
crates/ui/src/prelude.rs                                     | 20 +
18 files changed, 371 insertions(+), 19 deletions(-)

Detailed changes

crates/gpui/src/app/window.rs πŸ”—

@@ -71,7 +71,7 @@ pub struct Window {
     pub(crate) hovered_region_ids: Vec<MouseRegionId>,
     pub(crate) clicked_region_ids: Vec<MouseRegionId>,
     pub(crate) clicked_region: Option<(MouseRegionId, MouseButton)>,
-    text_layout_cache: TextLayoutCache,
+    text_layout_cache: Arc<TextLayoutCache>,
     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<TextLayoutCache> {
         &self.window.text_layout_cache
     }
 

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<V: 'static> Element<V> for Text {
         _view: &mut V,
         cx: &mut ViewContext<V>,
     ) -> 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<V: 'static> Element<V> 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<V: 'static> Element<V> 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<V: 'static> IntoElement<V> for Text {
 }
 
 pub struct TextLayout {
-    line_layout: Arc<LineLayout>,
+    line_layout: Arc<Line>,
     line_height: f32,
 }

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;

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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, LanguageSelector>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(LanguageSelector::new())
+    }
+}

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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        Story::container(cx)
+            .child(Story::title_for::<_, MultiBuffer<V>>(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),
+            ]))
+    }
+}

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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, RecentProjects>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(RecentProjects::new())
+    }
+}

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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, ThemeSelector>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(ThemeSelector::new())
+    }
+}

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 => {

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::*;

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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        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),
+        )
+    }
+}

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<V: 'static> {
+    view_type: PhantomData<V>,
+    buffers: Vec<Buffer>,
+}
+
+impl<V: 'static> MultiBuffer<V> {
+    pub fn new(buffers: Vec<Buffer>) -> Self {
+        Self {
+            view_type: PhantomData,
+            buffers,
+        }
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        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)
+            }))
+    }
+}

crates/ui/src/components/palette.rs πŸ”—

@@ -93,19 +93,17 @@ impl<V: 'static> Palette<V> {
                                     .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<Keybinding>,
 }
 
@@ -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<L: Into<Option<&'static str>>>(mut self, sublabel: L) -> Self {
+        self.sublabel = sublabel.into();
+        self
+    }
+
     pub fn keybinding<K>(mut self, keybinding: K) -> Self
     where
         K: Into<Option<Keybinding>>,
@@ -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())
     }
 }

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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        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),
+        )
+    }
+}

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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        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),
+        )
+    }
+}

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<V: 'static> {
+    origin: ToastOrigin,
+    children: HackyChildren<V>,
+    payload: HackyChildrenPayload,
+}
+
+impl<V: 'static> Toast<V> {
+    pub fn new(
+        origin: ToastOrigin,
+        children: HackyChildren<V>,
+        payload: HackyChildrenPayload,
+    ) -> Self {
+        Self {
+            origin,
+            children,
+            payload,
+        }
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        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()))
+    }
+}

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::<Arc<Theme>>().unwrap();
+
+        //         vec![Label::new("label").into_any()]
+        //     },
+        //     Box::new(theme.clone()),
+        // ))
     }
 }

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

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]