WIP

Nathan Sobo created

Change summary

Cargo.lock                                     |   9 +
Cargo.toml                                     |   1 
crates/gpui/playground/Cargo.toml              |   2 
crates/gpui/playground/src/playground.rs       |  55 +++---
crates/gpui/playground/ui/Cargo.toml           |  12 +
crates/gpui/playground/ui/src/playground_ui.rs |  91 +++++++++++
crates/gpui/playground/ui/src/tokens.rs        | 157 ++++++++++++++++++++
crates/gpui/src/color.rs                       |  54 +++++
crates/gpui/src/elements.rs                    |   1 
crates/gpui/src/elements/node.rs               | 136 ++++++++++-------
crates/gpui/src/elements/text.rs               |  52 ++++++
crates/gpui_macros/Cargo.toml                  |   4 
crates/gpui_macros/src/gpui_macros.rs          |  51 +++++-
13 files changed, 518 insertions(+), 107 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3041,6 +3041,7 @@ dependencies = [
 name = "gpui_macros"
 version = "0.1.0"
 dependencies = [
+ "lazy_static",
  "proc-macro2",
  "quote",
  "syn 1.0.109",
@@ -5043,9 +5044,17 @@ version = "0.1.0"
 dependencies = [
  "gpui",
  "log",
+ "playground_ui",
  "simplelog",
 ]
 
+[[package]]
+name = "playground_ui"
+version = "0.1.0"
+dependencies = [
+ "gpui",
+]
+
 [[package]]
 name = "plist"
 version = "1.5.0"

Cargo.toml 🔗

@@ -29,6 +29,7 @@ members = [
     "crates/go_to_line",
     "crates/gpui",
     "crates/gpui/playground",
+    "crates/gpui/playground/ui",
     "crates/gpui_macros",
     "crates/install_cli",
     "crates/journal",

crates/gpui/playground/Cargo.toml 🔗

@@ -8,6 +8,8 @@ version = "0.1.0"
 edition = "2021"
 
 [dependencies]
+playground_ui = { path = "ui" }
+
 gpui = { path = ".." }
 log.workspace = true
 simplelog = "0.9"

crates/gpui/playground/src/playground.rs 🔗

@@ -1,52 +1,45 @@
-use elements::{Length, Node, NodeStyle};
-use gpui::{color::Color, AnyElement, Element, Entity, View, ViewContext};
+use std::ops::{Deref, DerefMut};
+
+use gpui::{AnyElement, Element, Entity, View};
 use log::LevelFilter;
 use simplelog::SimpleLogger;
 
-mod elements;
-
 fn main() {
     SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
 
     gpui::App::new(()).unwrap().run(|cx| {
         cx.platform().activate(true);
-        cx.add_window(Default::default(), |_| PlaygroundView);
+        cx.add_window(Default::default(), |_| Playground::default());
     });
 }
 
-struct PlaygroundView;
+#[derive(Clone, Default)]
+struct Playground(playground_ui::Playground);
+
+impl Deref for Playground {
+    type Target = playground_ui::Playground;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl DerefMut for Playground {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
 
-impl Entity for PlaygroundView {
+impl Entity for Playground {
     type Event = ();
 }
 
-impl View for PlaygroundView {
+impl View for Playground {
     fn ui_name() -> &'static str {
         "PlaygroundView"
     }
 
-    fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> AnyElement<PlaygroundView> {
-        // Node::with_style(NodeStyle)
-        // Node::new().width(100.0).fill(Color::red())
-        //
-        Node::new()
-            .width(Length::auto(1.))
-            .fill(Color::red())
-            .row()
-            .children([
-                Node::new().width(20.).height(20.).fill(Color::green()),
-                Node::new().width(20.).height(20.).fill(Color::blue()),
-                Node::new().width(30.).height(30.).fill(Color::yellow()),
-                Node::new().width(50.).height(50.).fill(Color::yellow()),
-            ])
-            .into_any()
-
-        // Node::with_style(
-        //     NodeStyle::default()
-        //         .width(100.)
-        //         .height(100.)
-        //         .fill(Color::red()),
-        // )
-        // .into_any()
+    fn render(&mut self, _: &mut gpui::ViewContext<Self>) -> AnyElement<Playground> {
+        self.0.clone().into_any()
     }
 }

crates/gpui/playground/ui/Cargo.toml 🔗

@@ -0,0 +1,12 @@
+[package]
+name = "playground_ui"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+name = "playground_ui"
+path = "src/playground_ui.rs"
+crate-type = ["dylib"]
+
+[dependencies]
+gpui = { path = "../.." }

crates/gpui/playground/ui/src/playground_ui.rs 🔗

@@ -0,0 +1,91 @@
+use gpui::{
+    elements::{
+        node::{column, length::auto, row},
+        Text,
+    },
+    AnyElement, Element, View, ViewContext,
+};
+use std::{borrow::Cow, marker::PhantomData};
+use tokens::{margin::m4, text::lg};
+
+mod tokens;
+
+#[derive(Element, Clone, Default)]
+pub struct Playground;
+
+impl Playground {
+    pub fn render<V: View>(&mut self, _: &mut V, _: &mut gpui::ViewContext<V>) -> AnyElement<V> {
+        column()
+            .width(auto())
+            .child(dialog(
+                "This is a dialog",
+                "You would see a description here.",
+            ))
+            .into_any()
+    }
+}
+
+pub trait DialogDelegate<V: View>: 'static {
+    fn handle_submit<B>(&mut self, view: &mut V, button: B);
+}
+
+impl<V: View> DialogDelegate<V> for () {
+    fn handle_submit<B>(&mut self, _: &mut V, _: B) {}
+}
+
+#[derive(Element)]
+pub struct Dialog<V: View, D: DialogDelegate<V>> {
+    title: Cow<'static, str>,
+    description: Cow<'static, str>,
+    delegate: Option<D>,
+    view_type: PhantomData<V>,
+}
+
+pub fn dialog<V: View>(
+    title: impl Into<Cow<'static, str>>,
+    description: impl Into<Cow<'static, str>>,
+) -> Dialog<V, ()> {
+    Dialog {
+        title: title.into(),
+        description: description.into(),
+        delegate: None,
+        view_type: PhantomData,
+    }
+}
+
+impl<V: View, D: DialogDelegate<V>> Dialog<V, D> {
+    pub fn with_delegate(mut self, delegate: D) -> Dialog<V, D> {
+        let old_delegate = self.delegate.replace(delegate);
+        debug_assert!(old_delegate.is_none(), "delegate already set");
+        self
+    }
+}
+
+struct Button<V: View, F: FnOnce(&mut V, &mut ViewContext<V>)> {
+    label: Cow<'static, str>,
+    on_click: Option<F>,
+    view_type: PhantomData<V>,
+}
+
+fn button<V: View, F: FnOnce(&mut V, &mut ViewContext<V>)>(
+    label: impl Into<Cow<'static, str>>,
+) -> Button<V, F> {
+    Button {
+        label: label.into(),
+        on_click: None,
+        view_type: PhantomData,
+    }
+}
+
+impl<V: View, D: DialogDelegate<V>> Dialog<V, D> {
+    pub fn render(&mut self, _: &mut V, _: &mut gpui::ViewContext<V>) -> AnyElement<V> {
+        column()
+            .child(text(self.title.clone()).text_size(lg()))
+            .child(text(self.description.clone()).margins(m4(), auto()))
+            .child(row().children([
+                button("Cancel").margin_left(auto()),
+                button("OK").margin_left(m4()),
+            ]))
+            .into_any()
+    }
+}

crates/gpui/playground/ui/src/tokens.rs 🔗

@@ -0,0 +1,157 @@
+pub mod color {
+    use gpui::color::Color;
+
+    pub fn background(elevation: f32) -> Color {
+        todo!()
+    }
+}
+
+pub mod text {
+    pub fn xs() -> f32 {
+        0.75
+    }
+
+    pub fn sm() -> f32 {
+        0.875
+    }
+
+    pub fn base() -> f32 {
+        1.0
+    }
+
+    pub fn lg() -> f32 {
+        1.125
+    }
+
+    pub fn xl() -> f32 {
+        1.25
+    }
+
+    pub fn xxl() -> f32 {
+        1.5
+    }
+
+    pub fn xxxl() -> f32 {
+        1.875
+    }
+
+    pub fn xxxx() -> f32 {
+        2.25
+    }
+
+    pub fn xxxxx() -> f32 {
+        3.0
+    }
+
+    pub fn xxxxxx() -> f32 {
+        4.0
+    }
+}
+
+pub mod padding {
+    pub fn p1() -> f32 {
+        0.25
+    }
+
+    pub fn p2() -> f32 {
+        0.5
+    }
+
+    pub fn p3() -> f32 {
+        0.75
+    }
+
+    pub fn p4() -> f32 {
+        1.0
+    }
+
+    pub fn p5() -> f32 {
+        1.25
+    }
+
+    pub fn p6() -> f32 {
+        1.5
+    }
+
+    pub fn p8() -> f32 {
+        2.0
+    }
+
+    pub fn p10() -> f32 {
+        2.5
+    }
+
+    pub fn p12() -> f32 {
+        3.0
+    }
+
+    pub fn p16() -> f32 {
+        4.0
+    }
+
+    pub fn p20() -> f32 {
+        5.0
+    }
+
+    pub fn p24() -> f32 {
+        6.0
+    }
+
+    pub fn p32() -> f32 {
+        8.0
+    }
+}
+
+pub mod margin {
+    pub fn m1() -> f32 {
+        0.25
+    }
+
+    pub fn m2() -> f32 {
+        0.5
+    }
+
+    pub fn m3() -> f32 {
+        0.75
+    }
+
+    pub fn m4() -> f32 {
+        1.0
+    }
+
+    pub fn m5() -> f32 {
+        1.25
+    }
+
+    pub fn m6() -> f32 {
+        1.5
+    }
+
+    pub fn m8() -> f32 {
+        2.0
+    }
+
+    pub fn m10() -> f32 {
+        2.5
+    }
+
+    pub fn m12() -> f32 {
+        3.0
+    }
+
+    pub fn m16() -> f32 {
+        4.0
+    }
+
+    pub fn m20() -> f32 {
+        5.0
+    }
+
+    pub fn m24() -> f32 {
+        6.0
+    }
+
+    pub fn m32() -> f32 {
+        8.0
+    }
+}

crates/gpui/src/color.rs 🔗

@@ -17,33 +17,73 @@ use serde_json::json;
 #[repr(transparent)]
 pub struct Color(#[schemars(with = "String")] ColorU);
 
+pub fn color(rgba: u32) -> Color {
+    color(rgba)
+}
+
+pub fn rgb(r: f32, g: f32, b: f32) -> Color {
+    Color(ColorF::new(r, g, b, 1.).to_u8())
+}
+
+pub fn rgba(r: f32, g: f32, b: f32, a: f32) -> Color {
+    Color(ColorF::new(r, g, b, a).to_u8())
+}
+
+pub fn transparent_black() -> Color {
+    Color(ColorU::transparent_black())
+}
+
+pub fn black() -> Color {
+    Color(ColorU::black())
+}
+
+pub fn white() -> Color {
+    Color(ColorU::white())
+}
+
+pub fn red() -> Color {
+    color(0xff0000ff)
+}
+
+pub fn green() -> Color {
+    color(0x00ff00ff)
+}
+
+pub fn blue() -> Color {
+    color(0x0000ffff)
+}
+
+pub fn yellow() -> Color {
+    color(0xffff00ff)
+}
+
 impl Color {
     pub fn transparent_black() -> Self {
-        Self(ColorU::transparent_black())
+        transparent_black()
     }
 
     pub fn black() -> Self {
-        Self(ColorU::black())
+        black()
     }
 
     pub fn white() -> Self {
-        Self(ColorU::white())
+        white()
     }
 
     pub fn red() -> Self {
-        Self(ColorU::from_u32(0xff0000ff))
+        Color::from_u32(0xff0000ff)
     }
 
     pub fn green() -> Self {
-        Self(ColorU::from_u32(0x00ff00ff))
+        Color::from_u32(0x00ff00ff)
     }
 
     pub fn blue() -> Self {
-        Self(ColorU::from_u32(0x0000ffff))
+        Color::from_u32(0x0000ffff)
     }
 
     pub fn yellow() -> Self {
-        Self(ColorU::from_u32(0xffff00ff))
+        Color::from_u32(0xffff00ff)
     }
 
     pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {

crates/gpui/src/elements.rs 🔗

@@ -12,6 +12,7 @@ mod keystroke_label;
 mod label;
 mod list;
 mod mouse_event_handler;
+pub mod node;
 mod overlay;
 mod resizable;
 mod stack;

crates/gpui/playground/src/elements.rs → crates/gpui/src/elements/node.rs 🔗

@@ -1,4 +1,4 @@
-use gpui::{
+use crate::{
     color::Color,
     geometry::{
         rect::RectF,
@@ -9,47 +9,62 @@ use gpui::{
     serde_json::Value,
     AnyElement, Element, LayoutContext, Quad, SceneBuilder, SizeConstraint, View, ViewContext,
 };
-use std::{any::Any, f32, ops::Range};
-
-// Core idea is that everything is a channel, and channels are heirarchical.
-//
-// Tree 🌲 of channels
-//   - (Potentially v0.2) All channels associated with a conversation (Slack model)
-//   - Audio
-//   - You can share projects into the channel
-//   - 1.
-//
-//
-// - 2 thoughts:
-//  - Difference from where we are to the above:
-//      - Channels = rooms + chat + persistence
-//      - Chat = multiplayer assistant panel + server integrated persistence
-//  - The tree structure, is good for navigating chats, AND it's good for distributing permissions.
-// #zed-public// /zed- <- Share a pointer (URL) for this
-//
-//
+use std::{any::Any, borrow::Cow, f32, ops::Range};
+
+use self::length::Length;
 
 pub struct Node<V: View> {
     style: NodeStyle,
-    children: Vec<AnyElement<V>>,
+    content: Content<V>,
+}
+
+enum Content<V: View> {
+    Children(Vec<AnyElement<V>>),
+    Text(Cow<'static, str>),
+}
+
+impl<V: View> Default for Content<V> {
+    fn default() -> Self {
+        Self::Children(Vec::new())
+    }
+}
+
+pub fn column<V: View>() -> Node<V> {
+    Node::default()
+}
+
+pub fn row<V: View>() -> Node<V> {
+    Node {
+        style: NodeStyle {
+            axis: Axis3d::X,
+            ..Default::default()
+        },
+        content: Default::default(),
+    }
+}
+
+pub fn stack<V: View>() -> Node<V> {
+    Node {
+        style: NodeStyle {
+            axis: Axis3d::Z,
+            ..Default::default()
+        },
+        content: Default::default(),
+    }
 }
 
 impl<V: View> Default for Node<V> {
     fn default() -> Self {
         Self {
             style: Default::default(),
-            children: Default::default(),
+            content: Default::default(),
         }
     }
 }
 
 impl<V: View> Node<V> {
-    pub fn new() -> Self {
-        Self::default()
-    }
-
     pub fn child(mut self, child: impl Element<V>) -> Self {
-        self.children.push(child.into_any());
+        self.content.push(child.into_any());
         self
     }
 
@@ -58,7 +73,7 @@ impl<V: View> Node<V> {
         I: IntoIterator<Item = E>,
         E: Element<V>,
     {
-        self.children
+        self.content
             .extend(children.into_iter().map(|child| child.into_any()));
         self
     }
@@ -100,7 +115,7 @@ impl<V: View> Node<V> {
         let mut cross_axis_max: f32 = 0.0;
 
         // First pass: Layout non-flex children only
-        for child in &mut self.children {
+        for child in &mut self.content {
             let child_flex = child.metadata::<NodeStyle>().and_then(|style| match axis {
                 Axis2d::X => style.width.flex(),
                 Axis2d::Y => style.height.flex(),
@@ -138,7 +153,7 @@ impl<V: View> Node<V> {
             if total_flex > 0. {
                 let space_per_flex = remaining_space.max(0.) / total_flex;
 
-                for child in &mut self.children {
+                for child in &mut self.content {
                     let child_flex = child.metadata::<NodeStyle>().and_then(|style| match axis {
                         Axis2d::X => style.width.flex(),
                         Axis2d::Y => style.height.flex(),
@@ -209,7 +224,7 @@ impl<V: View> Node<V> {
             align_vertically,
         );
 
-        for child in &mut self.children {
+        for child in &mut self.content {
             // Align each child along the cross axis
             align_horizontally = !align_horizontally;
             align_vertically = !align_vertically;
@@ -400,7 +415,7 @@ impl<V: View> Element<V> for Node<V> {
             });
         }
 
-        if !self.children.is_empty() {
+        if !self.content.is_empty() {
             // Account for padding first.
             let padding = &self.style.padding;
             let padded_bounds = RectF::from_points(
@@ -442,7 +457,7 @@ impl<V: View> Element<V> for Node<V> {
         view: &V,
         cx: &ViewContext<V>,
     ) -> Option<RectF> {
-        self.children
+        self.content
             .iter()
             .find_map(|child| child.rect_for_text_range(range_utf16.clone(), view, cx))
     }
@@ -456,9 +471,10 @@ impl<V: View> Element<V> for Node<V> {
         cx: &ViewContext<V>,
     ) -> Value {
         json!({
-            "type": "Cell",
+            "type": "Node",
             "bounds": bounds.to_json(),
-            "children": self.children.iter().map(|child| child.debug(view, cx)).collect::<Vec<Value>>()
+            // TODO!
+            // "children": self.content.iter().map(|child| child.debug(view, cx)).collect::<Vec<Value>>()
         })
     }
 
@@ -574,26 +590,30 @@ impl Border {
     }
 }
 
-#[derive(Clone, Copy, Default)]
-pub enum Length {
-    #[default]
-    Hug,
-    Fixed(f32),
-    Auto {
-        flex: f32,
-        min: f32,
-        max: f32,
-    },
-}
+pub mod length {
+    #[derive(Clone, Copy, Default)]
+    pub enum Length {
+        #[default]
+        Hug,
+        Fixed(f32),
+        Auto {
+            flex: f32,
+            min: f32,
+            max: f32,
+        },
+    }
+
+    impl From<f32> for Length {
+        fn from(value: f32) -> Self {
+            Length::Fixed(value)
+        }
+    }
 
-impl From<f32> for Length {
-    fn from(value: f32) -> Self {
-        Length::Fixed(value)
+    pub fn auto() -> Length {
+        flex(1.)
     }
-}
 
-impl Length {
-    pub fn auto(flex: f32) -> Self {
+    pub fn flex(flex: f32) -> Length {
         Length::Auto {
             flex,
             min: 0.,
@@ -601,7 +621,7 @@ impl Length {
         }
     }
 
-    pub fn auto_constrained(flex: f32, min: Option<f32>, max: Option<f32>) -> Self {
+    pub fn constrained(flex: f32, min: Option<f32>, max: Option<f32>) -> Length {
         Length::Auto {
             flex,
             min: min.unwrap_or(0.),
@@ -609,10 +629,12 @@ impl Length {
         }
     }
 
-    pub fn flex(&self) -> Option<f32> {
-        match self {
-            Length::Auto { flex, .. } => Some(*flex),
-            _ => None,
+    impl Length {
+        pub fn flex(&self) -> Option<f32> {
+            match self {
+                Length::Auto { flex, .. } => Some(*flex),
+                _ => None,
+            }
         }
     }
 }

crates/gpui/src/elements/text.rs 🔗

@@ -400,6 +400,58 @@ fn layout_highlighted_chunks<'a>(
     layouts
 }
 
+// Need to figure out how fonts flow through the tree to implement this.
+impl<V: View> Element<V> for Cow<'static, str> {
+    type LayoutState = ();
+
+    type PaintState = ();
+
+    fn layout(
+        &mut self,
+        constraint: SizeConstraint,
+        view: &mut V,
+        cx: &mut LayoutContext<V>,
+    ) -> (Vector2F, Self::LayoutState) {
+        todo!()
+    }
+
+    fn paint(
+        &mut self,
+        scene: &mut SceneBuilder,
+        bounds: RectF,
+        visible_bounds: RectF,
+        layout: &mut Self::LayoutState,
+        view: &mut V,
+        cx: &mut ViewContext<V>,
+    ) -> Self::PaintState {
+        todo!()
+    }
+
+    fn rect_for_text_range(
+        &self,
+        range_utf16: Range<usize>,
+        bounds: RectF,
+        visible_bounds: RectF,
+        layout: &Self::LayoutState,
+        paint: &Self::PaintState,
+        view: &V,
+        cx: &ViewContext<V>,
+    ) -> Option<RectF> {
+        todo!()
+    }
+
+    fn debug(
+        &self,
+        bounds: RectF,
+        layout: &Self::LayoutState,
+        paint: &Self::PaintState,
+        view: &V,
+        cx: &ViewContext<V>,
+    ) -> crate::serde_json::Value {
+        todo!()
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

crates/gpui_macros/Cargo.toml 🔗

@@ -10,7 +10,7 @@ proc-macro = true
 doctest = false
 
 [dependencies]
+lazy_static.workspace = true
+proc-macro2 = "1.0"
 syn = "1.0"
 quote = "1.0"
-proc-macro2 = "1.0"
-

crates/gpui_macros/src/gpui_macros.rs 🔗

@@ -1,10 +1,11 @@
 use proc_macro::TokenStream;
 use proc_macro2::Ident;
-use quote::{format_ident, quote};
+use quote::{format_ident, quote, ToTokens};
 use std::mem;
 use syn::{
     parse_macro_input, parse_quote, spanned::Spanned as _, AttributeArgs, DeriveInput, FnArg,
-    ItemFn, Lit, Meta, NestedMeta, Type,
+    GenericParam, Generics, ItemFn, Lit, Meta, NestedMeta, Type, TypeGenerics, TypeParam,
+    WhereClause,
 };
 
 #[proc_macro_attribute]
@@ -278,14 +279,44 @@ fn parse_bool(literal: &Lit) -> Result<bool, TokenStream> {
 
 #[proc_macro_derive(Element)]
 pub fn element_derive(input: TokenStream) -> TokenStream {
-    // Parse the input tokens into a syntax tree
-    let input = parse_macro_input!(input as DeriveInput);
+    let ast = parse_macro_input!(input as DeriveInput);
+    let type_name = ast.ident;
 
-    // The name of the struct/enum
-    let name = input.ident;
+    let placeholder_view_generics: Generics = parse_quote! { <V: View> };
+    let placeholder_view_type_name: Ident = parse_quote! { V };
+    let view_type_name: Ident;
+    let impl_generics: syn::ImplGenerics<'_>;
+    let type_generics: Option<syn::TypeGenerics<'_>>;
+    let where_clause: Option<&'_ WhereClause>;
+
+    match ast.generics.params.iter().find_map(|param| {
+        if let GenericParam::Type(type_param) = param {
+            Some(type_param.ident.clone())
+        } else {
+            None
+        }
+    }) {
+        Some(type_name) => {
+            view_type_name = type_name;
+            let generics = ast.generics.split_for_impl();
+            impl_generics = generics.0;
+            type_generics = Some(generics.1);
+            where_clause = generics.2;
+        }
+        _ => {
+            view_type_name = placeholder_view_type_name;
+            let generics = placeholder_view_generics.split_for_impl();
+            impl_generics = generics.0;
+            type_generics = None;
+            where_clause = generics.2;
+        }
+    }
+
+    let gen = quote! {
+        impl #impl_generics Element<#view_type_name> for #type_name #type_generics
+        #where_clause
+        {
 
-    let expanded = quote! {
-        impl<V: gpui::View> gpui::elements::Element<V> for #name {
             type LayoutState = gpui::elements::AnyElement<V>;
             type PaintState = ();
 
@@ -337,6 +368,6 @@ pub fn element_derive(input: TokenStream) -> TokenStream {
             }
         }
     };
-    // Return generated code
-    TokenStream::from(expanded)
+
+    gen.into()
 }