refactor(shell): split 1062-line gpui_harness_tests god-component
`gpui_harness_tests.rs` was the repo's largest file (1062 lines) and the
worst violator of the 500-line / no-god-component audit. Its 13
`#[gpui::test]`s each build their own local fixtures and share only a
pair of type aliases, an `impl super::ElyShell` test helper, and two free
fns (`active_tab_overlay_state`, `example_url`).
Keep those shared items plus tests 1-4 in the root module; move tests 5-8
to `gpui_harness_tests_b.rs` and tests 9-13 to `gpui_harness_tests_c.rs`,
each opening with `use super::*;` so they inherit the parent's imports
and shared items with no per-item churn. Declared via `#[path]` mod, the
established sibling-test pattern in this crate.
475 / 319 / 276 lines. No tests added, removed, or renamed (paths gain a
`gpui_harness_tests_{b,c}::` segment). clippy --all-targets -D warnings
clean; `cargo test -p ely_app` 167 passed / 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -444,598 +444,6 @@ async fn baseline_overlay_under_overflow_hidden_relative_receives_click(cx: &mut
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bisect probe layer 2: stack the full ElyShell wrapper chain that
|
|
||||||
/// sits between the window root and the overlay — root size_full,
|
|
||||||
/// absolute inset_0 flex container, flex_1 + flex_col main pane with
|
|
||||||
/// rounded/border/shadow/overflow_hidden, flex_1 content wrapper,
|
|
||||||
/// then the relative+overflow_hidden surface wrapper from
|
|
||||||
/// `render_web_surface`. If this passes, the bug is in something
|
|
||||||
/// `render_external_web_canvas` adds (not in the plain layout chain).
|
|
||||||
#[gpui::test]
|
|
||||||
async fn baseline_overlay_under_full_elyshell_wrapper_chain_receives_click(
|
|
||||||
cx: &mut TestAppContext,
|
|
||||||
) {
|
|
||||||
let click_count = Rc::new(RefCell::new(0u32));
|
|
||||||
let counter_for_render = click_count.clone();
|
|
||||||
|
|
||||||
struct DeepProbe {
|
|
||||||
on_up_counter: Rc<RefCell<u32>>,
|
|
||||||
}
|
|
||||||
impl Render for DeepProbe {
|
|
||||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
let counter = self.on_up_counter.clone();
|
|
||||||
// Root: matches render_browser's outer div.
|
|
||||||
div().size_full().child(
|
|
||||||
// Absolute flex container matches render_browser's child layout.
|
|
||||||
div().absolute().inset_0().p(px(16.0)).gap(px(12.0)).flex().child(
|
|
||||||
// Main pane: matches render_main_pane.
|
|
||||||
div()
|
|
||||||
.flex_1()
|
|
||||||
.h_full()
|
|
||||||
.min_w_0()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.rounded(px(18.0))
|
|
||||||
.border_1()
|
|
||||||
.overflow_hidden()
|
|
||||||
.child(
|
|
||||||
// Content wrapper: matches the flex_1 child of main_pane.
|
|
||||||
div().flex_1().overflow_hidden().child(
|
|
||||||
// Surface wrapper: matches render_web_surface root.
|
|
||||||
div().relative().size_full().min_w_0().overflow_hidden().child(
|
|
||||||
div()
|
|
||||||
.absolute()
|
|
||||||
.size_full()
|
|
||||||
.occlude()
|
|
||||||
.on_mouse_down(MouseButton::Left, |_event, _window, _cx| {})
|
|
||||||
.capture_any_mouse_up(move |_event, _window, _cx| {
|
|
||||||
*counter.borrow_mut() += 1;
|
|
||||||
})
|
|
||||||
.on_mouse_move(|_event, _window, _cx| {})
|
|
||||||
.on_scroll_wheel(|_event, _window, _cx| {}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let (_probe, cx) =
|
|
||||||
cx.add_window_view(|_window, _cx| DeepProbe { on_up_counter: counter_for_render });
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
cx.simulate_mouse_move(point(px(400.0), px(400.0)), None, Modifiers::default());
|
|
||||||
cx.simulate_click(point(px(400.0), px(400.0)), Modifiers::default());
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
*click_count.borrow(),
|
|
||||||
1,
|
|
||||||
"Bisect layer 2: the full root → absolute-flex → main-pane → \
|
|
||||||
content-wrapper → surface-wrapper chain (sans listeners) \
|
|
||||||
should still deliver the click. If this fails, the bug is in \
|
|
||||||
this wrapper chain itself; if it passes, the bug is in \
|
|
||||||
something render_external_web_canvas adds (canvas tracker, \
|
|
||||||
absolute content wrapper sibling, or a gpui-component widget)."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Diagnostic probes (T13 bisect): each layer of the ElyShell render
|
|
||||||
/// tree was reproduced in isolation and **all passed**, confirming the
|
|
||||||
/// listener combo, layout wrapper chain, canvas sibling, entity
|
|
||||||
/// update side effects, and track_focus root listeners are NOT the
|
|
||||||
/// cause. The bug is elsewhere — likely in ElyShell::new's setup
|
|
||||||
/// (subscriptions, timer, InputState side effects) or in the
|
|
||||||
/// sync_address_input call inside navigate_active_tab that mutates
|
|
||||||
/// Input widget state which may invalidate the rendered_frame
|
|
||||||
/// between MouseMove and MouseUp. Kept for regression coverage.
|
|
||||||
/// T13 layer 6: replicate ElyShell::new's gpui-component widget
|
|
||||||
/// construction (InputState creation + subscription) on top of the
|
|
||||||
/// passing layout, then click. If this fails, the InputState entity
|
|
||||||
/// or its subscription is what breaks hit_test for descendant
|
|
||||||
/// occlude divs.
|
|
||||||
#[gpui::test]
|
|
||||||
async fn baseline_overlay_with_input_state_construction_receives_click(cx: &mut TestAppContext) {
|
|
||||||
use gpui::AppContext;
|
|
||||||
use gpui::Entity;
|
|
||||||
use gpui::Subscription;
|
|
||||||
use gpui_component::input::{InputEvent, InputState};
|
|
||||||
|
|
||||||
cx.update(gpui_component::init);
|
|
||||||
|
|
||||||
let click_count = Rc::new(RefCell::new(0u32));
|
|
||||||
let counter_for_render = click_count.clone();
|
|
||||||
|
|
||||||
struct ProbeWithInput {
|
|
||||||
on_up_counter: Rc<RefCell<u32>>,
|
|
||||||
_command_input: Entity<InputState>,
|
|
||||||
_command_subscription: Subscription,
|
|
||||||
}
|
|
||||||
impl Render for ProbeWithInput {
|
|
||||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
let counter = self.on_up_counter.clone();
|
|
||||||
div().relative().size_full().min_w_0().overflow_hidden().child(
|
|
||||||
div()
|
|
||||||
.absolute()
|
|
||||||
.size_full()
|
|
||||||
.occlude()
|
|
||||||
.on_mouse_down(MouseButton::Left, |_e, _w, _c| {})
|
|
||||||
.capture_any_mouse_up(move |_e, _w, _c| {
|
|
||||||
*counter.borrow_mut() += 1;
|
|
||||||
})
|
|
||||||
.on_mouse_move(|_e, _w, _c| {})
|
|
||||||
.on_scroll_wheel(|_e, _w, _c| {}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let (_probe, cx) = cx.add_window_view(|window, cx| {
|
|
||||||
let command_input = cx.new(|cx| InputState::new(window, cx).placeholder("test"));
|
|
||||||
let command_subscription = cx.subscribe_in(
|
|
||||||
&command_input,
|
|
||||||
window,
|
|
||||||
|_probe: &mut ProbeWithInput, _input, _event: &InputEvent, _window, _cx| {
|
|
||||||
// mimic the shape ElyShell::new uses
|
|
||||||
},
|
|
||||||
);
|
|
||||||
ProbeWithInput {
|
|
||||||
on_up_counter: counter_for_render,
|
|
||||||
_command_input: command_input,
|
|
||||||
_command_subscription: command_subscription,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
cx.simulate_mouse_move(point(px(400.0), px(400.0)), None, Modifiers::default());
|
|
||||||
cx.simulate_click(point(px(400.0), px(400.0)), Modifiers::default());
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
*click_count.borrow(),
|
|
||||||
1,
|
|
||||||
"Bisect layer 6: an InputState entity + a subscribe_in to it \
|
|
||||||
must not break click delivery to a sibling occlude div. If \
|
|
||||||
this fails, the InputState construction (which spawns \
|
|
||||||
BlinkCursor, registers window-activation/focus/blur \
|
|
||||||
observers) corrupts hit_test for the rest of the tree."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// T13 next-layer diagnostic: same shape as the passing baseline
|
|
||||||
/// (`.relative().size_full().min_w_0().overflow_hidden()` parent +
|
|
||||||
/// the input_overlay listener combo) but with `gpui_component::init`
|
|
||||||
/// called first. The real ElyShell test calls init; the probes
|
|
||||||
/// don't. If this fails, `gpui_component::init`'s side effect on
|
|
||||||
/// the App is what breaks the rendered_frame's hitbox registration
|
|
||||||
/// for descendant occlude divs.
|
|
||||||
#[gpui::test]
|
|
||||||
async fn baseline_overlay_after_gpui_component_init_receives_click(cx: &mut TestAppContext) {
|
|
||||||
cx.update(gpui_component::init);
|
|
||||||
|
|
||||||
let click_count = Rc::new(RefCell::new(0u32));
|
|
||||||
let counter_for_render = click_count.clone();
|
|
||||||
|
|
||||||
struct AfterInitProbe {
|
|
||||||
on_up_counter: Rc<RefCell<u32>>,
|
|
||||||
}
|
|
||||||
impl Render for AfterInitProbe {
|
|
||||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
let counter = self.on_up_counter.clone();
|
|
||||||
div().relative().size_full().min_w_0().overflow_hidden().child(
|
|
||||||
div()
|
|
||||||
.absolute()
|
|
||||||
.size_full()
|
|
||||||
.occlude()
|
|
||||||
.on_mouse_down(MouseButton::Left, |_e, _w, _c| {})
|
|
||||||
.capture_any_mouse_up(move |_e, _w, _c| {
|
|
||||||
*counter.borrow_mut() += 1;
|
|
||||||
})
|
|
||||||
.on_mouse_move(|_e, _w, _c| {})
|
|
||||||
.on_scroll_wheel(|_e, _w, _c| {}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let (_probe, cx) =
|
|
||||||
cx.add_window_view(|_window, _cx| AfterInitProbe { on_up_counter: counter_for_render });
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
cx.simulate_mouse_move(point(px(400.0), px(400.0)), None, Modifiers::default());
|
|
||||||
cx.simulate_click(point(px(400.0), px(400.0)), Modifiers::default());
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
*click_count.borrow(),
|
|
||||||
1,
|
|
||||||
"Bisect: gpui_component::init must not break click delivery to \
|
|
||||||
an occlude div with the input_overlay listener combo. If this \
|
|
||||||
fails, init registers some App-level state that interferes \
|
|
||||||
with rendered_frame hitbox registration for descendants."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Bisect probe layer 5: the full chain WITH the root-level
|
|
||||||
/// `track_focus + on_mouse_up(Left, bubble)` listeners ElyShell
|
|
||||||
/// puts on its outermost div. If track_focus's auto-focus MouseDown
|
|
||||||
/// handler, or the root's bubble-phase MouseUp listener, somehow
|
|
||||||
/// invalidates the rendered_frame between MouseDown and MouseUp,
|
|
||||||
/// input_overlay's hitbox will no longer match the listener
|
|
||||||
/// snapshot's id and `is_hovered` will return false. That's the
|
|
||||||
/// exact symptom we see: hover_point is None too, so it's not
|
|
||||||
/// capture-specific — every listener on input_overlay is missing
|
|
||||||
/// its hit.
|
|
||||||
#[gpui::test]
|
|
||||||
async fn baseline_overlay_under_root_with_track_focus_receives_click(cx: &mut TestAppContext) {
|
|
||||||
use gpui::FocusHandle;
|
|
||||||
|
|
||||||
let click_count = Rc::new(RefCell::new(0u32));
|
|
||||||
let move_count = Rc::new(RefCell::new(0u32));
|
|
||||||
let counter_for_up = click_count.clone();
|
|
||||||
let counter_for_move = move_count.clone();
|
|
||||||
|
|
||||||
struct TrackFocusProbe {
|
|
||||||
focus: FocusHandle,
|
|
||||||
on_up_counter: Rc<RefCell<u32>>,
|
|
||||||
on_move_counter: Rc<RefCell<u32>>,
|
|
||||||
}
|
|
||||||
impl Render for TrackFocusProbe {
|
|
||||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
let counter_up = self.on_up_counter.clone();
|
|
||||||
let counter_move = self.on_move_counter.clone();
|
|
||||||
div()
|
|
||||||
.size_full()
|
|
||||||
.track_focus(&self.focus)
|
|
||||||
.on_mouse_up(MouseButton::Left, |_event, _window, _cx| {})
|
|
||||||
.child(
|
|
||||||
div().absolute().inset_0().p(px(16.0)).gap(px(12.0)).flex().child(
|
|
||||||
div()
|
|
||||||
.flex_1()
|
|
||||||
.h_full()
|
|
||||||
.min_w_0()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.rounded(px(18.0))
|
|
||||||
.border_1()
|
|
||||||
.overflow_hidden()
|
|
||||||
.child(
|
|
||||||
div().flex_1().overflow_hidden().child(
|
|
||||||
div()
|
|
||||||
.relative()
|
|
||||||
.size_full()
|
|
||||||
.min_w_0()
|
|
||||||
.overflow_hidden()
|
|
||||||
.child(div().absolute().inset_0().child(div().size_full()))
|
|
||||||
.child(
|
|
||||||
canvas(move |_b, _w, _c| {}, |_, _, _, _| {})
|
|
||||||
.absolute()
|
|
||||||
.size_full(),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.absolute()
|
|
||||||
.size_full()
|
|
||||||
.occlude()
|
|
||||||
.on_mouse_down(MouseButton::Left, |_e, _w, _c| {})
|
|
||||||
.capture_any_mouse_up(move |_e, _w, _c| {
|
|
||||||
*counter_up.borrow_mut() += 1;
|
|
||||||
})
|
|
||||||
.on_mouse_move(move |_e, _w, _c| {
|
|
||||||
*counter_move.borrow_mut() += 1;
|
|
||||||
})
|
|
||||||
.on_scroll_wheel(|_e, _w, _c| {}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let (_probe, cx) = cx.add_window_view(|_window, cx| TrackFocusProbe {
|
|
||||||
focus: cx.focus_handle(),
|
|
||||||
on_up_counter: counter_for_up,
|
|
||||||
on_move_counter: counter_for_move,
|
|
||||||
});
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
cx.simulate_mouse_move(point(px(400.0), px(400.0)), None, Modifiers::default());
|
|
||||||
cx.run_until_parked();
|
|
||||||
cx.simulate_click(point(px(400.0), px(400.0)), Modifiers::default());
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
*move_count.borrow() > 0,
|
|
||||||
"Bisect layer 5: input_overlay's on_mouse_move never fired even \
|
|
||||||
though the cursor was simulated over it. The same `is_hovered` \
|
|
||||||
check fails for every listener, exactly mirroring the T7 red \
|
|
||||||
test's observation that hover_point is None."
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
*click_count.borrow(),
|
|
||||||
1,
|
|
||||||
"Bisect layer 5: capture_any_mouse_up did not fire under the \
|
|
||||||
full track_focus + bubble-mouse_up root chain. This isolates \
|
|
||||||
the culprit to the root-level listeners that ElyShell adds \
|
|
||||||
around its widget tree."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Bisect probe layer 4: same shape as the canvas-sibling probe but
|
|
||||||
/// the on_mouse_down listener calls `cx.update` on a self-entity
|
|
||||||
/// (mirroring `down_entity.update(cx, |shell, _| shell.focus_web_surface(window))`).
|
|
||||||
/// `entity.update` notifies subscribers; if that side effect during
|
|
||||||
/// mouse_down's bubble phase disturbs mouse dispatch — invalidates
|
|
||||||
/// the rendered_frame, regenerates hitboxes, or otherwise corrupts
|
|
||||||
/// the in-flight dispatch — the subsequent MouseUp will land on a
|
|
||||||
/// frame whose hitboxes no longer match the listeners' captured
|
|
||||||
/// snapshots, and this test will go red.
|
|
||||||
#[gpui::test]
|
|
||||||
async fn baseline_overlay_with_entity_update_in_mouse_down_receives_click(cx: &mut TestAppContext) {
|
|
||||||
let click_count = Rc::new(RefCell::new(0u32));
|
|
||||||
let counter_for_render = click_count.clone();
|
|
||||||
|
|
||||||
struct EntityUpdateProbe {
|
|
||||||
on_up_counter: Rc<RefCell<u32>>,
|
|
||||||
tick: u32,
|
|
||||||
}
|
|
||||||
impl Render for EntityUpdateProbe {
|
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
let counter = self.on_up_counter.clone();
|
|
||||||
let self_entity = cx.entity().clone();
|
|
||||||
div()
|
|
||||||
.relative()
|
|
||||||
.size_full()
|
|
||||||
.min_w_0()
|
|
||||||
.overflow_hidden()
|
|
||||||
.child(div().absolute().inset_0().child(div().size_full()))
|
|
||||||
.child(
|
|
||||||
canvas(move |_bounds, _window, _cx| {}, |_, _, _, _| {}).absolute().size_full(),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.absolute()
|
|
||||||
.size_full()
|
|
||||||
.occlude()
|
|
||||||
.on_mouse_down(MouseButton::Left, move |_event, _window, cx| {
|
|
||||||
self_entity.update(cx, |probe, _cx| {
|
|
||||||
probe.tick += 1;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.capture_any_mouse_up(move |_event, _window, _cx| {
|
|
||||||
*counter.borrow_mut() += 1;
|
|
||||||
})
|
|
||||||
.on_mouse_move(|_event, _window, _cx| {})
|
|
||||||
.on_scroll_wheel(|_event, _window, _cx| {}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let (_probe, cx) = cx.add_window_view(|_window, _cx| EntityUpdateProbe {
|
|
||||||
on_up_counter: counter_for_render,
|
|
||||||
tick: 0,
|
|
||||||
});
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
cx.simulate_mouse_move(point(px(400.0), px(400.0)), None, Modifiers::default());
|
|
||||||
cx.simulate_click(point(px(400.0), px(400.0)), Modifiers::default());
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
*click_count.borrow(),
|
|
||||||
1,
|
|
||||||
"Bisect layer 4: on_mouse_down's bubble fires entity.update \
|
|
||||||
which automatically notifies subscribers. If this test fails, \
|
|
||||||
the cx.notify side effect during in-flight dispatch corrupts \
|
|
||||||
the rendered_frame for the immediately-following MouseUp."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Bisect probe layer 3: add a canvas sibling BEFORE the overlay,
|
|
||||||
/// matching render_web_surface's viewport_tracker sibling exactly.
|
|
||||||
/// The canvas's prepaint callback fires during paint phase; if it
|
|
||||||
/// somehow disturbs hitbox registration or mouse_listeners ordering,
|
|
||||||
/// this test will go red and pinpoint the suspect.
|
|
||||||
#[gpui::test]
|
|
||||||
async fn baseline_overlay_with_canvas_sibling_receives_click(cx: &mut TestAppContext) {
|
|
||||||
let click_count = Rc::new(RefCell::new(0u32));
|
|
||||||
let counter_for_render = click_count.clone();
|
|
||||||
|
|
||||||
struct CanvasSiblingProbe {
|
|
||||||
on_up_counter: Rc<RefCell<u32>>,
|
|
||||||
}
|
|
||||||
impl Render for CanvasSiblingProbe {
|
|
||||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
let counter = self.on_up_counter.clone();
|
|
||||||
div()
|
|
||||||
.relative()
|
|
||||||
.size_full()
|
|
||||||
.min_w_0()
|
|
||||||
.overflow_hidden()
|
|
||||||
.child(div().absolute().inset_0().child(div().size_full()))
|
|
||||||
.child(
|
|
||||||
canvas(move |_bounds, _window, _cx| {}, |_, _, _, _| {}).absolute().size_full(),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.absolute()
|
|
||||||
.size_full()
|
|
||||||
.occlude()
|
|
||||||
.on_mouse_down(MouseButton::Left, |_event, _window, _cx| {})
|
|
||||||
.capture_any_mouse_up(move |_event, _window, _cx| {
|
|
||||||
*counter.borrow_mut() += 1;
|
|
||||||
})
|
|
||||||
.on_mouse_move(|_event, _window, _cx| {})
|
|
||||||
.on_scroll_wheel(|_event, _window, _cx| {}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let (_probe, cx) =
|
|
||||||
cx.add_window_view(|_window, _cx| CanvasSiblingProbe { on_up_counter: counter_for_render });
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
cx.simulate_mouse_move(point(px(400.0), px(400.0)), None, Modifiers::default());
|
|
||||||
cx.simulate_click(point(px(400.0), px(400.0)), Modifiers::default());
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
*click_count.borrow(),
|
|
||||||
1,
|
|
||||||
"Bisect layer 3: adding a canvas sibling (the shape \
|
|
||||||
render_viewport_tracker uses) between the content wrapper and \
|
|
||||||
the overlay should not break click delivery. If this fails, \
|
|
||||||
the canvas element itself disturbs mouse dispatch — likely \
|
|
||||||
via its prepaint callback's interaction with hit_test."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn baseline_overlay_with_full_listener_combo_receives_click(cx: &mut TestAppContext) {
|
|
||||||
let click_count = Rc::new(RefCell::new(0u32));
|
|
||||||
let counter_for_render = click_count.clone();
|
|
||||||
|
|
||||||
struct ComboProbe {
|
|
||||||
on_up_counter: Rc<RefCell<u32>>,
|
|
||||||
}
|
|
||||||
impl Render for ComboProbe {
|
|
||||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
let counter = self.on_up_counter.clone();
|
|
||||||
div().relative().size_full().child(
|
|
||||||
div()
|
|
||||||
.absolute()
|
|
||||||
.size_full()
|
|
||||||
.occlude()
|
|
||||||
.on_mouse_down(MouseButton::Left, |_event, _window, _cx| {})
|
|
||||||
.capture_any_mouse_up(move |_event, _window, _cx| {
|
|
||||||
*counter.borrow_mut() += 1;
|
|
||||||
})
|
|
||||||
.on_mouse_move(|_event, _window, _cx| {})
|
|
||||||
.on_scroll_wheel(|_event, _window, _cx| {}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let (_probe, cx) =
|
|
||||||
cx.add_window_view(|_window, _cx| ComboProbe { on_up_counter: counter_for_render });
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
cx.simulate_mouse_move(point(px(100.0), px(100.0)), None, Modifiers::default());
|
|
||||||
cx.simulate_click(point(px(100.0), px(100.0)), Modifiers::default());
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
*click_count.borrow(),
|
|
||||||
1,
|
|
||||||
"GPUI baseline with input_overlay's full listener combo: click should \
|
|
||||||
reach capture_any_mouse_up. If this fails, the listener combo itself \
|
|
||||||
is the problem, not the surrounding shell."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn baseline_overlay_div_receives_simulated_click(cx: &mut TestAppContext) {
|
|
||||||
let click_count = Rc::new(RefCell::new(0u32));
|
|
||||||
let counter_for_render = click_count.clone();
|
|
||||||
|
|
||||||
struct Probe {
|
|
||||||
on_up_counter: Rc<RefCell<u32>>,
|
|
||||||
}
|
|
||||||
impl Render for Probe {
|
|
||||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
let counter = self.on_up_counter.clone();
|
|
||||||
div().relative().size_full().child(
|
|
||||||
div().absolute().size_full().occlude().capture_any_mouse_up(
|
|
||||||
move |_event, _window, _cx| {
|
|
||||||
*counter.borrow_mut() += 1;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let (_probe, cx) =
|
|
||||||
cx.add_window_view(|_window, _cx| Probe { on_up_counter: counter_for_render });
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
cx.simulate_mouse_move(point(px(100.0), px(100.0)), None, Modifiers::default());
|
|
||||||
cx.simulate_click(point(px(100.0), px(100.0)), Modifiers::default());
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
*click_count.borrow(),
|
|
||||||
1,
|
|
||||||
"GPUI baseline: a div with .absolute().size_full().occlude() and \
|
|
||||||
capture_any_mouse_up never received the simulated click. The test \
|
|
||||||
harness or GPUI primitive is broken — ElyShell test results are \
|
|
||||||
meaningless until this passes."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// TDD red guard for T10: today every `WebSurfaceFrame::from_live_frame`
|
|
||||||
/// call allocates a fresh `Arc::new(RenderImage::new(...))` regardless
|
|
||||||
/// of whether the underlying pixels changed. At 60 fps on a 1080p
|
|
||||||
/// canvas that is `~8 MB / frame` of host-side RGBA cloning + a new
|
|
||||||
/// GPUI texture upload, the cost Linus + Karpathy + Jony all flagged
|
|
||||||
/// as the next material bottleneck after the file-system pipe.
|
|
||||||
///
|
|
||||||
/// The contract this test pins is the cheapest invariant we can hold
|
|
||||||
/// against today's `SoftwareRenderingContext`: two frames carrying
|
|
||||||
/// **byte-identical RGBA payloads must produce the same underlying
|
|
||||||
/// `Arc<RenderImage>`**. Today they do not — every `from_live_frame`
|
|
||||||
/// blindly reallocates. The fix path is either dedup the upload
|
|
||||||
/// against the last bytes or switch to direct platform-surface presentation.
|
|
||||||
///
|
|
||||||
/// Regression guard: with the single-slot `LAST_FRAME_IMAGE` cache in
|
|
||||||
/// `web_surface_frame.rs`, two `ServoLiveFrame` inputs carrying
|
|
||||||
/// byte-identical RGBA payloads now share the same `Arc<RenderImage>`.
|
|
||||||
/// Without this guard a regression that drops the cache silently
|
|
||||||
/// returns to ~960 MB/s of host-side RGBA cloning + per-frame GPUI
|
|
||||||
/// texture allocations.
|
|
||||||
#[test]
|
|
||||||
fn identical_live_frames_share_render_image_arc() -> Result<(), String> {
|
|
||||||
let width = 16u32;
|
|
||||||
let height = 8u32;
|
|
||||||
let rgba_bytes = vec![0xAAu8; (width as usize) * (height as usize) * 4];
|
|
||||||
|
|
||||||
let first = WebSurfaceFrame::from_live_frame(
|
|
||||||
"https://example.com/".to_string(),
|
|
||||||
WebSurfaceScrollOffset::default(),
|
|
||||||
100,
|
|
||||||
ServoLiveFrame::for_test(width, height, rgba_bytes.clone()),
|
|
||||||
)
|
|
||||||
.map_err(|error| error.to_string())?;
|
|
||||||
let second = WebSurfaceFrame::from_live_frame(
|
|
||||||
"https://example.com/".to_string(),
|
|
||||||
WebSurfaceScrollOffset::default(),
|
|
||||||
100,
|
|
||||||
ServoLiveFrame::for_test(width, height, rgba_bytes),
|
|
||||||
)
|
|
||||||
.map_err(|error| error.to_string())?;
|
|
||||||
|
|
||||||
let first_image = first
|
|
||||||
.image
|
|
||||||
.as_ref()
|
|
||||||
.ok_or_else(|| "software path must produce an Arc<RenderImage>".to_string())?;
|
|
||||||
let second_image = second
|
|
||||||
.image
|
|
||||||
.as_ref()
|
|
||||||
.ok_or_else(|| "software path must produce an Arc<RenderImage>".to_string())?;
|
|
||||||
assert!(
|
|
||||||
Arc::ptr_eq(first_image, second_image),
|
|
||||||
"TDD red: two ServoLiveFrames with byte-identical RGBA produced \
|
|
||||||
distinct Arc<RenderImage> instances (first={:p}, second={:p}). \
|
|
||||||
WebSurfaceFrame::from_parts must dedup the upload against the \
|
|
||||||
previous frame's bytes, or the rendering pipeline must switch \
|
|
||||||
to direct platform-surface presentation so per-frame host \
|
|
||||||
allocations stop entirely.",
|
|
||||||
Arc::as_ptr(first_image),
|
|
||||||
Arc::as_ptr(second_image),
|
|
||||||
);
|
|
||||||
assert!(first.has_same_software_render_as(&second));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn active_tab_overlay_state(
|
fn active_tab_overlay_state(
|
||||||
shell: &gpui::Entity<super::ElyShell>,
|
shell: &gpui::Entity<super::ElyShell>,
|
||||||
cx: &mut gpui::VisualTestContext,
|
cx: &mut gpui::VisualTestContext,
|
||||||
@@ -1060,3 +468,8 @@ fn active_tab_overlay_state(
|
|||||||
fn example_url() -> Result<UrlText, ely_domain::DomainError> {
|
fn example_url() -> Result<UrlText, ely_domain::DomainError> {
|
||||||
UrlText::parse("https://example.com/".to_string())
|
UrlText::parse("https://example.com/".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[path = "gpui_harness_tests_b.rs"]
|
||||||
|
mod gpui_harness_tests_b;
|
||||||
|
#[path = "gpui_harness_tests_c.rs"]
|
||||||
|
mod gpui_harness_tests_c;
|
||||||
|
|||||||
@@ -0,0 +1,319 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Bisect probe layer 2: stack the full ElyShell wrapper chain that
|
||||||
|
/// sits between the window root and the overlay — root size_full,
|
||||||
|
/// absolute inset_0 flex container, flex_1 + flex_col main pane with
|
||||||
|
/// rounded/border/shadow/overflow_hidden, flex_1 content wrapper,
|
||||||
|
/// then the relative+overflow_hidden surface wrapper from
|
||||||
|
/// `render_web_surface`. If this passes, the bug is in something
|
||||||
|
/// `render_external_web_canvas` adds (not in the plain layout chain).
|
||||||
|
#[gpui::test]
|
||||||
|
async fn baseline_overlay_under_full_elyshell_wrapper_chain_receives_click(
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
let click_count = Rc::new(RefCell::new(0u32));
|
||||||
|
let counter_for_render = click_count.clone();
|
||||||
|
|
||||||
|
struct DeepProbe {
|
||||||
|
on_up_counter: Rc<RefCell<u32>>,
|
||||||
|
}
|
||||||
|
impl Render for DeepProbe {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let counter = self.on_up_counter.clone();
|
||||||
|
// Root: matches render_browser's outer div.
|
||||||
|
div().size_full().child(
|
||||||
|
// Absolute flex container matches render_browser's child layout.
|
||||||
|
div().absolute().inset_0().p(px(16.0)).gap(px(12.0)).flex().child(
|
||||||
|
// Main pane: matches render_main_pane.
|
||||||
|
div()
|
||||||
|
.flex_1()
|
||||||
|
.h_full()
|
||||||
|
.min_w_0()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.rounded(px(18.0))
|
||||||
|
.border_1()
|
||||||
|
.overflow_hidden()
|
||||||
|
.child(
|
||||||
|
// Content wrapper: matches the flex_1 child of main_pane.
|
||||||
|
div().flex_1().overflow_hidden().child(
|
||||||
|
// Surface wrapper: matches render_web_surface root.
|
||||||
|
div().relative().size_full().min_w_0().overflow_hidden().child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.size_full()
|
||||||
|
.occlude()
|
||||||
|
.on_mouse_down(MouseButton::Left, |_event, _window, _cx| {})
|
||||||
|
.capture_any_mouse_up(move |_event, _window, _cx| {
|
||||||
|
*counter.borrow_mut() += 1;
|
||||||
|
})
|
||||||
|
.on_mouse_move(|_event, _window, _cx| {})
|
||||||
|
.on_scroll_wheel(|_event, _window, _cx| {}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (_probe, cx) =
|
||||||
|
cx.add_window_view(|_window, _cx| DeepProbe { on_up_counter: counter_for_render });
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
cx.simulate_mouse_move(point(px(400.0), px(400.0)), None, Modifiers::default());
|
||||||
|
cx.simulate_click(point(px(400.0), px(400.0)), Modifiers::default());
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*click_count.borrow(),
|
||||||
|
1,
|
||||||
|
"Bisect layer 2: the full root → absolute-flex → main-pane → \
|
||||||
|
content-wrapper → surface-wrapper chain (sans listeners) \
|
||||||
|
should still deliver the click. If this fails, the bug is in \
|
||||||
|
this wrapper chain itself; if it passes, the bug is in \
|
||||||
|
something render_external_web_canvas adds (canvas tracker, \
|
||||||
|
absolute content wrapper sibling, or a gpui-component widget)."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diagnostic probes (T13 bisect): each layer of the ElyShell render
|
||||||
|
/// tree was reproduced in isolation and **all passed**, confirming the
|
||||||
|
/// listener combo, layout wrapper chain, canvas sibling, entity
|
||||||
|
/// update side effects, and track_focus root listeners are NOT the
|
||||||
|
/// cause. The bug is elsewhere — likely in ElyShell::new's setup
|
||||||
|
/// (subscriptions, timer, InputState side effects) or in the
|
||||||
|
/// sync_address_input call inside navigate_active_tab that mutates
|
||||||
|
/// Input widget state which may invalidate the rendered_frame
|
||||||
|
/// between MouseMove and MouseUp. Kept for regression coverage.
|
||||||
|
/// T13 layer 6: replicate ElyShell::new's gpui-component widget
|
||||||
|
/// construction (InputState creation + subscription) on top of the
|
||||||
|
/// passing layout, then click. If this fails, the InputState entity
|
||||||
|
/// or its subscription is what breaks hit_test for descendant
|
||||||
|
/// occlude divs.
|
||||||
|
#[gpui::test]
|
||||||
|
async fn baseline_overlay_with_input_state_construction_receives_click(cx: &mut TestAppContext) {
|
||||||
|
use gpui::AppContext;
|
||||||
|
use gpui::Entity;
|
||||||
|
use gpui::Subscription;
|
||||||
|
use gpui_component::input::{InputEvent, InputState};
|
||||||
|
|
||||||
|
cx.update(gpui_component::init);
|
||||||
|
|
||||||
|
let click_count = Rc::new(RefCell::new(0u32));
|
||||||
|
let counter_for_render = click_count.clone();
|
||||||
|
|
||||||
|
struct ProbeWithInput {
|
||||||
|
on_up_counter: Rc<RefCell<u32>>,
|
||||||
|
_command_input: Entity<InputState>,
|
||||||
|
_command_subscription: Subscription,
|
||||||
|
}
|
||||||
|
impl Render for ProbeWithInput {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let counter = self.on_up_counter.clone();
|
||||||
|
div().relative().size_full().min_w_0().overflow_hidden().child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.size_full()
|
||||||
|
.occlude()
|
||||||
|
.on_mouse_down(MouseButton::Left, |_e, _w, _c| {})
|
||||||
|
.capture_any_mouse_up(move |_e, _w, _c| {
|
||||||
|
*counter.borrow_mut() += 1;
|
||||||
|
})
|
||||||
|
.on_mouse_move(|_e, _w, _c| {})
|
||||||
|
.on_scroll_wheel(|_e, _w, _c| {}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (_probe, cx) = cx.add_window_view(|window, cx| {
|
||||||
|
let command_input = cx.new(|cx| InputState::new(window, cx).placeholder("test"));
|
||||||
|
let command_subscription = cx.subscribe_in(
|
||||||
|
&command_input,
|
||||||
|
window,
|
||||||
|
|_probe: &mut ProbeWithInput, _input, _event: &InputEvent, _window, _cx| {
|
||||||
|
// mimic the shape ElyShell::new uses
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ProbeWithInput {
|
||||||
|
on_up_counter: counter_for_render,
|
||||||
|
_command_input: command_input,
|
||||||
|
_command_subscription: command_subscription,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
cx.simulate_mouse_move(point(px(400.0), px(400.0)), None, Modifiers::default());
|
||||||
|
cx.simulate_click(point(px(400.0), px(400.0)), Modifiers::default());
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*click_count.borrow(),
|
||||||
|
1,
|
||||||
|
"Bisect layer 6: an InputState entity + a subscribe_in to it \
|
||||||
|
must not break click delivery to a sibling occlude div. If \
|
||||||
|
this fails, the InputState construction (which spawns \
|
||||||
|
BlinkCursor, registers window-activation/focus/blur \
|
||||||
|
observers) corrupts hit_test for the rest of the tree."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// T13 next-layer diagnostic: same shape as the passing baseline
|
||||||
|
/// (`.relative().size_full().min_w_0().overflow_hidden()` parent +
|
||||||
|
/// the input_overlay listener combo) but with `gpui_component::init`
|
||||||
|
/// called first. The real ElyShell test calls init; the probes
|
||||||
|
/// don't. If this fails, `gpui_component::init`'s side effect on
|
||||||
|
/// the App is what breaks the rendered_frame's hitbox registration
|
||||||
|
/// for descendant occlude divs.
|
||||||
|
#[gpui::test]
|
||||||
|
async fn baseline_overlay_after_gpui_component_init_receives_click(cx: &mut TestAppContext) {
|
||||||
|
cx.update(gpui_component::init);
|
||||||
|
|
||||||
|
let click_count = Rc::new(RefCell::new(0u32));
|
||||||
|
let counter_for_render = click_count.clone();
|
||||||
|
|
||||||
|
struct AfterInitProbe {
|
||||||
|
on_up_counter: Rc<RefCell<u32>>,
|
||||||
|
}
|
||||||
|
impl Render for AfterInitProbe {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let counter = self.on_up_counter.clone();
|
||||||
|
div().relative().size_full().min_w_0().overflow_hidden().child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.size_full()
|
||||||
|
.occlude()
|
||||||
|
.on_mouse_down(MouseButton::Left, |_e, _w, _c| {})
|
||||||
|
.capture_any_mouse_up(move |_e, _w, _c| {
|
||||||
|
*counter.borrow_mut() += 1;
|
||||||
|
})
|
||||||
|
.on_mouse_move(|_e, _w, _c| {})
|
||||||
|
.on_scroll_wheel(|_e, _w, _c| {}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (_probe, cx) =
|
||||||
|
cx.add_window_view(|_window, _cx| AfterInitProbe { on_up_counter: counter_for_render });
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
cx.simulate_mouse_move(point(px(400.0), px(400.0)), None, Modifiers::default());
|
||||||
|
cx.simulate_click(point(px(400.0), px(400.0)), Modifiers::default());
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*click_count.borrow(),
|
||||||
|
1,
|
||||||
|
"Bisect: gpui_component::init must not break click delivery to \
|
||||||
|
an occlude div with the input_overlay listener combo. If this \
|
||||||
|
fails, init registers some App-level state that interferes \
|
||||||
|
with rendered_frame hitbox registration for descendants."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bisect probe layer 5: the full chain WITH the root-level
|
||||||
|
/// `track_focus + on_mouse_up(Left, bubble)` listeners ElyShell
|
||||||
|
/// puts on its outermost div. If track_focus's auto-focus MouseDown
|
||||||
|
/// handler, or the root's bubble-phase MouseUp listener, somehow
|
||||||
|
/// invalidates the rendered_frame between MouseDown and MouseUp,
|
||||||
|
/// input_overlay's hitbox will no longer match the listener
|
||||||
|
/// snapshot's id and `is_hovered` will return false. That's the
|
||||||
|
/// exact symptom we see: hover_point is None too, so it's not
|
||||||
|
/// capture-specific — every listener on input_overlay is missing
|
||||||
|
/// its hit.
|
||||||
|
#[gpui::test]
|
||||||
|
async fn baseline_overlay_under_root_with_track_focus_receives_click(cx: &mut TestAppContext) {
|
||||||
|
use gpui::FocusHandle;
|
||||||
|
|
||||||
|
let click_count = Rc::new(RefCell::new(0u32));
|
||||||
|
let move_count = Rc::new(RefCell::new(0u32));
|
||||||
|
let counter_for_up = click_count.clone();
|
||||||
|
let counter_for_move = move_count.clone();
|
||||||
|
|
||||||
|
struct TrackFocusProbe {
|
||||||
|
focus: FocusHandle,
|
||||||
|
on_up_counter: Rc<RefCell<u32>>,
|
||||||
|
on_move_counter: Rc<RefCell<u32>>,
|
||||||
|
}
|
||||||
|
impl Render for TrackFocusProbe {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let counter_up = self.on_up_counter.clone();
|
||||||
|
let counter_move = self.on_move_counter.clone();
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.track_focus(&self.focus)
|
||||||
|
.on_mouse_up(MouseButton::Left, |_event, _window, _cx| {})
|
||||||
|
.child(
|
||||||
|
div().absolute().inset_0().p(px(16.0)).gap(px(12.0)).flex().child(
|
||||||
|
div()
|
||||||
|
.flex_1()
|
||||||
|
.h_full()
|
||||||
|
.min_w_0()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.rounded(px(18.0))
|
||||||
|
.border_1()
|
||||||
|
.overflow_hidden()
|
||||||
|
.child(
|
||||||
|
div().flex_1().overflow_hidden().child(
|
||||||
|
div()
|
||||||
|
.relative()
|
||||||
|
.size_full()
|
||||||
|
.min_w_0()
|
||||||
|
.overflow_hidden()
|
||||||
|
.child(div().absolute().inset_0().child(div().size_full()))
|
||||||
|
.child(
|
||||||
|
canvas(move |_b, _w, _c| {}, |_, _, _, _| {})
|
||||||
|
.absolute()
|
||||||
|
.size_full(),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.size_full()
|
||||||
|
.occlude()
|
||||||
|
.on_mouse_down(MouseButton::Left, |_e, _w, _c| {})
|
||||||
|
.capture_any_mouse_up(move |_e, _w, _c| {
|
||||||
|
*counter_up.borrow_mut() += 1;
|
||||||
|
})
|
||||||
|
.on_mouse_move(move |_e, _w, _c| {
|
||||||
|
*counter_move.borrow_mut() += 1;
|
||||||
|
})
|
||||||
|
.on_scroll_wheel(|_e, _w, _c| {}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (_probe, cx) = cx.add_window_view(|_window, cx| TrackFocusProbe {
|
||||||
|
focus: cx.focus_handle(),
|
||||||
|
on_up_counter: counter_for_up,
|
||||||
|
on_move_counter: counter_for_move,
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
cx.simulate_mouse_move(point(px(400.0), px(400.0)), None, Modifiers::default());
|
||||||
|
cx.run_until_parked();
|
||||||
|
cx.simulate_click(point(px(400.0), px(400.0)), Modifiers::default());
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
*move_count.borrow() > 0,
|
||||||
|
"Bisect layer 5: input_overlay's on_mouse_move never fired even \
|
||||||
|
though the cursor was simulated over it. The same `is_hovered` \
|
||||||
|
check fails for every listener, exactly mirroring the T7 red \
|
||||||
|
test's observation that hover_point is None."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
*click_count.borrow(),
|
||||||
|
1,
|
||||||
|
"Bisect layer 5: capture_any_mouse_up did not fire under the \
|
||||||
|
full track_focus + bubble-mouse_up root chain. This isolates \
|
||||||
|
the culprit to the root-level listeners that ElyShell adds \
|
||||||
|
around its widget tree."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Bisect probe layer 4: same shape as the canvas-sibling probe but
|
||||||
|
/// the on_mouse_down listener calls `cx.update` on a self-entity
|
||||||
|
/// (mirroring `down_entity.update(cx, |shell, _| shell.focus_web_surface(window))`).
|
||||||
|
/// `entity.update` notifies subscribers; if that side effect during
|
||||||
|
/// mouse_down's bubble phase disturbs mouse dispatch — invalidates
|
||||||
|
/// the rendered_frame, regenerates hitboxes, or otherwise corrupts
|
||||||
|
/// the in-flight dispatch — the subsequent MouseUp will land on a
|
||||||
|
/// frame whose hitboxes no longer match the listeners' captured
|
||||||
|
/// snapshots, and this test will go red.
|
||||||
|
#[gpui::test]
|
||||||
|
async fn baseline_overlay_with_entity_update_in_mouse_down_receives_click(cx: &mut TestAppContext) {
|
||||||
|
let click_count = Rc::new(RefCell::new(0u32));
|
||||||
|
let counter_for_render = click_count.clone();
|
||||||
|
|
||||||
|
struct EntityUpdateProbe {
|
||||||
|
on_up_counter: Rc<RefCell<u32>>,
|
||||||
|
tick: u32,
|
||||||
|
}
|
||||||
|
impl Render for EntityUpdateProbe {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let counter = self.on_up_counter.clone();
|
||||||
|
let self_entity = cx.entity().clone();
|
||||||
|
div()
|
||||||
|
.relative()
|
||||||
|
.size_full()
|
||||||
|
.min_w_0()
|
||||||
|
.overflow_hidden()
|
||||||
|
.child(div().absolute().inset_0().child(div().size_full()))
|
||||||
|
.child(
|
||||||
|
canvas(move |_bounds, _window, _cx| {}, |_, _, _, _| {}).absolute().size_full(),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.size_full()
|
||||||
|
.occlude()
|
||||||
|
.on_mouse_down(MouseButton::Left, move |_event, _window, cx| {
|
||||||
|
self_entity.update(cx, |probe, _cx| {
|
||||||
|
probe.tick += 1;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.capture_any_mouse_up(move |_event, _window, _cx| {
|
||||||
|
*counter.borrow_mut() += 1;
|
||||||
|
})
|
||||||
|
.on_mouse_move(|_event, _window, _cx| {})
|
||||||
|
.on_scroll_wheel(|_event, _window, _cx| {}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (_probe, cx) = cx.add_window_view(|_window, _cx| EntityUpdateProbe {
|
||||||
|
on_up_counter: counter_for_render,
|
||||||
|
tick: 0,
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
cx.simulate_mouse_move(point(px(400.0), px(400.0)), None, Modifiers::default());
|
||||||
|
cx.simulate_click(point(px(400.0), px(400.0)), Modifiers::default());
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*click_count.borrow(),
|
||||||
|
1,
|
||||||
|
"Bisect layer 4: on_mouse_down's bubble fires entity.update \
|
||||||
|
which automatically notifies subscribers. If this test fails, \
|
||||||
|
the cx.notify side effect during in-flight dispatch corrupts \
|
||||||
|
the rendered_frame for the immediately-following MouseUp."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bisect probe layer 3: add a canvas sibling BEFORE the overlay,
|
||||||
|
/// matching render_web_surface's viewport_tracker sibling exactly.
|
||||||
|
/// The canvas's prepaint callback fires during paint phase; if it
|
||||||
|
/// somehow disturbs hitbox registration or mouse_listeners ordering,
|
||||||
|
/// this test will go red and pinpoint the suspect.
|
||||||
|
#[gpui::test]
|
||||||
|
async fn baseline_overlay_with_canvas_sibling_receives_click(cx: &mut TestAppContext) {
|
||||||
|
let click_count = Rc::new(RefCell::new(0u32));
|
||||||
|
let counter_for_render = click_count.clone();
|
||||||
|
|
||||||
|
struct CanvasSiblingProbe {
|
||||||
|
on_up_counter: Rc<RefCell<u32>>,
|
||||||
|
}
|
||||||
|
impl Render for CanvasSiblingProbe {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let counter = self.on_up_counter.clone();
|
||||||
|
div()
|
||||||
|
.relative()
|
||||||
|
.size_full()
|
||||||
|
.min_w_0()
|
||||||
|
.overflow_hidden()
|
||||||
|
.child(div().absolute().inset_0().child(div().size_full()))
|
||||||
|
.child(
|
||||||
|
canvas(move |_bounds, _window, _cx| {}, |_, _, _, _| {}).absolute().size_full(),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.size_full()
|
||||||
|
.occlude()
|
||||||
|
.on_mouse_down(MouseButton::Left, |_event, _window, _cx| {})
|
||||||
|
.capture_any_mouse_up(move |_event, _window, _cx| {
|
||||||
|
*counter.borrow_mut() += 1;
|
||||||
|
})
|
||||||
|
.on_mouse_move(|_event, _window, _cx| {})
|
||||||
|
.on_scroll_wheel(|_event, _window, _cx| {}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (_probe, cx) =
|
||||||
|
cx.add_window_view(|_window, _cx| CanvasSiblingProbe { on_up_counter: counter_for_render });
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
cx.simulate_mouse_move(point(px(400.0), px(400.0)), None, Modifiers::default());
|
||||||
|
cx.simulate_click(point(px(400.0), px(400.0)), Modifiers::default());
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*click_count.borrow(),
|
||||||
|
1,
|
||||||
|
"Bisect layer 3: adding a canvas sibling (the shape \
|
||||||
|
render_viewport_tracker uses) between the content wrapper and \
|
||||||
|
the overlay should not break click delivery. If this fails, \
|
||||||
|
the canvas element itself disturbs mouse dispatch — likely \
|
||||||
|
via its prepaint callback's interaction with hit_test."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn baseline_overlay_with_full_listener_combo_receives_click(cx: &mut TestAppContext) {
|
||||||
|
let click_count = Rc::new(RefCell::new(0u32));
|
||||||
|
let counter_for_render = click_count.clone();
|
||||||
|
|
||||||
|
struct ComboProbe {
|
||||||
|
on_up_counter: Rc<RefCell<u32>>,
|
||||||
|
}
|
||||||
|
impl Render for ComboProbe {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let counter = self.on_up_counter.clone();
|
||||||
|
div().relative().size_full().child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.size_full()
|
||||||
|
.occlude()
|
||||||
|
.on_mouse_down(MouseButton::Left, |_event, _window, _cx| {})
|
||||||
|
.capture_any_mouse_up(move |_event, _window, _cx| {
|
||||||
|
*counter.borrow_mut() += 1;
|
||||||
|
})
|
||||||
|
.on_mouse_move(|_event, _window, _cx| {})
|
||||||
|
.on_scroll_wheel(|_event, _window, _cx| {}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (_probe, cx) =
|
||||||
|
cx.add_window_view(|_window, _cx| ComboProbe { on_up_counter: counter_for_render });
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
cx.simulate_mouse_move(point(px(100.0), px(100.0)), None, Modifiers::default());
|
||||||
|
cx.simulate_click(point(px(100.0), px(100.0)), Modifiers::default());
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*click_count.borrow(),
|
||||||
|
1,
|
||||||
|
"GPUI baseline with input_overlay's full listener combo: click should \
|
||||||
|
reach capture_any_mouse_up. If this fails, the listener combo itself \
|
||||||
|
is the problem, not the surrounding shell."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn baseline_overlay_div_receives_simulated_click(cx: &mut TestAppContext) {
|
||||||
|
let click_count = Rc::new(RefCell::new(0u32));
|
||||||
|
let counter_for_render = click_count.clone();
|
||||||
|
|
||||||
|
struct Probe {
|
||||||
|
on_up_counter: Rc<RefCell<u32>>,
|
||||||
|
}
|
||||||
|
impl Render for Probe {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let counter = self.on_up_counter.clone();
|
||||||
|
div().relative().size_full().child(
|
||||||
|
div().absolute().size_full().occlude().capture_any_mouse_up(
|
||||||
|
move |_event, _window, _cx| {
|
||||||
|
*counter.borrow_mut() += 1;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (_probe, cx) =
|
||||||
|
cx.add_window_view(|_window, _cx| Probe { on_up_counter: counter_for_render });
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
cx.simulate_mouse_move(point(px(100.0), px(100.0)), None, Modifiers::default());
|
||||||
|
cx.simulate_click(point(px(100.0), px(100.0)), Modifiers::default());
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*click_count.borrow(),
|
||||||
|
1,
|
||||||
|
"GPUI baseline: a div with .absolute().size_full().occlude() and \
|
||||||
|
capture_any_mouse_up never received the simulated click. The test \
|
||||||
|
harness or GPUI primitive is broken — ElyShell test results are \
|
||||||
|
meaningless until this passes."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TDD red guard for T10: today every `WebSurfaceFrame::from_live_frame`
|
||||||
|
/// call allocates a fresh `Arc::new(RenderImage::new(...))` regardless
|
||||||
|
/// of whether the underlying pixels changed. At 60 fps on a 1080p
|
||||||
|
/// canvas that is `~8 MB / frame` of host-side RGBA cloning + a new
|
||||||
|
/// GPUI texture upload, the cost Linus + Karpathy + Jony all flagged
|
||||||
|
/// as the next material bottleneck after the file-system pipe.
|
||||||
|
///
|
||||||
|
/// The contract this test pins is the cheapest invariant we can hold
|
||||||
|
/// against today's `SoftwareRenderingContext`: two frames carrying
|
||||||
|
/// **byte-identical RGBA payloads must produce the same underlying
|
||||||
|
/// `Arc<RenderImage>`**. Today they do not — every `from_live_frame`
|
||||||
|
/// blindly reallocates. The fix path is either dedup the upload
|
||||||
|
/// against the last bytes or switch to direct platform-surface presentation.
|
||||||
|
///
|
||||||
|
/// Regression guard: with the single-slot `LAST_FRAME_IMAGE` cache in
|
||||||
|
/// `web_surface_frame.rs`, two `ServoLiveFrame` inputs carrying
|
||||||
|
/// byte-identical RGBA payloads now share the same `Arc<RenderImage>`.
|
||||||
|
/// Without this guard a regression that drops the cache silently
|
||||||
|
/// returns to ~960 MB/s of host-side RGBA cloning + per-frame GPUI
|
||||||
|
/// texture allocations.
|
||||||
|
#[test]
|
||||||
|
fn identical_live_frames_share_render_image_arc() -> Result<(), String> {
|
||||||
|
let width = 16u32;
|
||||||
|
let height = 8u32;
|
||||||
|
let rgba_bytes = vec![0xAAu8; (width as usize) * (height as usize) * 4];
|
||||||
|
|
||||||
|
let first = WebSurfaceFrame::from_live_frame(
|
||||||
|
"https://example.com/".to_string(),
|
||||||
|
WebSurfaceScrollOffset::default(),
|
||||||
|
100,
|
||||||
|
ServoLiveFrame::for_test(width, height, rgba_bytes.clone()),
|
||||||
|
)
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
let second = WebSurfaceFrame::from_live_frame(
|
||||||
|
"https://example.com/".to_string(),
|
||||||
|
WebSurfaceScrollOffset::default(),
|
||||||
|
100,
|
||||||
|
ServoLiveFrame::for_test(width, height, rgba_bytes),
|
||||||
|
)
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
|
||||||
|
let first_image = first
|
||||||
|
.image
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| "software path must produce an Arc<RenderImage>".to_string())?;
|
||||||
|
let second_image = second
|
||||||
|
.image
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| "software path must produce an Arc<RenderImage>".to_string())?;
|
||||||
|
assert!(
|
||||||
|
Arc::ptr_eq(first_image, second_image),
|
||||||
|
"TDD red: two ServoLiveFrames with byte-identical RGBA produced \
|
||||||
|
distinct Arc<RenderImage> instances (first={:p}, second={:p}). \
|
||||||
|
WebSurfaceFrame::from_parts must dedup the upload against the \
|
||||||
|
previous frame's bytes, or the rendering pipeline must switch \
|
||||||
|
to direct platform-surface presentation so per-frame host \
|
||||||
|
allocations stop entirely.",
|
||||||
|
Arc::as_ptr(first_image),
|
||||||
|
Arc::as_ptr(second_image),
|
||||||
|
);
|
||||||
|
assert!(first.has_same_software_render_as(&second));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user