WIP

Nathan Sobo created

Change summary

Cargo.lock                                          |   1 
crates/gpui/playground/Cargo.toml                   |   3 
crates/gpui/playground/ui/Cargo.toml                |   4 
crates/gpui/playground/ui/src/color.rs              | 189 ++++++++++++
crates/gpui/playground/ui/src/editor_layout_demo.rs | 165 +++++++++++
crates/gpui/playground/ui/src/node.rs               | 222 +++++++++-----
crates/gpui/playground/ui/src/playground_ui.rs      |  28 -
crates/gpui/playground/ui/src/themes.rs             |   1 
crates/gpui/playground/ui/src/themes/rose_pine.rs   |  86 +++++
crates/gpui/playground/ui/src/tokens.rs             |  11 
crates/gpui/src/app.rs                              |  20 +
crates/gpui/src/app/test_app_context.rs             |  13 
crates/gpui/src/color.rs                            |   6 
crates/gpui/src/elements.rs                         |   2 
crates/gpui_macros/src/gpui_macros.rs               |   2 
15 files changed, 647 insertions(+), 106 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5178,6 +5178,7 @@ dependencies = [
  "gpui",
  "log",
  "optional_struct",
+ "smallvec",
 ]
 
 [[package]]

crates/gpui/playground/Cargo.toml 🔗

@@ -13,3 +13,6 @@ playground_ui = { path = "ui" }
 gpui = { path = ".." }
 log.workspace = true
 simplelog = "0.9"
+
+[dev-dependencies]
+gpui = { path = "..", features = ["test-support"] }

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

@@ -13,3 +13,7 @@ derive_more = "0.99.17"
 gpui = { path = "../.." }
 log.workspace = true
 optional_struct = "0.3.1"
+smallvec.workspace = true
+
+[dev-dependencies]
+gpui = { path = "../..", features = ["test-support"] }

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

