Checkpoint: Working toward compatibility with themes

Nathan Sobo created

Change summary

crates/gpui/playground/src/color.rs      |  52 ++++++
crates/gpui/playground/src/components.rs |   6 
crates/gpui/playground/src/playground.rs |  12 -
crates/gpui/playground/src/themes.rs     | 201 ++++++++++++++-----------
crates/gpui/playground/src/workspace.rs  |  25 +-
5 files changed, 181 insertions(+), 115 deletions(-)

Detailed changes

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

@@ -1,8 +1,9 @@
 #![allow(dead_code)]
 
-use std::{num::ParseIntError, ops::Range};
-
+use serde::de::{self, Deserialize, Deserializer, Visitor};
 use smallvec::SmallVec;
+use std::fmt;
+use std::{num::ParseIntError, ops::Range};
 
 pub fn rgb<C: From<Rgba>>(hex: u32) -> C {
     let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
@@ -19,6 +20,40 @@ pub struct Rgba {
     pub a: f32,
 }
 
+struct RgbaVisitor;
+
+impl<'de> Visitor<'de> for RgbaVisitor {
+    type Value = Rgba;
+
+    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+        formatter.write_str("a string in the format #rrggbb or #rrggbbaa")
+    }
+
+    fn visit_str<E: de::Error>(self, value: &str) -> Result<Rgba, E> {
+        if value.len() == 7 || value.len() == 9 {
+            let r = u8::from_str_radix(&value[1..3], 16).unwrap() as f32 / 255.0;
+            let g = u8::from_str_radix(&value[3..5], 16).unwrap() as f32 / 255.0;
+            let b = u8::from_str_radix(&value[5..7], 16).unwrap() as f32 / 255.0;
+            let a = if value.len() == 9 {
+                u8::from_str_radix(&value[7..9], 16).unwrap() as f32 / 255.0
+            } else {
+                1.0
+            };
+            Ok(Rgba { r, g, b, a })
+        } else {
+            Err(E::custom(
+                "Bad format for RGBA. Expected #rrggbb or #rrggbbaa.",
+            ))
+        }
+    }
+}
+
+impl<'de> Deserialize<'de> for Rgba {
+    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+        deserializer.deserialize_str(RgbaVisitor)
+    }
+}
+
 pub trait Lerp {
     fn lerp(&self, level: f32) -> Hsla;
 }
@@ -219,6 +254,19 @@ impl Into<gpui::color::Color> for Hsla {
     }
 }
 
+impl<'de> Deserialize<'de> for Hsla {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        // First, deserialize it into Rgba
+        let rgba = Rgba::deserialize(deserializer)?;
+
+        // Then, use the From<Rgba> for Hsla implementation to convert it
+        Ok(Hsla::from(rgba))
+    }
+}
+
 pub struct ColorScale {
     colors: SmallVec<[Hsla; 2]>,
     positions: SmallVec<[f32; 2]>,

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

@@ -4,7 +4,7 @@ use crate::{
     interactive::Interactive,
     style::StyleHelpers,
     text::ArcCow,
-    themes::Theme,
+    // themes::Theme,
 };
 use gpui::{platform::MouseButton, ViewContext};
 use playground_macros::Element;
@@ -82,10 +82,10 @@ impl<V: 'static, D: 'static> Button<V, D> {
         view: &mut V,
         cx: &mut ViewContext<V>,
     ) -> impl IntoElement<V> + Interactive<V> {
-        let colors = &cx.theme::<Theme>().colors;
+        // let colors = &cx.theme::<Theme>().colors;
 
         let button = div()
-            .fill(colors.error(0.5))
+            // .fill(colors.error(0.5))
             .h_4()
             .children(self.label.clone());
 

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

@@ -1,12 +1,12 @@
 #![allow(dead_code, unused_variables)]
-use element::Element;
+use crate::element::Element;
 use gpui::{
     geometry::{rect::RectF, vector::vec2f},
     platform::WindowOptions,
 };
 use log::LevelFilter;
 use simplelog::SimpleLogger;
-use themes::{rose_pine, Theme, ThemeColors};
+use themes::Theme;
 use view::view;
 use workspace::workspace;
 
@@ -39,13 +39,7 @@ fn main() {
                 center: true,
                 ..Default::default()
             },
-            |_| {
-                view(|cx| {
-                    playground(Theme {
-                        colors: rose_pine::dawn(),
-                    })
-                })
-            },
+            |_| view(|cx| playground(Theme::default())),
         );
         cx.platform().activate(true);
     });

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

