notifications: click-through-except-buttons input region + push-down entrance

surface::click_through set a completely empty input region on the toast
so every pointer event passed through -- including to the toast's own
action buttons and inline-reply entry. Replace it with
surface::set_hit_region(window, widgets), which sets the input region to
the union of the given widgets' rectangles instead of empty; everywhere
else on the surface stays click-through exactly as before.

popup.rs recomputes this via refresh_hit_region every time the card set
could have changed (shown, dismissed, expired -- routed through the one
dismiss() function) and keeps recomputing every frame for HIT_TRACK_MS
afterward, since a card's own entrance animation or the stack's
push-down reflow can still be moving a button on the frame the change
happens. collect_interactive walks the real widget tree for
GtkButton/GtkEntry rather than tracking a flat list, so it can't drift
out of sync with make_card's structure. build_window's connect_map
handles the first-map race the same way surface::click_through used to.

Also: spring_in_card grows a newly shown card's height from 0 to its
natural size via bread_theme::anim::spring_to (same technique as
main.rs's animate_drawer_height), so the existing stack gets pushed
down smoothly instead of jumping.

KeyboardMode::None is unchanged on the toast; history.rs's OnDemand
mode is untouched.

Still to do: the actual dismiss button in make_card (this commit wires
the mechanism that will hit-test it, but no card has one yet).
This commit is contained in:
Breadway 2026-08-26 19:30:08 +08:00
parent ba4e3480d4
commit 3eada87c7d
4 changed files with 216 additions and 40 deletions

View file

@ -87,27 +87,50 @@ pub fn apply(window: &gtk4::Window, namespace: &str) {
}
}
/// Makes `window` fully click-through: an empty layer-shell input region
/// means every pointer event passes to whatever is underneath instead of
/// being consumed by this surface. Deliberately opt-in, not part of
/// `apply()` — like `exclusive` zone and `keyboard` mode (see the module
/// doc comment), this isn't a `[surfaces.*]` schema concept, and most of
/// breadbar's satellites (history, the panel, the dismiss-scrim) genuinely
/// need real hit-testing. Only a purely-informational surface — today just
/// the notification toast — should call this.
/// Sets `window`'s input region to the union of `widgets`' current
/// allocations, each measured relative to `window` itself (the surface's
/// own coordinate space, same as `bar::workspaces::button_geom`'s own
/// `compute_bounds` call relative to its Fixed host) — everywhere else on
/// the surface stays click-through. An empty (or all-invisible/all-
/// unallocated) `widgets` slice is not a special case:
/// `Region::create_rectangles(&[])` is already the fully click-through
/// empty region — passing `&[]` here is exactly the old blanket
/// `Region::create()` this function replaces (see git history around
/// `notifications/popup.rs`'s "stop toast popups from stealing focus or
/// blocking clicks" fix, and NOTIFICATION INTERACTION #B in the current
/// task notes: a toast must never block clicks or steal focus from
/// whatever's underneath it EXCEPT on its own buttons — an all-empty
/// region made those unreachable too).
///
/// Must be applied in a `connect_map` handler: the surface (and therefore
/// `window.surface()`) doesn't exist until the window is mapped.
/// Only meaningful after `window.surface()` exists (i.e. from
/// `connect_map` onward — the surface doesn't exist before the window is
/// mapped) and after `widgets` have a real allocation — a widget with no
/// allocation yet (`compute_bounds` returning `None`) is simply skipped
/// rather than contributing a garbage rectangle, so a call made one frame
/// too early just yields a smaller-than-intended region for that one frame
/// rather than a wrong one.
///
/// This exists so a future migration of a surface's window-setup code
/// (like the one from hand-rolled layer-shell calls to this module) can't
/// silently drop a click-through requirement the way `breadbar-notif` did
/// once already — see the git history of `notifications/popup.rs` around
/// the "stop toast popups from stealing focus or blocking clicks" fix.
pub fn click_through(window: &gtk4::Window) {
window.connect_map(|win| {
if let Some(surface) = win.surface() {
surface.set_input_region(Some(&gtk4::cairo::Region::create()));
}
});
/// Callers are responsible for RECOMPUTING this every time the hittable
/// set could have moved: a widget added or removed, or a layout pass (an
/// entrance animation, a push-down reflow) still in flight. A stale region
/// either swallows clicks meant for the window below or leaves a real
/// button dead.
pub fn set_hit_region(window: &gtk4::Window, widgets: &[gtk4::Widget]) {
let Some(surface) = window.surface() else {
return;
};
let rects: Vec<gtk4::cairo::RectangleInt> = widgets
.iter()
.filter(|w| w.is_visible())
.filter_map(|w| {
let b = w.compute_bounds(window)?;
Some(gtk4::cairo::RectangleInt::new(
b.x().floor() as i32,
b.y().floor() as i32,
b.width().ceil() as i32,
b.height().ceil() as i32,
))
})
.collect();
surface.set_input_region(Some(&gtk4::cairo::Region::create_rectangles(&rects)));
}