@@ -0,0 +1,189 @@
+use smallvec::SmallVec;
+
+pub fn rgb(hex: u32) -> Rgba {
+    let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
+    let g = ((hex >> 8) & 0xFF) as f32 / 255.0;
+    let b = (hex & 0xFF) as f32 / 255.0;
+    Rgba { r, g, b, a: 1.0 }
+}
+
+#[derive(Clone, Copy, Default, Debug)]
+pub struct Rgba {
+    pub r: f32,
+    pub g: f32,
+    pub b: f32,
+    pub a: f32,
+}
+
+impl From<Hsla> for Rgba {
+    fn from(color: Hsla) -> Self {
+        let h = color.h;
+        let s = color.s;
+        let l = color.l;
+
+        let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
+        let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs());
+        let m = l - c / 2.0;
+        let cm = c + m;
+        let xm = x + m;
+
+        let (r, g, b) = match (h * 6.0).floor() as i32 {
+            0 | 6 => (cm, xm, m),
+            1 => (xm, cm, m),
+            2 => (m, cm, xm),
+            3 => (m, xm, cm),
+            4 => (xm, m, cm),
+            _ => (cm, m, xm),
+        };
+
+        Rgba {
+            r,
+            g,
+            b,
+            a: color.a,
+        }
+    }
+}
+
+impl Into<gpui::color::Color> for Rgba {
+    fn into(self) -> gpui::color::Color {
+        gpui::color::rgba(self.r, self.g, self.b, self.a)
+    }
+}
+
+#[derive(Copy, Clone)]
+pub struct Hsla {
+    h: f32,
+    s: f32,
+    l: f32,
+    a: f32,
+}
+
+impl From<Rgba> for Hsla {
+    fn from(color: Rgba) -> Self {
+        let r = color.r;
+        let g = color.g;
+        let b = color.b;
+
+        let max = r.max(g.max(b));
+        let min = r.min(g.min(b));
+        let delta = max - min;
+
+        let l = (max + min) / 2.0;
+        let s = match l {
+            0.0 | 1.0 => 0.0,
+            l if l < 0.5 => delta / (2.0 * l),
+            l => delta / (2.0 - 2.0 * l),
+        };
+
+        let h = if delta == 0.0 {
+            0.0
+        } else if max == r {
+            ((g - b) / delta).rem_euclid(6.0) / 6.0
+        } else if max == g {
+            ((b - r) / delta + 2.0) / 6.0
+        } else {
+            ((r - g) / delta + 4.0) / 6.0
+        };
+
+        Hsla {
+            h,
+            s,
+            l,
+            a: color.a,
+        }
+    }
+}
+
+impl Hsla {
+    /// Increases the saturation of the color by a certain amount, with a max
+    /// value of 1.0.
+    pub fn saturate(mut self, amount: f32) -> Self {
+        self.s += amount;
+        self.s = self.s.clamp(0.0, 1.0);
+        self
+    }
+
+    /// Decreases the saturation of the color by a certain amount, with a min
+    /// value of 0.0.
+    pub fn desaturate(mut self, amount: f32) -> Self {
+        self.s -= amount;
+        self.s = self.s.max(0.0);
+        if self.s < 0.0 {
+            self.s = 0.0;
+        }
+        self
+    }
+
+    /// Brightens the color by increasing the lightness by a certain amount,
+    /// with a max value of 1.0.
+    pub fn brighten(mut self, amount: f32) -> Self {
+        self.l += amount;
+        self.l = self.l.clamp(0.0, 1.0);
+        self
+    }
+
+    /// Darkens the color by decreasing the lightness by a certain amount,
+    /// with a max value of 0.0.
+    pub fn darken(mut self, amount: f32) -> Self {
+        self.l -= amount;
+        self.l = self.l.clamp(0.0, 1.0);
+        self
+    }
+}
+
+pub struct ColorScale {
+    colors: SmallVec<[Hsla; 2]>,
+    positions: SmallVec<[f32; 2]>,
+}
+
+pub fn scale<I, C>(colors: I) -> ColorScale
+where
+    I: IntoIterator<Item = C>,
+    C: Into<Hsla>,
+{
+    let mut scale = ColorScale {
+        colors: colors.into_iter().map(Into::into).collect(),
+        positions: SmallVec::new(),
+    };
+    let num_colors: f32 = scale.colors.len() as f32 - 1.0;
+    scale.positions = (0..scale.colors.len())
+        .map(|i| i as f32 / num_colors)
+        .collect();
+    scale
+}
+
+impl ColorScale {
+    fn at(&self, t: f32) -> Hsla {
+        // Ensure that the input is within [0.0, 1.0]
+        debug_assert!(
+            0.0 <= t && t <= 1.0,
+            "t value {} is out of range. Expected value in range 0.0 to 1.0",
+            t
+        );
+
+        let position = match self
+            .positions
+            .binary_search_by(|a| a.partial_cmp(&t).unwrap())
+        {
+            Ok(index) | Err(index) => index,
+        };
+        let lower_bound = position.saturating_sub(1);
+        let upper_bound = position.min(self.colors.len() - 1);
+        let lower_color = &self.colors[lower_bound];
+        let upper_color = &self.colors[upper_bound];
+
+        match upper_bound.checked_sub(lower_bound) {
+            Some(0) | None => *lower_color,
+            Some(_) => {
+                let interval_t = (t - self.positions[lower_bound])
+                    / (self.positions[upper_bound] - self.positions[lower_bound]);
+                let h = lower_color.h + interval_t * (upper_color.h - lower_color.h);
+                let s = lower_color.s + interval_t * (upper_color.s - lower_color.s);
+                let l = lower_color.l + interval_t * (upper_color.l - lower_color.l);
+                let a = lower_color.a + interval_t * (upper_color.a - lower_color.a);
+                Hsla { h, s, l, a }
+            }
+        }
+    }
+}

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

