Apply cursor styles during paint

Nathan Sobo and Antonio Scandurra created

This makes the editor's cursor an IBeam and properly deals with nested cursor styles.

Co-Authored-By: Antonio Scandurra <me@as-cii.com>

Change summary

assets/themes/dark.json                         | 11 +++--
assets/themes/light.json                        | 11 +++--
crates/editor/src/element.rs                    |  2 +
crates/go_to_line/src/go_to_line.rs             | 36 ++++++++----------
crates/gpui/src/app.rs                          | 33 +---------------
crates/gpui/src/elements/container.rs           | 19 +++++++--
crates/gpui/src/elements/mouse_event_handler.rs | 38 +++++++-----------
crates/gpui/src/platform.rs                     |  4 +
crates/gpui/src/platform/mac/platform.rs        |  1 
crates/gpui/src/presenter.rs                    | 20 +++++++++-
crates/gpui/src/scene.rs                        | 22 +++++++++++
crates/picker/src/picker.rs                     |  2 -
crates/theme/src/theme.rs                       |  1 
crates/workspace/src/workspace.rs               |  9 ++++
styles/src/styleTree/selectorModal.ts           |  4 --
styles/src/styleTree/workspace.ts               |  7 +++
16 files changed, 124 insertions(+), 96 deletions(-)

Detailed changes

assets/themes/dark.json 🔗

@@ -89,10 +89,6 @@
         "top": 7
       }
     },
-    "margin": {
-      "bottom": 52,
-      "top": 52
-    },
     "shadow": {
       "blur": 16,
       "color": "#00000052",
@@ -158,6 +154,13 @@
         "right": 8
       }
     },
+    "modal": {
+      "margin": {
+        "bottom": 52,
+        "top": 52
+      },
+      "cursor": "Arrow"
+    },
     "left_sidebar": {
       "width": 30,
       "background": "#1c1c1c",

assets/themes/light.json 🔗

@@ -89,10 +89,6 @@
         "top": 7
       }
     },
-    "margin": {
-      "bottom": 52,
-      "top": 52
-    },
     "shadow": {
       "blur": 16,
       "color": "#0000001f",
@@ -158,6 +154,13 @@
         "right": 8
       }
     },
