Fix bugs in resizing the docks + Add debug styling (#3645)

Mikayla Maki created

This PR:
- Fixes several bugs with dock resizing
- Adds a new drag move API for resizes 
- Adds special debug styling for elements, including auto-opening Zed at
the element's location in source
- Changes the `cx.paint_quad()` API to take a type and adds several
helpers to create this type.
- Adds `()` as the empty element, and uses it to implement a derive
macro for the `Render` trait.

Release Notes:

- N/A

Change summary

crates/collab_ui2/src/collab_panel.rs         |  26 +-
crates/editor2/src/element.rs                 | 104 ++++--------
crates/gpui2/src/app.rs                       |   4 
crates/gpui2/src/element.rs                   |  73 +++-----
crates/gpui2/src/elements/canvas.rs           |   6 
crates/gpui2/src/elements/div.rs              | 172 ++++++++++++++++++++
crates/gpui2/src/elements/uniform_list.rs     |  71 ++++----
crates/gpui2/src/geometry.rs                  |  33 ++++
crates/gpui2/src/platform.rs                  |   1 
crates/gpui2/src/platform/mac/window.rs       |  27 ++
crates/gpui2/src/platform/test/window.rs      |   4 
crates/gpui2/src/style.rs                     |  49 +++++
crates/gpui2/src/styled.rs                    |  12 +
crates/gpui2/src/text_system/line.rs          |  26 --
crates/gpui2/src/window.rs                    | 152 +++++++++++++++---
crates/gpui2_macros/src/derive_render.rs      |  23 ++
crates/gpui2_macros/src/gpui2_macros.rs       |   6 
crates/terminal_view2/src/terminal_element.rs |  24 --
crates/workspace2/src/dock.rs                 |  53 +++---
crates/workspace2/src/workspace2.rs           |  55 +++---
20 files changed, 618 insertions(+), 303 deletions(-)

Detailed changes

crates/collab_ui2/src/collab_panel.rs 🔗

@@ -175,12 +175,12 @@ use editor::Editor;
 use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
-    actions, canvas, div, img, impl_actions, overlay, point, prelude::*, px, rems, serde_json,
-    size, Action, AnyElement, AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent,
-    Div, EventEmitter, FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement,
-    IntoElement, Length, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad,
-    Render, RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task,
-    View, ViewContext, VisualContext, WeakView,
+    actions, canvas, div, fill, img, impl_actions, overlay, point, prelude::*, px, rems,
+    serde_json, size, Action, AnyElement, AppContext, AsyncWindowContext, Bounds, ClipboardItem,
+    DismissEvent, Div, EventEmitter, FocusHandle, Focusable, FocusableView, Hsla,
+    InteractiveElement, IntoElement, Length, Model, MouseDownEvent, ParentElement, Pixels, Point,
+    PromptLevel, Quad, Render, RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled,
+    Subscription, Task, View, ViewContext, VisualContext, WeakView,
 };
 use project::{Fs, Project};
 use serde_derive::{Deserialize, Serialize};
@@ -2994,7 +2994,7 @@ fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement
         let right = bounds.right();
         let top = bounds.top();
 
-        cx.paint_quad(
+        cx.paint_quad(fill(
             Bounds::from_corners(
                 point(start_x, top),
                 point(
@@ -3002,18 +3002,12 @@ fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement
                     if is_last { start_y } else { bounds.bottom() },
                 ),
             ),
-            Default::default(),
             color,
-            Default::default(),
-            Hsla::transparent_black(),
-        );
-        cx.paint_quad(
+        ));
+        cx.paint_quad(fill(
             Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
-            Default::default(),
             color,
-            Default::default(),
-            Hsla::transparent_black(),
-        );
+        ));
     })
     .w(width)
     .h(line_height)

crates/editor2/src/element.rs 🔗

@@ -23,13 +23,14 @@ use anyhow::Result;
 use collections::{BTreeMap, HashMap};
 use git::diff::DiffHunkStatus;
 use gpui::{
-    div, overlay, point, px, relative, size, transparent_black, Action, AnchorCorner, AnyElement,
-    AsyncWindowContext, AvailableSpace, BorrowWindow, Bounds, ContentMask, Corners, CursorStyle,
-    DispatchPhase, Edges, Element, ElementId, ElementInputHandler, Entity, EntityId, Hsla,
-    InteractiveBounds, InteractiveElement, IntoElement, LineLayout, ModifiersChangedEvent,
-    MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, RenderOnce,
-    ScrollWheelEvent, ShapedLine, SharedString, Size, StackingOrder, StatefulInteractiveElement,
-    Style, Styled, TextRun, TextStyle, View, ViewContext, WeakView, WindowContext, WrappedLine,
+    div, fill, outline, overlay, point, px, quad, relative, size, transparent_black, Action,
+    AnchorCorner, AnyElement, AsyncWindowContext, AvailableSpace, BorrowWindow, Bounds,
+    ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementId,
+    ElementInputHandler, Entity, EntityId, Hsla, InteractiveBounds, InteractiveElement,
+    IntoElement, LineLayout, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
+    MouseUpEvent, ParentElement, Pixels, RenderOnce, ScrollWheelEvent, ShapedLine, SharedString,
+    Size, StackingOrder, StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, View,
+    ViewContext, WeakView, WindowContext, WrappedLine,
 };
 use itertools::Itertools;
 use language::{language_settings::ShowWhitespaceSetting, Language};
@@ -620,20 +621,8 @@ impl EditorElement {
         let scroll_top =
             layout.position_map.snapshot.scroll_position().y * layout.position_map.line_height;
         let gutter_bg = cx.theme().colors().editor_gutter_background;
-        cx.paint_quad(
-            gutter_bounds,
-            Corners::default(),
-            gutter_bg,
-            Edges::default(),
-            transparent_black(),
-        );
-        cx.paint_quad(
-            text_bounds,
-            Corners::default(),
-            self.style.background,
-            Edges::default(),
-            transparent_black(),
-        );
+        cx.paint_quad(fill(gutter_bounds, gutter_bg));
+        cx.paint_quad(fill(text_bounds, self.style.background));
 
         if let EditorMode::Full = layout.mode {
             let mut active_rows = layout.active_rows.iter().peekable();
@@ -657,13 +646,7 @@ impl EditorElement {
                         layout.position_map.line_height * (end_row - start_row + 1) as f32,
                     );
                     let active_line_bg = cx.theme().colors().editor_active_line_background;
-                    cx.paint_quad(
-                        Bounds { origin, size },
-                        Corners::default(),
-                        active_line_bg,
-                        Edges::default(),
-                        transparent_black(),
-                    );
+                    cx.paint_quad(fill(Bounds { origin, size }, active_line_bg));
                 }
             }
 
@@ -679,13 +662,7 @@ impl EditorElement {
                     layout.position_map.line_height * highlighted_rows.len() as f32,
                 );
                 let highlighted_line_bg = cx.theme().colors().editor_highlighted_line_background;
-                cx.paint_quad(
-                    Bounds { origin, size },
-                    Corners::default(),
-                    highlighted_line_bg,
-                    Edges::default(),
-                    transparent_black(),
-                );
+                cx.paint_quad(fill(Bounds { origin, size }, highlighted_line_bg));
             }
 
             let scroll_left =
