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).
136 lines
6.6 KiB
Rust
136 lines
6.6 KiB
Rust
//! Applies a `[surfaces.<namespace>]` entry (plan §4/§6, Phase 2) to a
|
|
//! satellite layer-shell window: anchor, margin (from `offset`), width and
|
|
//! layer. Deliberately narrow — it only understands the three anchor shapes
|
|
//! breadbar's four built-in surfaces actually use today ("breadbar-notif",
|
|
//! "breadbar-osd", "breadbar-panel", "breadbar-dismiss": `top_right`,
|
|
//! `bottom_centre`, `fill`), not a general anchor DSL (the plan's own
|
|
//! anti-goal, §2). `exclusive` zone and `keyboard` mode aren't part of the
|
|
//! `[surfaces.*]` schema (`bread_theme::shell::Surface` has no such fields)
|
|
//! and stay hardcoded at each call site, same as before this refactor.
|
|
|
|
use bread_theme::shell::SurfaceWidth;
|
|
use gtk4::prelude::*;
|
|
use gtk4_layer_shell::{Edge, Layer, LayerShell};
|
|
|
|
/// `namespace` should be a key in the active theme's `[surfaces.*]` table —
|
|
/// every call site in this crate passes one of breadbar's own namespace
|
|
/// literals, so a miss here means the active theme fell out of sync with
|
|
/// the Rust source, not a bad runtime value. Logs and leaves the window at
|
|
/// gtk4-layer-shell's own defaults rather than panicking, matching every
|
|
/// other "malformed/incomplete theme" fallback in this system.
|
|
///
|
|
/// The width applied here is not authoritative for a namespace shared by
|
|
/// more than one window with genuinely different widths (`breadbar-notif`'s
|
|
/// live toast is 320px, its history sibling is 360px, and only the toast's
|
|
/// width is modeled in `[surfaces.*]` — see the Phase 0 constant inventory);
|
|
/// callers that need a different width than the theme's own set it
|
|
/// explicitly afterward. Because a `Px` width is pinned with BOTH
|
|
/// `set_default_width` and `set_size_request` (see below — the latter is
|
|
/// what actually holds against a wide child), such a caller must override
|
|
/// both, not just `set_default_width`, or the pin from here wins.
|
|
pub fn apply(window: >k4::Window, namespace: &str) {
|
|
let theme = crate::theme::shell_theme();
|
|
let Some(surf) = theme.surfaces().get(namespace) else {
|
|
eprintln!(
|
|
"breadbar: no [surfaces.{namespace}] entry in the active theme; \
|
|
window left at layer-shell defaults"
|
|
);
|
|
return;
|
|
};
|
|
|
|
window.set_layer(if surf.layer == "top" {
|
|
Layer::Top
|
|
} else {
|
|
Layer::Overlay
|
|
});
|
|
|
|
match surf.anchor.as_str() {
|
|
"top_right" => {
|
|
window.set_anchor(Edge::Top, true);
|
|
window.set_anchor(Edge::Right, true);
|
|
// offset = [right, top] for this anchor shape.
|
|
let right = surf.offset.first().copied().unwrap_or(0.0) as i32;
|
|
let top = surf.offset.get(1).copied().unwrap_or(0.0) as i32;
|
|
window.set_margin(Edge::Right, right);
|
|
window.set_margin(Edge::Top, top);
|
|
}
|
|
"bottom_centre" => {
|
|
window.set_anchor(Edge::Bottom, true);
|
|
let bottom = surf.offset.first().copied().unwrap_or(0.0) as i32;
|
|
window.set_margin(Edge::Bottom, bottom);
|
|
}
|
|
"fill" => {
|
|
for edge in [Edge::Top, Edge::Bottom, Edge::Left, Edge::Right] {
|
|
window.set_anchor(edge, true);
|
|
}
|
|
// Only a top margin is meaningful here — a fullscreen click-away
|
|
// scrim that starts below the bar rather than covering it.
|
|
let top = surf.offset.first().copied().unwrap_or(0.0) as i32;
|
|
window.set_margin(Edge::Top, top);
|
|
}
|
|
other => eprintln!(
|
|
"breadbar: surfaces.{namespace}.anchor = \"{other}\" is not one of \
|
|
top_right|bottom_centre|fill — breadbar's satellite windows don't \
|
|
understand any other shape yet, leaving this window unanchored"
|
|
),
|
|
}
|
|
|
|
if let SurfaceWidth::Px(px) = surf.width {
|
|
window.set_default_width(px);
|
|
// set_default_width alone is only a preference — a wide child (an
|
|
// unwrapped app-name label, or a long summary/body before GTK has
|
|
// any allocation narrower than its natural width to wrap against)
|
|
// overrides it, so the window renders wider than the theme's
|
|
// requested px and stops matching the theme. Same trap, same fix,
|
|
// as main.rs's capsule `Width::Px` handling — see its comment.
|
|
window.set_size_request(px, -1);
|
|
}
|
|
}
|
|
|
|
/// 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).
|
|
///
|
|
/// 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.
|
|
///
|
|
/// 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: >k4::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(>k4::cairo::Region::create_rectangles(&rects)));
|
|
}
|