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
This commit is contained in:
Michael Sloan 2025-05-29 15:41:15 -06:00 committed by GitHub
parent 2abc5893c1
commit 9086784038
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 231 additions and 99 deletions

View file

@ -2162,7 +2162,7 @@ impl ActiveThread {
.inset_0() .inset_0()
.bg(panel_bg) .bg(panel_bg)
.opacity(0.8) .opacity(0.8)
.stop_mouse_events_except_scroll() .block_mouse_except_scroll()
.on_click(cx.listener(Self::handle_cancel_click)); .on_click(cx.listener(Self::handle_cancel_click));
v_flex() v_flex()

View file

@ -699,7 +699,7 @@ fn render_diff_hunk_controls(
.rounded_b_md() .rounded_b_md()
.bg(cx.theme().colors().editor_background) .bg(cx.theme().colors().editor_background)
.gap_1() .gap_1()
.stop_mouse_events_except_scroll() .block_mouse_except_scroll()
.shadow_md() .shadow_md()
.children(vec![ .children(vec![
Button::new(("reject", row as u64), "Reject") Button::new(("reject", row as u64), "Reject")

View file

@ -21907,7 +21907,7 @@ fn render_diff_hunk_controls(
.rounded_b_lg() .rounded_b_lg()
.bg(cx.theme().colors().editor_background) .bg(cx.theme().colors().editor_background)
.gap_1() .gap_1()
.stop_mouse_events_except_scroll() .block_mouse_except_scroll()
.shadow_md() .shadow_md()
.child(if status.has_secondary_hunk() { .child(if status.has_secondary_hunk() {
Button::new(("stage", row as u64), "Stage") Button::new(("stage", row as u64), "Stage")

View file

@ -42,13 +42,13 @@ use git::{
use gpui::{ use gpui::{
Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle, Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle,
Bounds, ClickEvent, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, Edges, Bounds, ClickEvent, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, Edges,
Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox, Hsla, Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox,
InteractiveElement, IntoElement, IsZero, Keystroke, Length, ModifiersChangedEvent, MouseButton, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, Keystroke, Length,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, ParentElement, Pixels, ScrollDelta, ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString,
Style, Styled, TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, Size, StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, WeakEntity,
linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background, Window, anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px,
transparent_black, quad, relative, size, solid_background, transparent_black,
}; };
use itertools::Itertools; use itertools::Itertools;
use language::language_settings::{ use language::language_settings::{
@ -1620,7 +1620,7 @@ impl EditorElement {
); );
let layout = ScrollbarLayout::for_minimap( let layout = ScrollbarLayout::for_minimap(
window.insert_hitbox(minimap_bounds, false), window.insert_hitbox(minimap_bounds, HitboxBehavior::Normal),
visible_editor_lines, visible_editor_lines,
total_editor_lines, total_editor_lines,
minimap_line_height, minimap_line_height,
@ -1791,7 +1791,7 @@ impl EditorElement {
if matches!(hunk, DisplayDiffHunk::Unfolded { .. }) { if matches!(hunk, DisplayDiffHunk::Unfolded { .. }) {
let hunk_bounds = let hunk_bounds =
Self::diff_hunk_bounds(snapshot, line_height, gutter_hitbox.bounds, hunk); 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| { let hitbox = line_origin.map(|line_origin| {
window.insert_hitbox( window.insert_hitbox(
Bounds::new(line_origin, size(shaped_line.width, line_height)), Bounds::new(line_origin, size(shaped_line.width, line_height)),
false, HitboxBehavior::Normal,
) )
}); });
#[cfg(test)] #[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); delta = delta.coalesce(event.delta);
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
let position_map: &PositionMap = &position_map; let position_map: &PositionMap = &position_map;
@ -7651,15 +7651,17 @@ impl Element for EditorElement {
.map(|(guide, active)| (self.column_pixels(*guide, window, cx), *active)) .map(|(guide, active)| (self.column_pixels(*guide, window, cx), *active))
.collect::<SmallVec<[_; 2]>>(); .collect::<SmallVec<[_; 2]>>();
let hitbox = window.insert_hitbox(bounds, false); let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
let gutter_hitbox = let gutter_hitbox = window.insert_hitbox(
window.insert_hitbox(gutter_bounds(bounds, gutter_dimensions), false); gutter_bounds(bounds, gutter_dimensions),
HitboxBehavior::Normal,
);
let text_hitbox = window.insert_hitbox( let text_hitbox = window.insert_hitbox(
Bounds { Bounds {
origin: gutter_hitbox.top_right(), origin: gutter_hitbox.top_right(),
size: size(text_width, bounds.size.height), size: size(text_width, bounds.size.height),
}, },
false, HitboxBehavior::Normal,
); );
let content_origin = text_hitbox.origin + content_offset; let content_origin = text_hitbox.origin + content_offset;
@ -8880,7 +8882,7 @@ impl EditorScrollbars {
}) })
.map(|(viewport_size, scroll_range)| { .map(|(viewport_size, scroll_range)| {
ScrollbarLayout::new( ScrollbarLayout::new(
window.insert_hitbox(scrollbar_bounds_for(axis), false), window.insert_hitbox(scrollbar_bounds_for(axis), HitboxBehavior::Normal),
viewport_size, viewport_size,
scroll_range, scroll_range,
glyph_grid_cell.along(axis), glyph_grid_cell.along(axis),

View file

@ -1,8 +1,8 @@
use gpui::{ use gpui::{
App, Application, Bounds, Context, CursorStyle, Decorations, Hsla, MouseButton, Pixels, Point, App, Application, Bounds, Context, CursorStyle, Decorations, HitboxBehavior, Hsla, MouseButton,
ResizeEdge, Size, Window, WindowBackgroundAppearance, WindowBounds, WindowDecorations, Pixels, Point, ResizeEdge, Size, Window, WindowBackgroundAppearance, WindowBounds,
WindowOptions, black, canvas, div, green, point, prelude::*, px, rgb, size, transparent_black, WindowDecorations, WindowOptions, black, canvas, div, green, point, prelude::*, px, rgb, size,
white, transparent_black, white,
}; };
struct WindowShadow {} struct WindowShadow {}
@ -37,7 +37,7 @@ impl Render for WindowShadow {
point(px(0.0), px(0.0)), point(px(0.0), px(0.0)),
window.window_bounds().get_bounds().size, window.window_bounds().get_bounds().size,
), ),
false, HitboxBehavior::Normal,
) )
}, },
move |_bounds, hitbox, window, _cx| { move |_bounds, hitbox, window, _cx| {

View file

@ -17,10 +17,10 @@
use crate::{ use crate::{
Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, Bounds, ClickEvent, DispatchPhase, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, Bounds, ClickEvent, DispatchPhase,
Element, ElementId, Entity, FocusHandle, Global, GlobalElementId, Hitbox, HitboxId, Element, ElementId, Entity, FocusHandle, Global, GlobalElementId, Hitbox, HitboxBehavior,
InspectorElementId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, HitboxId, InspectorElementId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Overflow, LayoutId, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style, Overflow, ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style,
StyleRefinement, Styled, Task, TooltipId, Visibility, Window, point, px, size, StyleRefinement, Styled, Task, TooltipId, Visibility, Window, point, px, size,
}; };
use collections::HashMap; use collections::HashMap;
@ -313,7 +313,7 @@ impl Interactivity {
) { ) {
self.scroll_wheel_listeners self.scroll_wheel_listeners
.push(Box::new(move |event, phase, hitbox, window, cx| { .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); (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`] /// The imperative API equivalent to [`InteractiveElement::occlude`]
pub fn occlude_mouse(&mut self) { 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`] /// The imperative API equivalent to [`InteractiveElement::block_mouse_except_scroll`]
pub fn stop_mouse_events_except_scroll(&mut self) { pub fn block_mouse_except_scroll(&mut self) {
self.on_any_mouse_down(|_, _, cx| cx.stop_propagation()); self.hitbox_behavior = HitboxBehavior::BlockMouseExceptScroll;
self.on_any_mouse_up(|_, _, cx| cx.stop_propagation());
self.on_click(|_, _, cx| cx.stop_propagation());
self.on_hover(|_, _, cx| cx.stop_propagation());
} }
} }
@ -949,7 +950,8 @@ pub trait InteractiveElement: Sized {
self 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`] /// The fluent API equivalent to [`Interactivity::occlude_mouse`]
fn occlude(mut self) -> Self { fn occlude(mut self) -> Self {
self.interactivity().occlude_mouse(); self.interactivity().occlude_mouse();
@ -961,10 +963,12 @@ pub trait InteractiveElement: Sized {
self.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) 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`] /// The fluent API equivalent to [`Interactivity::block_mouse_except_scroll`]
fn stop_mouse_events_except_scroll(mut self) -> Self { fn block_mouse_except_scroll(mut self) -> Self {
self.interactivity().stop_mouse_events_except_scroll(); self.interactivity().block_mouse_except_scroll();
self self
} }
} }
@ -1448,7 +1452,7 @@ pub struct Interactivity {
pub(crate) drag_listener: Option<(Arc<dyn Any>, DragListener)>, pub(crate) drag_listener: Option<(Arc<dyn Any>, DragListener)>,
pub(crate) hover_listener: Option<Box<dyn Fn(&bool, &mut Window, &mut App)>>, pub(crate) hover_listener: Option<Box<dyn Fn(&bool, &mut Window, &mut App)>>,
pub(crate) tooltip_builder: Option<TooltipBuilder>, pub(crate) tooltip_builder: Option<TooltipBuilder>,
pub(crate) occlude_mouse: bool, pub(crate) hitbox_behavior: HitboxBehavior,
#[cfg(any(feature = "inspector", debug_assertions))] #[cfg(any(feature = "inspector", debug_assertions))]
pub(crate) source_location: Option<&'static core::panic::Location<'static>>, pub(crate) source_location: Option<&'static core::panic::Location<'static>>,
@ -1594,7 +1598,7 @@ impl Interactivity {
style.overflow_mask(bounds, window.rem_size()), style.overflow_mask(bounds, window.rem_size()),
|window| { |window| {
let hitbox = if self.should_insert_hitbox(&style, window, cx) { 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 { } else {
None None
}; };
@ -1611,7 +1615,7 @@ impl Interactivity {
} }
fn should_insert_hitbox(&self, style: &Style, window: &Window, cx: &App) -> bool { 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() || style.mouse_cursor.is_some()
|| self.group.is_some() || self.group.is_some()
|| self.scroll_offset.is_some() || self.scroll_offset.is_some()
@ -2270,7 +2274,7 @@ impl Interactivity {
let hitbox = hitbox.clone(); let hitbox = hitbox.clone();
let current_view = window.current_view(); let current_view = window.current_view();
window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| { 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 mut scroll_offset = scroll_offset.borrow_mut();
let old_scroll_offset = *scroll_offset; let old_scroll_offset = *scroll_offset;
let delta = event.delta.pixel_delta(line_height); let delta = event.delta.pixel_delta(line_height);

View file

@ -9,8 +9,9 @@
use crate::{ use crate::{
AnyElement, App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, Element, EntityId, AnyElement, App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, Element, EntityId,
FocusHandle, GlobalElementId, Hitbox, InspectorElementId, IntoElement, Overflow, Pixels, Point, FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, IntoElement,
ScrollWheelEvent, Size, Style, StyleRefinement, Styled, Window, point, px, size, Overflow, Pixels, Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled, Window, point,
px, size,
}; };
use collections::VecDeque; use collections::VecDeque;
use refineable::Refineable as _; use refineable::Refineable as _;
@ -906,7 +907,7 @@ impl Element for List {
let mut style = Style::default(); let mut style = Style::default();
style.refine(&self.style); 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 the width of the list has changed, invalidate all cached item heights
if state.last_layout_bounds.map_or(true, |last_bounds| { 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 scroll_top = prepaint.layout.scroll_top;
let hitbox_id = prepaint.hitbox.id; let hitbox_id = prepaint.hitbox.id;
window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| { 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( list_state.0.borrow_mut().scroll(
&scroll_top, &scroll_top,
height, height,

View file

@ -1,8 +1,8 @@
use crate::{ use crate::{
ActiveTooltip, AnyView, App, Bounds, DispatchPhase, Element, ElementId, GlobalElementId, ActiveTooltip, AnyView, App, Bounds, DispatchPhase, Element, ElementId, GlobalElementId,
HighlightStyle, Hitbox, InspectorElementId, IntoElement, LayoutId, MouseDownEvent, HighlightStyle, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, LayoutId,
MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextOverflow, TextRun, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextOverflow,
TextStyle, TooltipId, WhiteSpace, Window, WrappedLine, WrappedLineLayout, TextRun, TextStyle, TooltipId, WhiteSpace, Window, WrappedLine, WrappedLineLayout,
register_tooltip_mouse_handlers, set_tooltip_on_window, register_tooltip_mouse_handlers, set_tooltip_on_window,
}; };
use anyhow::Context as _; use anyhow::Context as _;
@ -739,7 +739,7 @@ impl Element for InteractiveText {
self.text self.text
.prepaint(None, inspector_id, bounds, state, window, cx); .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) (hitbox, interactive_state)
}, },
) )

View file

@ -413,14 +413,42 @@ pub(crate) struct CursorStyleRequest {
pub(crate) style: CursorStyle, pub(crate) style: CursorStyle,
} }
/// An identifier for a [Hitbox]. #[derive(Default, Eq, PartialEq)]
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)] pub(crate) struct HitTest {
pub struct HitboxId(usize); 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 { impl HitboxId {
/// Checks if the hitbox with this id is currently hovered. /// Checks if the hitbox with this ID is currently hovered. Except when handling
pub fn is_hovered(&self, window: &Window) -> bool { /// `ScrollWheelEvent`, this is typically what you want when determining whether to handle mouse
window.mouse_hit_test.0.contains(self) /// 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<Pixels>, pub bounds: Bounds<Pixels>,
/// The content mask when the hitbox was inserted. /// The content mask when the hitbox was inserted.
pub content_mask: ContentMask<Pixels>, pub content_mask: ContentMask<Pixels>,
/// Whether the hitbox occludes other hitboxes inserted prior. /// Flags that specify hitbox behavior.
pub opaque: bool, pub behavior: HitboxBehavior,
} }
impl Hitbox { 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 { pub fn is_hovered(&self, window: &Window) -> bool {
self.id.is_hovered(window) 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)] /// How the hitbox affects mouse behavior.
pub(crate) struct HitTest(SmallVec<[HitboxId; 8]>); #[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. /// An identifier for a tooltip.
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
@ -578,16 +685,26 @@ impl Frame {
} }
pub(crate) fn hit_test(&self, position: Point<Pixels>) -> HitTest { pub(crate) fn hit_test(&self, position: Point<Pixels>) -> HitTest {
let mut set_hover_hitbox_count = false;
let mut hit_test = HitTest::default(); let mut hit_test = HitTest::default();
for hitbox in self.hitboxes.iter().rev() { for hitbox in self.hitboxes.iter().rev() {
let bounds = hitbox.bounds.intersect(&hitbox.content_mask.bounds); let bounds = hitbox.bounds.intersect(&hitbox.content_mask.bounds);
if bounds.contains(&position) { if bounds.contains(&position) {
hit_test.0.push(hitbox.id); hit_test.ids.push(hitbox.id);
if hitbox.opaque { 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; break;
} }
} }
} }
if !set_hover_hitbox_count {
hit_test.hover_hitbox_count = hit_test.ids.len();
}
hit_test hit_test
} }
@ -638,7 +755,7 @@ pub struct Window {
pub(crate) image_cache_stack: Vec<AnyImageCache>, pub(crate) image_cache_stack: Vec<AnyImageCache>,
pub(crate) rendered_frame: Frame, pub(crate) rendered_frame: Frame,
pub(crate) next_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) next_tooltip_id: TooltipId,
pub(crate) tooltip_bounds: Option<TooltipBounds>, pub(crate) tooltip_bounds: Option<TooltipBounds>,
next_frame_callbacks: Rc<RefCell<Vec<FrameCallback>>>, next_frame_callbacks: Rc<RefCell<Vec<FrameCallback>>>,
@ -927,7 +1044,7 @@ impl Window {
rendered_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())), 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: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
next_frame_callbacks, next_frame_callbacks,
next_hitbox_id: HitboxId::default(), next_hitbox_id: HitboxId(0),
next_tooltip_id: TooltipId::default(), next_tooltip_id: TooltipId::default(),
tooltip_bounds: None, tooltip_bounds: None,
dirty_views: FxHashSet::default(), dirty_views: FxHashSet::default(),
@ -2870,17 +2987,17 @@ impl Window {
/// to determine whether the inserted hitbox was the topmost. /// to determine whether the inserted hitbox was the topmost.
/// ///
/// This method should only be called as part of the prepaint phase of element drawing. /// This method should only be called as part of the prepaint phase of element drawing.
pub fn insert_hitbox(&mut self, bounds: Bounds<Pixels>, opaque: bool) -> Hitbox { pub fn insert_hitbox(&mut self, bounds: Bounds<Pixels>, behavior: HitboxBehavior) -> Hitbox {
self.invalidator.debug_assert_prepaint(); self.invalidator.debug_assert_prepaint();
let content_mask = self.content_mask(); let content_mask = self.content_mask();
let id = self.next_hitbox_id; let mut id = self.next_hitbox_id;
self.next_hitbox_id.0 += 1; self.next_hitbox_id = self.next_hitbox_id.next();
let hitbox = Hitbox { let hitbox = Hitbox {
id, id,
bounds, bounds,
content_mask, content_mask,
opaque, behavior,
}; };
self.next_frame.hitboxes.push(hitbox.clone()); self.next_frame.hitboxes.push(hitbox.clone());
hitbox hitbox
@ -4042,7 +4159,7 @@ impl Window {
inspector.update(cx, |inspector, _cx| { inspector.update(cx, |inspector, _cx| {
if let Some(depth) = inspector.pick_depth.as_mut() { if let Some(depth) = inspector.pick_depth.as_mut() {
*depth += delta_y.0 / SCROLL_PIXELS_PER_LAYER; *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 { if *depth < 0.0 {
*depth = 0.0; *depth = 0.0;
} else if *depth > max_depth { } else if *depth > max_depth {
@ -4067,9 +4184,9 @@ impl Window {
) -> Option<(HitboxId, crate::InspectorElementId)> { ) -> Option<(HitboxId, crate::InspectorElementId)> {
if let Some(pick_depth) = inspector.pick_depth { if let Some(pick_depth) = inspector.pick_depth {
let depth = (pick_depth as i64).try_into().unwrap_or(0); 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); 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) { if let Some(inspector_id) = frame.inspector_hitboxes.get(hitbox_id) {
return Some((*hitbox_id, inspector_id.clone())); return Some((*hitbox_id, inspector_id.clone()));
} }

View file

@ -3,6 +3,7 @@ mod path_range;
use base64::Engine as _; use base64::Engine as _;
use futures::FutureExt as _; use futures::FutureExt as _;
use gpui::HitboxBehavior;
use language::LanguageName; use language::LanguageName;
use log::Level; use log::Level;
pub use path_range::{LineCol, PathWithRange}; pub use path_range::{LineCol, PathWithRange};
@ -1211,7 +1212,7 @@ impl Element for MarkdownElement {
window.set_focus_handle(&focus_handle, cx); window.set_focus_handle(&focus_handle, cx);
window.set_view_id(self.markdown.entity_id()); 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); rendered_markdown.element.prepaint(window, cx);
self.autoscroll(&rendered_markdown.text, window, cx); self.autoscroll(&rendered_markdown.text, window, cx);
hitbox hitbox

View file

@ -136,7 +136,9 @@ pub struct IndentGuideLayout {
/// Implements the necessary functionality for rendering indent guides inside a uniform list. /// Implements the necessary functionality for rendering indent guides inside a uniform list.
mod uniform_list { mod uniform_list {
use gpui::{DispatchPhase, Hitbox, MouseButton, MouseDownEvent, MouseMoveEvent}; use gpui::{
DispatchPhase, Hitbox, HitboxBehavior, MouseButton, MouseDownEvent, MouseMoveEvent,
};
use super::*; use super::*;
@ -256,7 +258,12 @@ mod uniform_list {
.indent_guides .indent_guides
.as_ref() .as_ref()
.iter() .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(); .collect();
Self::PrepaintState::Interactive { Self::PrepaintState::Interactive {
hitboxes: Rc::new(hitboxes), hitboxes: Rc::new(hitboxes),

View file

@ -2,9 +2,9 @@ use std::{cell::RefCell, rc::Rc};
use gpui::{ use gpui::{
AnyElement, AnyView, App, Bounds, Corner, DismissEvent, DispatchPhase, Element, ElementId, AnyElement, AnyView, App, Bounds, Corner, DismissEvent, DispatchPhase, Element, ElementId,
Entity, Focusable as _, GlobalElementId, HitboxId, InteractiveElement, IntoElement, LayoutId, Entity, Focusable as _, GlobalElementId, HitboxBehavior, HitboxId, InteractiveElement,
Length, ManagedView, MouseDownEvent, ParentElement, Pixels, Point, Style, Window, anchored, IntoElement, LayoutId, Length, ManagedView, MouseDownEvent, ParentElement, Pixels, Point,
deferred, div, point, prelude::FluentBuilder, px, size, Style, Window, anchored, deferred, div, point, prelude::FluentBuilder, px, size,
}; };
use crate::prelude::*; use crate::prelude::*;
@ -421,7 +421,7 @@ impl<M: ManagedView> Element for PopoverMenu<M> {
((), element_state) ((), element_state)
}); });
window.insert_hitbox(bounds, false).id window.insert_hitbox(bounds, HitboxBehavior::Normal).id
}) })
} }

View file

@ -2,9 +2,9 @@ use std::{cell::RefCell, rc::Rc};
use gpui::{ use gpui::{
AnyElement, App, Bounds, Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, AnyElement, App, Bounds, Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity,
Focusable as _, GlobalElementId, Hitbox, InteractiveElement, IntoElement, LayoutId, Focusable as _, GlobalElementId, Hitbox, HitboxBehavior, InteractiveElement, IntoElement,
ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Window, anchored, LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Window,
deferred, div, px, anchored, deferred, div, px,
}; };
pub struct RightClickMenu<M: ManagedView> { pub struct RightClickMenu<M: ManagedView> {
@ -185,7 +185,7 @@ impl<M: ManagedView> Element for RightClickMenu<M> {
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> PrepaintState { ) -> 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() { if let Some(child) = request_layout.child_element.as_mut() {
child.prepaint(window, cx); child.prepaint(window, cx);

View file

@ -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 crate::{IntoElement, prelude::*, px, relative};
use gpui::{ use gpui::{
Along, App, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element, Along, App, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element,
ElementId, Entity, EntityId, GlobalElementId, Hitbox, Hsla, IsZero, LayoutId, ListState, ElementId, Entity, EntityId, GlobalElementId, Hitbox, HitboxBehavior, Hsla, IsZero, LayoutId,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, ScrollHandle, ScrollWheelEvent, ListState, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, ScrollHandle,
Size, Style, UniformListScrollHandle, Window, quad, ScrollWheelEvent, Size, Style, UniformListScrollHandle, Window, quad,
}; };
pub struct Scrollbar { pub struct Scrollbar {
@ -226,7 +226,7 @@ impl Element for Scrollbar {
_: &mut App, _: &mut App,
) -> Self::PrepaintState { ) -> Self::PrepaintState {
window.with_content_mask(Some(ContentMask { bounds }), |window| { window.with_content_mask(Some(ContentMask { bounds }), |window| {
window.insert_hitbox(bounds, false) window.insert_hitbox(bounds, HitboxBehavior::Normal)
}) })
} }

View file

@ -902,9 +902,9 @@ mod element {
use std::{cell::RefCell, iter, rc::Rc, sync::Arc}; use std::{cell::RefCell, iter, rc::Rc, sync::Arc};
use gpui::{ use gpui::{
Along, AnyElement, App, Axis, BorderStyle, Bounds, Element, GlobalElementId, IntoElement, Along, AnyElement, App, Axis, BorderStyle, Bounds, Element, GlobalElementId,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Size, Style, HitboxBehavior, IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement,
WeakEntity, Window, px, relative, size, Pixels, Point, Size, Style, WeakEntity, Window, px, relative, size,
}; };
use gpui::{CursorStyle, Hitbox}; use gpui::{CursorStyle, Hitbox};
use parking_lot::Mutex; use parking_lot::Mutex;
@ -1091,7 +1091,7 @@ mod element {
}; };
PaneAxisHandleLayout { PaneAxisHandleLayout {
hitbox: window.insert_hitbox(handle_bounds, true), hitbox: window.insert_hitbox(handle_bounds, HitboxBehavior::Normal),
divider_bounds, divider_bounds,
} }
} }

View file

@ -37,10 +37,10 @@ use futures::{
use gpui::{ use gpui::{
Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds, Context, Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds, Context,
CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle,
Focusable, Global, Hsla, KeyContext, Keystroke, ManagedView, MouseButton, PathPromptOptions, Focusable, Global, HitboxBehavior, Hsla, KeyContext, Keystroke, ManagedView, MouseButton,
Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription, Task, Tiling, WeakEntity, PathPromptOptions, Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription, Task,
WindowBounds, WindowHandle, WindowId, WindowOptions, action_as, actions, canvas, Tiling, WeakEntity, WindowBounds, WindowHandle, WindowId, WindowOptions, action_as, actions,
impl_action_as, impl_actions, point, relative, size, transparent_black, canvas, impl_action_as, impl_actions, point, relative, size, transparent_black,
}; };
pub use history_manager::*; pub use history_manager::*;
pub use item::{ pub use item::{
@ -7344,7 +7344,7 @@ pub fn client_side_decorations(
point(px(0.0), px(0.0)), point(px(0.0), px(0.0)),
window.window_bounds().get_bounds().size, window.window_bounds().get_bounds().size,
), ),
false, HitboxBehavior::Normal,
) )
}, },
move |_bounds, hitbox, window, cx| { move |_bounds, hitbox, window, cx| {