Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion crates/moon-gpui/src/elements/div.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,28 @@ pub trait InteractiveElement: Sized {
self
}

/// Take this element out of inspector picking, so a click or a hover selects whatever is
/// beneath it.
///
/// For an element that is there to POSITION other things rather than to be one: a transparent
/// layer stretched over a viewport so two scrollbar tracks can be absolute inside it, a spacer,
/// a gradient painted across a list. Picking has no way to tell those from a control, because
/// while it is on every element is given a hitbox — see [`Interactivity::should_insert_hitbox`]
/// — and the pick takes the topmost hitbox that carries an inspector id. Anything painted over
/// the content therefore answers for every point of it.
///
/// Measured: a terminal that documents its own interface through the inspector could not name
/// one control inside any scrollable surface — six tables and lists came back as their
/// scrollbar, at the size of the whole panel.
///
/// This changes NOTHING outside picking. A decorative layer has no listeners, no hover style
/// and no cursor, so it gets no hitbox in an ordinary frame either way, and the mouse has
/// always passed through it.
fn inspector_transparent(mut self) -> Self {
self.interactivity().inspector_transparent = true;
self
}

/// 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`].
Expand Down Expand Up @@ -1859,6 +1881,12 @@ pub struct Interactivity {
pub(crate) tooltip_show_delay: Option<Duration>,
pub(crate) window_control: Option<WindowControlArea>,
pub(crate) hitbox_behavior: HitboxBehavior,
/// Whether inspector picking should look straight through this element.
///
/// See [`InteractiveElement::inspector_transparent`]. Not behind `cfg(debug_assertions)`: a
/// `bool` on a struct this size costs nothing, and gating it would put a `cfg` on every element
/// that sets it.
pub(crate) inspector_transparent: bool,
pub(crate) tab_index: Option<isize>,
pub(crate) tab_group: bool,
pub(crate) tab_stop: bool,
Expand Down Expand Up @@ -2082,7 +2110,11 @@ impl Interactivity {
|| self.drag_listener.is_some()
|| !self.drop_listeners.is_empty()
|| self.tooltip_builder.is_some()
|| window.is_inspector_picking(cx)
// Picking gives EVERY element a hitbox, so that any of them can be selected — except
// the ones that asked not to be. Without the exception a transparent layer laid over a
// list is the topmost hitbox at every point of it, and the inspector can only ever name
// the layer. See [`InteractiveElement::inspector_transparent`].
|| (window.is_inspector_picking(cx) && !self.inspector_transparent)
}

fn clamp_scroll_position(
Expand Down
47 changes: 47 additions & 0 deletions crates/moon-gpui/src/elements/div/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -448,3 +448,50 @@ fn tooltip_hides_after_mouse_leaves_origin() {

assert!(active_tooltip.borrow().is_none());
}

/// Catches dropping the `inspector_transparent` term from `Interactivity::should_insert_hitbox`,
/// which would put a hitbox back on every decorative overlay while the inspector is picking.
///
/// The consequence is not visible and not local: picking takes the topmost hitbox that carries an
/// inspector id, so a transparent layer stretched over a list answers for every row under it. A
/// terminal that documents its own interface through the inspector could name nothing inside any
/// scrollable surface — six tables and lists all came back as their scrollbar, sized to the whole
/// panel.
#[test]
fn inspector_picking_looks_through_an_element_that_declared_itself_transparent() {
let mut test_app = TestAppContext::single();
let window = test_app.add_window(move |_, _| TestTooltipView);
let any_window = window.into();

test_app
.update_window(any_window, |_, window, cx| {
let style = Style::default();
let plain = Interactivity::default();
let transparent = Interactivity {
inspector_transparent: true,
..Default::default()
};

// Outside picking neither of them is interactive, so neither takes a hitbox — the
// half of the behaviour the flag must leave exactly as it was.
assert!(!plain.should_insert_hitbox(&style, window, cx));
assert!(!transparent.should_insert_hitbox(&style, window, cx));

// `Inspector::new` starts in picking mode, which is what gives every element a hitbox
// so that any of them can be selected.
window.toggle_inspector(cx);
assert!(
window.is_inspector_picking(cx),
"the inspector opens already picking"
);
assert!(
plain.should_insert_hitbox(&style, window, cx),
"picking offers every element"
);
assert!(
!transparent.should_insert_hitbox(&style, window, cx),
"except one that asked to be looked through"
);
})
.unwrap();
}
7 changes: 7 additions & 0 deletions crates/moon-ui-components/src/moon/scroll_area.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,15 @@ pub fn moon_scrollbar_overlay_with_palette(
window.request_animation_frame();
}

// Transparent to picking, because this layer is not a control: it spans the whole viewport for
// the sole purpose of giving the two tracks something to be absolute inside, and it carries no
// handler, no hover style and no state of its own. Painted after the content, it would
// otherwise be the topmost hitbox at every point of a list — and the inspector would answer
// "scrollbar" for every row in it. The track and the thumb below keep their own ids and are
// what a pointer is meant to find.
let mut layer = div()
.id(ElementId::from(id.clone()))
.inspector_transparent()
.absolute()
.top_0()
.right_0()
Expand Down
Loading