@@ -706,16 +683,13 @@ impl EditorElement {
                 } else {
                     cx.theme().colors().editor_wrap_guide
                 };
-                cx.paint_quad(
+                cx.paint_quad(fill(
                     Bounds {
                         origin: point(x, text_bounds.origin.y),
                         size: size(px(1.), text_bounds.size.height),
                     },
-                    Corners::default(),
                     color,
-                    Edges::default(),
-                    transparent_black(),
-                );
+                ));
             }
         }
     }
@@ -812,13 +786,13 @@ impl EditorElement {
                     let highlight_origin = bounds.origin + point(-width, start_y);
                     let highlight_size = size(width * 2., end_y - start_y);
                     let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
-                    cx.paint_quad(
+                    cx.paint_quad(quad(
                         highlight_bounds,
                         Corners::all(1. * line_height),
                         gpui::yellow(), // todo!("use the right color")
                         Edges::default(),
                         transparent_black(),
-                    );
+                    ));
 
                     continue;
                 }
@@ -845,13 +819,13 @@ impl EditorElement {
                     let highlight_origin = bounds.origin + point(-width, start_y);
                     let highlight_size = size(width * 2., end_y - start_y);
                     let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
-                    cx.paint_quad(
+                    cx.paint_quad(quad(
                         highlight_bounds,
                         Corners::all(1. * line_height),
                         cx.theme().status().deleted,
                         Edges::default(),
                         transparent_black(),
-                    );
+                    ));
 
                     continue;
                 }
@@ -867,13 +841,13 @@ impl EditorElement {
             let highlight_origin = bounds.origin + point(-width, start_y);
             let highlight_size = size(width * 2., end_y - start_y);
             let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
-            cx.paint_quad(
+            cx.paint_quad(quad(
                 highlight_bounds,
                 Corners::all(0.05 * line_height),
                 color, // todo!("use the right color")
                 Edges::default(),
                 transparent_black(),
-            );
+            ));
         }
     }
 
@@ -1278,7 +1252,7 @@ impl EditorElement {
         let thumb_bounds = Bounds::from_corners(point(left, thumb_top), point(right, thumb_bottom));
 
         if layout.show_scrollbars {
-            cx.paint_quad(
+            cx.paint_quad(quad(
                 track_bounds,
                 Corners::default(),
                 cx.theme().colors().scrollbar_track_background,
@@ -1289,7 +1263,7 @@ impl EditorElement {
                     left: px(1.),
                 },
                 cx.theme().colors().scrollbar_track_border,
-            );
+            ));
             let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
             if layout.is_singleton && scrollbar_settings.selections {
                 let start_anchor = Anchor::min();
@@ -1309,7 +1283,7 @@ impl EditorElement {
                         end_y = start_y + px(1.);
                     }
                     let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
-                    cx.paint_quad(
+                    cx.paint_quad(quad(
                         bounds,
                         Corners::default(),
                         cx.theme().status().info,
@@ -1320,7 +1294,7 @@ impl EditorElement {
                             left: px(1.),
                         },
                         cx.theme().colors().scrollbar_thumb_border,
-                    );
+                    ));
                 }
             }
 
@@ -1352,7 +1326,7 @@ impl EditorElement {
                         DiffHunkStatus::Modified => cx.theme().status().modified,
                         DiffHunkStatus::Removed => cx.theme().status().deleted,
                     };
-                    cx.paint_quad(
+                    cx.paint_quad(quad(
                         bounds,
                         Corners::default(),
                         color,
@@ -1363,11 +1337,11 @@ impl EditorElement {
                             left: px(1.),
                         },
                         cx.theme().colors().scrollbar_thumb_border,
-                    );
+                    ));
                 }
             }
 
-            cx.paint_quad(
+            cx.paint_quad(quad(
                 thumb_bounds,
                 Corners::default(),
                 cx.theme().colors().scrollbar_thumb_background,
@@ -1378,7 +1352,7 @@ impl EditorElement {
                     left: px(1.),
                 },
                 cx.theme().colors().scrollbar_thumb_border,
-            );
+            ));
         }
 
         let mouse_position = cx.mouse_position();
@@ -3085,23 +3059,13 @@ impl Cursor {
         };
 
         //Draw background or border quad
