Style Grab Bag (#3487)

Nate Butler created

[[PR Description]]

- Add pane empty state
- Ensure tab bar doesn't resize when a tab is added
- Make ButtonLike respect the style of a disabled button
- Add additional cursors to gpui2

Release Notes:

- N/A

Change summary

crates/gpui2/src/platform.rs                    |  18 ++
crates/gpui2/src/platform/mac/platform.rs       |  29 +++
crates/gpui2/src/styled.rs                      | 119 +++++++++++++++++++
crates/storybook2/src/stories.rs                |   2 
crates/storybook2/src/stories/cursor.rs         | 112 +++++++++++++++++
crates/storybook2/src/story_selector.rs         |   2 
crates/theme2/src/one_themes.rs                 |   4 
crates/ui2/src/components/button/button_like.rs |   8 
crates/workspace2/src/pane.rs                   |  69 +++++-----
9 files changed, 317 insertions(+), 46 deletions(-)

Detailed changes

crates/gpui2/src/platform.rs 🔗

@@ -472,13 +472,27 @@ pub enum PromptLevel {
     Critical,
 }
 
+/// The style of the cursor (pointer)
 #[derive(Copy, Clone, Debug)]
 pub enum CursorStyle {
     Arrow,
+    IBeam,
+    Crosshair,
+    ClosedHand,
+    OpenHand,
+    PointingHand,
+    ResizeLeft,
+    ResizeRight,
     ResizeLeftRight,
+    ResizeUp,
+    ResizeDown,
     ResizeUpDown,
-    PointingHand,
-    IBeam,
+    DisappearingItem,
+    IBeamCursorForVerticalLayout,
+    OperationNotAllowed,
+    DragLink,
+    DragCopy,
+    ContextualMenu,
 }
 
 impl Default for CursorStyle {

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

@@ -724,16 +724,35 @@ impl Platform for MacPlatform {
         }
     }
 
+    /// Match cursor style to one of the styles available
+    /// in macOS's [NSCursor](https://developer.apple.com/documentation/appkit/nscursor).
     fn set_cursor_style(&self, style: CursorStyle) {
         unsafe {
             let new_cursor: id = match style {
                 CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
-                CursorStyle::ResizeLeftRight => {
-                    msg_send![class!(NSCursor), resizeLeftRightCursor]
-                }
-                CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
-                CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
                 CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor],
+                CursorStyle::Crosshair => msg_send![class!(NSCursor), crosshairCursor],
+                CursorStyle::ClosedHand => msg_send![class!(NSCursor), closedHandCursor],
+                CursorStyle::OpenHand => msg_send![class!(NSCursor), openHandCursor],
+                CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
+                CursorStyle::ResizeLeft => msg_send![class!(NSCursor), resizeLeftCursor],
+                CursorStyle::ResizeRight => msg_send![class!(NSCursor), resizeRightCursor],
+                CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
+                CursorStyle::ResizeUp => msg_send![class!(NSCursor), resizeUpCursor],
+                CursorStyle::ResizeDown => msg_send![class!(NSCursor), resizeDownCursor],
+                CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
+                CursorStyle::DisappearingItem => {
+                    msg_send![class!(NSCursor), disappearingItemCursor]
+                }
+                CursorStyle::IBeamCursorForVerticalLayout => {
+                    msg_send![class!(NSCursor), IBeamCursorForVerticalLayout]
+                }
+                CursorStyle::OperationNotAllowed => {
+                    msg_send![class!(NSCursor), operationNotAllowedCursor]
+                }
+                CursorStyle::DragLink => msg_send![class!(NSCursor), dragLinkCursor],
+                CursorStyle::DragCopy => msg_send![class!(NSCursor), dragCopyCursor],
+                CursorStyle::ContextualMenu => msg_send![class!(NSCursor), contextualMenuCursor],
             };
 
             let old_cursor: id = msg_send![class!(NSCursor), currentCursor];

crates/gpui2/src/styled.rs 🔗

@@ -101,6 +101,125 @@ pub trait Styled: Sized {
         self
     }
 
+    /// Sets cursor style when hovering over an element to `text`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_text(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::IBeam);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `move`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_move(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::ClosedHand);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `not-allowed`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_not_allowed(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::OperationNotAllowed);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `context-menu`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_context_menu(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::ContextualMenu);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `crosshair`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_crosshair(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::Crosshair);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `vertical-text`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_vertical_text(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::IBeamCursorForVerticalLayout);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `alias`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_alias(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::DragLink);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `copy`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_copy(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::DragCopy);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `no-drop`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_no_drop(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::OperationNotAllowed);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `grab`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_grab(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::OpenHand);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `grabbing`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_grabbing(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::ClosedHand);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `col-resize`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_col_resize(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::ResizeLeftRight);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `row-resize`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_row_resize(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::ResizeUpDown);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `n-resize`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_n_resize(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::ResizeUp);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `e-resize`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_e_resize(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::ResizeRight);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `s-resize`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_s_resize(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::ResizeDown);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `w-resize`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_w_resize(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::ResizeLeft);
+        self
+    }
+
     /// Sets the whitespace of the element to `normal`.
     /// [Docs](https://tailwindcss.com/docs/whitespace#normal)
     fn whitespace_normal(mut self) -> Self {

crates/storybook2/src/stories.rs 🔗

@@ -1,4 +1,5 @@
 mod auto_height_editor;
+mod cursor;
 mod focus;
 mod kitchen_sink;
 mod picker;
@@ -7,6 +8,7 @@ mod text;
 mod z_index;
 
 pub use auto_height_editor::*;
+pub use cursor::*;
 pub use focus::*;
 pub use kitchen_sink::*;
 pub use picker::*;

crates/storybook2/src/stories/cursor.rs 🔗

@@ -0,0 +1,112 @@
+use gpui::{Div, Render, Stateful};
+use story::Story;
+use ui::prelude::*;
+
+pub struct CursorStory;
+
+impl Render for CursorStory {
+    type Element = Div;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        let all_cursors: [(&str, Box<dyn Fn(Stateful<Div>) -> Stateful<Div>>); 19] = [
+            (
+                "cursor_default",
+                Box::new(|el: Stateful<Div>| el.cursor_default()),
+            ),
+            (
+                "cursor_pointer",
+                Box::new(|el: Stateful<Div>| el.cursor_pointer()),
+            ),
+            (
+                "cursor_text",
+                Box::new(|el: Stateful<Div>| el.cursor_text()),
+            ),
+            (
+                "cursor_move",
+                Box::new(|el: Stateful<Div>| el.cursor_move()),
+            ),
+            (
+                "cursor_not_allowed",
+                Box::new(|el: Stateful<Div>| el.cursor_not_allowed()),
+            ),
+            (
+                "cursor_context_menu",
+                Box::new(|el: Stateful<Div>| el.cursor_context_menu()),
+            ),
+            (
+                "cursor_crosshair",
+                Box::new(|el: Stateful<Div>| el.cursor_crosshair()),
+            ),
+            (
+                "cursor_vertical_text",
+                Box::new(|el: Stateful<Div>| el.cursor_vertical_text()),
+            ),
+            (
+                "cursor_alias",
+                Box::new(|el: Stateful<Div>| el.cursor_alias()),
+            ),
+            (
+                "cursor_copy",
+                Box::new(|el: Stateful<Div>| el.cursor_copy()),
+            ),
+            (
+                "cursor_no_drop",
+                Box::new(|el: Stateful<Div>| el.cursor_no_drop()),
+            ),
+            (
+                "cursor_grab",
+                Box::new(|el: Stateful<Div>| el.cursor_grab()),
+            ),
+            (
+                "cursor_grabbing",
+                Box::new(|el: Stateful<Div>| el.cursor_grabbing()),
+            ),
+            (
+                "cursor_col_resize",
+                Box::new(|el: Stateful<Div>| el.cursor_col_resize()),
+            ),
+            (
+                "cursor_row_resize",
+                Box::new(|el: Stateful<Div>| el.cursor_row_resize()),
+            ),
+            (
+                "cursor_n_resize",
+                Box::new(|el: Stateful<Div>| el.cursor_n_resize()),
+            ),
+            (
+                "cursor_e_resize",
+                Box::new(|el: Stateful<Div>| el.cursor_e_resize()),
+            ),
+            (
+                "cursor_s_resize",
+                Box::new(|el: Stateful<Div>| el.cursor_s_resize()),
+            ),
+            (
+                "cursor_w_resize",
+                Box::new(|el: Stateful<Div>| el.cursor_w_resize()),
+            ),
+        ];
+
+        Story::container()
+            .flex()
+            .gap_1()
+            .child(Story::title("cursor"))
+            .children(all_cursors.map(|(name, apply_cursor)| {
+                div().gap_1().flex().text_color(gpui::white()).child(
+                    div()
+                        .flex()
+                        .items_center()
+                        .justify_center()
+                        .id(name)
+                        .map(apply_cursor)
+                        .w_64()
+                        .h_8()
+                        .bg(gpui::red())
+                        .hover(|style| style.bg(gpui::blue()))
+                        .active(|style| style.bg(gpui::green()))
+                        .text_sm()
+                        .child(Story::label(name)),
+                )
+            }))
+    }
+}

crates/storybook2/src/story_selector.rs 🔗

@@ -17,6 +17,7 @@ pub enum ComponentStory {
     Button,
     Checkbox,
     ContextMenu,
+    Cursor,
     Disclosure,
     Focus,
     Icon,
@@ -40,6 +41,7 @@ impl ComponentStory {
             Self::Button => cx.build_view(|_| ui::ButtonStory).into(),
             Self::Checkbox => cx.build_view(|_| ui::CheckboxStory).into(),
             Self::ContextMenu => cx.build_view(|_| ui::ContextMenuStory).into(),
+            Self::Cursor => cx.build_view(|_| crate::stories::CursorStory).into(),
             Self::Disclosure => cx.build_view(|_| ui::DisclosureStory).into(),
             Self::Focus => FocusStory::view(cx).into(),
             Self::Icon => cx.build_view(|_| ui::IconStory).into(),

crates/theme2/src/one_themes.rs 🔗

@@ -52,13 +52,13 @@ pub(crate) fn one_dark() -> Theme {
                 element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0),
                 element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0),
                 element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0),
-                element_disabled: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0),
+                element_disabled: SystemColors::default().transparent,
                 drop_target_background: hsla(220.0 / 360., 8.3 / 100., 21.4 / 100., 1.0),
                 ghost_element_background: SystemColors::default().transparent,
                 ghost_element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0),
                 ghost_element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0),
                 ghost_element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0),
