Get playground rendering with backward compatible theming

Nathan Sobo created

Change summary

Cargo.lock                               |  3 +
crates/gpui/playground/Cargo.toml        |  3 +
crates/gpui/playground/src/playground.rs | 55 +++++++++++++++++++-
crates/gpui/playground/src/themes.rs     | 69 +++++++++++++------------
crates/gpui/playground/src/workspace.rs  | 11 ++-
crates/theme/src/theme.rs                | 11 +++
styles/src/build_themes.ts               |  3 +
7 files changed, 115 insertions(+), 40 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5176,9 +5176,12 @@ dependencies = [
  "parking_lot 0.11.2",
  "playground_macros",
  "refineable",
+ "rust-embed",
  "serde",
+ "settings",
  "simplelog",
  "smallvec",
+ "theme",
  "util",
 ]
 

crates/gpui/playground/Cargo.toml 🔗

@@ -16,9 +16,12 @@ log.workspace = true
 playground_macros = { path = "../playground_macros" }
 parking_lot.workspace = true
 refineable.workspace = true
+rust-embed.workspace = true
 serde.workspace = true
+settings = { path = "../../settings" }
 simplelog = "0.9"
 smallvec.workspace = true
+theme = { path = "../../theme" }
 util = { path = "../../util" }
 
 [dev-dependencies]

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

@@ -3,9 +3,12 @@ use crate::element::Element;
 use gpui::{
     geometry::{rect::RectF, vector::vec2f},
     platform::WindowOptions,
+    serde_json, ViewContext,
 };
 use log::LevelFilter;
+use settings::{default_settings, SettingsStore};
 use simplelog::SimpleLogger;
+use theme::ThemeSettings;
 use themes::Theme;
 use view::view;
 use workspace::workspace;
@@ -30,6 +33,13 @@ fn main() {
     SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
 
     gpui::App::new(()).unwrap().run(|cx| {
+        let mut store = SettingsStore::default();
+        store
+            .set_default_settings(default_settings().as_ref(), cx)
+            .unwrap();
+        cx.set_global(store);
+        theme::init(Assets, cx);
+
         cx.add_window(
             WindowOptions {
                 bounds: gpui::platform::WindowBounds::Fixed(RectF::new(
@@ -39,12 +49,51 @@ fn main() {
                 center: true,
                 ..Default::default()
             },
-            |_| view(|cx| playground(Theme::default())),
+            |_| view(|cx| playground(cx)),
         );
         cx.platform().activate(true);
     });
 }
 
-fn playground<V: 'static>(theme: Theme) -> impl Element<V> {
-    workspace().themed(theme)
+fn playground<V: 'static>(cx: &mut ViewContext<V>) -> impl Element<V> {
+    workspace().themed(current_theme(cx))
+}
+
+// Nathan: During the transition, we will include the base theme on the legacy Theme struct.
+fn current_theme<V: 'static>(cx: &mut ViewContext<V>) -> Theme {
+    settings::get::<ThemeSettings>(cx)
+        .theme
+        .deserialized_base_theme
+        .lock()
+        .get_or_insert_with(|| {
+            let theme: Theme =
+                serde_json::from_value(settings::get::<ThemeSettings>(cx).theme.base_theme.clone())
+                    .unwrap();
+            Box::new(theme)
+        })
+        .downcast_ref::<Theme>()
+        .unwrap()
+        .clone()
+}
+
+use anyhow::{anyhow, Result};
+use gpui::AssetSource;
+use rust_embed::RustEmbed;
+
+#[derive(RustEmbed)]
+#[folder = "../../../assets"]
+#[include = "themes/**/*"]
+#[exclude = "*.DS_Store"]
+pub struct Assets;
+
+impl AssetSource for Assets {
+    fn load(&self, path: &str) -> Result<std::borrow::Cow<[u8]>> {
+        Self::get(path)
+            .map(|f| f.data)
+            .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
+    }
+
+    fn list(&self, path: &str) -> Vec<std::borrow::Cow<'static, str>> {
+        Self::iter().filter(|p| p.starts_with(path)).collect()
+    }
 }

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

