Add basic inspector

Nathan Sobo created

Change summary

crates/gpui/src/app/window.rs                                | 36 ++
crates/gpui/src/geometry.rs                                  | 54 +++
crates/gpui2/src/elements/div.rs                             | 65 ++++
crates/gpui2/src/elements/hoverable.rs                       |  1 
crates/gpui2/src/style.rs                                    |  6 
crates/refineable/derive_refineable/src/derive_refineable.rs | 56 +++
crates/storybook/src/storybook.rs                            | 12 
crates/storybook/src/workspace.rs                            | 85 ++---
8 files changed, 240 insertions(+), 75 deletions(-)

Detailed changes

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

@@ -51,6 +51,7 @@ pub struct Window {
     pub(crate) parents: HashMap<usize, usize>,
     pub(crate) is_active: bool,
     pub(crate) is_fullscreen: bool,
+    inspector_enabled: bool,
     pub(crate) invalidation: Option<WindowInvalidation>,
     pub(crate) platform_window: Box<dyn platform::Window>,
     pub(crate) rendered_views: HashMap<usize, Box<dyn AnyRootElement>>,
@@ -65,6 +66,7 @@ pub struct Window {
     event_handlers: Vec<EventHandler>,
     last_mouse_moved_event: Option<Event>,
     last_mouse_position: Vector2F,
+    pressed_buttons: HashSet<MouseButton>,
     pub(crate) hovered_region_ids: Vec<MouseRegionId>,
     pub(crate) clicked_region_ids: Vec<MouseRegionId>,
     pub(crate) clicked_region: Option<(MouseRegionId, MouseButton)>,
@@ -92,6 +94,7 @@ impl Window {
             is_active: false,
             invalidation: None,
             is_fullscreen: false,
+            inspector_enabled: false,
             platform_window,
             rendered_views: Default::default(),
             text_style_stack: Vec::new(),
@@ -104,6 +107,7 @@ impl Window {
             text_layout_cache: TextLayoutCache::new(cx.font_system.clone()),
             last_mouse_moved_event: None,
             last_mouse_position: Vector2F::zero(),
+            pressed_buttons: Default::default(),
             hovered_region_ids: Default::default(),
             clicked_region_ids: Default::default(),
             clicked_region: None,
@@ -235,6 +239,18 @@ impl<'a> WindowContext<'a> {
             .push_back(Effect::RepaintWindow { window });
     }
 
+    pub fn enable_inspector(&mut self) {
+        self.window.inspector_enabled = true;
+    }
+
+    pub fn is_inspector_enabled(&self) -> bool {
+        self.window.inspector_enabled
+    }
+
+    pub fn is_mouse_down(&self, button: MouseButton) -> bool {
+        self.window.pressed_buttons.contains(&button)
+    }
+
     pub fn rem_size(&self) -> f32 {
         16.
     }
@@ -521,7 +537,7 @@ impl<'a> WindowContext<'a> {
 
     pub(crate) fn dispatch_event(&mut self, event: Event, event_reused: bool) -> bool {
         if !event_reused {
-            self.dispatch_to_new_event_handlers(&event);
+            self.dispatch_event_2(&event);
         }
 
         let mut mouse_events = SmallVec::<[_; 2]>::new();
@@ -898,12 +914,24 @@ impl<'a> WindowContext<'a> {
         any_event_handled
     }
 
-    fn dispatch_to_new_event_handlers(&mut self, event: &Event) {
+    fn dispatch_event_2(&mut self, event: &Event) {
+        match event {
+            Event::MouseDown(event) => {
+                self.window.pressed_buttons.insert(event.button);
+            }
+            Event::MouseUp(event) => {
+                self.window.pressed_buttons.remove(&event.button);
+            }
+            _ => {}
+        }
+
         if let Some(mouse_event) = event.mouse_event() {
             let event_handlers = self.window.take_event_handlers();
             for event_handler in event_handlers.iter().rev() {
                 if event_handler.event_type == mouse_event.type_id() {
-                    (event_handler.handler)(mouse_event, self);
+                    if !(event_handler.handler)(mouse_event, self) {
+                        break;
+                    }
                 }
             }
             self.window.event_handlers = event_handlers;
@@ -1394,7 +1422,7 @@ pub struct MeasureParams {
     pub available_space: Size<AvailableSpace>,
 }
 
-#[derive(Clone)]
+#[derive(Clone, Debug)]
 pub enum AvailableSpace {
     /// The amount of space available is the specified number of pixels
     Pixels(f32),

crates/gpui/src/geometry.rs 🔗

@@ -1,3 +1,5 @@
+use std::fmt::Debug;
+
 use super::scene::{Path, PathVertex};
 use crate::{color::Color, json::ToJson};
 pub use pathfinder_geometry::*;
@@ -133,13 +135,14 @@ impl ToJson for RectF {
     }
 }
 
-#[derive(Refineable)]
-pub struct Point<T: Clone + Default> {
+#[derive(Refineable, Debug)]
+#[refineable(debug)]
+pub struct Point<T: Clone + Default + Debug> {
     pub x: T,
     pub y: T,
 }
 
-impl<T: Clone + Default> Clone for Point<T> {
+impl<T: Clone + Default + Debug> Clone for Point<T> {
     fn clone(&self) -> Self {
         Self {
             x: self.x.clone(),
@@ -148,7 +151,7 @@ impl<T: Clone + Default> Clone for Point<T> {
     }
 }
 
-impl<T: Clone + Default> Into<taffy::geometry::Point<T>> for Point<T> {
+impl<T: Clone + Default + Debug> Into<taffy::geometry::Point<T>> for Point<T> {
     fn into(self) -> taffy::geometry::Point<T> {
         taffy::geometry::Point {
             x: self.x,
@@ -157,13 +160,14 @@ impl<T: Clone + Default> Into<taffy::geometry::Point<T>> for Point<T> {
     }
 }
 
-#[derive(Clone, Refineable, Debug)]
-pub struct Size<T: Clone + Default> {
+#[derive(Refineable, Clone, Debug)]
+#[refineable(debug)]
+pub struct Size<T: Clone + Default + Debug> {
     pub width: T,
     pub height: T,
 }
 
-impl<S, T: Clone + Default> From<taffy::geometry::Size<S>> for Size<T>
+impl<S, T: Clone + Default + Debug> From<taffy::geometry::Size<S>> for Size<T>
 where
     S: Into<T>,
 {
@@ -175,7 +179,7 @@ where
     }
 }
 
-impl<S, T: Clone + Default> Into<taffy::geometry::Size<S>> for Size<T>
+impl<S, T: Clone + Default + Debug> Into<taffy::geometry::Size<S>> for Size<T>
 where
     T: Into<S>,
 {
@@ -222,8 +226,9 @@ impl Size<Length> {
     }
 }
 
-#[derive(Clone, Default, Refineable)]
-pub struct Edges<T: Clone + Default> {
+#[derive(Clone, Default, Refineable, Debug)]
+#[refineable(debug)]
+pub struct Edges<T: Clone + Default + Debug> {
     pub top: T,
     pub right: T,
     pub bottom: T,
@@ -323,6 +328,15 @@ pub enum AbsoluteLength {
     Rems(f32),
 }
 
+impl std::fmt::Debug for AbsoluteLength {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            AbsoluteLength::Pixels(pixels) => write!(f, "{}px", pixels),
+            AbsoluteLength::Rems(rems) => write!(f, "{}rems", rems),
+        }
+    }
+}
+
 impl AbsoluteLength {
     pub fn to_pixels(&self, rem_size: f32) -> f32 {
         match self {
@@ -349,7 +363,7 @@ impl Default for AbsoluteLength {
 #[derive(Clone, Copy)]
 pub enum DefiniteLength {
     Absolute(AbsoluteLength),
-    Relative(f32), // Percent, from 0 to 100.
+    Relative(f32), // 0. to 1.
 }
 
 impl DefiniteLength {
@@ -368,6 +382,15 @@ impl DefiniteLength {
     }
 }
 
+impl std::fmt::Debug for DefiniteLength {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            DefiniteLength::Absolute(length) => std::fmt::Debug::fmt(length, f),
+            DefiniteLength::Relative(fract) => write!(f, "{}%", (fract * 100.0) as i32),
+        }
+    }
+}
+
 impl From<AbsoluteLength> for DefiniteLength {
     fn from(length: AbsoluteLength) -> Self {
         Self::Absolute(length)
@@ -387,6 +410,15 @@ pub enum Length {
     Auto,
 }
 
+impl std::fmt::Debug for Length {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Length::Definite(definite_length) => write!(f, "{:?}", definite_length),
+            Length::Auto => write!(f, "auto"),
+        }
+    }
+}
+
 pub fn relative<T: From<DefiniteLength>>(fraction: f32) -> T {
     DefiniteLength::Relative(fraction).into()
 }

crates/gpui2/src/elements/div.rs 🔗

@@ -1,12 +1,19 @@
+use std::cell::Cell;
+
 use crate::{
     element::{AnyElement, Element, IntoElement, Layout, ParentElement},
+    hsla,
     layout_context::LayoutContext,
     paint_context::PaintContext,
-    style::{Style, StyleHelpers, Styleable},
+    style::{CornerRadii, Style, StyleHelpers, Styleable},
     InteractionHandlers, Interactive,
 };
 use anyhow::Result;
-use gpui::LayoutId;
+use gpui::{
+    platform::{MouseButton, MouseButtonEvent, MouseMovedEvent},
+    scene::{self},
+    LayoutId,
+};
 use refineable::{Refineable, RefinementCascade};
 use smallvec::SmallVec;
 use util::ResultExt;
@@ -63,7 +70,7 @@ impl<V: 'static> Element<V> for Div<V> {
     ) where
         Self: Sized,
     {
-        let style = &self.computed_style();
+        let style = self.computed_style();
         let pop_text_style = style.text_style(cx).map_or(false, |style| {
             cx.push_text_style(&style).log_err().is_some()
         });
@@ -77,6 +84,58 @@ impl<V: 'static> Element<V> for Div<V> {
         if pop_text_style {
             cx.pop_text_style();
         }
+
+        if cx.is_inspector_enabled() {
+            self.paint_inspector(layout, cx);
+        }
+    }
+}
+
+impl<V: 'static> Div<V> {
+    fn paint_inspector(&self, layout: &Layout, cx: &mut PaintContext<V>) {
+        let style = self.styles.merged();
+
+        let hovered = layout.bounds.contains_point(cx.mouse_position());
+        if hovered {
+            let rem_size = cx.rem_size();
+            cx.scene.push_quad(scene::Quad {
+                bounds: layout.bounds,
+                background: Some(hsla(0., 0., 1., 0.05).into()),
+                border: gpui::Border {
+                    color: hsla(0., 0., 1., 0.2).into(),
+                    top: 1.,
+                    right: 1.,
+                    bottom: 1.,
+                    left: 1.,
+                },
+                corner_radii: CornerRadii::default()
+                    .refined(&style.corner_radii)
+                    .to_gpui(layout.bounds.size(), rem_size),
+            })
+        }
+
+        let bounds = layout.bounds;
+        let pressed = Cell::new(hovered && cx.is_mouse_down(MouseButton::Left));
+        cx.on_event(layout.order, move |_, event: &MouseButtonEvent, _| {
+            if bounds.contains_point(event.position) {
+                if event.is_down {
+                    pressed.set(true);
+                } else if pressed.get() {
+                    pressed.set(false);
+                    eprintln!("clicked div {:?} {:#?}", bounds, style);
+                }
+            }
+        });
+
+        let hovered = Cell::new(hovered);
+        cx.on_event(layout.order, move |_, event: &MouseMovedEvent, cx| {
+            cx.bubble_event();
+            let hovered_now = bounds.contains_point(event.position);
+            if hovered.get() != hovered_now {
+                hovered.set(hovered_now);
+                cx.repaint();
+            }
+        });
     }
 }
 

crates/gpui2/src/elements/hoverable.rs 🔗

@@ -72,6 +72,7 @@ impl<V: 'static, E: Element<V> + Styleable> Element<V> for Hoverable<E> {
         let hovered = self.hovered.clone();
         let bounds = layout.bounds;
         cx.on_event(layout.order, move |_view, _: &MouseMovedEvent, cx| {
+            cx.bubble_event();
             if bounds.contains_point(cx.mouse_position()) != hovered.get() {
                 cx.repaint();
             }

crates/gpui2/src/style.rs 🔗

@@ -22,7 +22,8 @@ use gpui2_macros::styleable_helpers;
 use refineable::{Refineable, RefinementCascade};
 use std::sync::Arc;
 
-#[derive(Clone, Refineable)]
+#[derive(Clone, Refineable, Debug)]
+#[refineable(debug)]
 pub struct Style {
     /// What layout strategy should be used?
     pub display: Display,
@@ -266,7 +267,8 @@ impl From<Hsla> for Fill {
     }
 }
 
-#[derive(Clone, Refineable, Default)]
+#[derive(Clone, Refineable, Default, Debug)]
+#[refineable(debug)]
 pub struct CornerRadii {
     top_left: AbsoluteLength,
     top_right: AbsoluteLength,

crates/refineable/derive_refineable/src/derive_refineable.rs 🔗

@@ -12,9 +12,14 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
         ident,
         data,
         generics,
+        attrs,
         ..
     } = parse_macro_input!(input);
 
+    let impl_debug_on_refinement = attrs
+        .iter()
+        .any(|attr| attr.path.is_ident("refineable") && attr.tokens.to_string().contains("debug"));
+
     let refinement_ident = format_ident!("{}Refinement", ident);
     let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
 
@@ -120,6 +125,41 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
         })
         .collect();
 
+    let debug_impl = if impl_debug_on_refinement {
+        let refinement_field_debugs: Vec<TokenStream2> = fields
+            .iter()
+            .map(|field| {
+                let name = &field.ident;
+                quote! {
+                    if self.#name.is_some() {
+                        debug_struct.field(stringify!(#name), &self.#name);
+                    } else {
+                        all_some = false;
+                    }
+                }
+            })
+            .collect();
+
+        quote! {
+            impl #impl_generics std::fmt::Debug for #refinement_ident #ty_generics
+                #where_clause
+            {
+                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+                    let mut debug_struct = f.debug_struct(stringify!(#refinement_ident));
+                    let mut all_some = true;
+                    #( #refinement_field_debugs )*
+                    if all_some {
+                        debug_struct.finish()
+                    } else {
+                        debug_struct.finish_non_exhaustive()
+                    }
+                }
+            }
+        }
+    } else {
+        quote! {}
+    };
+
     let gen = quote! {
         #[derive(Default, Clone)]
         pub struct #refinement_ident #impl_generics {
@@ -145,8 +185,22 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
                 #( #refinement_field_assignments )*
             }
         }
-    };
 
+        impl #impl_generics #refinement_ident #ty_generics
+            #where_clause
+        {
+            pub fn is_some(&self) -> bool {
+                #(
+                    if self.#field_names.is_some() {
+                        return true;
+                    }
+                )*
+                false
+            }
+        }
+
+        #debug_impl
+    };
     gen.into()
 }
 

crates/storybook/src/storybook.rs 🔗

@@ -15,6 +15,11 @@ mod element_ext;
 mod theme;
 mod workspace;
 
+gpui2::actions! {
+    storybook,
+    [ToggleInspector]
+}
+
 fn main() {
     SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
 
@@ -33,7 +38,12 @@ fn main() {
                 center: true,
                 ..Default::default()
             },
-            |_| view(|cx| storybook(cx)),
+            |cx| {
+                view(|cx| {
+                    cx.enable_inspector();
+                    storybook(cx)
+                })
+            },
         );
         cx.platform().activate(true);
     });

crates/storybook/src/workspace.rs 🔗

@@ -406,62 +406,41 @@ 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 = theme(cx);
+
         div()
             .size_full()
             .flex()
-            .flex_col()
-            .font("Zed Sans Extended")
-            .gap_0()
-            .justify_start()
-            .items_start()
-            .text_color(theme.lowest.base.default.foreground)
-            .fill(theme.middle.warning.default.background)
-            .child(
-                div()
-                    .w_full()
-                    .h_8()
-                    .fill(theme.lowest.negative.default.background)
-                    .child(titlebar()),
-            )
-            .child(
-                div()
-                    .flex()
-                    .flex_1()
-                    .child(collab_panel())
-                    .child(div().flex_1().fill(theme.lowest.accent.default.background))
-                    .child(div().w_64().fill(theme.lowest.positive.default.background)),
-            )
-            .child(
-                div()
-                    .w_full()
-                    .h_9()
-                    .fill(theme.lowest.positive.default.background)
-                    .child(statusbar())
-                    .child(
-                        div()
-                            .h_px()
-                            .w_full()
-                            .fill(theme.lowest.negative.default.background),
-                    )
-                    .child(
-                        div()
-                            .h_px()
-                            .w_full()
-                            .fill(theme.lowest.positive.default.background),
-                    )
-                    .child(
-                        div()
-                            .h_px()
-                            .w_full()
-                            .fill(theme.lowest.accent.default.background),
-                    )
-                    .child(
-                        div()
-                            .h_px()
-                            .w_full()
-                            .fill(theme.lowest.warning.default.background),
-                    ),
-            )
+            .flex_row()
+            .child(collab_panel())
+            .child(collab_panel())
+
+        // div()
+        //     .size_full()
+        //     .flex()
+        //     .flex_col()
+        //     .font("Zed Sans Extended")
+        //     .gap_0()
+        //     .justify_start()
+        //     .items_start()
+        //     .text_color(theme.lowest.base.default.foreground)
+        //     // .fill(theme.middle.warning.default.background)
+        //     .child(titlebar())
+        //     .child(
+        //         div()
+        //             .flex_1()
+        //             .w_full()
+        //             .flex()
+        //             .flex_row()
+        //             .child(collab_panel())
+        //             // .child(
+        //             //     div()
+        //             //         .h_full()
+        //             //         .flex_1()
+        //             //         .fill(theme.highest.accent.default.background),
+        //             // )
+        //             .child(collab_panel()),
+        //     )
+        //     .child(statusbar())
     }
 }