+    "modal": {
+      "margin": {
+        "bottom": 52,
+        "top": 52
+      },
+      "cursor": "Arrow"
+    },
     "left_sidebar": {
       "width": 30,
       "background": "#f8f8f8",

crates/editor/src/element.rs 🔗

@@ -16,6 +16,7 @@ use gpui::{
         PathBuilder,
     },
     json::{self, ToJson},
+    platform::CursorStyle,
     text_layout::{self, Line, RunStyle, TextLayoutCache},
     AppContext, Axis, Border, Element, ElementBox, Event, EventContext, LayoutContext,
     MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
@@ -329,6 +330,7 @@ impl EditorElement {
         let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.);
 
         cx.scene.push_layer(Some(bounds));
+        cx.scene.push_cursor_style(bounds, CursorStyle::IBeam);
 
         for (range, color) in &layout.highlighted_ranges {
             self.paint_highlighted_range(

crates/go_to_line/src/go_to_line.rs 🔗

@@ -161,29 +161,25 @@ impl View for GoToLine {
             self.max_point.row + 1
         );
 
-        Align::new(
-            ConstrainedBox::new(
-                Container::new(
-                    Flex::new(Axis::Vertical)
-                        .with_child(
-                            Container::new(ChildView::new(&self.line_editor).boxed())
-                                .with_style(theme.input_editor.container)
-                                .boxed(),
-                        )
-                        .with_child(
-                            Container::new(Label::new(label, theme.empty.label.clone()).boxed())
-                                .with_style(theme.empty.container)
-                                .boxed(),
-                        )
-                        .boxed(),
-                )
-                .with_style(theme.container)
-                .boxed(),
+        ConstrainedBox::new(
+            Container::new(
+                Flex::new(Axis::Vertical)
+                    .with_child(
+                        Container::new(ChildView::new(&self.line_editor).boxed())
+                            .with_style(theme.input_editor.container)
+                            .boxed(),
+                    )
+                    .with_child(
+                        Container::new(Label::new(label, theme.empty.label.clone()).boxed())
+                            .with_style(theme.empty.container)
+                            .boxed(),
+                    )
+                    .boxed(),
             )
-            .with_max_width(500.0)
+            .with_style(theme.container)
             .boxed(),
         )
-        .top()
+        .with_max_width(500.0)
         .named("go to line")
     }
 

crates/gpui/src/app.rs 🔗

@@ -4,7 +4,7 @@ use crate::{
     elements::ElementBox,
     executor::{self, Task},
     keymap::{self, Binding, Keystroke},
-    platform::{self, CursorStyle, Platform, PromptLevel, WindowOptions},
+    platform::{self, Platform, PromptLevel, WindowOptions},
     presenter::Presenter,
     util::post_inc,
     AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache,
@@ -31,10 +31,7 @@ use std::{
     path::{Path, PathBuf},
     pin::Pin,
     rc::{self, Rc},
-    sync::{
-        atomic::{AtomicUsize, Ordering::SeqCst},
-        Arc, Weak,
-    },
+    sync::{Arc, Weak},
     time::Duration,
 };
 
@@ -766,7 +763,6 @@ pub struct MutableAppContext {
     pending_global_notifications: HashSet<TypeId>,
     pending_flushes: usize,
     flushing_effects: bool,
-    next_cursor_style_handle_id: Arc<AtomicUsize>,
     halt_action_dispatch: bool,
 }
 
@@ -818,7 +814,6 @@ impl MutableAppContext {
             pending_global_notifications: HashSet::new(),
             pending_flushes: 0,
             flushing_effects: false,
-            next_cursor_style_handle_id: Default::default(),
             halt_action_dispatch: false,
         }
     }
@@ -1949,16 +1944,6 @@ impl MutableAppContext {
         self.presenters_and_platform_windows = presenters;
     }
 
-    pub fn set_cursor_style(&mut self, style: CursorStyle) -> CursorStyleHandle {
-        self.platform.set_cursor_style(style);
-        let id = self.next_cursor_style_handle_id.fetch_add(1, SeqCst);
-        CursorStyleHandle {
-            id,
-            next_cursor_style_handle_id: self.next_cursor_style_handle_id.clone(),
-            platform: self.platform(),
-        }
-    }
-
     fn handle_subscription_effect(
         &mut self,
         entity_id: usize,
@@ -4452,20 +4437,6 @@ impl<T> Drop for ElementStateHandle<T> {
     }
 }
 
-pub struct CursorStyleHandle {
-    id: usize,
-    next_cursor_style_handle_id: Arc<AtomicUsize>,
-    platform: Arc<dyn Platform>,
-}
-
-impl Drop for CursorStyleHandle {
-    fn drop(&mut self) {
-        if self.id + 1 == self.next_cursor_style_handle_id.load(SeqCst) {
-            self.platform.set_cursor_style(CursorStyle::Arrow);
-        }
-    }
-}
-
 #[must_use]
 pub enum Subscription {
     Subscription {

crates/gpui/src/elements/container.rs 🔗

@@ -1,17 +1,17 @@
-use pathfinder_geometry::rect::RectF;
-use serde::Deserialize;
-use serde_json::json;
-
 use crate::{
     color::Color,
     geometry::{
         deserialize_vec2f,
+        rect::RectF,
         vector::{vec2f, Vector2F},
     },
     json::ToJson,
+    platform::CursorStyle,
     scene::{self, Border, Quad},
     Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
 };
+use serde::Deserialize;
+use serde_json::json;
 
 #[derive(Clone, Copy, Debug, Default, Deserialize)]
 pub struct ContainerStyle {
@@ -27,6 +27,8 @@ pub struct ContainerStyle {
     pub corner_radius: f32,
     #[serde(default)]
     pub shadow: Option<Shadow>,
+    #[serde(default)]
+    pub cursor: Option<CursorStyle>,
 }
 
 pub struct Container {
@@ -128,6 +130,11 @@ impl Container {
         self
     }
 
+    pub fn with_cursor(mut self, style: CursorStyle) -> Self {
+        self.style.cursor = Some(style);
+        self
+    }
+
     fn margin_size(&self) -> Vector2F {
         vec2f(
             self.style.margin.left + self.style.margin.right,
@@ -205,6 +212,10 @@ impl Element for Container {
             });
         }
 
+        if let Some(style) = self.style.cursor {
+            cx.scene.push_cursor_style(quad_bounds, style);
+        }
+
         let child_origin =
             quad_bounds.origin() + vec2f(self.style.padding.left, self.style.padding.top);
 

crates/gpui/src/elements/mouse_event_handler.rs 🔗

@@ -5,8 +5,8 @@ use crate::{
         vector::{vec2f, Vector2F},
     },
     platform::CursorStyle,
-    CursorStyleHandle, DebugContext, Element, ElementBox, ElementStateContext, ElementStateHandle,
-    Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
+    DebugContext, Element, ElementBox, ElementStateContext, ElementStateHandle, Event,
+    EventContext, LayoutContext, PaintContext, SizeConstraint,
 };
 use serde_json::json;
 
@@ -25,7 +25,6 @@ pub struct MouseState {
     pub hovered: bool,
     pub clicked: bool,
     prev_drag_position: Option<Vector2F>,
-    cursor_style_handle: Option<CursorStyleHandle>,
 }
 
 impl MouseEventHandler {
@@ -72,6 +71,14 @@ impl MouseEventHandler {
         self.padding = padding;
         self
     }
+
+    fn hit_bounds(&self, bounds: RectF) -> RectF {
+        RectF::from_points(
+            bounds.origin() - vec2f(self.padding.left, self.padding.top),
+            bounds.lower_right() + vec2f(self.padding.right, self.padding.bottom),
+        )
+        .round_out()
+    }
 }
 
 impl Element for MouseEventHandler {
@@ -93,6 +100,10 @@ impl Element for MouseEventHandler {
         _: &mut Self::LayoutState,
         cx: &mut PaintContext,
     ) -> Self::PaintState {
+        if let Some(cursor_style) = self.cursor_style {
+            cx.scene
+                .push_cursor_style(self.hit_bounds(bounds), cursor_style);
+        }
         self.child.paint(bounds.origin(), visible_bounds, cx);
     }
 
@@ -105,19 +116,13 @@ impl Element for MouseEventHandler {
         _: &mut Self::PaintState,
         cx: &mut EventContext,
     ) -> bool {
-        let cursor_style = self.cursor_style;
+        let hit_bounds = self.hit_bounds(visible_bounds);
         let mouse_down_handler = self.mouse_down_handler.as_mut();
         let click_handler = self.click_handler.as_mut();
         let drag_handler = self.drag_handler.as_mut();
 
         let handled_in_child = self.child.dispatch_event(event, cx);
 
-        let hit_bounds = RectF::from_points(
-            visible_bounds.origin() - vec2f(self.padding.left, self.padding.top),
-            visible_bounds.lower_right() + vec2f(self.padding.right, self.padding.bottom),
-        )
-        .round_out();
-
         self.state.update(cx, |state, cx| match event {
             Event::MouseMoved {
                 position,
@@ -127,16 +132,6 @@ impl Element for MouseEventHandler {
                     let mouse_in = hit_bounds.contains_point(*position);
                     if state.hovered != mouse_in {
                         state.hovered = mouse_in;
-                        if let Some(cursor_style) = cursor_style {
-                            if !state.clicked {
-                                if state.hovered {
-                                    state.cursor_style_handle =
-                                        Some(cx.set_cursor_style(cursor_style));
-                                } else {
-                                    state.cursor_style_handle = None;
-                                }
-                            }
-                        }
                         cx.notify();
                         return true;
                     }
@@ -160,9 +155,6 @@ impl Element for MouseEventHandler {
                 state.prev_drag_position = None;
                 if !handled_in_child && state.clicked {
                     state.clicked = false;
-                    if !state.hovered {
-                        state.cursor_style_handle = None;
-                    }
                     cx.notify();
                     if let Some(handler) = click_handler {
                         if hit_bounds.contains_point(*position) {

crates/gpui/src/platform.rs 🔗

@@ -21,6 +21,7 @@ use anyhow::{anyhow, Result};
 use async_task::Runnable;
 pub use event::{Event, NavigationDirection};
 use postage::oneshot;
+use serde::Deserialize;
 use std::{
     any::Any,
     path::{Path, PathBuf},
@@ -125,11 +126,12 @@ pub enum PromptLevel {
     Critical,
 }
 
-#[derive(Copy, Clone, Debug)]
+#[derive(Copy, Clone, Debug, Deserialize)]
 pub enum CursorStyle {
     Arrow,
     ResizeLeftRight,
     PointingHand,
+    IBeam,
 }
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]

crates/gpui/src/platform/mac/platform.rs 🔗

@@ -583,6 +583,7 @@ impl platform::Platform for MacPlatform {
                 CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
                 CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
                 CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
+                CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor],
             };
             let _: () = msg_send![cursor, set];
         }

crates/gpui/src/presenter.rs 🔗

@@ -4,7 +4,7 @@ use crate::{
     font_cache::FontCache,
     geometry::rect::RectF,
     json::{self, ToJson},
-    platform::Event,
+    platform::{CursorStyle, Event},
     text_layout::TextLayoutCache,
     Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox,
     ElementStateContext, Entity, FontSystem, ModelHandle, ReadModel, ReadView, Scene,
@@ -22,6 +22,7 @@ pub struct Presenter {
     window_id: usize,
     pub(crate) rendered_views: HashMap<usize, ElementBox>,
     parents: HashMap<usize, usize>,
+    cursor_styles: Vec<(RectF, CursorStyle)>,
     font_cache: Arc<FontCache>,
     text_layout_cache: TextLayoutCache,
     asset_cache: Arc<AssetCache>,
@@ -42,6 +43,7 @@ impl Presenter {
             window_id,
             rendered_views: cx.render_views(window_id, titlebar_height),
             parents: HashMap::new(),
+            cursor_styles: Default::default(),
             font_cache,
             text_layout_cache,
             asset_cache,
@@ -118,6 +120,7 @@ impl Presenter {
                 RectF::new(Vector2F::zero(), window_size),
             );
             self.text_layout_cache.finish_frame();
+            self.cursor_styles = scene.cursor_styles();
 
             if let Some(event) = self.last_mouse_moved_event.clone() {
                 self.dispatch_event(event, cx)
@@ -171,8 +174,21 @@ impl Presenter {
     pub fn dispatch_event(&mut self, event: Event, cx: &mut MutableAppContext) {
         if let Some(root_view_id) = cx.root_view_id(self.window_id) {
             match event {
-                Event::MouseMoved { .. } => {
+                Event::MouseMoved {
+                    position,
+                    left_mouse_down,
+                } => {
                     self.last_mouse_moved_event = Some(event.clone());
+
+                    if !left_mouse_down {
+                        cx.platform().set_cursor_style(CursorStyle::Arrow);
+                        for (bounds, style) in self.cursor_styles.iter().rev() {
+                            if bounds.contains_point(position) {
+                                cx.platform().set_cursor_style(*style);
+                                break;
+                            }
+                        }
+                    }
                 }
                 Event::LeftMouseDragged { position } => {
                     self.last_mouse_moved_event = Some(Event::MouseMoved {

crates/gpui/src/scene.rs 🔗

@@ -7,6 +7,7 @@ use crate::{
     fonts::{FontId, GlyphId},
     geometry::{rect::RectF, vector::Vector2F},
     json::ToJson,
+    platform::CursorStyle,
     ImageData,
 };
 
@@ -32,6 +33,7 @@ pub struct Layer {
     image_glyphs: Vec<ImageGlyph>,
     icons: Vec<Icon>,
     paths: Vec<Path>,
+    cursor_styles: Vec<(RectF, CursorStyle)>,
 }
 
 #[derive(Default, Debug)]
@@ -173,6 +175,13 @@ impl Scene {
         self.stacking_contexts.iter().flat_map(|s| &s.layers)
     }
 
+    pub fn cursor_styles(&self) -> Vec<(RectF, CursorStyle)> {
+        self.layers()
+            .flat_map(|layer| &layer.cursor_styles)
+            .copied()
+            .collect()
+    }
+
     pub fn push_stacking_context(&mut self, clip_bounds: Option<RectF>) {
         self.active_stacking_context_stack
             .push(self.stacking_contexts.len());
@@ -197,6 +206,10 @@ impl Scene {
         self.active_layer().push_quad(quad)
     }
 
+    pub fn push_cursor_style(&mut self, bounds: RectF, style: CursorStyle) {
+        self.active_layer().push_cursor_style(bounds, style);
+    }
+
     pub fn push_image(&mut self, image: Image) {
         self.active_layer().push_image(image)
     }
@@ -285,6 +298,7 @@ impl Layer {
             glyphs: Default::default(),
             icons: Default::default(),
             paths: Default::default(),
+            cursor_styles: Default::default(),
         }
     }
 
@@ -302,6 +316,14 @@ impl Layer {
         self.quads.as_slice()
     }
 
+    fn push_cursor_style(&mut self, bounds: RectF, style: CursorStyle) {
+        if let Some(bounds) = bounds.intersection(self.clip_bounds.unwrap_or(bounds)) {
+            if can_draw(bounds) {
+                self.cursor_styles.push((bounds, style));
+            }
+        }
+    }
+
     fn push_underline(&mut self, underline: Underline) {
         if underline.width > 0. {
             self.underlines.push(underline);

crates/picker/src/picker.rs 🔗

@@ -99,8 +99,6 @@ impl<D: PickerDelegate> View for Picker<D> {
             .constrained()
             .with_max_width(self.max_size.x())
             .with_max_height(self.max_size.y())
-            .aligned()
-            .top()
             .named("picker")
     }
 

crates/theme/src/theme.rs 🔗

@@ -43,6 +43,7 @@ pub struct Workspace {
     pub status_bar: StatusBar,
     pub toolbar: Toolbar,
     pub disconnected_overlay: ContainedText,
+    pub modal: ContainerStyle,
 }
 
 #[derive(Clone, Deserialize, Default)]

crates/workspace/src/workspace.rs 🔗

@@ -1983,7 +1983,14 @@ impl View for Workspace {
                                 content.add_child(self.right_sidebar.render(&theme, cx));
                                 content.boxed()
                             })
-                            .with_children(self.modal.as_ref().map(|m| ChildView::new(m).boxed()))
+                            .with_children(self.modal.as_ref().map(|m| {
+                                ChildView::new(m)
+                                    .contained()
+                                    .with_style(theme.workspace.modal)
+                                    .aligned()
+                                    .top()
+                                    .boxed()
+                            }))
                             .flex(1.0, true)
                             .boxed(),
                     )

styles/src/styleTree/selectorModal.ts 🔗

@@ -50,10 +50,6 @@ export default function selectorModal(theme: Theme): Object {
         top: 7,
       },
     },
-    margin: {
-      bottom: 52,
-      top: 52,
-    },
     shadow: shadow(theme),
   };
 }

styles/src/styleTree/workspace.ts 🔗

@@ -75,6 +75,13 @@ export default function workspace(theme: Theme) {
     leaderBorderWidth: 2.0,
     tab,
     activeTab,
+    modal: {
+      margin: {
+        bottom: 52,
+        top: 52,
+      },
+      cursor: "Arrow"
+    },
     leftSidebar: {
       ...sidebar,
       border: border(theme, "primary", { right: true }),