@@ -1,107 +1,134 @@
 use crate::{
-    color::{Hsla, Lerp},
+    color::Hsla,
     element::{Element, PaintContext},
     layout_context::LayoutContext,
 };
-use gpui::{AppContext, WindowContext};
-use std::{marker::PhantomData, ops::Range};
+use gpui::WindowContext;
+use serde::{de::Visitor, Deserialize, Deserializer};
+use std::{collections::HashMap, fmt, marker::PhantomData};
 
-pub mod rose_pine;
-
-#[derive(Clone, Debug)]
+#[derive(Deserialize, Clone, Default, Debug)]
 pub struct Theme {
-    pub colors: ThemeColors,
+    name: String,
+    is_light: bool,
+    lowest: Layer,
+    middle: Layer,
+    highest: Layer,
+    popover_shadow: Shadow,
+    modal_shadow: Shadow,
+    #[serde(deserialize_with = "deserialize_player_colors")]
+    players: Vec<PlayerColors>,
+    #[serde(deserialize_with = "deserialize_syntax_colors")]
+    syntax: HashMap<String, Hsla>,
 }
 
-pub fn theme<'a>(cx: &'a WindowContext) -> &'a Theme {
-    cx.theme::<Theme>()
+#[derive(Deserialize, Clone, Default, Debug)]
+pub struct Layer {
+    base: StyleSet,
+    variant: StyleSet,
+    on: StyleSet,
+    accent: StyleSet,
+    positive: StyleSet,
+    warning: StyleSet,
+    negative: StyleSet,
 }
 
-#[derive(Clone, Debug)]
-pub struct ThemeColors {
-    pub base: Range<Hsla>,
-    pub surface: Range<Hsla>,
-    pub overlay: Range<Hsla>,
-    pub muted: Range<Hsla>,
-    pub subtle: Range<Hsla>,
-    pub text: Range<Hsla>,
-    pub highlight_low: Range<Hsla>,
-    pub highlight_med: Range<Hsla>,
-    pub highlight_high: Range<Hsla>,
-    pub success: Range<Hsla>,
-    pub warning: Range<Hsla>,
-    pub error: Range<Hsla>,
-    pub inserted: Range<Hsla>,
-    pub deleted: Range<Hsla>,
-    pub modified: Range<Hsla>,
+#[derive(Deserialize, Clone, Default, Debug)]
+pub struct StyleSet {
+    #[serde(rename = "default")]
+    default: ContainerColors,
+    hovered: ContainerColors,
+    pressed: ContainerColors,
+    active: ContainerColors,
+    disabled: ContainerColors,
+    inverted: ContainerColors,
 }
 
-impl ThemeColors {
-    fn current(cx: &AppContext) -> &Self {
-        cx.global::<Vec<Self>>()
-            .last()
-            .expect("must call within a theme provider")
-    }
-
-    pub fn base(&self, level: f32) -> Hsla {
-        self.base.lerp(level)
-    }
-
-    pub fn surface(&self, level: f32) -> Hsla {
-        self.surface.lerp(level)
-    }
-
-    pub fn overlay(&self, level: f32) -> Hsla {
-        self.overlay.lerp(level)
-    }
-
-    pub fn muted(&self, level: f32) -> Hsla {
-        self.muted.lerp(level)
-    }
-
-    pub fn subtle(&self, level: f32) -> Hsla {
-        self.subtle.lerp(level)
-    }
-
-    pub fn text(&self, level: f32) -> Hsla {
-        self.text.lerp(level)
-    }
-
-    pub fn highlight_low(&self, level: f32) -> Hsla {
-        self.highlight_low.lerp(level)
-    }
-
-    pub fn highlight_med(&self, level: f32) -> Hsla {
-        self.highlight_med.lerp(level)
-    }
-
-    pub fn highlight_high(&self, level: f32) -> Hsla {
-        self.highlight_high.lerp(level)
-    }
-
-    pub fn success(&self, level: f32) -> Hsla {
-        self.success.lerp(level)
-    }
+#[derive(Deserialize, Clone, Default, Debug)]
+pub struct ContainerColors {
+    background: Hsla,
+    foreground: Hsla,
+    border: Hsla,
+}
 
-    pub fn warning(&self, level: f32) -> Hsla {
-        self.warning.lerp(level)
-    }
+#[derive(Deserialize, Clone, Default, Debug)]
+pub struct PlayerColors {
+    selection: Hsla,
+    cursor: Hsla,
+}
 
-    pub fn error(&self, level: f32) -> Hsla {
-        self.error.lerp(level)
-    }
+#[derive(Deserialize, Clone, Default, Debug)]
+pub struct Shadow {
+    blur: u8,
+    color: Hsla,
+    offset: Vec<u8>,
+}
 
-    pub fn inserted(&self, level: f32) -> Hsla {
-        self.inserted.lerp(level)
-    }
+pub fn theme<'a>(cx: &'a WindowContext) -> &'a Theme {
+    cx.theme::<Theme>()
+}
 