@@ -9,59 +9,59 @@ use std::{collections::HashMap, fmt, marker::PhantomData};
 
 #[derive(Deserialize, Clone, Default, Debug)]
 pub struct Theme {
-    name: String,
-    is_light: bool,
-    lowest: Layer,
-    middle: Layer,
-    highest: Layer,
-    popover_shadow: Shadow,
-    modal_shadow: Shadow,
+    pub name: String,
+    pub is_light: bool,
+    pub lowest: Layer,
+    pub middle: Layer,
+    pub highest: Layer,
+    pub popover_shadow: Shadow,
+    pub modal_shadow: Shadow,
     #[serde(deserialize_with = "deserialize_player_colors")]
-    players: Vec<PlayerColors>,
+    pub players: Vec<PlayerColors>,
     #[serde(deserialize_with = "deserialize_syntax_colors")]
-    syntax: HashMap<String, Hsla>,
+    pub syntax: HashMap<String, Hsla>,
 }
 
 #[derive(Deserialize, Clone, Default, Debug)]
 pub struct Layer {
-    base: StyleSet,
-    variant: StyleSet,
-    on: StyleSet,
-    accent: StyleSet,
-    positive: StyleSet,
-    warning: StyleSet,
-    negative: StyleSet,
+    pub base: StyleSet,
+    pub variant: StyleSet,
+    pub on: StyleSet,
+    pub accent: StyleSet,
+    pub positive: StyleSet,
+    pub warning: StyleSet,
+    pub negative: StyleSet,
 }
 
 #[derive(Deserialize, Clone, Default, Debug)]
 pub struct StyleSet {
     #[serde(rename = "default")]
-    default: ContainerColors,
-    hovered: ContainerColors,
-    pressed: ContainerColors,
-    active: ContainerColors,
-    disabled: ContainerColors,
-    inverted: ContainerColors,
+    pub default: ContainerColors,
+    pub hovered: ContainerColors,
+    pub pressed: ContainerColors,
+    pub active: ContainerColors,
+    pub disabled: ContainerColors,
+    pub inverted: ContainerColors,
 }
 
 #[derive(Deserialize, Clone, Default, Debug)]
 pub struct ContainerColors {
-    background: Hsla,
-    foreground: Hsla,
-    border: Hsla,
+    pub background: Hsla,
+    pub foreground: Hsla,
+    pub border: Hsla,
 }
 
 #[derive(Deserialize, Clone, Default, Debug)]
 pub struct PlayerColors {
-    selection: Hsla,
-    cursor: Hsla,
+    pub selection: Hsla,
+    pub cursor: Hsla,
 }
 
 #[derive(Deserialize, Clone, Default, Debug)]
 pub struct Shadow {
-    blur: u8,
-    color: Hsla,
-    offset: Vec<u8>,
+    pub blur: u8,
+    pub color: Hsla,
+    pub offset: Vec<u8>,
 }
 
 pub fn theme<'a>(cx: &'a WindowContext) -> &'a Theme {
@@ -107,6 +107,11 @@ fn deserialize_syntax_colors<'de, D>(deserializer: D) -> Result<HashMap<String,
 where
     D: serde::Deserializer<'de>,
 {
+    #[derive(Deserialize)]
+    struct ColorWrapper {
+        color: Hsla,
+    }
+
     struct SyntaxVisitor;
 
     impl<'de> Visitor<'de> for SyntaxVisitor {
@@ -122,8 +127,8 @@ where
         {
             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);
+                let wrapper: ColorWrapper = map.next_value()?; // Deserialize values as Hsla
+                result.insert(key, wrapper.color);
             }
             Ok(result)
         }

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

@@ -2,6 +2,7 @@ use crate::{
     div::div,
     element::{Element, IntoElement, ParentElement},
     style::StyleHelpers,
+    themes::theme,
 };
 use gpui::{geometry::pixels, ViewContext};
 use playground_macros::Element;
@@ -16,20 +17,22 @@ 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 = theme(cx);
         div()
             .full()
             .flex()
             .flex_col()
-            // .fill(theme.base(0.5))
+            .fill(theme.middle.base.default.background)
             .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 theme = theme(cx);
+        div()
+            .h(pixels(cx.titlebar_height()))
+            .fill(theme.lowest.base.default.background)
     }
 
     fn status_bar<V: 'static>(&mut self, cx: &mut ViewContext<V>) -> impl IntoElement<V> {

crates/theme/src/theme.rs 🔗

@@ -10,11 +10,12 @@ use gpui::{
     fonts::{HighlightStyle, TextStyle},
     platform, AppContext, AssetSource, Border, MouseState,
 };
+use parking_lot::Mutex;
 use schemars::JsonSchema;
 use serde::{de::DeserializeOwned, Deserialize};
 use serde_json::Value;
 use settings::SettingsStore;
-use std::{collections::HashMap, ops::Deref, sync::Arc};
+use std::{any::Any, collections::HashMap, ops::Deref, sync::Arc};
 use ui::{CheckboxStyle, CopilotCTAButton, IconStyle, ModalStyle};
 
 pub use theme_registry::*;
@@ -67,6 +68,14 @@ pub struct Theme {
     pub welcome: WelcomeStyle,
     pub titlebar: Titlebar,
     pub component_test: ComponentTest,
+    // Nathan: New elements are styled in Rust, directly from the base theme.
+    // We store it on the legacy theme so we can mix both kinds of elements during the transition.
+    #[schemars(skip)]
+    pub base_theme: serde_json::Value,
+    // A place to cache deserialized base theme.
+    #[serde(skip_deserializing)]
+    #[schemars(skip)]
+    pub deserialized_base_theme: Mutex<Option<Box<dyn Any + Send + Sync>>>,
 }
 
 #[derive(Deserialize, Default, Clone, JsonSchema)]

styles/src/build_themes.ts 🔗

@@ -32,6 +32,9 @@ function write_themes(themes: Theme[], output_directory: string) {
         setTheme(theme)
 
         const style_tree = app()
+        // Nathan: New elements will read directly from the theme colors.
+        // Adding this during the transition. Afterwards, we can port all themes to Rust.
+        style_tree.base_theme = theme
         const style_tree_json = JSON.stringify(style_tree, null, 2)
         const temp_path = path.join(temp_directory, `${theme.name}.json`)
         const out_path = path.join(