Mainline GPUI2 UI work (#3099)

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>
This commit is contained in:
Marshall Bowers 2023-10-06 13:18:56 -04:00 committed by GitHub
parent c46137e40d
commit 456baaa112
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 371 additions and 19 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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