//! Applies a `[surfaces.]` 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 = 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))); }