-    pub fn deleted(&self, level: f32) -> Hsla {
-        self.deleted.lerp(level)
-    }
+fn deserialize_player_colors<'de, D>(deserializer: D) -> Result<Vec<PlayerColors>, D::Error>
+where
+    D: Deserializer<'de>,
+{
+    struct PlayerArrayVisitor;
+
+    impl<'de> Visitor<'de> for PlayerArrayVisitor {
+        type Value = Vec<PlayerColors>;
+
+        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+            formatter.write_str("an object with integer keys")
+        }
+
+        fn visit_map<A: serde::de::MapAccess<'de>>(
+            self,
+            mut map: A,
+        ) -> Result<Self::Value, A::Error> {
+            let mut players = Vec::with_capacity(8);
+            while let Some((key, value)) = map.next_entry::<usize, PlayerColors>()? {
+                if key < 8 {
+                    players.push(value);
+                } else {
+                    return Err(serde::de::Error::invalid_value(
+                        serde::de::Unexpected::Unsigned(key as u64),
+                        &"a key in range 0..7",
+                    ));
+                }
+            }
+            Ok(players)
+        }
+    }
+
+    deserializer.deserialize_map(PlayerArrayVisitor)
+}
 
-    pub fn modified(&self, level: f32) -> Hsla {
-        self.modified.lerp(level)
-    }
+fn deserialize_syntax_colors<'de, D>(deserializer: D) -> Result<HashMap<String, Hsla>, D::Error>
+where
+    D: serde::Deserializer<'de>,
+{
+    struct SyntaxVisitor;
+
+    impl<'de> Visitor<'de> for SyntaxVisitor {
+        type Value = HashMap<String, Hsla>;
+
+        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+            formatter.write_str("a map with keys and objects with a single color field as values")
+        }
+
+        fn visit_map<M>(self, mut map: M) -> Result<HashMap<String, Hsla>, M::Error>
+        where
+            M: serde::de::MapAccess<'de>,
+        {
+            let mut result = HashMap::new();
+            while let Some(key) = map.next_key()? {
+                let hsla: Hsla = map.next_value()?; // Deserialize values as Hsla
+                result.insert(key, hsla);
+            }
+            Ok(result)
+        }
+    }
+    deserializer.deserialize_map(SyntaxVisitor)
 }
 
 pub struct Themed<V: 'static, E> {

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

@@ -1,10 +1,9 @@
-use crate::div::div;
-use crate::element::{IntoElement, ParentElement};
-use crate::style::StyleHelpers;
-use crate::themes::theme;
-use crate::{element::Element, themes::Theme};
-use gpui::geometry::pixels;
-use gpui::ViewContext;
+use crate::{
+    div::div,
+    element::{Element, IntoElement, ParentElement},
+    style::StyleHelpers,
+};
+use gpui::{geometry::pixels, ViewContext};
 use playground_macros::Element;
 
 use crate as playground;
@@ -17,29 +16,27 @@ pub fn workspace<V: 'static>() -> impl Element<V> {
 
 impl WorkspaceElement {
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = &cx.theme::<Theme>().colors;
+        // let theme = &cx.theme::<Theme>().colors;
         div()
             .full()
             .flex()
             .flex_col()
-            .fill(theme.base(0.5))
+            // .fill(theme.base(0.5))
             .child(self.title_bar(cx))
             .child(self.stage(cx))
             .child(self.status_bar(cx))
     }
 
     fn title_bar<V: 'static>(&mut self, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let colors = &theme(cx).colors;
-        div().h(pixels(cx.titlebar_height())).fill(colors.base(0.))
+        // let colors = &theme(cx).colors;
+        div().h(pixels(cx.titlebar_height())) //.fill(colors.base(0.))
     }
 
     fn status_bar<V: 'static>(&mut self, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let colors = &theme(cx).colors;
-        div().h(pixels(cx.titlebar_height())).fill(colors.base(0.))
+        div().h(pixels(cx.titlebar_height())) //.fill(colors.base(0.))
     }
 
     fn stage<V: 'static>(&mut self, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let colors = &theme(cx).colors;
         div().flex_grow()
     }
 }