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
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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::<SmallVec<[_; 2]>>();
|
||||
|
||||
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),
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -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<dyn Any>, DragListener)>,
|
||||
pub(crate) hover_listener: Option<Box<dyn Fn(&bool, &mut Window, &mut App)>>,
|
||||
pub(crate) tooltip_builder: Option<TooltipBuilder>,
|
||||
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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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<M: ManagedView> Element for PopoverMenu<M> {
|
|||
((), element_state)
|
||||
});
|
||||
|
||||
window.insert_hitbox(bounds, false).id
|
||||
window.insert_hitbox(bounds, HitboxBehavior::Normal).id
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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<M: ManagedView> {
|
||||
|
@ -185,7 +185,7 @@ impl<M: ManagedView> Element for RightClickMenu<M> {
|
|||
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);
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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| {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue