From 908678403871e0ef18bbe20f78a84654dfdd431d Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Thu, 29 May 2025 15:41:15 -0600 Subject: [PATCH] gpui: Support hitbox blocking mouse interaction except scrolling (#31712) tl;dr: This adds `.block_mouse_except_scroll()` which should typically be used instead of `.occlude()` for cases when the mouse shouldn't interact with elements drawn below an element. The rationale for treating scroll events differently: * Mouse move / click / styles / tooltips are for elements the user is interacting with directly. * Mouse scroll events are about finding the current outer scroll container. Most use of `occlude` should probably be switched to this, but I figured I'd derisk this change by minimizing behavior changes to just the 3 uses of `block_mouse_except_scroll`. GPUI changes: * Added `InteractiveElement::block_mouse_except_scroll()`, and removes `stop_mouse_events_except_scroll()` * Added `Hitbox::should_handle_scroll()` to be used when handling scroll wheel events. * `Window::insert_hitbox` now takes `HitboxBehavior` instead of `occlude: bool`. - `false` for that bool is now `HitboxBehavior::Normal`. - `true` for that bool is now `HitboxBehavior::BlockMouse`. - The new mode is `HitboxBehavior::BlockMouseExceptScroll`. * Removes `Default` impl for `HitboxId` since applications should not manually create `HitboxId(0)`. Release Notes: - N/A --- crates/agent/src/active_thread.rs | 2 +- crates/agent/src/agent_diff.rs | 2 +- crates/editor/src/editor.rs | 2 +- crates/editor/src/element.rs | 34 ++-- crates/gpui/examples/window_shadow.rs | 10 +- crates/gpui/src/elements/div.rs | 46 +++--- crates/gpui/src/elements/list.rs | 9 +- crates/gpui/src/elements/text.rs | 8 +- crates/gpui/src/window.rs | 161 ++++++++++++++++--- crates/markdown/src/markdown.rs | 3 +- crates/ui/src/components/indent_guides.rs | 11 +- crates/ui/src/components/popover_menu.rs | 8 +- crates/ui/src/components/right_click_menu.rs | 8 +- crates/ui/src/components/scrollbar.rs | 8 +- crates/workspace/src/pane_group.rs | 8 +- crates/workspace/src/workspace.rs | 10 +- 16 files changed, 231 insertions(+), 99 deletions(-) diff --git a/crates/agent/src/active_thread.rs b/crates/agent/src/active_thread.rs index cfe4b895fe..ebad961a71 100644 --- a/crates/agent/src/active_thread.rs +++ b/crates/agent/src/active_thread.rs @@ -2162,7 +2162,7 @@ impl ActiveThread { .inset_0() .bg(panel_bg) .opacity(0.8) - .stop_mouse_events_except_scroll() + .block_mouse_except_scroll() .on_click(cx.listener(Self::handle_cancel_click)); v_flex() diff --git a/crates/agent/src/agent_diff.rs b/crates/agent/src/agent_diff.rs index cb55585326..df49123845 100644 --- a/crates/agent/src/agent_diff.rs +++ b/crates/agent/src/agent_diff.rs @@ -699,7 +699,7 @@ fn render_diff_hunk_controls( .rounded_b_md() .bg(cx.theme().colors().editor_background) .gap_1() - .stop_mouse_events_except_scroll() + .block_mouse_except_scroll() .shadow_md() .children(vec![ Button::new(("reject", row as u64), "Reject") diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ed0a267db0..2f60208b61 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -21907,7 +21907,7 @@ fn render_diff_hunk_controls( .rounded_b_lg() .bg(cx.theme().colors().editor_background) .gap_1() - .stop_mouse_events_except_scroll() + .block_mouse_except_scroll() .shadow_md() .child(if status.has_secondary_hunk() { Button::new(("stage", row as u64), "Stage") diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b1eabec4b5..b6996b9a91 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -42,13 +42,13 @@ use git::{ use gpui::{ Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle, Bounds, ClickEvent, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, Edges, - Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox, Hsla, - InteractiveElement, IntoElement, IsZero, Keystroke, Length, ModifiersChangedEvent, MouseButton, - MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, - ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, - Style, Styled, TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, - linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background, - transparent_black, + Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox, + HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, Keystroke, Length, + ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, + ParentElement, Pixels, ScrollDelta, ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, + Size, StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, WeakEntity, + Window, anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, + quad, relative, size, solid_background, transparent_black, }; use itertools::Itertools; use language::language_settings::{ @@ -1620,7 +1620,7 @@ impl EditorElement { ); let layout = ScrollbarLayout::for_minimap( - window.insert_hitbox(minimap_bounds, false), + window.insert_hitbox(minimap_bounds, HitboxBehavior::Normal), visible_editor_lines, total_editor_lines, minimap_line_height, @@ -1791,7 +1791,7 @@ impl EditorElement { if matches!(hunk, DisplayDiffHunk::Unfolded { .. }) { let hunk_bounds = Self::diff_hunk_bounds(snapshot, line_height, gutter_hitbox.bounds, hunk); - *hitbox = Some(window.insert_hitbox(hunk_bounds, true)); + *hitbox = Some(window.insert_hitbox(hunk_bounds, HitboxBehavior::BlockMouse)); } } } @@ -2883,7 +2883,7 @@ impl EditorElement { let hitbox = line_origin.map(|line_origin| { window.insert_hitbox( Bounds::new(line_origin, size(shaped_line.width, line_height)), - false, + HitboxBehavior::Normal, ) }); #[cfg(test)] @@ -6371,7 +6371,7 @@ impl EditorElement { } }; - if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { + if phase == DispatchPhase::Bubble && hitbox.should_handle_scroll(window) { delta = delta.coalesce(event.delta); editor.update(cx, |editor, cx| { let position_map: &PositionMap = &position_map; @@ -7651,15 +7651,17 @@ impl Element for EditorElement { .map(|(guide, active)| (self.column_pixels(*guide, window, cx), *active)) .collect::>(); - let hitbox = window.insert_hitbox(bounds, false); - let gutter_hitbox = - window.insert_hitbox(gutter_bounds(bounds, gutter_dimensions), false); + let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); + let gutter_hitbox = window.insert_hitbox( + gutter_bounds(bounds, gutter_dimensions), + HitboxBehavior::Normal, + ); let text_hitbox = window.insert_hitbox( Bounds { origin: gutter_hitbox.top_right(), size: size(text_width, bounds.size.height), }, - false, + HitboxBehavior::Normal, ); let content_origin = text_hitbox.origin + content_offset; @@ -8880,7 +8882,7 @@ impl EditorScrollbars { }) .map(|(viewport_size, scroll_range)| { ScrollbarLayout::new( - window.insert_hitbox(scrollbar_bounds_for(axis), false), + window.insert_hitbox(scrollbar_bounds_for(axis), HitboxBehavior::Normal), viewport_size, scroll_range, glyph_grid_cell.along(axis), diff --git a/crates/gpui/examples/window_shadow.rs b/crates/gpui/examples/window_shadow.rs index 875ebb93c6..e75e50e31a 100644 --- a/crates/gpui/examples/window_shadow.rs +++ b/crates/gpui/examples/window_shadow.rs @@ -1,8 +1,8 @@ use gpui::{ - App, Application, Bounds, Context, CursorStyle, Decorations, Hsla, MouseButton, Pixels, Point, - ResizeEdge, Size, Window, WindowBackgroundAppearance, WindowBounds, WindowDecorations, - WindowOptions, black, canvas, div, green, point, prelude::*, px, rgb, size, transparent_black, - white, + App, Application, Bounds, Context, CursorStyle, Decorations, HitboxBehavior, Hsla, MouseButton, + Pixels, Point, ResizeEdge, Size, Window, WindowBackgroundAppearance, WindowBounds, + WindowDecorations, WindowOptions, black, canvas, div, green, point, prelude::*, px, rgb, size, + transparent_black, white, }; struct WindowShadow {} @@ -37,7 +37,7 @@ impl Render for WindowShadow { point(px(0.0), px(0.0)), window.window_bounds().get_bounds().size, ), - false, + HitboxBehavior::Normal, ) }, move |_bounds, hitbox, window, _cx| { diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 7167943771..c6a0520b26 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -17,10 +17,10 @@ use crate::{ Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, Bounds, ClickEvent, DispatchPhase, - Element, ElementId, Entity, FocusHandle, Global, GlobalElementId, Hitbox, HitboxId, - InspectorElementId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, - ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Overflow, - ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style, + Element, ElementId, Entity, FocusHandle, Global, GlobalElementId, Hitbox, HitboxBehavior, + HitboxId, InspectorElementId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, + LayoutId, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, + Overflow, ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, Task, TooltipId, Visibility, Window, point, px, size, }; use collections::HashMap; @@ -313,7 +313,7 @@ impl Interactivity { ) { self.scroll_wheel_listeners .push(Box::new(move |event, phase, hitbox, window, cx| { - if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { + if phase == DispatchPhase::Bubble && hitbox.should_handle_scroll(window) { (listener)(event, window, cx); } })); @@ -567,19 +567,20 @@ impl Interactivity { }); } - /// Block the mouse from interacting with this element or any of its children + /// Block the mouse from all interactions with elements behind this element's hitbox. Typically + /// `block_mouse_except_scroll` should be preferred. + /// /// The imperative API equivalent to [`InteractiveElement::occlude`] pub fn occlude_mouse(&mut self) { - self.occlude_mouse = true; + self.hitbox_behavior = HitboxBehavior::BlockMouse; } - /// Registers event handles that stop propagation of mouse events for non-scroll events. + /// Block non-scroll mouse interactions with elements behind this element's hitbox. See + /// [`Hitbox::is_hovered`] for details. + /// /// The imperative API equivalent to [`InteractiveElement::block_mouse_except_scroll`] - pub fn stop_mouse_events_except_scroll(&mut self) { - self.on_any_mouse_down(|_, _, cx| cx.stop_propagation()); - self.on_any_mouse_up(|_, _, cx| cx.stop_propagation()); - self.on_click(|_, _, cx| cx.stop_propagation()); - self.on_hover(|_, _, cx| cx.stop_propagation()); + pub fn block_mouse_except_scroll(&mut self) { + self.hitbox_behavior = HitboxBehavior::BlockMouseExceptScroll; } } @@ -949,7 +950,8 @@ pub trait InteractiveElement: Sized { self } - /// Block the mouse from interacting with this element or any of its children + /// Block the mouse from all interactions with elements behind this element's hitbox. Typically + /// `block_mouse_except_scroll` should be preferred. /// The fluent API equivalent to [`Interactivity::occlude_mouse`] fn occlude(mut self) -> Self { self.interactivity().occlude_mouse(); @@ -961,10 +963,12 @@ pub trait InteractiveElement: Sized { self.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) } - /// Registers event handles that stop propagation of mouse events for non-scroll events. + /// Block non-scroll mouse interactions with elements behind this element's hitbox. See + /// [`Hitbox::is_hovered`] for details. + /// /// The fluent API equivalent to [`Interactivity::block_mouse_except_scroll`] - fn stop_mouse_events_except_scroll(mut self) -> Self { - self.interactivity().stop_mouse_events_except_scroll(); + fn block_mouse_except_scroll(mut self) -> Self { + self.interactivity().block_mouse_except_scroll(); self } } @@ -1448,7 +1452,7 @@ pub struct Interactivity { pub(crate) drag_listener: Option<(Arc, DragListener)>, pub(crate) hover_listener: Option>, pub(crate) tooltip_builder: Option, - pub(crate) occlude_mouse: bool, + pub(crate) hitbox_behavior: HitboxBehavior, #[cfg(any(feature = "inspector", debug_assertions))] pub(crate) source_location: Option<&'static core::panic::Location<'static>>, @@ -1594,7 +1598,7 @@ impl Interactivity { style.overflow_mask(bounds, window.rem_size()), |window| { let hitbox = if self.should_insert_hitbox(&style, window, cx) { - Some(window.insert_hitbox(bounds, self.occlude_mouse)) + Some(window.insert_hitbox(bounds, self.hitbox_behavior)) } else { None }; @@ -1611,7 +1615,7 @@ impl Interactivity { } fn should_insert_hitbox(&self, style: &Style, window: &Window, cx: &App) -> bool { - self.occlude_mouse + self.hitbox_behavior != HitboxBehavior::Normal || style.mouse_cursor.is_some() || self.group.is_some() || self.scroll_offset.is_some() @@ -2270,7 +2274,7 @@ impl Interactivity { let hitbox = hitbox.clone(); let current_view = window.current_view(); window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| { - if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { + if phase == DispatchPhase::Bubble && hitbox.should_handle_scroll(window) { let mut scroll_offset = scroll_offset.borrow_mut(); let old_scroll_offset = *scroll_offset; let delta = event.delta.pixel_delta(line_height); diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index c9731026c2..6b9df6ab29 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -9,8 +9,9 @@ use crate::{ AnyElement, App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, Element, EntityId, - FocusHandle, GlobalElementId, Hitbox, InspectorElementId, IntoElement, Overflow, Pixels, Point, - ScrollWheelEvent, Size, Style, StyleRefinement, Styled, Window, point, px, size, + FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, + Overflow, Pixels, Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled, Window, point, + px, size, }; use collections::VecDeque; use refineable::Refineable as _; @@ -906,7 +907,7 @@ impl Element for List { let mut style = Style::default(); style.refine(&self.style); - let hitbox = window.insert_hitbox(bounds, false); + let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); // If the width of the list has changed, invalidate all cached item heights if state.last_layout_bounds.map_or(true, |last_bounds| { @@ -962,7 +963,7 @@ impl Element for List { let scroll_top = prepaint.layout.scroll_top; let hitbox_id = prepaint.hitbox.id; window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| { - if phase == DispatchPhase::Bubble && hitbox_id.is_hovered(window) { + if phase == DispatchPhase::Bubble && hitbox_id.should_handle_scroll(window) { list_state.0.borrow_mut().scroll( &scroll_top, height, diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 0fd30ed4f4..86cf4407b5 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -1,8 +1,8 @@ use crate::{ ActiveTooltip, AnyView, App, Bounds, DispatchPhase, Element, ElementId, GlobalElementId, - HighlightStyle, Hitbox, InspectorElementId, IntoElement, LayoutId, MouseDownEvent, - MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextOverflow, TextRun, - TextStyle, TooltipId, WhiteSpace, Window, WrappedLine, WrappedLineLayout, + HighlightStyle, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, LayoutId, + MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextOverflow, + TextRun, TextStyle, TooltipId, WhiteSpace, Window, WrappedLine, WrappedLineLayout, register_tooltip_mouse_handlers, set_tooltip_on_window, }; use anyhow::Context as _; @@ -739,7 +739,7 @@ impl Element for InteractiveText { self.text .prepaint(None, inspector_id, bounds, state, window, cx); - let hitbox = window.insert_hitbox(bounds, false); + let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); (hitbox, interactive_state) }, ) diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index f78bcad3ec..4e4c683d61 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -413,14 +413,42 @@ pub(crate) struct CursorStyleRequest { pub(crate) style: CursorStyle, } -/// An identifier for a [Hitbox]. -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)] -pub struct HitboxId(usize); +#[derive(Default, Eq, PartialEq)] +pub(crate) struct HitTest { + pub(crate) ids: SmallVec<[HitboxId; 8]>, + pub(crate) hover_hitbox_count: usize, +} + +/// An identifier for a [Hitbox] which also includes [HitboxBehavior]. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct HitboxId(u64); impl HitboxId { - /// Checks if the hitbox with this id is currently hovered. - pub fn is_hovered(&self, window: &Window) -> bool { - window.mouse_hit_test.0.contains(self) + /// Checks if the hitbox with this ID is currently hovered. Except when handling + /// `ScrollWheelEvent`, this is typically what you want when determining whether to handle mouse + /// events or paint hover styles. + /// + /// See [`Hitbox::is_hovered`] for details. + pub fn is_hovered(self, window: &Window) -> bool { + let hit_test = &window.mouse_hit_test; + for id in hit_test.ids.iter().take(hit_test.hover_hitbox_count) { + if self == *id { + return true; + } + } + return false; + } + + /// Checks if the hitbox with this ID contains the mouse and should handle scroll events. + /// Typically this should only be used when handling `ScrollWheelEvent`, and otherwise + /// `is_hovered` should be used. See the documentation of `Hitbox::is_hovered` for details about + /// this distinction. + pub fn should_handle_scroll(self, window: &Window) -> bool { + window.mouse_hit_test.ids.contains(&self) + } + + fn next(mut self) -> HitboxId { + HitboxId(self.0.wrapping_add(1)) } } @@ -435,19 +463,98 @@ pub struct Hitbox { pub bounds: Bounds, /// The content mask when the hitbox was inserted. pub content_mask: ContentMask, - /// Whether the hitbox occludes other hitboxes inserted prior. - pub opaque: bool, + /// Flags that specify hitbox behavior. + pub behavior: HitboxBehavior, } impl Hitbox { - /// Checks if the hitbox is currently hovered. + /// Checks if the hitbox is currently hovered. Except when handling `ScrollWheelEvent`, this is + /// typically what you want when determining whether to handle mouse events or paint hover + /// styles. + /// + /// This can return `false` even when the hitbox contains the mouse, if a hitbox in front of + /// this sets `HitboxBehavior::BlockMouse` (`InteractiveElement::occlude`) or + /// `HitboxBehavior::BlockMouseExceptScroll` (`InteractiveElement::block_mouse_except_scroll`). + /// + /// Handling of `ScrollWheelEvent` should typically use `should_handle_scroll` instead. + /// Concretely, this is due to use-cases like overlays that cause the elements under to be + /// non-interactive while still allowing scrolling. More abstractly, this is because + /// `is_hovered` is about element interactions directly under the mouse - mouse moves, clicks, + /// hover styling, etc. In contrast, scrolling is about finding the current outer scrollable + /// container. pub fn is_hovered(&self, window: &Window) -> bool { self.id.is_hovered(window) } + + /// Checks if the hitbox contains the mouse and should handle scroll events. Typically this + /// should only be used when handling `ScrollWheelEvent`, and otherwise `is_hovered` should be + /// used. See the documentation of `Hitbox::is_hovered` for details about this distinction. + /// + /// This can return `false` even when the hitbox contains the mouse, if a hitbox in front of + /// this sets `HitboxBehavior::BlockMouse` (`InteractiveElement::occlude`). + pub fn should_handle_scroll(&self, window: &Window) -> bool { + self.id.should_handle_scroll(window) + } } -#[derive(Default, Eq, PartialEq)] -pub(crate) struct HitTest(SmallVec<[HitboxId; 8]>); +/// How the hitbox affects mouse behavior. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum HitboxBehavior { + /// Normal hitbox mouse behavior, doesn't affect mouse handling for other hitboxes. + #[default] + Normal, + + /// All hitboxes behind this hitbox will be ignored and so will have `hitbox.is_hovered() == + /// false` and `hitbox.should_handle_scroll() == false`. Typically for elements this causes + /// skipping of all mouse events, hover styles, and tooltips. This flag is set by + /// [`InteractiveElement::occlude`]. + /// + /// For mouse handlers that check those hitboxes, this behaves the same as registering a + /// bubble-phase handler for every mouse event type: + /// + /// ``` + /// window.on_mouse_event(move |_: &EveryMouseEventTypeHere, phase, window, cx| { + /// if phase == DispatchPhase::Capture && hitbox.is_hovered(window) { + /// cx.stop_propagation(); + /// } + /// } + /// ``` + /// + /// This has effects beyond event handling - any use of hitbox checking, such as hover + /// styles and tooltops. These other behaviors are the main point of this mechanism. An + /// alternative might be to not affect mouse event handling - but this would allow + /// inconsistent UI where clicks and moves interact with elements that are not considered to + /// be hovered. + BlockMouse, + + /// All hitboxes behind this hitbox will have `hitbox.is_hovered() == false`, even when + /// `hitbox.should_handle_scroll() == true`. Typically for elements this causes all mouse + /// interaction except scroll events to be ignored - see the documentation of + /// [`Hitbox::is_hovered`] for details. This flag is set by + /// [`InteractiveElement::block_mouse_except_scroll`]. + /// + /// For mouse handlers that check those hitboxes, this behaves the same as registering a + /// bubble-phase handler for every mouse event type **except** `ScrollWheelEvent`: + /// + /// ``` + /// window.on_mouse_event(move |_: &EveryMouseEventTypeExceptScroll, phase, window, _cx| { + /// if phase == DispatchPhase::Bubble && hitbox.should_handle_scroll(window) { + /// cx.stop_propagation(); + /// } + /// } + /// ``` + /// + /// See the documentation of [`Hitbox::is_hovered`] for details of why `ScrollWheelEvent` is + /// handled differently than other mouse events. If also blocking these scroll events is + /// desired, then a `cx.stop_propagation()` handler like the one above can be used. + /// + /// This has effects beyond event handling - this affects any use of `is_hovered`, such as + /// hover styles and tooltops. These other behaviors are the main point of this mechanism. + /// An alternative might be to not affect mouse event handling - but this would allow + /// inconsistent UI where clicks and moves interact with elements that are not considered to + /// be hovered. + BlockMouseExceptScroll, +} /// An identifier for a tooltip. #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] @@ -578,16 +685,26 @@ impl Frame { } pub(crate) fn hit_test(&self, position: Point) -> HitTest { + let mut set_hover_hitbox_count = false; let mut hit_test = HitTest::default(); for hitbox in self.hitboxes.iter().rev() { let bounds = hitbox.bounds.intersect(&hitbox.content_mask.bounds); if bounds.contains(&position) { - hit_test.0.push(hitbox.id); - if hitbox.opaque { + hit_test.ids.push(hitbox.id); + if !set_hover_hitbox_count + && hitbox.behavior == HitboxBehavior::BlockMouseExceptScroll + { + hit_test.hover_hitbox_count = hit_test.ids.len(); + set_hover_hitbox_count = true; + } + if hitbox.behavior == HitboxBehavior::BlockMouse { break; } } } + if !set_hover_hitbox_count { + hit_test.hover_hitbox_count = hit_test.ids.len(); + } hit_test } @@ -638,7 +755,7 @@ pub struct Window { pub(crate) image_cache_stack: Vec, pub(crate) rendered_frame: Frame, pub(crate) next_frame: Frame, - pub(crate) next_hitbox_id: HitboxId, + next_hitbox_id: HitboxId, pub(crate) next_tooltip_id: TooltipId, pub(crate) tooltip_bounds: Option, next_frame_callbacks: Rc>>, @@ -927,7 +1044,7 @@ impl Window { rendered_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())), next_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())), next_frame_callbacks, - next_hitbox_id: HitboxId::default(), + next_hitbox_id: HitboxId(0), next_tooltip_id: TooltipId::default(), tooltip_bounds: None, dirty_views: FxHashSet::default(), @@ -2870,17 +2987,17 @@ impl Window { /// to determine whether the inserted hitbox was the topmost. /// /// This method should only be called as part of the prepaint phase of element drawing. - pub fn insert_hitbox(&mut self, bounds: Bounds, opaque: bool) -> Hitbox { + pub fn insert_hitbox(&mut self, bounds: Bounds, behavior: HitboxBehavior) -> Hitbox { self.invalidator.debug_assert_prepaint(); let content_mask = self.content_mask(); - let id = self.next_hitbox_id; - self.next_hitbox_id.0 += 1; + let mut id = self.next_hitbox_id; + self.next_hitbox_id = self.next_hitbox_id.next(); let hitbox = Hitbox { id, bounds, content_mask, - opaque, + behavior, }; self.next_frame.hitboxes.push(hitbox.clone()); hitbox @@ -4042,7 +4159,7 @@ impl Window { inspector.update(cx, |inspector, _cx| { if let Some(depth) = inspector.pick_depth.as_mut() { *depth += delta_y.0 / SCROLL_PIXELS_PER_LAYER; - let max_depth = self.mouse_hit_test.0.len() as f32 - 0.5; + let max_depth = self.mouse_hit_test.ids.len() as f32 - 0.5; if *depth < 0.0 { *depth = 0.0; } else if *depth > max_depth { @@ -4067,9 +4184,9 @@ impl Window { ) -> Option<(HitboxId, crate::InspectorElementId)> { if let Some(pick_depth) = inspector.pick_depth { let depth = (pick_depth as i64).try_into().unwrap_or(0); - let max_skipped = self.mouse_hit_test.0.len().saturating_sub(1); + let max_skipped = self.mouse_hit_test.ids.len().saturating_sub(1); let skip_count = (depth as usize).min(max_skipped); - for hitbox_id in self.mouse_hit_test.0.iter().skip(skip_count) { + for hitbox_id in self.mouse_hit_test.ids.iter().skip(skip_count) { if let Some(inspector_id) = frame.inspector_hitboxes.get(hitbox_id) { return Some((*hitbox_id, inspector_id.clone())); } diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 1f04e463fd..626ffcef6f 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -3,6 +3,7 @@ mod path_range; use base64::Engine as _; use futures::FutureExt as _; +use gpui::HitboxBehavior; use language::LanguageName; use log::Level; pub use path_range::{LineCol, PathWithRange}; @@ -1211,7 +1212,7 @@ impl Element for MarkdownElement { window.set_focus_handle(&focus_handle, cx); window.set_view_id(self.markdown.entity_id()); - let hitbox = window.insert_hitbox(bounds, false); + let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); rendered_markdown.element.prepaint(window, cx); self.autoscroll(&rendered_markdown.text, window, cx); hitbox diff --git a/crates/ui/src/components/indent_guides.rs b/crates/ui/src/components/indent_guides.rs index dacfa16325..f6f256323d 100644 --- a/crates/ui/src/components/indent_guides.rs +++ b/crates/ui/src/components/indent_guides.rs @@ -136,7 +136,9 @@ pub struct IndentGuideLayout { /// Implements the necessary functionality for rendering indent guides inside a uniform list. mod uniform_list { - use gpui::{DispatchPhase, Hitbox, MouseButton, MouseDownEvent, MouseMoveEvent}; + use gpui::{ + DispatchPhase, Hitbox, HitboxBehavior, MouseButton, MouseDownEvent, MouseMoveEvent, + }; use super::*; @@ -256,7 +258,12 @@ mod uniform_list { .indent_guides .as_ref() .iter() - .map(|guide| window.insert_hitbox(guide.hitbox.unwrap_or(guide.bounds), false)) + .map(|guide| { + window.insert_hitbox( + guide.hitbox.unwrap_or(guide.bounds), + HitboxBehavior::Normal, + ) + }) .collect(); Self::PrepaintState::Interactive { hitboxes: Rc::new(hitboxes), diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs index 385b686bda..077c18f69e 100644 --- a/crates/ui/src/components/popover_menu.rs +++ b/crates/ui/src/components/popover_menu.rs @@ -2,9 +2,9 @@ use std::{cell::RefCell, rc::Rc}; use gpui::{ AnyElement, AnyView, App, Bounds, Corner, DismissEvent, DispatchPhase, Element, ElementId, - Entity, Focusable as _, GlobalElementId, HitboxId, InteractiveElement, IntoElement, LayoutId, - Length, ManagedView, MouseDownEvent, ParentElement, Pixels, Point, Style, Window, anchored, - deferred, div, point, prelude::FluentBuilder, px, size, + Entity, Focusable as _, GlobalElementId, HitboxBehavior, HitboxId, InteractiveElement, + IntoElement, LayoutId, Length, ManagedView, MouseDownEvent, ParentElement, Pixels, Point, + Style, Window, anchored, deferred, div, point, prelude::FluentBuilder, px, size, }; use crate::prelude::*; @@ -421,7 +421,7 @@ impl Element for PopoverMenu { ((), element_state) }); - window.insert_hitbox(bounds, false).id + window.insert_hitbox(bounds, HitboxBehavior::Normal).id }) } diff --git a/crates/ui/src/components/right_click_menu.rs b/crates/ui/src/components/right_click_menu.rs index 79d5130079..3328644e8e 100644 --- a/crates/ui/src/components/right_click_menu.rs +++ b/crates/ui/src/components/right_click_menu.rs @@ -2,9 +2,9 @@ use std::{cell::RefCell, rc::Rc}; use gpui::{ AnyElement, App, Bounds, Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, - Focusable as _, GlobalElementId, Hitbox, InteractiveElement, IntoElement, LayoutId, - ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Window, anchored, - deferred, div, px, + Focusable as _, GlobalElementId, Hitbox, HitboxBehavior, InteractiveElement, IntoElement, + LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Window, + anchored, deferred, div, px, }; pub struct RightClickMenu { @@ -185,7 +185,7 @@ impl Element for RightClickMenu { window: &mut Window, cx: &mut App, ) -> PrepaintState { - let hitbox = window.insert_hitbox(bounds, false); + let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); if let Some(child) = request_layout.child_element.as_mut() { child.prepaint(window, cx); diff --git a/crates/ui/src/components/scrollbar.rs b/crates/ui/src/components/scrollbar.rs index 74832ea46d..4ee2760c93 100644 --- a/crates/ui/src/components/scrollbar.rs +++ b/crates/ui/src/components/scrollbar.rs @@ -3,9 +3,9 @@ use std::{any::Any, cell::Cell, fmt::Debug, ops::Range, rc::Rc, sync::Arc}; use crate::{IntoElement, prelude::*, px, relative}; use gpui::{ Along, App, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element, - ElementId, Entity, EntityId, GlobalElementId, Hitbox, Hsla, IsZero, LayoutId, ListState, - MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, ScrollHandle, ScrollWheelEvent, - Size, Style, UniformListScrollHandle, Window, quad, + ElementId, Entity, EntityId, GlobalElementId, Hitbox, HitboxBehavior, Hsla, IsZero, LayoutId, + ListState, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, ScrollHandle, + ScrollWheelEvent, Size, Style, UniformListScrollHandle, Window, quad, }; pub struct Scrollbar { @@ -226,7 +226,7 @@ impl Element for Scrollbar { _: &mut App, ) -> Self::PrepaintState { window.with_content_mask(Some(ContentMask { bounds }), |window| { - window.insert_hitbox(bounds, false) + window.insert_hitbox(bounds, HitboxBehavior::Normal) }) } diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index c781854741..7700907f06 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -902,9 +902,9 @@ mod element { use std::{cell::RefCell, iter, rc::Rc, sync::Arc}; use gpui::{ - Along, AnyElement, App, Axis, BorderStyle, Bounds, Element, GlobalElementId, IntoElement, - MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Size, Style, - WeakEntity, Window, px, relative, size, + Along, AnyElement, App, Axis, BorderStyle, Bounds, Element, GlobalElementId, + HitboxBehavior, IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, + Pixels, Point, Size, Style, WeakEntity, Window, px, relative, size, }; use gpui::{CursorStyle, Hitbox}; use parking_lot::Mutex; @@ -1091,7 +1091,7 @@ mod element { }; PaneAxisHandleLayout { - hitbox: window.insert_hitbox(handle_bounds, true), + hitbox: window.insert_hitbox(handle_bounds, HitboxBehavior::Normal), divider_bounds, } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 0c989884c2..e8cdf7aa4f 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -37,10 +37,10 @@ use futures::{ use gpui::{ Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds, Context, CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, - Focusable, Global, Hsla, KeyContext, Keystroke, ManagedView, MouseButton, PathPromptOptions, - Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription, Task, Tiling, WeakEntity, - WindowBounds, WindowHandle, WindowId, WindowOptions, action_as, actions, canvas, - impl_action_as, impl_actions, point, relative, size, transparent_black, + Focusable, Global, HitboxBehavior, Hsla, KeyContext, Keystroke, ManagedView, MouseButton, + PathPromptOptions, Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription, Task, + Tiling, WeakEntity, WindowBounds, WindowHandle, WindowId, WindowOptions, action_as, actions, + canvas, impl_action_as, impl_actions, point, relative, size, transparent_black, }; pub use history_manager::*; pub use item::{ @@ -7344,7 +7344,7 @@ pub fn client_side_decorations( point(px(0.0), px(0.0)), window.window_bounds().get_bounds().size, ), - false, + HitboxBehavior::Normal, ) }, move |_bounds, hitbox, window, cx| {