-        if matches!(self.shape, CursorShape::Hollow) {
-            cx.paint_quad(
-                bounds,
-                Corners::default(),
-                transparent_black(),
-                Edges::all(px(1.)),
-                self.color,
-            );
+        let cursor = if matches!(self.shape, CursorShape::Hollow) {
+            outline(bounds, self.color)
         } else {
-            cx.paint_quad(
-                bounds,
-                Corners::default(),
-                self.color,
-                Edges::default(),
-                transparent_black(),
-            );
-        }
+            fill(bounds, self.color)
+        };
+
+        cx.paint_quad(cursor);
 
         if let Some(block_text) = &self.block_text {
             block_text.paint(self.origin + origin, self.line_height, cx);

crates/gpui2/src/app.rs 🔗

@@ -1138,6 +1138,10 @@ impl AppContext {
     pub fn has_active_drag(&self) -> bool {
         self.active_drag.is_some()
     }
+
+    pub fn active_drag(&self) -> Option<AnyView> {
+        self.active_drag.as_ref().map(|drag| drag.view.clone())
+    }
 }
 
 impl Context for AppContext {

crates/gpui2/src/element.rs 🔗

@@ -482,48 +482,31 @@ impl IntoElement for AnyElement {
     }
 }
 
-// impl<V, E, F> Element for Option<F>
-// where
-//     V: 'static,
-//     E: Element,
-//     F: FnOnce(&mut V, &mut WindowContext<'_, V>) -> E + 'static,
-// {
-//     type State = Option<AnyElement>;
-
-//     fn element_id(&self) -> Option<ElementId> {
-//         None
-//     }
-
-//     fn layout(
-//         &mut self,
-//         _: Option<Self::State>,
-//         cx: &mut WindowContext,
-//     ) -> (LayoutId, Self::State) {
-//         let render = self.take().unwrap();
-//         let mut element = (render)(view_state, cx).into_any();
-//         let layout_id = element.layout(view_state, cx);
-//         (layout_id, Some(element))
-//     }
-
-//     fn paint(
-//         self,
-//         _bounds: Bounds<Pixels>,
-//         rendered_element: &mut Self::State,
-//         cx: &mut WindowContext,
-//     ) {
-//         rendered_element.take().unwrap().paint(view_state, cx);
-//     }
-// }
-
-// impl<V, E, F> RenderOnce for Option<F>
-// where
-//     V: 'static,
-//     E: Element,
-//     F: FnOnce(&mut V, &mut WindowContext) -> E + 'static,
-// {
-//     type Element = Self;
-
-//     fn render(self) -> Self::Element {
-//         self
-//     }
-// }
+/// The empty element, which renders nothing.
+pub type Empty = ();
+
+impl IntoElement for () {
+    type Element = Self;
+
+    fn element_id(&self) -> Option<ElementId> {
+        None
+    }
+
+    fn into_element(self) -> Self::Element {
+        self
+    }
+}
+
+impl Element for () {
+    type State = ();
+
+    fn layout(
+        &mut self,
+        _state: Option<Self::State>,
+        cx: &mut WindowContext,
+    ) -> (LayoutId, Self::State) {
+        (cx.request_layout(&crate::Style::default(), None), ())
+    }
+
+    fn paint(self, _bounds: Bounds<Pixels>, _state: &mut Self::State, _cx: &mut WindowContext) {}
+}

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

@@ -2,7 +2,7 @@ use refineable::Refineable as _;
 
 use crate::{Bounds, Element, IntoElement, Pixels, Style, StyleRefinement, Styled, WindowContext};
 
-pub fn canvas(callback: impl 'static + FnOnce(Bounds<Pixels>, &mut WindowContext)) -> Canvas {
+pub fn canvas(callback: impl 'static + FnOnce(&Bounds<Pixels>, &mut WindowContext)) -> Canvas {
     Canvas {
         paint_callback: Box::new(callback),
         style: StyleRefinement::default(),
@@ -10,7 +10,7 @@ pub fn canvas(callback: impl 'static + FnOnce(Bounds<Pixels>, &mut WindowContext
 }
 
 pub struct Canvas {
-    paint_callback: Box<dyn FnOnce(Bounds<Pixels>, &mut WindowContext)>,
+    paint_callback: Box<dyn FnOnce(&Bounds<Pixels>, &mut WindowContext)>,
     style: StyleRefinement,
 }
 
@@ -41,7 +41,7 @@ impl Element for Canvas {
     }
 
     fn paint(self, bounds: Bounds<Pixels>, _: &mut (), cx: &mut WindowContext) {
-        (self.paint_callback)(bounds, cx)
+        (self.paint_callback)(&bounds, cx)
     }
 }
 

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

@@ -6,6 +6,7 @@ use crate::{
     SharedString, Size, StackingOrder, Style, StyleRefinement, Styled, Task, View, Visibility,
     WindowContext,
 };
+
 use collections::HashMap;
 use refineable::Refineable;
 use smallvec::SmallVec;
@@ -29,6 +30,11 @@ pub struct GroupStyle {
     pub style: Box<StyleRefinement>,
 }
 
+pub struct DragMoveEvent<W: Render> {
+    pub event: MouseMoveEvent,
+    pub drag: View<W>,
+}
+
 pub trait InteractiveElement: Sized {
     fn interactivity(&mut self) -> &mut Interactivity;
 
@@ -192,6 +198,34 @@ pub trait InteractiveElement: Sized {
         self
     }
 
+    fn on_drag_move<W>(
+        mut self,
+        listener: impl Fn(&DragMoveEvent<W>, &mut WindowContext) + 'static,
+    ) -> Self
+    where
+        W: Render,
+    {
+        self.interactivity().mouse_move_listeners.push(Box::new(
+            move |event, bounds, phase, cx| {
+                if phase == DispatchPhase::Capture
+                    && bounds.drag_target_contains(&event.position, cx)
+                {
+                    if let Some(view) = cx.active_drag().and_then(|view| view.downcast::<W>().ok())
+                    {
+                        (listener)(
+                            &DragMoveEvent {
+                                event: event.clone(),
+                                drag: view,
+                            },
+                            cx,
+                        );
+                    }
+                }
+            },
+        ));
+        self
+    }
+
     fn on_scroll_wheel(
         mut self,
         listener: impl Fn(&ScrollWheelEvent, &mut WindowContext) + 'static,
@@ -403,7 +437,7 @@ pub trait StatefulInteractiveElement: InteractiveElement {
         self
     }
 
-    fn on_drag<W>(mut self, listener: impl Fn(&mut WindowContext) -> View<W> + 'static) -> Self
+    fn on_drag<W>(mut self, constructor: impl Fn(&mut WindowContext) -> View<W> + 'static) -> Self
     where
         Self: Sized,
         W: 'static + Render,
@@ -413,7 +447,7 @@ pub trait StatefulInteractiveElement: InteractiveElement {
             "calling on_drag more than once on the same element is not supported"
         );
         self.interactivity().drag_listener = Some(Box::new(move |cursor_offset, cx| AnyDrag {
-            view: listener(cx).into(),
+            view: constructor(cx).into(),
             cursor_offset,
         }));
         self
@@ -493,11 +527,19 @@ pub type DragEventListener = Box<dyn Fn(&MouseMoveEvent, &mut WindowContext) + '
 
 pub type ActionListener = Box<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>;
 
+#[track_caller]
 pub fn div() -> Div {
-    Div {
+    let mut div = Div {
         interactivity: Interactivity::default(),
         children: SmallVec::default(),
+    };
+
+    #[cfg(debug_assertions)]
+    {
+        div.interactivity.location = Some(*core::panic::Location::caller());
     }
+
+    div
 }
 
 pub struct Div {
@@ -607,10 +649,7 @@ impl Element for Div {
                 let z_index = style.z_index.unwrap_or(0);
 
                 cx.with_z_index(z_index, |cx| {
-                    cx.with_z_index(0, |cx| {
-                        style.paint(bounds, cx);
-                    });
-                    cx.with_z_index(1, |cx| {
+                    style.paint(bounds, cx, |cx| {
                         cx.with_text_style(style.text_style().cloned(), |cx| {
                             cx.with_content_mask(style.overflow_mask(bounds), |cx| {
                                 cx.with_element_offset(scroll_offset, |cx| {
@@ -620,7 +659,7 @@ impl Element for Div {
                                 })
                             })
                         })
-                    })
+                    });
                 })
             },
         );
@@ -678,6 +717,9 @@ pub struct Interactivity {
     pub drag_listener: Option<DragListener>,
     pub hover_listener: Option<Box<dyn Fn(&bool, &mut WindowContext)>>,
     pub tooltip_builder: Option<TooltipBuilder>,
+
+    #[cfg(debug_assertions)]
+    pub location: Option<core::panic::Location<'static>>,
 }
 
 #[derive(Clone, Debug)]
@@ -737,6 +779,117 @@ impl Interactivity {
     ) {
         let style = self.compute_style(Some(bounds), element_state, cx);
 
+        #[cfg(debug_assertions)]
+        if self.element_id.is_some()
+            && (style.debug || style.debug_below || cx.has_global::<crate::DebugBelow>())
+            && bounds.contains(&cx.mouse_position())
+        {
+            const FONT_SIZE: crate::Pixels = crate::Pixels(10.);
+            let element_id = format!("{:?}", self.element_id.unwrap());
+            let str_len = element_id.len();
+
+            let render_debug_text = |cx: &mut WindowContext| {
+                if let Some(text) = cx
+                    .text_system()
+                    .shape_text(
+                        &element_id,
+                        FONT_SIZE,
+                        &[cx.text_style().to_run(str_len)],
+                        None,
+                    )
+                    .ok()
+                    .map(|mut text| text.pop())
+                    .flatten()
+                {
+                    text.paint(bounds.origin, FONT_SIZE, cx).ok();
+
+                    let text_bounds = crate::Bounds {
+                        origin: bounds.origin,
+                        size: text.size(FONT_SIZE),
+                    };
+                    if self.location.is_some()
+                        && text_bounds.contains(&cx.mouse_position())
+                        && cx.modifiers().command
+                    {
+                        let command_held = cx.modifiers().command;
+                        cx.on_key_event({
+                            let text_bounds = text_bounds.clone();
+                            move |e: &crate::ModifiersChangedEvent, _phase, cx| {
+                                if e.modifiers.command != command_held
+                                    && text_bounds.contains(&cx.mouse_position())
+                                {
+                                    cx.notify();
+                                }
+                            }
+                        });
+
+                        let hovered = bounds.contains(&cx.mouse_position());
+                        cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
+                            if phase == DispatchPhase::Capture {
+                                if bounds.contains(&event.position) != hovered {
+                                    cx.notify();
+                                }
+                            }
+                        });
+
+                        cx.on_mouse_event({
+                            let location = self.location.clone().unwrap();
+                            let text_bounds = text_bounds.clone();
+                            move |e: &crate::MouseDownEvent, phase, cx| {
+                                if text_bounds.contains(&e.position) && phase.capture() {
+                                    cx.stop_propagation();
+                                    let Ok(dir) = std::env::current_dir() else {
+                                        return;
+                                    };
+
+                                    eprintln!(
+                                        "This element is created at:\n{}:{}:{}",
+                                        location.file(),
+                                        location.line(),
+                                        location.column()
+                                    );
+
+                                    std::process::Command::new("zed")
+                                        .arg(format!(
+                                            "{}/{}:{}:{}",
+                                            dir.to_string_lossy(),
+                                            location.file(),
+                                            location.line(),
+                                            location.column()
+                                        ))
+                                        .spawn()
+                                        .ok();
+                                }
+                            }
+                        });
+                        cx.paint_quad(crate::outline(
+                            crate::Bounds {
+                                origin: bounds.origin
+                                    + crate::point(crate::px(0.), FONT_SIZE - px(2.)),
+                                size: crate::Size {
+                                    width: text_bounds.size.width,
+                                    height: crate::px(1.),
+                                },
+                            },
+                            crate::red(),
+                        ))
+                    }
+                }
+            };
+
+            cx.with_z_index(1, |cx| {
+                cx.with_text_style(
+                    Some(crate::TextStyleRefinement {
+                        color: Some(crate::red()),
+                        line_height: Some(FONT_SIZE.into()),
+                        background_color: Some(crate::white()),
+                        ..Default::default()
+                    }),
+                    render_debug_text,
+                )
+            });
+        }
+
         if style
             .background
             .as_ref()
@@ -1234,6 +1387,9 @@ impl Default for Interactivity {
             drag_listener: None,
             hover_listener: None,
             tooltip_builder: None,
+
+            #[cfg(debug_assertions)]
+            location: None,
         }
     }
 }

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

@@ -10,6 +10,7 @@ use taffy::style::Overflow;
 /// uniform_list provides lazy rendering for a set of items that are of uniform height.
 /// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
 /// uniform_list will only render the visible subset of items.
+#[track_caller]
 pub fn uniform_list<I, R, V>(
     view: View<V>,
     id: I,
@@ -42,6 +43,10 @@ where
         interactivity: Interactivity {
             element_id: Some(id.into()),
             base_style: Box::new(base_style),
+
+            #[cfg(debug_assertions)]
+            location: Some(*core::panic::Location::caller()),
+
             ..Default::default()
         },
         scroll_handle: None,
@@ -197,41 +202,41 @@ impl Element for UniformList {
                 );
 
                 cx.with_z_index(style.z_index.unwrap_or(0), |cx| {
-                    style.paint(bounds, cx);
-
-                    if self.item_count > 0 {
-                        if let Some(scroll_handle) = self.scroll_handle.clone() {
-                            scroll_handle.0.borrow_mut().replace(ScrollHandleState {
-                                item_height,
-                                list_height: padded_bounds.size.height,
-                                scroll_offset: shared_scroll_offset,
+                    style.paint(bounds, cx, |cx| {
+                        if self.item_count > 0 {
+                            if let Some(scroll_handle) = self.scroll_handle.clone() {
+                                scroll_handle.0.borrow_mut().replace(ScrollHandleState {
+                                    item_height,
+                                    list_height: padded_bounds.size.height,
+                                    scroll_offset: shared_scroll_offset,
+                                });
+                            }
+
+                            let first_visible_element_ix =
+                                (-scroll_offset.y / item_height).floor() as usize;
+                            let last_visible_element_ix =
+                                ((-scroll_offset.y + padded_bounds.size.height) / item_height)
+                                    .ceil() as usize;
+                            let visible_range = first_visible_element_ix
+                                ..cmp::min(last_visible_element_ix, self.item_count);
+
+                            let items = (self.render_items)(visible_range.clone(), cx);
+                            cx.with_z_index(1, |cx| {
+                                let content_mask = ContentMask { bounds };
+                                cx.with_content_mask(Some(content_mask), |cx| {
+                                    for (item, ix) in items.into_iter().zip(visible_range) {
+                                        let item_origin = padded_bounds.origin
+                                            + point(px(0.), item_height * ix + scroll_offset.y);
+                                        let available_space = size(
+                                            AvailableSpace::Definite(padded_bounds.size.width),
+                                            AvailableSpace::Definite(item_height),
+                                        );
+                                        item.draw(item_origin, available_space, cx);
+                                    }
+                                });
                             });
                         }
-
-                        let first_visible_element_ix =
-                            (-scroll_offset.y / item_height).floor() as usize;
-                        let last_visible_element_ix =
-                            ((-scroll_offset.y + padded_bounds.size.height) / item_height).ceil()
-                                as usize;
-                        let visible_range = first_visible_element_ix
-                            ..cmp::min(last_visible_element_ix, self.item_count);
-
-                        let items = (self.render_items)(visible_range.clone(), cx);
-                        cx.with_z_index(1, |cx| {
-                            let content_mask = ContentMask { bounds };
-                            cx.with_content_mask(Some(content_mask), |cx| {
-                                for (item, ix) in items.into_iter().zip(visible_range) {
-                                    let item_origin = padded_bounds.origin
-                                        + point(px(0.), item_height * ix + scroll_offset.y);
-                                    let available_space = size(
-                                        AvailableSpace::Definite(padded_bounds.size.width),
-                                        AvailableSpace::Definite(item_height),
-                                    );
-                                    item.draw(item_origin, available_space, cx);
-                                }
-                            });
-                        });
-                    }
+                    });
                 })
             },
         );

crates/gpui2/src/geometry.rs 🔗

@@ -1592,6 +1592,17 @@ impl Edges<Pixels> {
     }
 }
 
+impl Into<Edges<Pixels>> for f32 {
+    fn into(self) -> Edges<Pixels> {
+        Edges {
+            top: self.into(),
+            right: self.into(),
+            bottom: self.into(),
+            left: self.into(),
+        }
+    }
+}
+
 /// Represents the corners of a box in a 2D space, such as border radius.
 ///
 /// Each field represents the size of the corner on one side of the box: `top_left`, `top_right`, `bottom_right`, and `bottom_left`.
@@ -1808,6 +1819,28 @@ where
 
 impl<T> Copy for Corners<T> where T: Copy + Clone + Default + Debug {}
 
+impl Into<Corners<Pixels>> for f32 {
+    fn into(self) -> Corners<Pixels> {
+        Corners {
+            top_left: self.into(),
+            top_right: self.into(),
+            bottom_right: self.into(),
+            bottom_left: self.into(),
+        }
+    }
+}
+
+impl Into<Corners<Pixels>> for Pixels {
+    fn into(self) -> Corners<Pixels> {
+        Corners {
+            top_left: self,
+            top_right: self,
+            bottom_right: self,
+            bottom_left: self,
+        }
+    }
+}
+
 /// Represents a length in pixels, the base unit of measurement in the UI framework.
 ///
 /// `Pixels` is a value type that represents an absolute length in pixels, which is used

crates/gpui2/src/platform.rs 🔗

@@ -147,6 +147,7 @@ pub trait PlatformWindow {
     fn appearance(&self) -> WindowAppearance;
     fn display(&self) -> Rc<dyn PlatformDisplay>;
     fn mouse_position(&self) -> Point<Pixels>;
+    fn modifiers(&self) -> Modifiers;
     fn as_any_mut(&mut self) -> &mut dyn Any;
     fn set_input_handler(&mut self, input_handler: Box<dyn PlatformInputHandler>);
     fn clear_input_handler(&mut self);

crates/gpui2/src/platform/mac/window.rs 🔗

@@ -9,9 +9,10 @@ use crate::{
 use block::ConcreteBlock;
 use cocoa::{
     appkit::{
-        CGPoint, NSApplication, NSBackingStoreBuffered, NSFilenamesPboardType, NSPasteboard,
-        NSScreen, NSView, NSViewHeightSizable, NSViewWidthSizable, NSWindow, NSWindowButton,
-        NSWindowCollectionBehavior, NSWindowStyleMask, NSWindowTitleVisibility,
+        CGPoint, NSApplication, NSBackingStoreBuffered, NSEventModifierFlags,
+        NSFilenamesPboardType, NSPasteboard, NSScreen, NSView, NSViewHeightSizable,
+        NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowCollectionBehavior,
+        NSWindowStyleMask, NSWindowTitleVisibility,
     },
     base::{id, nil},
     foundation::{
@@ -744,6 +745,26 @@ impl PlatformWindow for MacWindow {
         convert_mouse_position(position, self.content_size().height)
     }
 
+    fn modifiers(&self) -> Modifiers {
+        unsafe {
+            let modifiers: NSEventModifierFlags = msg_send![class!(NSEvent), modifierFlags];
+
+            let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
+            let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask);
+            let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
+            let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
+            let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask);
+
+            Modifiers {
+                control,
+                alt,
+                shift,
+                command,
+                function,
+            }
+        }
+    }
+
     fn as_any_mut(&mut self) -> &mut dyn Any {
         self
     }

crates/gpui2/src/platform/test/window.rs 🔗

@@ -79,6 +79,10 @@ impl PlatformWindow for TestWindow {
         Point::default()
     }
 
+    fn modifiers(&self) -> crate::Modifiers {
+        crate::Modifiers::default()
+    }
+
     fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
         self
     }

crates/gpui2/src/style.rs 🔗

@@ -1,9 +1,9 @@
 use std::{iter, mem, ops::Range};
 
 use crate::{
-    black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask,
-    Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font,
-    FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
+    black, phi, point, quad, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds,
+    ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement,
+    Font, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
     SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext,
 };
 use collections::HashSet;
@@ -14,6 +14,9 @@ pub use taffy::style::{
     Overflow, Position,
 };
 
+#[cfg(debug_assertions)]
+pub struct DebugBelow;
+
 pub type StyleCascade = Cascade<Style>;
 
 #[derive(Clone, Refineable, Debug)]
@@ -108,6 +111,11 @@ pub struct Style {
     pub mouse_cursor: Option<CursorStyle>,
 
     pub z_index: Option<u32>,
+
+    #[cfg(debug_assertions)]
+    pub debug: bool,
+    #[cfg(debug_assertions)]
+    pub debug_below: bool,
 }
 
 impl Styled for StyleRefinement {
@@ -334,7 +342,22 @@ impl Style {
     }
 
     /// Paints the background of an element styled with this style.
-    pub fn paint(&self, bounds: Bounds<Pixels>, cx: &mut WindowContext) {
+    pub fn paint(
+        &self,
+        bounds: Bounds<Pixels>,
+        cx: &mut WindowContext,
+        continuation: impl FnOnce(&mut WindowContext),
+    ) {
+        #[cfg(debug_assertions)]
+        if self.debug_below {
+            cx.set_global(DebugBelow)
+        }
+
+        #[cfg(debug_assertions)]
+        if self.debug || cx.has_global::<DebugBelow>() {
+            cx.paint_quad(crate::outline(bounds, crate::red()));
+        }
+
         let rem_size = cx.rem_size();
 
         cx.with_z_index(0, |cx| {
@@ -348,15 +371,24 @@ impl Style {
         let background_color = self.background.as_ref().and_then(Fill::color);
         if background_color.is_some() || self.is_border_visible() {
             cx.with_z_index(1, |cx| {
-                cx.paint_quad(
+                cx.paint_quad(quad(
                     bounds,
                     self.corner_radii.to_pixels(bounds.size, rem_size),
                     background_color.unwrap_or_default(),
                     self.border_widths.to_pixels(rem_size),
                     self.border_color.unwrap_or_default(),
-                );
+                ));
             });
         }
+
+        cx.with_z_index(2, |cx| {
+            continuation(cx);
+        });
+
+        #[cfg(debug_assertions)]
+        if self.debug_below {
+            cx.remove_global::<DebugBelow>();
+        }
     }
 
     fn is_border_visible(&self) -> bool {
@@ -404,6 +436,11 @@ impl Default for Style {
             text: TextStyleRefinement::default(),
             mouse_cursor: None,
             z_index: None,
+
+            #[cfg(debug_assertions)]
+            debug: false,
+            #[cfg(debug_assertions)]
+            debug_below: false,
         }
     }
 }

crates/gpui2/src/styled.rs 🔗

@@ -633,4 +633,16 @@ pub trait Styled: Sized {
             .line_height = Some(line_height.into());
         self
     }
+
+    #[cfg(debug_assertions)]
+    fn debug(mut self) -> Self {
+        self.style().debug = Some(true);
+        self
+    }
+
+    #[cfg(debug_assertions)]
+    fn debug_below(mut self) -> Self {
+        self.style().debug_below = Some(true);
+        self
+    }
 }

crates/gpui2/src/text_system/line.rs 🔗

@@ -1,7 +1,6 @@
 use crate::{
-    black, point, px, size, transparent_black, BorrowWindow, Bounds, Corners, Edges, Hsla,
-    LineLayout, Pixels, Point, Result, SharedString, UnderlineStyle, WindowContext, WrapBoundary,
-    WrappedLineLayout,
+    black, fill, point, px, size, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result,
+    SharedString, UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout,
 };
 use derive_more::{Deref, DerefMut};
 use smallvec::SmallVec;
@@ -109,16 +108,13 @@ fn paint_line(
             if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
                 wraps.next();
                 if let Some((background_origin, background_color)) = current_background.as_mut() {
-                    cx.paint_quad(
+                    cx.paint_quad(fill(
                         Bounds {
                             origin: *background_origin,
                             size: size(glyph_origin.x - background_origin.x, line_height),
                         },
-                        Corners::default(),
                         *background_color,
-                        Edges::default(),
-                        transparent_black(),
-                    );
+                    ));
                     background_origin.x = origin.x;
                     background_origin.y += line_height;
                 }
@@ -180,16 +176,13 @@ fn paint_line(
             }
 
             if let Some((background_origin, background_color)) = finished_background {
-                cx.paint_quad(
+                cx.paint_quad(fill(
                     Bounds {
                         origin: background_origin,
                         size: size(glyph_origin.x - background_origin.x, line_height),
                     },
-                    Corners::default(),
                     background_color,
-                    Edges::default(),
-                    transparent_black(),
-                );
+                ));
             }
 
             if let Some((underline_origin, underline_style)) = finished_underline {
@@ -235,16 +228,13 @@ fn paint_line(
     }
 
     if let Some((background_origin, background_color)) = current_background.take() {
-        cx.paint_quad(
+        cx.paint_quad(fill(
             Bounds {
                 origin: background_origin,
                 size: size(last_line_end_x - background_origin.x, line_height),
             },
-            Corners::default(),
             background_color,
-            Edges::default(),
-            transparent_black(),
-        );
+        ));
     }
 
     if let Some((underline_start, underline_style)) = current_underline.take() {

crates/gpui2/src/window.rs 🔗

@@ -1,15 +1,15 @@
 use crate::{
-    key_dispatch::DispatchActionListener, px, size, Action, AnyDrag, AnyView, AppContext,
-    AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle,
-    DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId,
-    EventEmitter, FileDropEvent, Flatten, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla,
-    ImageData, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeystrokeEvent, LayoutId,
-    Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseMoveEvent, MouseUpEvent,
-    Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
-    PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams,
-    RenderSvgParams, ScaledPixels, Scene, SceneBuilder, Shadow, SharedString, Size, Style,
-    SubscriberSet, Subscription, Surface, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View,
-    VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
+    key_dispatch::DispatchActionListener, px, size, transparent_black, Action, AnyDrag, AnyView,
+    AppContext, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners,
+    CursorStyle, DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity,
+    EntityId, EventEmitter, FileDropEvent, Flatten, FocusEvent, FontId, GlobalElementId, GlyphId,
+    Hsla, ImageData, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeystrokeEvent,
+    LayoutId, Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseMoveEvent,
+    MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler,
+    PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams,
+    RenderImageParams, RenderSvgParams, ScaledPixels, Scene, SceneBuilder, Shadow, SharedString,
+    Size, Style, SubscriberSet, Subscription, Surface, TaffyLayoutEngine, Task, Underline,
+    UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
 };
 use anyhow::{anyhow, Context as _, Result};
 use collections::HashMap;
@@ -231,6 +231,7 @@ pub struct Window {
     pub(crate) blur_listeners: SubscriberSet<(), AnyObserver>,
     default_prevented: bool,
     mouse_position: Point<Pixels>,
+    modifiers: Modifiers,
     requested_cursor_style: Option<CursorStyle>,
     scale_factor: f32,
     bounds: WindowBounds,
@@ -302,6 +303,7 @@ impl Window {
         let display_id = platform_window.display().id();
         let sprite_atlas = platform_window.sprite_atlas();
         let mouse_position = platform_window.mouse_position();
+        let modifiers = platform_window.modifiers();
         let content_size = platform_window.content_size();
         let scale_factor = platform_window.scale_factor();
         let bounds = platform_window.bounds();
@@ -365,6 +367,7 @@ impl Window {
             blur_listeners: SubscriberSet::new(),
             default_prevented: true,
             mouse_position,
+            modifiers,
             requested_cursor_style: None,
             scale_factor,
             bounds,
@@ -877,6 +880,11 @@ impl<'a> WindowContext<'a> {
         self.window.mouse_position
     }
 
+    /// The current state of the keyboard's modifiers
+    pub fn modifiers(&self) -> Modifiers {
+        self.window.modifiers
+    }
+
     pub fn set_cursor_style(&mut self, style: CursorStyle) {
         self.window.requested_cursor_style = Some(style)
     }
@@ -963,14 +971,8 @@ impl<'a> WindowContext<'a> {
 
     /// Paint one or more quads into the scene for the next frame at the current stacking context.
     /// Quads are colored rectangular regions with an optional background, border, and corner radius.
-    pub fn paint_quad(
-        &mut self,
-        bounds: Bounds<Pixels>,
-        corner_radii: Corners<Pixels>,
-        background: impl Into<Hsla>,
-        border_widths: Edges<Pixels>,
-        border_color: impl Into<Hsla>,
-    ) {
+    /// see [`fill`], [`outline`], and [`quad`] to construct this type.
+    pub fn paint_quad(&mut self, quad: PaintQuad) {
         let scale_factor = self.scale_factor();
         let content_mask = self.content_mask();
 
@@ -979,12 +981,12 @@ impl<'a> WindowContext<'a> {
             &window.next_frame.z_index_stack,
             Quad {
                 order: 0,
-                bounds: bounds.scale(scale_factor),
+                bounds: quad.bounds.scale(scale_factor),
                 content_mask: content_mask.scale(scale_factor),
-                background: background.into(),
-                border_color: border_color.into(),
-                corner_radii: corner_radii.scale(scale_factor),
-                border_widths: border_widths.scale(scale_factor),
+                background: quad.background,
+                border_color: quad.border_color,
+                corner_radii: quad.corner_radii.scale(scale_factor),
+                border_widths: quad.border_widths.scale(scale_factor),
             },
         );
     }
@@ -1342,16 +1344,34 @@ impl<'a> WindowContext<'a> {
             // API for the mouse position can only occur on the main thread.
             InputEvent::MouseMove(mouse_move) => {
                 self.window.mouse_position = mouse_move.position;
+                self.window.modifiers = mouse_move.modifiers;
                 InputEvent::MouseMove(mouse_move)
             }
             InputEvent::MouseDown(mouse_down) => {
                 self.window.mouse_position = mouse_down.position;
+                self.window.modifiers = mouse_down.modifiers;
                 InputEvent::MouseDown(mouse_down)
             }
             InputEvent::MouseUp(mouse_up) => {
                 self.window.mouse_position = mouse_up.position;
+                self.window.modifiers = mouse_up.modifiers;
                 InputEvent::MouseUp(mouse_up)
             }
+            InputEvent::MouseExited(mouse_exited) => {
+                // todo!("Should we record that the mouse is outside of the window somehow? Or are these global pixels?")
+                self.window.modifiers = mouse_exited.modifiers;
+
+                InputEvent::MouseExited(mouse_exited)
+            }
+            InputEvent::ModifiersChanged(modifiers_changed) => {
+                self.window.modifiers = modifiers_changed.modifiers;
+                InputEvent::ModifiersChanged(modifiers_changed)
+            }
+            InputEvent::ScrollWheel(scroll_wheel) => {
+                self.window.mouse_position = scroll_wheel.position;
+                self.window.modifiers = scroll_wheel.modifiers;
+                InputEvent::ScrollWheel(scroll_wheel)
+            }
             // Translate dragging and dropping of external files from the operating system
             // to internal drag and drop events.
             InputEvent::FileDrop(file_drop) => match file_drop {
@@ -1394,7 +1414,7 @@ impl<'a> WindowContext<'a> {
                     click_count: 1,
                 }),
             },
-            _ => event,
+            InputEvent::KeyDown(_) | InputEvent::KeyUp(_) => event,
         };
 
         if let Some(any_mouse_event) = event.mouse_event() {
@@ -2962,3 +2982,85 @@ impl From<(&'static str, u64)> for ElementId {
         ElementId::NamedInteger(name.into(), id as usize)
     }
 }
+
+/// A rectangle, to be rendered on the screen by GPUI at the given position and size.
+pub struct PaintQuad {
+    bounds: Bounds<Pixels>,
+    corner_radii: Corners<Pixels>,
+    background: Hsla,
+    border_widths: Edges<Pixels>,
+    border_color: Hsla,
+}
+
+impl PaintQuad {
+    /// Set the corner radii of the quad.
+    pub fn corner_radii(self, corner_radii: impl Into<Corners<Pixels>>) -> Self {
+        PaintQuad {
+            corner_radii: corner_radii.into(),
+            ..self
+        }
+    }
+
+    /// Set the border widths of the quad.
+    pub fn border_widths(self, border_widths: impl Into<Edges<Pixels>>) -> Self {
+        PaintQuad {
+            border_widths: border_widths.into(),
+            ..self
+        }
+    }
+
+    /// Set the border color of the quad.
+    pub fn border_color(self, border_color: impl Into<Hsla>) -> Self {
+        PaintQuad {
+            border_color: border_color.into(),
+            ..self
+        }
+    }
+
+    /// Set the background color of the quad.
+    pub fn background(self, background: impl Into<Hsla>) -> Self {
+        PaintQuad {
+            background: background.into(),
+            ..self
+        }
+    }
+}
+
+/// Create a quad with the given parameters.
+pub fn quad(
+    bounds: Bounds<Pixels>,
+    corner_radii: impl Into<Corners<Pixels>>,
+    background: impl Into<Hsla>,
+    border_widths: impl Into<Edges<Pixels>>,
+    border_color: impl Into<Hsla>,
+) -> PaintQuad {
+    PaintQuad {
+        bounds,
+        corner_radii: corner_radii.into(),
+        background: background.into(),
+        border_widths: border_widths.into(),
+        border_color: border_color.into(),
+    }
+}
+
+/// Create a filled quad with the given bounds and background color.
+pub fn fill(bounds: impl Into<Bounds<Pixels>>, background: impl Into<Hsla>) -> PaintQuad {
+    PaintQuad {
+        bounds: bounds.into(),
+        corner_radii: (0.).into(),
+        background: background.into(),
+        border_widths: (0.).into(),
+        border_color: transparent_black(),
+    }
+}
+
+/// Create a rectangle outline with the given bounds, border color, and a 1px border width
+pub fn outline(bounds: impl Into<Bounds<Pixels>>, border_color: impl Into<Hsla>) -> PaintQuad {
+    PaintQuad {
+        bounds: bounds.into(),
+        corner_radii: (0.).into(),
+        background: transparent_black(),
+        border_widths: (1.).into(),
+        border_color: border_color.into(),
+    }
+}

crates/gpui2_macros/src/derive_render.rs 🔗

@@ -0,0 +1,23 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{parse_macro_input, DeriveInput};
+
+pub fn derive_render(input: TokenStream) -> TokenStream {
+    let ast = parse_macro_input!(input as DeriveInput);
+    let type_name = &ast.ident;
+    let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
+
+    let gen = quote! {
+        impl #impl_generics gpui::Render for #type_name #type_generics
+        #where_clause
+        {
+            type Element = ();
+
+            fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+                ()
+            }
+        }
+    };
+
+    gen.into()
+}

crates/gpui2_macros/src/gpui2_macros.rs 🔗

@@ -1,4 +1,5 @@
 mod derive_into_element;
+mod derive_render;
 mod register_action;
 mod style_helpers;
 mod test;
@@ -15,6 +16,11 @@ pub fn derive_into_element(input: TokenStream) -> TokenStream {
     derive_into_element::derive_into_element(input)
 }
 
+#[proc_macro_derive(Render)]
+pub fn derive_render(input: TokenStream) -> TokenStream {
+    derive_render::derive_render(input)
+}
+
 #[proc_macro]
 pub fn style_helpers(input: TokenStream) -> TokenStream {
     style_helpers::style_helpers(input)

crates/terminal_view2/src/terminal_element.rs 🔗

@@ -1,9 +1,9 @@
 use editor::{Cursor, HighlightedRange, HighlightedRangeLine};
 use gpui::{
-    black, div, point, px, red, relative, transparent_black, AnyElement, AsyncWindowContext,
-    AvailableSpace, Bounds, DispatchPhase, Element, ElementId, ExternalPaths, FocusHandle, Font,
-    FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveElement, InteractiveElementState,
-    IntoElement, LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, Pixels,
+    black, div, fill, point, px, red, relative, AnyElement, AsyncWindowContext, AvailableSpace,
+    Bounds, DispatchPhase, Element, ElementId, ExternalPaths, FocusHandle, Font, FontStyle,
+    FontWeight, HighlightStyle, Hsla, InteractiveElement, InteractiveElementState, IntoElement,
+    LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, Pixels,
     PlatformInputHandler, Point, Rgba, ShapedLine, Size, StatefulInteractiveElement, Styled,
     TextRun, TextStyle, TextSystem, UnderlineStyle, WhiteSpace, WindowContext,
 };
@@ -133,13 +133,7 @@ impl LayoutRect {
         )
         .into();
 
-        cx.paint_quad(
-            Bounds::new(position, size),
-            Default::default(),
-            self.color,
-            Default::default(),
-            transparent_black(),
-        );
+        cx.paint_quad(fill(Bounds::new(position, size), self.color));
     }
 }
 
@@ -775,13 +769,7 @@ impl Element for TerminalElement {
 
         let theme = cx.theme();
 
-        cx.paint_quad(
-            bounds,
-            Default::default(),
-            layout.background_color,
-            Default::default(),
-            Hsla::default(),
-        );
+        cx.paint_quad(fill(bounds, layout.background_color));
         let origin = bounds.origin + Point::new(layout.gutter, px(0.));
 
         let terminal_input_handler = TerminalInputHandler {

crates/workspace2/src/dock.rs 🔗

@@ -1,5 +1,5 @@
+use crate::DraggedDock;
 use crate::{status_bar::StatusItemView, Workspace};
-use crate::{DockClickReset, DockDragState};
 use gpui::{
     div, px, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Div, Entity, EntityId,
     EventEmitter, FocusHandle, FocusableView, IntoElement, MouseButton, ParentElement, Render,
@@ -493,47 +493,37 @@ impl Render for Dock {
             let handler = div()
                 .id("resize-handle")
                 .bg(cx.theme().colors().border)
-                .on_mouse_down(gpui::MouseButton::Left, move |_, cx| {
-                    cx.update_global(|drag: &mut DockDragState, cx| drag.0 = Some(position))
-                })
+                .on_drag(move |cx| cx.build_view(|_| DraggedDock(position)))
                 .on_click(cx.listener(|v, e: &ClickEvent, cx| {
-                    if e.down.button == MouseButton::Left {
-                        cx.update_global(|state: &mut DockClickReset, cx| {
-                            if state.0.is_some() {
-                                state.0 = None;
-                                v.resize_active_panel(None, cx)
-                            } else {
-                                let double_click = cx.double_click_interval();
-                                let timer = cx.background_executor().timer(double_click);
-                                state.0 = Some(cx.spawn(|_, mut cx| async move {
-                                    timer.await;
-                                    cx.update_global(|state: &mut DockClickReset, cx| {
-                                        state.0 = None;
-                                    })
-                                    .ok();
-                                }));
-                            }
-                        })
+                    if e.down.button == MouseButton::Left && e.down.click_count == 2 {
+                        v.resize_active_panel(None, cx)
                     }
-                }));
+                }))
+                .z_index(1);
+
+            const HANDLE_SIZE: Pixels = Pixels(6.);
 
             match self.position() {
                 DockPosition::Left => {
-                    post_resize_handle = Some(handler.w_1().h_full().cursor_col_resize())
+                    post_resize_handle =
+                        Some(handler.min_w(HANDLE_SIZE).h_full().cursor_col_resize())
                 }
                 DockPosition::Bottom => {
-                    pre_resize_handle = Some(handler.w_full().h_1().cursor_row_resize())
+                    pre_resize_handle =
+                        Some(handler.w_full().min_h(HANDLE_SIZE).cursor_row_resize())
                 }
                 DockPosition::Right => {
-                    pre_resize_handle = Some(handler.w_full().h_1().cursor_col_resize())
+                    pre_resize_handle =
+                        Some(handler.min_w(HANDLE_SIZE).h_full().cursor_col_resize())
                 }
             }
 
             div()
+                .flex()
                 .border_color(cx.theme().colors().border)
                 .map(|this| match self.position().axis() {
-                    Axis::Horizontal => this.w(px(size)).h_full(),
-                    Axis::Vertical => this.h(px(size)).w_full(),
+                    Axis::Horizontal => this.w(px(size)).h_full().flex_row(),
+                    Axis::Vertical => this.h(px(size)).w_full().flex_col(),
                 })
                 .map(|this| match self.position() {
                     DockPosition::Left => this.border_r(),
@@ -541,7 +531,14 @@ impl Render for Dock {
                     DockPosition::Bottom => this.border_t(),
                 })
                 .children(pre_resize_handle)
-                .child(entry.panel.to_any())
+                .child(
+                    div()
+                        .map(|this| match self.position().axis() {
+                            Axis::Horizontal => this.min_w(px(size) - HANDLE_SIZE).h_full(),
+                            Axis::Vertical => this.min_h(px(size) - HANDLE_SIZE).w_full(),
+                        })
+                        .child(entry.panel.to_any()),
+                )
                 .children(post_resize_handle)
         } else {
             div()

crates/workspace2/src/workspace2.rs 🔗

@@ -30,11 +30,11 @@ use futures::{
 };
 use gpui::{
     actions, canvas, div, impl_actions, point, size, Action, AnyModel, AnyView, AnyWeakView,
-    AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, Entity,
-    EntityId, EventEmitter, FocusHandle, FocusableView, GlobalPixels, InteractiveElement,
-    KeyContext, ManagedView, Model, ModelContext, MouseMoveEvent, ParentElement, PathPromptOptions,
-    Pixels, Point, PromptLevel, Render, Size, Styled, Subscription, Task, View, ViewContext,
-    VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
+    AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div,
+    DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, GlobalPixels,
+    InteractiveElement, KeyContext, ManagedView, Model, ModelContext, ParentElement,
+    PathPromptOptions, Pixels, Point, PromptLevel, Render, Size, Styled, Subscription, Task, View,
+    ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
 use itertools::Itertools;
@@ -227,9 +227,6 @@ pub fn init_settings(cx: &mut AppContext) {
 }
 
 pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
-    cx.default_global::<DockDragState>();
-    cx.default_global::<DockClickReset>();
-
     init_settings(cx);
     notifications::init(cx);
 
@@ -466,6 +463,7 @@ pub struct Workspace {
     _observe_current_user: Task<Result<()>>,
     _schedule_serialize: Option<Task<()>>,
     pane_history_timestamp: Arc<AtomicUsize>,
+    bounds: Bounds<Pixels>,
 }
 
 impl EventEmitter<Event> for Workspace {}
@@ -708,6 +706,8 @@ impl Workspace {
             subscriptions,
             pane_history_timestamp,
             workspace_actions: Default::default(),
+            // This data will be incorrect, but it will be overwritten by the time it needs to be used.
+            bounds: Default::default(),
         }
     }
 
@@ -3580,13 +3580,8 @@ impl FocusableView for Workspace {
 
 struct WorkspaceBounds(Bounds<Pixels>);
 
-//todo!("remove this when better drag APIs are in GPUI2")
-#[derive(Default)]
-struct DockDragState(Option<DockPosition>);
-
-//todo!("remove this when better double APIs are in GPUI2")
-#[derive(Default)]
-struct DockClickReset(Option<Task<()>>);
+#[derive(Render)]
+struct DraggedDock(DockPosition);
 
 impl Render for Workspace {
     type Element = Div;
@@ -3632,37 +3627,37 @@ impl Render for Workspace {
                     .border_t()
                     .border_b()
                     .border_color(cx.theme().colors().border)
-                    .on_mouse_up(gpui::MouseButton::Left, |_, cx| {
-                        cx.update_global(|drag: &mut DockDragState, cx| {
-                            drag.0 = None;
-                        })
-                    })
-                    .on_mouse_move(cx.listener(|workspace, e: &MouseMoveEvent, cx| {
-                        if let Some(types) = &cx.global::<DockDragState>().0 {
-                            let workspace_bounds = cx.global::<WorkspaceBounds>().0;
-                            match types {
+                    .child(
+                        canvas(cx.listener(|workspace, bounds, cx| {
+                            workspace.bounds = *bounds;
+                        }))
+                        .absolute()
+                        .size_full(),
+                    )
+                    .on_drag_move(
+                        cx.listener(|workspace, e: &DragMoveEvent<DraggedDock>, cx| {
+                            match e.drag.read(cx).0 {
                                 DockPosition::Left => {
-                                    let size = e.position.x;
+                                    let size = workspace.bounds.left() + e.event.position.x;
                                     workspace.left_dock.update(cx, |left_dock, cx| {
                                         left_dock.resize_active_panel(Some(size.0), cx);
                                     });
                                 }
                                 DockPosition::Right => {
-                                    let size = workspace_bounds.size.width - e.position.x;
+                                    let size = workspace.bounds.right() - e.event.position.x;
                                     workspace.right_dock.update(cx, |right_dock, cx| {
                                         right_dock.resize_active_panel(Some(size.0), cx);
                                     });
                                 }
                                 DockPosition::Bottom => {
-                                    let size = workspace_bounds.size.height - e.position.y;
+                                    let size = workspace.bounds.bottom() - e.event.position.y;
                                     workspace.bottom_dock.update(cx, |bottom_dock, cx| {
                                         bottom_dock.resize_active_panel(Some(size.0), cx);
                                     });
                                 }
                             }
-                        }
-                    }))
-                    .child(canvas(|bounds, cx| cx.set_global(WorkspaceBounds(bounds))))
+                        }),
+                    )
                     .child(self.modal_layer.clone())
                     .child(
                         div()