Fix window drags when dragging button/input on titlebar in macOS (#51400)

Smit Barmase created

Closes https://github.com/zed-industries/zed/issues/27500

This PR fixes an issue on macOS where dragging title bar buttons and
other UI elements would drag the window instead of no-op, like in native
Mac apps. That made interactions like selecting text with the mouse
impossible in those areas, including the title input in Rules Library.

We don't want to handle this at GPUI level, since you might still want
this dragging behavior while having no native titlebar for some apps,
and without implementing your own handler. So, we just handle this for
Zed.

On macOS, we now set `is_movable: false` on all windows, which disables
that drag-anything behavior and relies on the native window drag handler
for window dragging instead.

This also meant implementing a platform title bar for the sidebar in
Rules Library, since dragging there was previously handled by the
`is_movable` behavior. We already had a full-width platform title bar
there on other platforms. On macOS, it is sidebar-only to keep existing
design.
  
Release Notes:

- N/A

Change summary

crates/platform_title_bar/src/platform_title_bar.rs | 10 ++
crates/rules_library/src/rules_library.rs           | 74 +++++++++-----
crates/zed/src/zed.rs                               |  2 
3 files changed, 57 insertions(+), 29 deletions(-)

Detailed changes

crates/platform_title_bar/src/platform_title_bar.rs 🔗

@@ -30,6 +30,7 @@ pub struct PlatformTitleBar {
     platform_style: PlatformStyle,
     children: SmallVec<[AnyElement; 2]>,
     should_move: bool,
+    background_color: Option<Hsla>,
     system_window_tabs: Entity<SystemWindowTabs>,
 }
 
@@ -43,11 +44,16 @@ impl PlatformTitleBar {
             platform_style,
             children: SmallVec::new(),
             should_move: false,
+            background_color: None,
             system_window_tabs,
         }
     }
 
     pub fn title_bar_color(&self, window: &mut Window, cx: &mut Context<Self>) -> Hsla {
+        if let Some(background_color) = self.background_color {
+            return background_color;
+        }
+
         if cfg!(any(target_os = "linux", target_os = "freebsd")) {
             if window.is_window_active() && !self.should_move {
                 cx.theme().colors().title_bar_background
@@ -66,6 +72,10 @@ impl PlatformTitleBar {
         self.children = children.into_iter().collect();
     }
 
+    pub fn set_background_color(&mut self, background_color: Option<Hsla>) {
+        self.background_color = background_color;
+    }
+
     pub fn init(cx: &mut App) {
         SystemWindowTabs::init(cx);
     }

crates/rules_library/src/rules_library.rs 🔗

@@ -3,9 +3,9 @@ use collections::{HashMap, HashSet};
 use editor::{CompletionProvider, SelectionEffects};
 use editor::{CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle, actions::Tab};
 use gpui::{
-    App, Bounds, DEFAULT_ADDITIONAL_WINDOW_SIZE, Entity, EventEmitter, Focusable, PromptLevel,
-    Subscription, Task, TextStyle, Tiling, TitlebarOptions, WindowBounds, WindowHandle,
-    WindowOptions, actions, point, size, transparent_black,
+    App, Bounds, DEFAULT_ADDITIONAL_WINDOW_SIZE, Entity, EventEmitter, Focusable, MouseButton,
+    PromptLevel, Subscription, Task, TextStyle, Tiling, TitlebarOptions, WindowBounds,
+    WindowHandle, WindowOptions, actions, point, size, transparent_black,
 };
 use language::{Buffer, LanguageRegistry, language_settings::SoftWrap};
 use language_model::{
@@ -133,6 +133,7 @@ pub fn open_rules_library(
                     window_decorations: Some(window_decorations),
                     window_min_size: Some(DEFAULT_ADDITIONAL_WINDOW_SIZE),
                     kind: gpui::WindowKind::Floating,
+                    is_movable: !cfg!(target_os = "macos"),
                     ..Default::default()
                 },
                 |window, cx| {
@@ -503,11 +504,7 @@ impl RulesLibrary {
         });
 
         Self {
-            title_bar: if !cfg!(target_os = "macos") {
-                Some(cx.new(|cx| PlatformTitleBar::new("rules-library-title-bar", cx)))
-            } else {
-                None
-            },
+            title_bar: Some(cx.new(|cx| PlatformTitleBar::new("rules-library-title-bar", cx))),
             store,
             language_registry,
             rule_editors: HashMap::default(),
@@ -1129,30 +1126,44 @@ impl RulesLibrary {
         v_flex()
             .id("rule-list")
             .capture_action(cx.listener(Self::focus_active_rule))
-            .px_1p5()
             .h_full()
             .w_64()
             .overflow_x_hidden()
             .bg(cx.theme().colors().panel_background)
+            .when(!cfg!(target_os = "macos"), |this| this.px_1p5())
             .map(|this| {
                 if cfg!(target_os = "macos") {
-                    this.child(
-                        h_flex()
-                            .p(DynamicSpacing::Base04.rems(cx))
-                            .h_9()
-                            .w_full()
-                            .flex_none()
-                            .justify_end()
-                            .child(
-                                IconButton::new("new-rule", IconName::Plus)
-                                    .tooltip(move |_window, cx| {
-                                        Tooltip::for_action("New Rule", &NewRule, cx)
-                                    })
-                                    .on_click(|_, window, cx| {
-                                        window.dispatch_action(Box::new(NewRule), cx);
-                                    }),
-                            ),
-                    )
+                    let Some(title_bar) = self.title_bar.as_ref() else {
+                        return this;
+                    };
+                    let button_padding = DynamicSpacing::Base08.rems(cx);
+                    let panel_background = cx.theme().colors().panel_background;
+                    title_bar.update(cx, |title_bar, _cx| {
+                        title_bar.set_background_color(Some(panel_background));
+                        title_bar.set_children(Some(
+                            h_flex()
+                                .w_full()
+                                .pr(button_padding)
+                                .justify_end()
+                                .child(
+                                    div()
+                                        .on_mouse_down(MouseButton::Left, |_, _, cx| {
+                                            cx.stop_propagation();
+                                        })
+                                        .child(
+                                            IconButton::new("new-rule", IconName::Plus)
+                                                .tooltip(move |_window, cx| {
+                                                    Tooltip::for_action("New Rule", &NewRule, cx)
+                                                })
+                                                .on_click(|_, window, cx| {
+                                                    window.dispatch_action(Box::new(NewRule), cx);
+                                                }),
+                                        ),
+                                )
+                                .into_any_element(),
+                        ));
+                    });
+                    this.child(title_bar.clone())
                 } else {
                     this.child(
                         h_flex().p_1().w_full().child(
@@ -1170,7 +1181,12 @@ impl RulesLibrary {
                     )
                 }
             })
-            .child(div().flex_grow().child(self.picker.clone()))
+            .child(
+                div()
+                    .flex_grow()
+                    .when(cfg!(target_os = "macos"), |this| this.px_1p5())
+                    .child(self.picker.clone()),
+            )
     }
 
     fn render_active_rule_editor(
@@ -1417,7 +1433,9 @@ impl Render for RulesLibrary {
                 .overflow_hidden()
                 .font(ui_font)
                 .text_color(theme.colors().text)
-                .children(self.title_bar.clone())
+                .when(!cfg!(target_os = "macos"), |this| {
+                    this.children(self.title_bar.clone())
+                })
                 .bg(theme.colors().background)
                 .child(
                     h_flex()

crates/zed/src/zed.rs 🔗

@@ -342,7 +342,7 @@ pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut App) -> WindowO
         focus: false,
         show: false,
         kind: WindowKind::Normal,
-        is_movable: true,
+        is_movable: !cfg!(target_os = "macos"),
         display_id: display.map(|display| display.id()),
         window_background: cx.theme().window_background_appearance(),
         app_id: Some(app_id.to_owned()),