Add ability to debug element trees as JSON

Nathan Sobo created

Change summary

Cargo.lock                           |  6 +
gpui/Cargo.toml                      |  2 
gpui/examples/text.rs                | 17 +++++-
gpui/src/color.rs                    |  9 +++
gpui/src/elements/align.rs           | 21 +++++++
gpui/src/elements/canvas.rs          | 16 ++++++
gpui/src/elements/constrained_box.rs | 23 +++++++--
gpui/src/elements/container.rs       | 72 ++++++++++++++++++++++++++++++
gpui/src/elements/empty.rs           | 23 ++++++++-
gpui/src/elements/event_handler.rs   | 24 ++++++++-
gpui/src/elements/flex.rs            | 48 +++++++++++++++++--
gpui/src/elements/label.rs           | 34 +++++++++++++
gpui/src/elements/line_box.rs        | 26 +++++++++-
gpui/src/elements/new.rs             | 28 +++++++++++
gpui/src/elements/stack.rs           | 19 +++++++
gpui/src/elements/svg.rs             | 23 ++++++++
gpui/src/elements/uniform_list.rs    | 19 +++++++
gpui/src/font_cache.rs               |  9 +++
gpui/src/fonts.rs                    | 57 +++++++++++++++++++++++
gpui/src/geometry.rs                 | 15 +++++
gpui/src/json.rs                     | 15 ++++++
gpui/src/lib.rs                      |  7 +-
gpui/src/presenter.rs                | 45 ++++++++++++++++++
gpui/src/scene.rs                    | 22 +++++++++
zed/src/editor/buffer_element.rs     | 15 ++++++
25 files changed, 554 insertions(+), 41 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -930,6 +930,8 @@ dependencies = [
  "rand 0.8.3",
  "replace_with",
  "resvg",
+ "serde",
+ "serde_json",
  "simplelog",
  "smallvec",
  "smol",
@@ -1691,9 +1693,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
 
 [[package]]
 name = "serde"
-version = "1.0.124"
+version = "1.0.125"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd761ff957cb2a45fbb9ab3da6512de9de55872866160b23c25f1a841e99d29f"
+checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171"
 
 [[package]]
 name = "serde_json"

gpui/Cargo.toml 🔗

@@ -18,6 +18,8 @@ pathfinder_geometry = "0.5"
 rand = "0.8.3"
 replace_with = "0.1.7"
 resvg = "0.14"
+serde = "1.0.125"
+serde_json = "1.0.64"
 smallvec = "1.6.1"
 smol = "1.2"
 tiny-skia = "0.5"

gpui/examples/text.rs 🔗

@@ -2,9 +2,10 @@ use gpui::{
     color::ColorU,
     fonts::{Properties, Weight},
     platform::{current as platform, Runner},
-    Element as _, Quad,
+    DebugContext, Element as _, Quad,
 };
 use log::LevelFilter;
+use pathfinder_geometry::rect::RectF;
 use simplelog::SimpleLogger;
 
 fn main() {
@@ -59,7 +60,7 @@ impl gpui::Element for TextElement {
 
     fn paint(
         &mut self,
-        bounds: pathfinder_geometry::rect::RectF,
+        bounds: RectF,
         _: &mut Self::LayoutState,
         ctx: &mut gpui::PaintContext,
     ) -> Self::PaintState {
@@ -109,11 +110,21 @@ impl gpui::Element for TextElement {
     fn dispatch_event(
         &mut self,
         _: &gpui::Event,
-        _: pathfinder_geometry::rect::RectF,
+        _: RectF,
         _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         _: &mut gpui::EventContext,
     ) -> bool {
         false
     }
+
+    fn debug(
+        &self,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &DebugContext,
+    ) -> gpui::json::Value {
+        todo!()
+    }
 }

gpui/src/color.rs 🔗

@@ -0,0 +1,9 @@
+use crate::json::ToJson;
+pub use pathfinder_color::*;
+use serde_json::json;
+
+impl ToJson for ColorU {
+    fn to_json(&self) -> serde_json::Value {
+        json!(format!("0x{:x}{:x}{:x}", self.r, self.g, self.b))
+    }
+}

gpui/src/elements/align.rs 🔗

@@ -1,8 +1,10 @@
 use crate::{
-    AfterLayoutContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
-    SizeConstraint,
+    json, AfterLayoutContext, DebugContext, Element, ElementBox, Event, EventContext,
+    LayoutContext, PaintContext, SizeConstraint,
 };
+use json::ToJson;
 use pathfinder_geometry::vector::{vec2f, Vector2F};
+use serde_json::json;
 
 pub struct Align {
     child: ElementBox,
@@ -79,4 +81,19 @@ impl Element for Align {
     ) -> bool {
         self.child.dispatch_event(event, ctx)
     }
+
+    fn debug(
+        &self,
+        bounds: pathfinder_geometry::rect::RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        ctx: &DebugContext,
+    ) -> json::Value {
+        json!({
+            "type": "Align",
+            "alignment": self.alignment.to_json(),
+            "bounds": bounds.to_json(),
+            "child": self.child.debug(ctx),
+        })
+    }
 }

gpui/src/elements/canvas.rs 🔗

@@ -1,5 +1,9 @@
 use super::Element;
-use crate::PaintContext;
+use crate::{
+    json::{self, json},
+    DebugContext, PaintContext,
+};
+use json::ToJson;
 use pathfinder_geometry::{
     rect::RectF,
     vector::{vec2f, Vector2F},
@@ -70,4 +74,14 @@ where
     ) -> bool {
         false
     }
+
+    fn debug(
+        &self,
+        bounds: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &DebugContext,
+    ) -> json::Value {
+        json!({"type": "Canvas", "bounds": bounds.to_json()})
+    }
 }

gpui/src/elements/constrained_box.rs 🔗

@@ -1,8 +1,11 @@
+use json::ToJson;
+use serde_json::json;
+
 use crate::{
-    AfterLayoutContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
-    SizeConstraint,
+    geometry::{rect::RectF, vector::Vector2F},
+    json, AfterLayoutContext, DebugContext, Element, ElementBox, Event, EventContext,
+    LayoutContext, PaintContext, SizeConstraint,
 };
-use pathfinder_geometry::vector::Vector2F;
 
 pub struct ConstrainedBox {
     child: ElementBox,
@@ -63,7 +66,7 @@ impl Element for ConstrainedBox {
 
     fn paint(
         &mut self,
-        bounds: pathfinder_geometry::rect::RectF,
+        bounds: RectF,
         _: &mut Self::LayoutState,
         ctx: &mut PaintContext,
     ) -> Self::PaintState {
@@ -73,11 +76,21 @@ impl Element for ConstrainedBox {
     fn dispatch_event(
         &mut self,
         event: &Event,
-        _: pathfinder_geometry::rect::RectF,
+        _: RectF,
         _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         ctx: &mut EventContext,
     ) -> bool {
         self.child.dispatch_event(event, ctx)
     }
+
+    fn debug(
+        &self,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        ctx: &DebugContext,
+    ) -> json::Value {
+        json!({"type": "ConstrainedBox", "constraint": self.constraint.to_json(), "child": self.child.debug(ctx)})
+    }
 }

gpui/src/elements/container.rs 🔗

@@ -1,8 +1,10 @@
 use pathfinder_geometry::rect::RectF;
+use serde_json::json;
 
 use crate::{
     color::ColorU,
     geometry::vector::{vec2f, Vector2F},
+    json::ToJson,
     scene::{self, Border, Quad},
     AfterLayoutContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
     SizeConstraint,
@@ -189,6 +191,28 @@ impl Element for Container {
     ) -> bool {
         self.child.dispatch_event(event, ctx)
     }
+
+    fn debug(
+        &self,
+        bounds: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        ctx: &crate::DebugContext,
+    ) -> serde_json::Value {
+        json!({
+            "type": "Container",
+            "details": {
+                "margin": self.margin.to_json(),
+                "padding": self.padding.to_json(),
+                "background_color": self.background_color.to_json(),
+                "border": self.border.to_json(),
+                "corner_radius": self.corner_radius,
+                "shadow": self.shadow.to_json(),
+            },
+            "bounds": bounds.to_json(),
+            "child": self.child.debug(ctx),
+        })
+    }
 }
 
 #[derive(Default)]
@@ -199,6 +223,25 @@ pub struct Margin {
     right: f32,
 }
 
+impl ToJson for Margin {
+    fn to_json(&self) -> serde_json::Value {
+        let mut value = json!({});
+        if self.top > 0. {
+            value["top"] = json!(self.top);
+        }
+        if self.right > 0. {
+            value["right"] = json!(self.right);
+        }
+        if self.bottom > 0. {
+            value["bottom"] = json!(self.bottom);
+        }
+        if self.left > 0. {
+            value["left"] = json!(self.left);
+        }
+        value
+    }
+}
+
 #[derive(Default)]
 pub struct Padding {
     top: f32,
@@ -207,9 +250,38 @@ pub struct Padding {
     right: f32,
 }
 
+impl ToJson for Padding {
+    fn to_json(&self) -> serde_json::Value {
+        let mut value = json!({});
+        if self.top > 0. {
+            value["top"] = json!(self.top);
+        }
+        if self.right > 0. {
+            value["right"] = json!(self.right);
+        }
+        if self.bottom > 0. {
+            value["bottom"] = json!(self.bottom);
+        }
+        if self.left > 0. {
+            value["left"] = json!(self.left);
+        }
+        value
+    }
+}
+
 #[derive(Default)]
 pub struct Shadow {
     offset: Vector2F,
     blur: f32,
     color: ColorU,
 }
+
+impl ToJson for Shadow {
+    fn to_json(&self) -> serde_json::Value {
+        json!({
+            "offset": self.offset.to_json(),
+            "blur": self.blur,
+            "color": self.color.to_json()
+        })
+    }
+}

gpui/src/elements/empty.rs 🔗

@@ -1,6 +1,10 @@
-use crate::geometry::{
-    rect::RectF,
-    vector::{vec2f, Vector2F},
+use crate::{
+    geometry::{
+        rect::RectF,
+        vector::{vec2f, Vector2F},
+    },
+    json::{json, ToJson},
+    DebugContext,
 };
 use crate::{
     AfterLayoutContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
@@ -58,4 +62,17 @@ impl Element for Empty {
     ) -> bool {
         false
     }
+
+    fn debug(
+        &self,
+        bounds: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &DebugContext,
+    ) -> serde_json::Value {
+        json!({
+            "type": "Empty",
+            "bounds": bounds.to_json(),
+        })
+    }
 }

gpui/src/elements/event_handler.rs 🔗

@@ -1,6 +1,9 @@
+use pathfinder_geometry::rect::RectF;
+use serde_json::json;
+
 use crate::{
-    geometry::vector::Vector2F, AfterLayoutContext, Element, ElementBox, Event, EventContext,
-    LayoutContext, PaintContext, SizeConstraint,
+    geometry::vector::Vector2F, AfterLayoutContext, DebugContext, Element, ElementBox, Event,
+    EventContext, LayoutContext, PaintContext, SizeConstraint,
 };
 
 pub struct EventHandler {
@@ -49,7 +52,7 @@ impl Element for EventHandler {
 
     fn paint(
         &mut self,
-        bounds: pathfinder_geometry::rect::RectF,
+        bounds: RectF,
         _: &mut Self::LayoutState,
         ctx: &mut PaintContext,
     ) -> Self::PaintState {
@@ -59,7 +62,7 @@ impl Element for EventHandler {
     fn dispatch_event(
         &mut self,
         event: &Event,
-        bounds: pathfinder_geometry::rect::RectF,
+        bounds: RectF,
         _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         ctx: &mut EventContext,
@@ -80,4 +83,17 @@ impl Element for EventHandler {
             }
         }
     }
+
+    fn debug(
+        &self,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        ctx: &DebugContext,
+    ) -> serde_json::Value {
+        json!({
+            "type": "EventHandler",
+            "child": self.child.debug(ctx),
+        })
+    }
 }

gpui/src/elements/flex.rs 🔗

@@ -1,10 +1,15 @@
 use std::any::Any;
 
 use crate::{
-    AfterLayoutContext, Axis, Element, ElementBox, Event, EventContext, LayoutContext,
-    PaintContext, SizeConstraint, Vector2FExt,
+    json::{self, ToJson, Value},
+    AfterLayoutContext, Axis, DebugContext, Element, ElementBox, Event, EventContext,
+    LayoutContext, PaintContext, SizeConstraint, Vector2FExt,
 };
-use pathfinder_geometry::vector::{vec2f, Vector2F};
+use pathfinder_geometry::{
+    rect::RectF,
+    vector::{vec2f, Vector2F},
+};
+use serde_json::json;
 
 pub struct Flex {
     axis: Axis,
@@ -130,7 +135,7 @@ impl Element for Flex {
 
     fn paint(
         &mut self,
-        bounds: pathfinder_geometry::rect::RectF,
+        bounds: RectF,
         _: &mut Self::LayoutState,
         ctx: &mut PaintContext,
     ) -> Self::PaintState {
@@ -147,7 +152,7 @@ impl Element for Flex {
     fn dispatch_event(
         &mut self,
         event: &Event,
-        _: pathfinder_geometry::rect::RectF,
+        _: RectF,
         _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         ctx: &mut EventContext,
@@ -158,6 +163,21 @@ impl Element for Flex {
         }
         handled
     }
+
+    fn debug(
+        &self,
+        bounds: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        ctx: &DebugContext,
+    ) -> json::Value {
+        json!({
+            "type": "Flex",
+            "axis": self.axis.to_json(),
+            "bounds": bounds.to_json(),
+            "children": self.children.iter().map(|child| child.debug(ctx)).collect::<Vec<json::Value>>()
+        })
+    }
 }
 
 struct FlexParentData {
@@ -202,7 +222,7 @@ impl Element for Expanded {
 
     fn paint(
         &mut self,
-        bounds: pathfinder_geometry::rect::RectF,
+        bounds: RectF,
         _: &mut Self::LayoutState,
         ctx: &mut PaintContext,
     ) -> Self::PaintState {
@@ -212,7 +232,7 @@ impl Element for Expanded {
     fn dispatch_event(
         &mut self,
         event: &Event,
-        _: pathfinder_geometry::rect::RectF,
+        _: RectF,
         _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         ctx: &mut EventContext,
@@ -223,4 +243,18 @@ impl Element for Expanded {
     fn metadata(&self) -> Option<&dyn Any> {
         Some(&self.metadata)
     }
+
+    fn debug(
+        &self,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        ctx: &DebugContext,
+    ) -> Value {
+        json!({
+            "type": "Expanded",
+            "flex": self.metadata.flex,
+            "child": self.child.debug(ctx)
+        })
+    }
 }

gpui/src/elements/label.rs 🔗

@@ -1,3 +1,5 @@
+use serde_json::json;
+
 use crate::{
     color::ColorU,
     font_cache::FamilyId,
@@ -6,8 +8,10 @@ use crate::{
         rect::RectF,
         vector::{vec2f, Vector2F},
     },
+    json::{ToJson, Value},
     text_layout::Line,
-    AfterLayoutContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
+    AfterLayoutContext, DebugContext, Element, Event, EventContext, LayoutContext, PaintContext,
+    SizeConstraint,
 };
 use std::{ops::Range, sync::Arc};
 
@@ -152,4 +156,32 @@ impl Element for Label {
     ) -> bool {
         false
     }
+
+    fn debug(
+        &self,
+        bounds: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        ctx: &DebugContext,
+    ) -> Value {
+        json!({
+            "type": "Label",
+            "font_size": self.font_size,
+            "bounds": bounds.to_json(),
+            "text": &self.text,
+            "family_id": ctx.font_cache.family_name(self.family_id).unwrap(),
+            "font_properties": self.font_properties.to_json(),
+            "highlights": self.highlights.to_json(),
+        })
+    }
+}
+
+impl ToJson for Highlights {
+    fn to_json(&self) -> Value {
+        json!({
+            "color": self.color.to_json(),
+            "indices": self.indices,
+            "font_properties": self.font_properties.to_json(),
+        })
+    }
 }

gpui/src/elements/line_box.rs 🔗

@@ -1,9 +1,13 @@
 use crate::{
     font_cache::FamilyId,
     fonts::Properties,
-    geometry::vector::{vec2f, Vector2F},
-    AfterLayoutContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
-    SizeConstraint,
+    geometry::{
+        rect::RectF,
+        vector::{vec2f, Vector2F},
+    },
+    json::{json, ToJson},
+    AfterLayoutContext, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext,
+    PaintContext, SizeConstraint,
 };
 
 pub struct LineBox {
@@ -85,4 +89,20 @@ impl Element for LineBox {
     ) -> bool {
         self.child.dispatch_event(event, ctx)
     }
+
+    fn debug(
+        &self,
+        bounds: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        ctx: &DebugContext,
+    ) -> serde_json::Value {
+        json!({
+            "bounds": bounds.to_json(),
+            "family_id": ctx.font_cache.family_name(self.family_id).unwrap(),
+            "font_size": self.font_size,
+            "font_properties": self.font_properties.to_json(),
+            "child": self.child.debug(ctx),
+        })
+    }
 }

gpui/src/elements/new.rs 🔗

@@ -1,6 +1,7 @@
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
-    AfterLayoutContext, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
+    json, AfterLayoutContext, DebugContext, Event, EventContext, LayoutContext, PaintContext,
+    SizeConstraint,
 };
 use core::panic;
 use replace_with::replace_with_or_abort;
@@ -11,6 +12,7 @@ trait AnyElement {
     fn after_layout(&mut self, _: &mut AfterLayoutContext) {}
     fn paint(&mut self, origin: Vector2F, ctx: &mut PaintContext);
     fn dispatch_event(&mut self, event: &Event, ctx: &mut EventContext) -> bool;
+    fn debug(&self, ctx: &DebugContext) -> serde_json::Value;
 
     fn size(&self) -> Vector2F;
     fn metadata(&self) -> Option<&dyn Any>;
@@ -53,6 +55,14 @@ pub trait Element {
         None
     }
 
+    fn debug(
+        &self,
+        bounds: RectF,
+        layout: &Self::LayoutState,
+        paint: &Self::PaintState,
+        ctx: &DebugContext,
+    ) -> serde_json::Value;
+
     fn boxed(self) -> ElementBox
     where
         Self: 'static + Sized,
@@ -165,6 +175,18 @@ impl<T: Element> AnyElement for Lifecycle<T> {
             | Lifecycle::PostPaint { element, .. } => element.metadata(),
         }
     }
+
+    fn debug(&self, ctx: &DebugContext) -> serde_json::Value {
+        match self {
+            Lifecycle::PostPaint {
+                element,
+                bounds,
+                layout,
+                paint,
+            } => element.debug(*bounds, layout, paint, ctx),
+            _ => panic!("invalid element lifecycle state"),
+        }
+    }
 }
 
 impl ElementBox {
@@ -191,4 +213,8 @@ impl ElementBox {
     pub fn metadata(&self) -> Option<&dyn Any> {
         self.0.metadata()
     }
+
+    pub fn debug(&self, ctx: &DebugContext) -> json::Value {
+        self.0.debug(ctx)
+    }
 }

gpui/src/elements/stack.rs 🔗

@@ -1,7 +1,8 @@
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
-    AfterLayoutContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
-    SizeConstraint,
+    json::{self, json, ToJson},
+    AfterLayoutContext, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext,
+    PaintContext, SizeConstraint,
 };
 
 pub struct Stack {
@@ -71,6 +72,20 @@ impl Element for Stack {
         }
         false
     }
+
+    fn debug(
+        &self,
+        bounds: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        ctx: &DebugContext,
+    ) -> json::Value {
+        json!({
+            "type": "Stack",
+            "bounds": bounds.to_json(),
+            "children": self.children.iter().map(|child| child.debug(ctx)).collect::<Vec<json::Value>>()
+        })
+    }
 }
 
 impl Extend<ElementBox> for Stack {

gpui/src/elements/svg.rs 🔗

@@ -1,11 +1,13 @@
+use serde_json::json;
+
 use crate::{
     color::ColorU,
     geometry::{
         rect::RectF,
         vector::{vec2f, Vector2F},
     },
-    scene, AfterLayoutContext, Element, Event, EventContext, LayoutContext, PaintContext,
-    SizeConstraint,
+    scene, AfterLayoutContext, DebugContext, Element, Event, EventContext, LayoutContext,
+    PaintContext, SizeConstraint,
 };
 
 pub struct Svg {
@@ -86,8 +88,25 @@ impl Element for Svg {
     ) -> bool {
         false
     }
+
+    fn debug(
+        &self,
+        bounds: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &DebugContext,
+    ) -> serde_json::Value {
+        json!({
+            "type": "Svg",
+            "bounds": bounds.to_json(),
+            "path": self.path,
+            "color": self.color.to_json(),
+        })
+    }
 }
 
+use crate::json::ToJson;
+
 fn from_usvg_rect(rect: usvg::Rect) -> RectF {
     RectF::new(
         vec2f(rect.x() as f32, rect.y() as f32),

gpui/src/elements/uniform_list.rs 🔗

@@ -7,8 +7,10 @@ use crate::{
         rect::RectF,
         vector::{vec2f, Vector2F},
     },
+    json::{self, json},
     ElementBox,
 };
+use json::ToJson;
 use parking_lot::Mutex;
 use std::{cmp, ops::Range, sync::Arc};
 
@@ -236,4 +238,21 @@ where
 
         handled
     }
+
+    fn debug(
+        &self,
+        bounds: RectF,
+        layout: &Self::LayoutState,
+        _: &Self::PaintState,
+        ctx: &crate::DebugContext,
+    ) -> json::Value {
+        json!({
+            "type": "UniformList",
+            "bounds": bounds.to_json(),
+            "scroll_max": layout.scroll_max,
+            "item_height": layout.item_height,
+            "items": layout.items.iter().map(|item| item.debug(ctx)).collect::<Vec<json::Value>>()
+
+        })
+    }
 }

gpui/src/font_cache.rs 🔗

@@ -36,6 +36,15 @@ impl FontCache {
         }))
     }
 
+    pub fn family_name(&self, family_id: FamilyId) -> Result<String> {
+        self.0
+            .read()
+            .families
+            .get(family_id.0)
+            .ok_or_else(|| anyhow!("invalid family id"))
+            .map(|family| family.name.clone())
+    }
+
     pub fn load_family(&self, names: &[&str]) -> Result<FamilyId> {
         for name in names {
             let state = self.0.upgradable_read();

gpui/src/fonts.rs 🔗

@@ -1,7 +1,62 @@
+use crate::json::json;
 pub use font_kit::metrics::Metrics;
-pub use font_kit::properties::{Properties, Weight};
+pub use font_kit::properties::{Properties, Stretch, Style, Weight};
+
+use crate::json::ToJson;
 
 #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
 pub struct FontId(pub usize);
 
 pub type GlyphId = u32;
+
+impl ToJson for Properties {
+    fn to_json(&self) -> crate::json::Value {
+        json!({
+            "style": self.style.to_json(),
+            "weight": self.weight.to_json(),
+            "stretch": self.stretch.to_json(),
+        })
+    }
+}
+
+impl ToJson for Style {
+    fn to_json(&self) -> crate::json::Value {
+        match self {
+            Style::Normal => json!("normal"),
+            Style::Italic => json!("italic"),
+            Style::Oblique => json!("oblique"),
+        }
+    }
+}
+
+impl ToJson for Weight {
+    fn to_json(&self) -> crate::json::Value {
+        if self.0 == Weight::THIN.0 {
+            json!("thin")
+        } else if self.0 == Weight::EXTRA_LIGHT.0 {
+            json!("extra light")
+        } else if self.0 == Weight::LIGHT.0 {
+            json!("light")
+        } else if self.0 == Weight::NORMAL.0 {
+            json!("normal")
+        } else if self.0 == Weight::MEDIUM.0 {
+            json!("medium")
+        } else if self.0 == Weight::SEMIBOLD.0 {
+            json!("semibold")
+        } else if self.0 == Weight::BOLD.0 {
+            json!("bold")
+        } else if self.0 == Weight::EXTRA_BOLD.0 {
+            json!("extra bold")
+        } else if self.0 == Weight::BLACK.0 {
+            json!("black")
+        } else {
+            json!(self.0)
+        }
+    }
+}
+
+impl ToJson for Stretch {
+    fn to_json(&self) -> serde_json::Value {
+        json!(self.0)
+    }
+}

gpui/src/geometry.rs 🔗

@@ -1,7 +1,8 @@
 use super::scene::{Path, PathVertex};
-use crate::color::ColorU;
+use crate::{color::ColorU, json::ToJson};
 pub use pathfinder_geometry::*;
 use rect::RectF;
+use serde_json::json;
 use vector::{vec2f, Vector2F};
 
 pub struct PathBuilder {
@@ -106,3 +107,15 @@ impl PathBuilder {
         }
     }
 }
+
+impl ToJson for Vector2F {
+    fn to_json(&self) -> serde_json::Value {
+        json!([self.x(), self.y()])
+    }
+}
+
+impl ToJson for RectF {
+    fn to_json(&self) -> serde_json::Value {
+        json!({"origin": self.origin().to_json(), "size": self.size().to_json()})
+    }
+}

gpui/src/json.rs 🔗

@@ -0,0 +1,15 @@
+pub use serde_json::*;
+
+pub trait ToJson {
+    fn to_json(&self) -> Value;
+}
+
+impl<T: ToJson> ToJson for Option<T> {
+    fn to_json(&self) -> Value {
+        if let Some(value) = self.as_ref() {
+            value.to_json()
+        } else {
+            json!(null)
+        }
+    }
+}

gpui/src/lib.rs 🔗

@@ -18,11 +18,12 @@ mod util;
 pub use elements::{Element, ElementBox};
 pub mod executor;
 pub use executor::Task;
+pub mod color;
+pub mod json;
 pub mod keymap;
 pub mod platform;
-pub use pathfinder_color as color;
 pub use platform::Event;
 pub use presenter::{
-    AfterLayoutContext, Axis, EventContext, LayoutContext, PaintContext, SizeConstraint,
-    Vector2FExt,
+    AfterLayoutContext, Axis, DebugContext, EventContext, LayoutContext, PaintContext,
+    SizeConstraint, Vector2FExt,
 };

gpui/src/presenter.rs 🔗

@@ -2,11 +2,13 @@ use crate::{
     app::{AppContext, MutableAppContext, WindowInvalidation},
     elements::Element,
     font_cache::FontCache,
+    json::ToJson,
     platform::Event,
     text_layout::TextLayoutCache,
     AssetCache, ElementBox, Scene,
 };
 use pathfinder_geometry::vector::{vec2f, Vector2F};
+use serde_json::json;
 use std::{any::Any, collections::HashMap, sync::Arc};
 
 pub struct Presenter {
@@ -224,6 +226,12 @@ impl<'a> EventContext<'a> {
     }
 }
 
+pub struct DebugContext<'a> {
+    rendered_views: &'a mut HashMap<usize, ElementBox>,
+    pub font_cache: &'a FontCache,
+    pub app: &'a AppContext,
+}
+
 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
 pub enum Axis {
     Horizontal,
@@ -239,6 +247,15 @@ impl Axis {
     }
 }
 
+impl ToJson for Axis {
+    fn to_json(&self) -> serde_json::Value {
+        match self {
+            Axis::Horizontal => json!("horizontal"),
+            Axis::Vertical => json!("vertical"),
+        }
+    }
+}
+
 pub trait Vector2FExt {
     fn along(self, axis: Axis) -> f32;
 }
@@ -291,6 +308,15 @@ impl SizeConstraint {
     }
 }
 
+impl ToJson for SizeConstraint {
+    fn to_json(&self) -> serde_json::Value {
+        json!({
+            "min": self.min.to_json(),
+            "max": self.max.to_json(),
+        })
+    }
+}
+
 pub struct ChildView {
     view_id: usize,
 }
@@ -342,6 +368,25 @@ impl Element for ChildView {
     ) -> bool {
         ctx.dispatch_event(self.view_id, event)
     }
+
+    fn debug(
+        &self,
+        bounds: pathfinder_geometry::rect::RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        ctx: &DebugContext,
+    ) -> serde_json::Value {
+        json!({
+            "type": "ChildView",
+            "view_id": self.view_id,
+            "bounds": bounds.to_json(),
+            "child": if let Some(view) = ctx.rendered_views.get(&self.view_id) {
+                view.debug(ctx)
+            } else {
+                json!(null)
+            }
+        })
+    }
 }
 
 #[cfg(test)]

gpui/src/scene.rs 🔗

@@ -1,7 +1,10 @@
+use serde_json::json;
+
 use crate::{
     color::ColorU,
     fonts::{FontId, GlyphId},
     geometry::{rect::RectF, vector::Vector2F},
+    json::ToJson,
 };
 
 pub struct Scene {
@@ -258,3 +261,22 @@ impl Border {
         }
     }
 }
+
+impl ToJson for Border {
+    fn to_json(&self) -> serde_json::Value {
+        let mut value = json!({});
+        if self.top {
+            value["top"] = json!(self.width);
+        }
+        if self.right {
+            value["right"] = json!(self.width);
+        }
+        if self.bottom {
+            value["bottom"] = json!(self.width);
+        }
+        if self.left {
+            value["left"] = json!(self.width);
+        }
+        value
+    }
+}

zed/src/editor/buffer_element.rs 🔗

@@ -6,10 +6,12 @@ use gpui::{
         vector::{vec2f, Vector2F},
         PathBuilder,
     },
+    json::{self, ToJson},
     text_layout::{self, TextLayoutCache},
     AfterLayoutContext, AppContext, Border, Element, Event, EventContext, FontCache, LayoutContext,
     PaintContext, Quad, Scene, SizeConstraint, ViewHandle,
 };
+use json::json;
 use smallvec::SmallVec;
 use std::cmp::Ordering;
 use std::{
@@ -477,6 +479,19 @@ impl Element for BufferElement {
             false
         }
     }
+
+    fn debug(
+        &self,
+        bounds: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &gpui::DebugContext,
+    ) -> json::Value {
+        json!({
+            "type": "BufferElement",
+            "bounds": bounds.to_json()
+        })
+    }
 }
 
 pub struct LayoutState {