@@ -0,0 +1,165 @@
+use gpui::{AnyElement, Element, LayoutContext, View, ViewContext};
+
+#[derive(Element, Clone, Default)]
+pub struct Playground<V: View>(PhantomData<V>);
+
+// example layout design here: https://www.figma.com/file/5QLTmxjO0xQpDD3CD4hR6T/Untitled?type=design&node-id=0%3A1&mode=design&t=SoJieVVIvDDDKagv-1
+
+impl<V: View> Playground<V> {
+    pub fn render(&mut self, _: &mut V, _: &mut gpui::ViewContext<V>) -> impl Element<V> {
+        col() // fullscreen container with header and main in it
+            .width(flex(1.))
+            .height(flex(1.))
+            .fill(colors(gray.900))
+            .children([
+                row() // header container
+                    .fill(colors(gray.900))
+                    .width(flex(1.))
+                    .children([
+                        row() // tab bar
+                            .width(flex(1.))
+                            .gap(spacing(2))
+                            .padding(spacing(3))
+                            .overflow_x(scroll())
+                            .chidren([
+                                row() // tab
+                                    .padding_x(spacing(3))
+                                    .padding_y(spacing(2))
+                                    .corner_radius(6.)
+                                    .gap(spacing(3))
+                                    .align(center())
+                                    .fill(colors(gray.800))
+                                    .children([text("Tab title 1"), svg("icon_name")]),
+                                row() // tab
+                                    .padding_x(spacing(3))
+                                    .padding_y(spacing(2))
+                                    .corner_radius(6.)
+                                    .gap(spacing(3))
+                                    .align(center())
+                                    .fill(colors(gray.800))
+                                    .children([text("Tab title 2"), svg("icon_name")]),
+                                row() // tab
+                                    .padding_x(spacing(3))
+                                    .padding_y(spacing(2))
+                                    .corner_radius(6.)
+                                    .gap(spacing(3))
+                                    .align(center())
+                                    .fill(colors(gray.800))
+                                    .children([text("Tab title 3"), svg("icon_name")]),
+                            ]),
+                        row() // tab bar actions
+                            .border_left(colors(gray.700))
+                            .gap(spacing(2))
+                            .padding(spacing(3))
+                            .chidren([
+                                row()
+                                    .width(spacing(8))
+                                    .height(spacing(8))
+                                    .corner_radius(6.)
+                                    .justify(center())
+                                    .align(center())
+                                    .fill(colors(gray.800))
+                                    .child(svg(icon_name)),
+                                row()
+                                    .width(spacing(8))
+                                    .height(spacing(8))
+                                    .corner_radius(6.)
+                                    .justify(center())
+                                    .align(center())
+                                    .fill(colors(gray.800))
+                                    .child(svg(icon_name)),
+                                row()
+                                    .width(spacing(8))
+                                    .height(spacing(8))
+                                    .corner_radius(6.)
+                                    .justify(center())
+                                    .align(center())
+                                    .fill(colors(gray.800))
+                                    .child(svg(icon_name)),
+                            ]),
+                    ]),
+                row() // main container
+                    .width(flex(1.))
+                    .height(flex(1.))
+                    .children([
+                        col() // left sidebar
+                            .fill(colors(gray.800))
+                            .border_right(colors(gray.700))
+                            .height(flex(1.))
+                            .width(260.)
+                            .children([
+                                col() // containter to hold list items and notification alert box
+                                    .justify(between())
+                                    .padding_x(spacing(6))
+                                    .padding_bottom(3)
+                                    .padding_top(spacing(6))
+                                    .children([
+                                        col().gap(spacing(3)).children([ // sidebar list
+                                            text("Item"),
+                                            text("Item"),
+                                            text("Item"),
+                                            text("Item"),
+                                            text("Item"),
+                                            text("Item"),
+                                            text("Item"),
+                                            text("Item"),
+                                        ]),
+                                        col().align(center()).gap(spacing(1)).children([ // notification alert box
+                                            text("Title text").size("lg"),
+                                            text("Description text goes here")
+                                                .text_color(colors(rose.200)),
+                                        ]),
+                                    ]),
+                                row()
+                                    .padding_x(spacing(3))
+                                    .padding_y(spacing(2))
+                                    .border_top(1., colors(gray.700))
+                                    .align(center())
+                                    .gap(spacing(2))
+                                    .fill(colors(gray.900))
+                                    .children([
+                                        row() // avatar container
+                                            .width(spacing(8))
+                                            .height(spacing(8))
+                                            .corner_radius(spacing(8))
+                                            .justify(center())
+                                            .align(center())
+                                            .child(image(image_url)),
+                                        text("FirstName Lastname"), // user name
+                                    ]),
+                            ]),
+                        col() // primary content container
+                            .align(center())
+                            .justify(center())
+                            .child(
+                                col().justify(center()).gap(spacing(8)).children([ // detail container wrapper for center positioning
+                                    col() // blue rectangle
+                                        .width(rem(30.))
+                                        .height(rem(20.))
+                                        .corner_radius(16.)
+                                        .fill(colors(blue.200)),
+                                    col().gap(spacing(1)).children([ // center content text items
+                                        text("This is a title").size("lg"),
+                                        text("This is a description").text_color(colors(gray.500)),
+                                    ]),
+                                ]),
+                            ),
+                        col(), // right sidebar
+                    ]),
+            ])
+    }
+}
+
+// row(
+//     padding(),
+//     width(),
+//     fill(),
+// )
+
+// .width(flex(1.))
+// .height(flex(1.))
+// .justify(end())
+// .align(start()) // default
+// .fill(green)
+// .child(other_tab_bar())
+// .child(profile_menu())

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