-                ghost_element_disabled: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0),
+                ghost_element_disabled: SystemColors::default().transparent,
                 text: hsla(221. / 360., 11. / 100., 86. / 100., 1.0),
                 text_muted: hsla(218.0 / 360., 7. / 100., 46. / 100., 1.0),
                 text_placeholder: hsla(220.0 / 360., 6.6 / 100., 44.5 / 100., 1.0),

crates/ui2/src/components/button/button_like.rs 🔗

@@ -323,12 +323,14 @@ impl RenderOnce for ButtonLike {
             .id(self.id.clone())
             .h(self.size.height())
             .rounded_md()
-            .when(!self.disabled, |el| el.cursor_pointer())
             .gap_1()
             .px_1()
             .bg(self.style.enabled(cx).background)
-            .hover(|hover| hover.bg(self.style.hovered(cx).background))
-            .active(|active| active.bg(self.style.active(cx).background))
+            .when(!self.disabled, |this| {
+                this.cursor_pointer()
+                    .hover(|hover| hover.bg(self.style.hovered(cx).background))
+                    .active(|active| active.bg(self.style.active(cx).background))
+            })
             .when_some(
                 self.on_click.filter(|_| !self.disabled),
                 |this, on_click| {

crates/workspace2/src/pane.rs 🔗

@@ -7,7 +7,7 @@ use crate::{
 use anyhow::Result;
 use collections::{HashMap, HashSet, VecDeque};
 use gpui::{
-    actions, overlay, prelude::*, Action, AnchorCorner, AnyWeakView, AppContext,
+    actions, overlay, prelude::*, rems, Action, AnchorCorner, AnyWeakView, AppContext,
     AsyncWindowContext, DismissEvent, Div, EntityId, EventEmitter, FocusHandle, Focusable,
     FocusableView, Model, Pixels, Point, PromptLevel, Render, Task, View, ViewContext,
     VisualContext, WeakView, WindowContext,
@@ -26,7 +26,9 @@ use std::{
     },
 };
 
-use ui::{prelude::*, right_click_menu, Color, Icon, IconButton, IconElement, Tooltip};
+use ui::{
+    h_stack, prelude::*, right_click_menu, Color, Icon, IconButton, IconElement, Label, Tooltip,
+};
 use ui::{v_stack, ContextMenu};
 use util::truncate_and_remove_front;
 
@@ -1554,47 +1556,43 @@ impl Pane {
 
     fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
         div()
-            .group("tab_bar")
             .id("tab_bar")
+            .group("tab_bar")
             .track_focus(&self.tab_bar_focus_handle)
             .w_full()
+            // 30px @ 16px/rem
+            .h(rems(1.875))
+            .overflow_hidden()
             .flex()
+            .flex_none()
             .bg(cx.theme().colors().tab_bar_background)
             // Left Side
             .child(
-                div()
-                    .relative()
-                    .px_1()
+                h_stack()
+                    .px_2()
                     .flex()
                     .flex_none()
-                    .gap_2()
+                    .gap_1()
                     // Nav Buttons
                     .child(
-                        div()
-                            .right_0()
-                            .flex()
-                            .items_center()
-                            .gap_px()
-                            .child(
-                                div().border().border_color(gpui::red()).child(
-                                    IconButton::new("navigate_backward", Icon::ArrowLeft)
-                                        .on_click({
-                                            let view = cx.view().clone();
-                                            move |_, cx| view.update(cx, Self::navigate_backward)
-                                        })
-                                        .disabled(!self.can_navigate_backward()),
-                                ),
-                            )
-                            .child(
-                                div().border().border_color(gpui::red()).child(
-                                    IconButton::new("navigate_forward", Icon::ArrowRight)
-                                        .on_click({
-                                            let view = cx.view().clone();
-                                            move |_, cx| view.update(cx, Self::navigate_backward)
-                                        })
-                                        .disabled(!self.can_navigate_forward()),
-                                ),
-                            ),
+                        div().border().border_color(gpui::red()).child(
+                            IconButton::new("navigate_backward", Icon::ArrowLeft)
+                                .on_click({
+                                    let view = cx.view().clone();
+                                    move |_, cx| view.update(cx, Self::navigate_backward)
+                                })
+                                .disabled(!self.can_navigate_backward()),
+                        ),
+                    )
+                    .child(
+                        div().border().border_color(gpui::red()).child(
+                            IconButton::new("navigate_forward", Icon::ArrowRight)
+                                .on_click({
+                                    let view = cx.view().clone();
+                                    move |_, cx| view.update(cx, Self::navigate_backward)
+                                })
+                                .disabled(!self.can_navigate_forward()),
+                        ),
                     ),
             )
             .child(
@@ -2186,8 +2184,11 @@ impl Render for Pane {
             .child(if let Some(item) = self.active_item() {
                 div().flex().flex_1().child(item.to_any())
             } else {
-                // todo!()
-                div().child("Empty Pane")
+                h_stack()
+                    .items_center()
+                    .size_full()
+                    .justify_center()
+                    .child(Label::new("Open a file or project to get started.").color(Color::Muted))
             })
 
         // enum MouseNavigationHandler {}