Fix cursor shape flickering and dead-zone on 1px border around list items in project and outline panels (#20202)

Stephan Aßmus and Stephan Aßmus created

Move click listener to outer div
- Avoids dead area when clicking the 1px border around a list item
- Avoids flickering cursor shape when moving the cursor above the list,
and especially when scrolling the list with a stationary cursor.

Closes #15614

Release Notes:

- Fixed mouse cursor shape flickering in project and outline panels when
crossing items
([#15614](https://github.com/zed-industries/zed/issues/15614))

---------

Co-authored-by: Stephan Aßmus <stephan.assmus@sap.com>

Change summary

crates/outline_panel/src/outline_panel.rs |  21 +-
crates/project_panel/src/project_panel.rs | 138 ++++++++++++------------
2 files changed, 79 insertions(+), 80 deletions(-)

Detailed changes

crates/outline_panel/src/outline_panel.rs 🔗

@@ -1996,6 +1996,17 @@ impl OutlinePanel {
         div()
             .text_ui(cx)
             .id(item_id.clone())
+            .on_click({
+                let clicked_entry = rendered_entry.clone();
+                cx.listener(move |outline_panel, event: &gpui::ClickEvent, cx| {
+                    if event.down.button == MouseButton::Right || event.down.first_mouse {
+                        return;
+                    }
+                    let change_selection = event.down.click_count > 1;
+                    outline_panel.open_entry(&clicked_entry, change_selection, cx);
+                })
+            })
+            .cursor_pointer()
             .child(
                 ListItem::new(item_id)
                     .indent_level(depth)
@@ -2005,16 +2016,6 @@ impl OutlinePanel {
                         list_item.child(h_flex().child(icon_element))
                     })
                     .child(h_flex().h_6().child(label_element).ml_1())
-                    .on_click({
-                        let clicked_entry = rendered_entry.clone();
-                        cx.listener(move |outline_panel, event: &gpui::ClickEvent, cx| {
-                            if event.down.button == MouseButton::Right || event.down.first_mouse {
-                                return;
-                            }
-                            let change_selection = event.down.click_count > 1;
-                            outline_panel.open_entry(&clicked_entry, change_selection, cx);
-                        })
-                    })
                     .on_secondary_mouse_down(cx.listener(
                         move |outline_panel, event: &MouseDownEvent, cx| {
                             // Stop propagation to prevent the catch-all context menu for the project

crates/project_panel/src/project_panel.rs 🔗

@@ -2571,6 +2571,74 @@ impl ProjectPanel {
                 this.hover_scroll_task.take();
                 this.drag_onto(selections, entry_id, kind.is_file(), cx);
             }))
+            .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| {
+                if event.down.button == MouseButton::Right || event.down.first_mouse {
+                    return;
+                }
+                if !show_editor {
+                    cx.stop_propagation();
+
+                    if let Some(selection) = this.selection.filter(|_| event.down.modifiers.shift) {
+                        let current_selection = this.index_for_selection(selection);
+                        let target_selection = this.index_for_selection(SelectedEntry {
+                            entry_id,
+                            worktree_id,
+                        });
+                        if let Some(((_, _, source_index), (_, _, target_index))) =
+                            current_selection.zip(target_selection)
+                        {
+                            let range_start = source_index.min(target_index);
+                            let range_end = source_index.max(target_index) + 1; // Make the range inclusive.
+                            let mut new_selections = BTreeSet::new();
+                            this.for_each_visible_entry(
+                                range_start..range_end,
+                                cx,
+                                |entry_id, details, _| {
+                                    new_selections.insert(SelectedEntry {
+                                        entry_id,
+                                        worktree_id: details.worktree_id,
+                                    });
+                                },
+                            );
+
+                            this.marked_entries = this
+                                .marked_entries
+                                .union(&new_selections)
+                                .cloned()
+                                .collect();
+
+                            this.selection = Some(SelectedEntry {
+                                entry_id,
+                                worktree_id,
+                            });
+                            // Ensure that the current entry is selected.
+                            this.marked_entries.insert(SelectedEntry {
+                                entry_id,
+                                worktree_id,
+                            });
+                        }
+                    } else if event.down.modifiers.secondary() {
+                        if event.down.click_count > 1 {
+                            this.split_entry(entry_id, cx);
+                        } else if !this.marked_entries.insert(selection) {
+                            this.marked_entries.remove(&selection);
+                        }
+                    } else if kind.is_dir() {
+                        this.toggle_expanded(entry_id, cx);
+                    } else {
+                        let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled;
+                        let click_count = event.up.click_count;
+                        this.open_entry(
+                            entry_id,
+                            cx.modifiers().secondary(),
+                            !preview_tabs_enabled || click_count > 1,
+                            !preview_tabs_enabled && click_count == 1,
+                            cx,
+                        );
+                    }
+                }
+            }))
+            .cursor_pointer()
             .child(
                 ListItem::new(entry_id.to_proto() as usize)
                     .indent_level(depth)
@@ -2671,76 +2739,6 @@ impl ProjectPanel {
                         }
                         .ml_1(),
                     )
-                    .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| {
-                        if event.down.button == MouseButton::Right || event.down.first_mouse {
-                            return;
-                        }
-                        if !show_editor {
-                            cx.stop_propagation();
-
-                            if let Some(selection) =
-                                this.selection.filter(|_| event.down.modifiers.shift)
-                            {
-                                let current_selection = this.index_for_selection(selection);
-                                let target_selection = this.index_for_selection(SelectedEntry {
-                                    entry_id,
-                                    worktree_id,
-                                });
-                                if let Some(((_, _, source_index), (_, _, target_index))) =
-                                    current_selection.zip(target_selection)
-                                {
-                                    let range_start = source_index.min(target_index);
-                                    let range_end = source_index.max(target_index) + 1; // Make the range inclusive.
-                                    let mut new_selections = BTreeSet::new();
-                                    this.for_each_visible_entry(
-                                        range_start..range_end,
-                                        cx,
-                                        |entry_id, details, _| {
-                                            new_selections.insert(SelectedEntry {
-                                                entry_id,
-                                                worktree_id: details.worktree_id,
-                                            });
-                                        },
-                                    );
-
-                                    this.marked_entries = this
-                                        .marked_entries
-                                        .union(&new_selections)
-                                        .cloned()
-                                        .collect();
-
-                                    this.selection = Some(SelectedEntry {
-                                        entry_id,
-                                        worktree_id,
-                                    });
-                                    // Ensure that the current entry is selected.
-                                    this.marked_entries.insert(SelectedEntry {
-                                        entry_id,
-                                        worktree_id,
-                                    });
-                                }
-                            } else if event.down.modifiers.secondary() {
-                                if event.down.click_count > 1 {
-                                    this.split_entry(entry_id, cx);
-                                } else if !this.marked_entries.insert(selection) {
-                                    this.marked_entries.remove(&selection);
-                                }
-                            } else if kind.is_dir() {
-                                this.toggle_expanded(entry_id, cx);
-                            } else {
-                                let preview_tabs_enabled =
-                                    PreviewTabsSettings::get_global(cx).enabled;
-                                let click_count = event.up.click_count;
-                                this.open_entry(
-                                    entry_id,
-                                    cx.modifiers().secondary(),
-                                    !preview_tabs_enabled || click_count > 1,
-                                    !preview_tabs_enabled && click_count == 1,
-                                    cx,
-                                );
-                            }
-                        }
-                    }))
                     .on_secondary_mouse_down(cx.listener(
                         move |this, event: &MouseDownEvent, cx| {
                             // Stop propagation to prevent the catch-all context menu for the project