@@ -1,5 +1,6 @@
-use derive_more::Add;
+use derive_more::{Add, Deref, DerefMut};
 use gpui::elements::layout_highlighted_chunks;
+use gpui::Entity;
 use gpui::{
     color::Color,
     fonts::HighlightStyle,
@@ -19,16 +20,14 @@ use log::warn;
 use optional_struct::*;
 use std::{any::Any, borrow::Cow, f32, ops::Range, sync::Arc};
 
+use crate::color::Rgba;
+
 pub struct Node<V: View> {
     style: NodeStyle,
     children: Vec<AnyElement<V>>,
     id: Option<Cow<'static, str>>,
 }
 
-pub fn node<V: View>(child: impl Element<V>) -> Node<V> {
-    Node::default().child(child)
-}
-
 pub fn column<V: View>() -> Node<V> {
     Node::default()
 }
@@ -91,10 +90,22 @@ impl<V: View> Element<V> for Node<V> {
         view: &mut V,
         cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
+        dbg!(self.id_string());
+        dbg!(bounds.origin(), bounds.size());
+
+        let bounds_center = dbg!(bounds.size()) / 2.;
+        let bounds_target = bounds_center + (bounds_center * self.style.align.0);
+        let layout_center = dbg!(layout.size) / 2.;
+        let layout_target = layout_center + layout_center * self.style.align.0;
+        let delta = bounds_target - layout_target;
+
+        let aligned_bounds = RectF::new(bounds.origin() + delta, layout.size);
+        dbg!(aligned_bounds.origin(), aligned_bounds.size());
         let margined_bounds = RectF::from_points(
-            bounds.origin() + vec2f(layout.margins.left, layout.margins.top),
-            bounds.lower_right() - vec2f(layout.margins.right, layout.margins.bottom),
+            aligned_bounds.origin() + vec2f(layout.margins.left, layout.margins.top),
+            aligned_bounds.lower_right() - vec2f(layout.margins.right, layout.margins.bottom),
         );
+        dbg!(margined_bounds.origin(), margined_bounds.size());
 
         // Paint drop shadow
         for shadow in &self.style.shadows {
@@ -118,17 +129,11 @@ impl<V: View> Element<V> for Node<V> {
 
         // Render the background and/or the border.
         let Fill::Color(fill_color) = self.style.fill;
-        let is_fill_visible = !fill_color.is_fully_transparent();
+        let is_fill_visible = fill_color.a > 0.;
         if is_fill_visible || self.style.borders.is_visible() {
-            eprintln!(
-                "{}: paint background: {:?}",
-                self.id.as_deref().unwrap_or(""),
-                margined_bounds
-            );
-
             scene.push_quad(Quad {
                 bounds: margined_bounds,
-                background: is_fill_visible.then_some(fill_color),
+                background: is_fill_visible.then_some(fill_color.into()),
                 border: scene::Border {
                     width: self.style.borders.width,
                     color: self.style.borders.color,
@@ -162,35 +167,7 @@ impl<V: View> Element<V> for Node<V> {
                 // let parent_size = padded_bounds.size();
                 let mut child_origin = padded_bounds.origin();
 
-                // Align all children together along the primary axis
-                // let mut align_horizontally = false;
-                // let mut align_vertically = false;
-                // match axis {
-                //     Axis2d::X => align_horizontally = true,
-                //     Axis2d::Y => align_vertically = true,
-                // }
-                // align_child(
-                //     &mut child_origin,
-                //     parent_size,
-                //     layout.content_size,
-                //     self.style.align.0,
-                //     align_horizontally,
-                //     align_vertically,
-                // );
-
                 for child in &mut self.children {
-                    // Align each child along the cross axis
-                    // align_horizontally = !align_horizontally;
-                    // align_vertically = !align_vertically;
-                    // align_child(
-                    //     &mut child_origin,
-                    //     parent_size,
-                    //     child.size(),
-                    //     self.style.align.0,
-                    //     align_horizontally,
-                    //     align_vertically,
-                    // );
-                    //
                     child.paint(scene, child_origin, visible_bounds, view, cx);
 
                     // Advance along the primary axis by the size of this child
@@ -284,6 +261,16 @@ impl<V: View> Node<V> {
         self
     }
 
+    pub fn margin_x(mut self, margin: impl Into<Length>) -> Self {
+        self.style.margins.set_x(margin.into());
+        self
+    }
+
+    pub fn margin_y(mut self, margin: impl Into<Length>) -> Self {
+        self.style.margins.set_y(margin.into());
+        self
+    }
+
     pub fn margin_top(mut self, top: Length) -> Self {
         self.style.margins.top = top;
         self
@@ -304,6 +291,23 @@ impl<V: View> Node<V> {
         self
     }
 
+    pub fn align(mut self, alignment: f32) -> Self {
+        let cross_axis = self
+            .style
+            .axis
+            .to_2d()
+            .map(Axis2d::rotate)
+            .unwrap_or(Axis2d::Y);
+        self.style.align.set(cross_axis, alignment);
+        self
+    }
+
+    pub fn justify(mut self, alignment: f32) -> Self {
+        let axis = self.style.axis.to_2d().unwrap_or(Axis2d::X);
+        self.style.align.set(axis, alignment);
+        self
+    }
+
     fn id_string(&self) -> String {
         self.id.as_deref().unwrap_or("<anonymous>").to_string()
     }
@@ -458,7 +462,7 @@ impl<V: View> Node<V> {
                     let mut margin_flex = self.style.margins.flex().get(axis);
                     let mut max_margin_length = constraint.max.get(axis) - fixed_length;
                     layout.margins.compute_flex_edges(
-                        &self.style.padding,
+                        &self.style.margins,
                         axis,
                         &mut margin_flex,
                         &mut max_margin_length,
@@ -502,7 +506,8 @@ impl<V: View> Node<V> {
             }
         }
 
-        layout
+        dbg!(self.id_string());
+        dbg!(layout)
     }
 }
 
@@ -549,27 +554,6 @@ impl From<Length> for LeftRight {
     }
 }
 
-fn align_child(
-    child_origin: &mut Vector2F,
-    parent_size: Vector2F,
-    child_size: Vector2F,
-    alignment: Vector2F,
-    horizontal: bool,
-    vertical: bool,
-) {
-    let parent_center = parent_size / 2.;
-    let parent_target = parent_center + parent_center * alignment;
-    let child_center = child_size / 2.;
-    let child_target = child_center + child_center * alignment;
-
-    if horizontal {
-        child_origin.set_x(child_origin.x() + parent_target.x() - child_target.x())
-    }
-    if vertical {
-        child_origin.set_y(child_origin.y() + parent_target.y() - child_target.y());
-    }
-}
-
 struct Interactive<Style> {
     default: Style,
     hovered: Style,
@@ -581,7 +565,7 @@ struct Interactive<Style> {
 pub struct NodeStyle {
     axis: Axis3d,
     wrap: bool,
-    align: Align,
+    align: Alignment,
     overflow_x: Overflow,
     overflow_y: Overflow,
     gap_x: Gap,
@@ -697,6 +681,18 @@ impl<T> Edges<T> {
     }
 }
 
+impl<T: Clone> Edges<T> {
+    pub fn set_x(&mut self, value: T) {
+        self.left = value.clone();
+        self.right = value
+    }
+
+    pub fn set_y(&mut self, value: T) {
+        self.top = value.clone();
+        self.bottom = value
+    }
+}
+
 impl Edges<f32> {
     fn size(&self) -> Vector2F {
         vec2f(self.left + self.right, self.top + self.bottom)
@@ -838,18 +834,18 @@ struct CornerRadii {
 
 #[derive(Clone)]
 pub enum Fill {
-    Color(Color),
+    Color(Rgba),
 }
 
-impl From<Color> for Fill {
-    fn from(value: Color) -> Self {
-        Fill::Color(value)
+impl<C: Into<Rgba>> From<C> for Fill {
+    fn from(value: C) -> Self {
+        Fill::Color(value.into())
     }
 }
 
 impl Default for Fill {
     fn default() -> Self {
-        Fill::Color(Color::default())
+        Fill::Color(Rgba::default())
     }
 }
 
@@ -1028,10 +1024,10 @@ pub mod length {
     }
 }
 
-#[derive(Clone)]
-struct Align(Vector2F);
+#[derive(Clone, Deref, DerefMut)]
+struct Alignment(Vector2F);
 
-impl Default for Align {
+impl Default for Alignment {
     fn default() -> Self {
         Self(vec2f(-1., -1.))
     }
@@ -1251,8 +1247,6 @@ impl<V: View> Element<V> for Text {
         _: &mut V,
         cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
-        dbg!(bounds);
-
         let mut origin = bounds.origin();
         let empty = Vec::new();
         let mut callback = |_, _, _: &mut SceneBuilder, _: &mut AppContext| {};
@@ -1466,7 +1460,7 @@ trait ElementExt<V: View> {
     where
         Self: Element<V> + Sized,
     {
-        node(self).margin_left(margin_left)
+        column().child(self).margin_left(margin_left)
     }
 }
 
@@ -1479,6 +1473,76 @@ where
     where
         Self: Sized,
     {
-        node(self).margin_left(margin_left)
+        column().child(self).margin_left(margin_left)
+    }
+}
+
+pub fn view<F, E>(mut function: F) -> ViewFn
+where
+    F: 'static + FnMut(&mut ViewContext<ViewFn>) -> E,
+    E: Element<ViewFn>,
+{
+    ViewFn(Box::new(move |cx| (function)(cx).into_any()))
+}
+
+pub struct ViewFn(Box<dyn FnMut(&mut ViewContext<ViewFn>) -> AnyElement<ViewFn>>);
+
+impl Entity for ViewFn {
+    type Event = ();
+}
+
+impl View for ViewFn {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        (self.0)(cx)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::themes::rose_pine::{self, RosePineThemes};
+
+    use super::{
+        length::{auto, rems},
+        *,
+    };
+    use gpui::TestAppContext;
+
+    #[gpui::test]
+    fn test_layout(cx: &mut TestAppContext) {
+        let view = cx
+            .add_window(|_| {
+                view(|_| {
+                    let theme = rose_pine::dawn();
+                    column()
+                        .width(auto())
+                        .height(auto())
+                        .justify(1.)
+                        .child(
+                            row()
+                                .width(auto())
+                                .height(rems(10.))
+                                .fill(theme.foam)
+                                .justify(1.)
+                                .child(row().width(rems(10.)).height(auto()).fill(theme.gold)),
+                        )
+                        .child(row())
+                });
+            })
+            .1;
+
+        // tree.layout(
+        //     SizeConstraint::strict(vec2f(100., 100.)),
+        //     &mut (),
+        //     LayoutContext::test(),
+        // );
+
+        // LayoutContext::new(
+        //     cx,
+        //     new_parents,
+        //     views_to_notify_if_ancestors_change,
+        //     refreshing,
+        // )
+
+        // tree.layout(SizeConstraint::strict(vec2f(100., 100.)), &mut (), cx)
     }
 }

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

@@ -1,33 +1,21 @@
-use gpui::{color::Color, AnyElement, Element, LayoutContext, View, ViewContext};
-use node::{
-    length::{auto, rems},
-    *,
-};
+use gpui::{AnyElement, Element, LayoutContext, View, ViewContext};
+use node::{length::auto, *};
 use std::{borrow::Cow, cell::RefCell, marker::PhantomData, rc::Rc};
 use tokens::{margin::m4, text::lg};
 
+mod color;
 mod node;
+mod themes;
 mod tokens;
 
 #[derive(Element, Clone, Default)]
 pub struct Playground<V: View>(PhantomData<V>);
 
+impl<V: View> Node<V> {}
+
 impl<V: View> Playground<V> {
-    pub fn render(&mut self, _: &mut V, _: &mut gpui::ViewContext<V>) -> AnyElement<V> {
-        row()
-            .id("green row")
-            .width(auto())
-            .height(rems(20.))
-            .fill(Color::green())
-        // .child(
-        //     row()
-        //         .id("blue box")
-        //         .width(rems(20.))
-        //         .height(auto())
-        //         .margin_left(auto())
-        //         .fill(Color::blue()),
-        // )
-        // .into_any()
+    pub fn render(&mut self, _: &mut V, _: &mut gpui::ViewContext<V>) -> impl Element<V> {
+        column()
     }
 }
 

crates/gpui/playground/ui/src/themes/rose_pine.rs 🔗

@@ -0,0 +1,86 @@
+use crate::color::{rgb, Rgba};
+
+#[derive(Clone, Copy, Debug)]
+pub struct ThemeColors {
+    pub base: Rgba,
+    pub surface: Rgba,
+    pub overlay: Rgba,
+    pub muted: Rgba,
+    pub subtle: Rgba,
+    pub text: Rgba,
+    pub love: Rgba,
+    pub gold: Rgba,
+    pub rose: Rgba,
+    pub pine: Rgba,
+    pub foam: Rgba,
+    pub iris: Rgba,
+    pub highlight_low: Rgba,
+    pub highlight_med: Rgba,
+    pub highlight_high: Rgba,
+}
+
+pub struct RosePineThemes {
+    pub default: ThemeColors,
+    pub dawn: ThemeColors,
+    pub moon: ThemeColors,
+}
+
+pub fn default() -> ThemeColors {
+    ThemeColors {
+        base: rgb(0x191724),
+        surface: rgb(0x1f1d2e),
+        overlay: rgb(0x26233a),
+        muted: rgb(0x6e6a86),
+        subtle: rgb(0x908caa),
+        text: rgb(0xe0def4),
+        love: rgb(0xeb6f92),
+        gold: rgb(0xf6c177),
+        rose: rgb(0xebbcba),
+        pine: rgb(0x31748f),
+        foam: rgb(0x9ccfd8),
+        iris: rgb(0xc4a7e7),
+        highlight_low: rgb(0x21202e),
+        highlight_med: rgb(0x403d52),
+        highlight_high: rgb(0x524f67),
+    }
+}
+
+pub fn moon() -> ThemeColors {
+    ThemeColors {
+        base: rgb(0x232136),
+        surface: rgb(0x2a273f),
+        overlay: rgb(0x393552),
+        muted: rgb(0x6e6a86),
+        subtle: rgb(0x908caa),
+        text: rgb(0xe0def4),
+        love: rgb(0xeb6f92),
+        gold: rgb(0xf6c177),
+        rose: rgb(0xea9a97),
+        pine: rgb(0x3e8fb0),
+        foam: rgb(0x9ccfd8),
+        iris: rgb(0xc4a7e7),
+        highlight_low: rgb(0x2a283e),
+        highlight_med: rgb(0x44415a),
+        highlight_high: rgb(0x56526e),
+    }
+}
+
+pub fn dawn() -> ThemeColors {
+    ThemeColors {
+        base: rgb(0xfaf4ed),
+        surface: rgb(0xfffaf3),
+        overlay: rgb(0xf2e9e1),
+        muted: rgb(0x9893a5),
+        subtle: rgb(0x797593),
+        text: rgb(0x575279),
+        love: rgb(0xb4637a),
+        gold: rgb(0xea9d34),
+        rose: rgb(0xd7827e),
+        pine: rgb(0x286983),
+        foam: rgb(0x56949f),
+        iris: rgb(0x907aa9),
+        highlight_low: rgb(0xf4ede8),
+        highlight_med: rgb(0xdfdad9),
+        highlight_high: rgb(0xcecacd),
+    }
+}

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

@@ -1,4 +1,13 @@
-pub mod color {}
+pub mod color {
+    use crate::color::{scale, ColorScale, Hsla};
+
+    pub fn ramp(color: impl Into<Hsla>) -> ColorScale {
+        let color = color.into();
+        let end_color = color.desaturate(0.1).brighten(0.5);
+        let start_color = color.desaturate(0.1).darken(0.4);
+        scale([start_color, color, end_color])
+    }
+}
 
 pub mod text {
     use crate::node::length::{rems, Rems};

crates/gpui/src/app.rs 🔗

@@ -42,7 +42,7 @@ pub use test_app_context::{ContextHandle, TestAppContext};
 use window_input_handler::WindowInputHandler;
 
 use crate::{
-    elements::{AnyElement, AnyRootElement, RootElement},
+    elements::{AnyElement, AnyRootElement, Empty, RootElement},
     executor::{self, Task},
     fonts::TextStyle,
     json,
@@ -53,7 +53,7 @@ use crate::{
     },
     util::post_inc,
     window::{Window, WindowContext},
-    AssetCache, AssetSource, ClipboardItem, FontCache, MouseRegionId,
+    AssetCache, AssetSource, ClipboardItem, Element, FontCache, MouseRegionId,
 };
 
 use self::ref_counts::RefCounts;
@@ -71,10 +71,12 @@ pub trait Entity: 'static {
 }
 
 pub trait View: Entity + Sized {
-    fn ui_name() -> &'static str;
     fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self>;
     fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {}
     fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {}
+    fn ui_name() -> &'static str {
+        type_name::<Self>()
+    }
     fn key_down(&mut self, _: &KeyDownEvent, _: &mut ViewContext<Self>) -> bool {
         false
     }
@@ -125,6 +127,16 @@ pub trait View: Entity + Sized {
     }
 }
 
+impl Entity for () {
+    type Event = ();
+}
+
+impl View for () {
+    fn render(&mut self, _: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
+        Empty::new().into_any()
+    }
+}
+
 pub trait BorrowAppContext {
     fn read_with<T, F: FnOnce(&AppContext) -> T>(&self, f: F) -> T;
     fn update<T, F: FnOnce(&mut AppContext) -> T>(&mut self, f: F) -> T;
@@ -3364,7 +3376,7 @@ impl<V> BorrowWindowContext for ViewContext<'_, '_, V> {
     }
 }
 
-pub struct LayoutContext<'a, 'b, 'c, V: View> {
+pub struct LayoutContext<'a, 'b, 'c, V> {
     view_context: &'c mut ViewContext<'a, 'b, V>,
     new_parents: &'c mut HashMap<usize, usize>,
     views_to_notify_if_ancestors_change: &'c mut HashMap<usize, SmallVec<[usize; 2]>>,

crates/gpui/src/app/test_app_context.rs 🔗

@@ -161,6 +161,19 @@ impl TestAppContext {
         (window_id, view)
     }
 
+    pub fn add_window2<T, F>(&mut self, build_root_view: F) -> WindowHandle<T>
+    where
+        T: View,
+        F: FnOnce(&mut ViewContext<T>) -> T,
+    {
+        let (window_id, view) = self
+            .cx
+            .borrow_mut()
+            .add_window(Default::default(), build_root_view);
+        self.simulate_window_activation(Some(window_id));
+        (window_id, view)
+    }
+
     pub fn add_view<T, F>(&mut self, window_id: usize, build_view: F) -> ViewHandle<T>
     where
         T: View,

crates/gpui/src/color.rs 🔗

@@ -141,6 +141,12 @@ impl<'de> Deserialize<'de> for Color {
     }
 }
 
+impl From<u32> for Color {
+    fn from(value: u32) -> Self {
+        Self(ColorU::from_u32(value))
+    }
+}
+
 impl ToJson for Color {
     fn to_json(&self) -> serde_json::Value {
         json!(format!(

crates/gpui/src/elements.rs 🔗

@@ -201,7 +201,7 @@ pub trait Element<V: View>: 'static {
     }
 }
 
-trait AnyElementState<V: View> {
+trait AnyElementState<V> {
     fn layout(
         &mut self,
         constraint: SizeConstraint,

crates/gpui_macros/src/gpui_macros.rs 🔗

@@ -325,7 +325,7 @@ pub fn element_derive(input: TokenStream) -> TokenStream {
                 view: &mut V,
                 cx: &mut gpui::LayoutContext<V>,
             ) -> (gpui::geometry::vector::Vector2F, gpui::elements::AnyElement<V>) {
-                let mut element = self.render(view, cx);
+                let mut element = self.render(view, cx).into_any();
                 let size = element.layout(constraint, view, cx);
                 (size, element)
             }