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:
parent
2abc5893c1
commit
9086784038
16 changed files with 231 additions and 99 deletions
|
@ -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<Pixels>,
|
||||
/// The content mask when the hitbox was inserted.
|
||||
pub content_mask: ContentMask<Pixels>,
|
||||
/// 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<Pixels>) -> 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<AnyImageCache>,
|
||||
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<TooltipBounds>,
|
||||
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())),
|
||||
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<Pixels>, opaque: bool) -> Hitbox {
|
||||
pub fn insert_hitbox(&mut self, bounds: Bounds<Pixels>, 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()));
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue