- main.rs: apply bar.window.margin.bottom to the layer-shell surface (was parsed but never set — WindowSpec::margin.bottom's own doc comment flagged this gap); fix the exclusive-zone calc to reserve the ANCHORED edge's margin instead of always margin.top (exclusive_zone_for, unit tested); tag the three bar slot-group boxes with an unconditional .bar-segment CSS class (a no-op unless the active theme sets bar_border = "segmented"). - surface.rs: add the bottom_right satellite anchor (mirrors top_right on the bottom edge) and extend the fill anchor's offset to a [top, bottom] pair so the dismiss scrim can leave its gap at the bottom of the screen instead of only the top. - theme.rs: tokens.light() swaps which of the fixed @bg/@on-bg pair plays paper-surface vs ink across every translucent card/panel/hover-wash in the stylesheet; bar_border = "segmented" makes window.breadbar fully transparent and gives .bar-segment its own pill surface/border/shadow; the Trail workspace style now actually substitutes accent_from/accent_to instead of a hardcoded @accent/@teal gradient; accent2 gives the media equaliser an accent independent of the workspace-trail one.
166 lines
8.4 KiB
Rust
166 lines
8.4 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 four anchor shapes
|
|
//! breadbar's four built-in surfaces actually use today ("breadbar-notif",
|
|
//! "breadbar-osd", "breadbar-panel", "breadbar-dismiss": `top_right`,
|
|
//! `bottom_right`, `bottom_centre`, `fill`), not a general anchor DSL (the
|
|
//! plan's own anti-goal, §2). `bottom_right` was added for daylight (plan
|
|
//! §11 phase 7, axis 2) — the first bottom-anchored bar, whose satellites
|
|
//! need to hug the bottom-right corner the way every top-anchored theme's
|
|
//! already hug the top-right one. `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);
|
|
}
|
|
// Axis 2 (daylight): every theme through spotlight anchors its bar
|
|
// to the TOP, so `top_right` always put breadbar-notif/
|
|
// breadbar-panel naturally close to the bar. A bottom-anchored bar
|
|
// has nothing in the original three shapes that keeps its
|
|
// satellites near it — `top_right` would land them at the opposite
|
|
// corner of the screen from the dock they belong to. Mirrors
|
|
// `top_right` exactly, just on the bottom edge.
|
|
"bottom_right" => {
|
|
window.set_anchor(Edge::Bottom, true);
|
|
window.set_anchor(Edge::Right, true);
|
|
// offset = [right, bottom], same convention as top_right's
|
|
// [right, top].
|
|
let right = surf.offset.first().copied().unwrap_or(0.0) as i32;
|
|
let bottom = surf.offset.get(1).copied().unwrap_or(0.0) as i32;
|
|
window.set_margin(Edge::Right, right);
|
|
window.set_margin(Edge::Bottom, bottom);
|
|
}
|
|
"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);
|
|
}
|
|
// offset = [top, bottom] — a fullscreen click-away scrim that
|
|
// leaves a gap clear of the bar on whichever edge the bar
|
|
// actually anchors to. Every theme through spotlight anchors
|
|
// top, so only `offset[0]` (top) was ever meaningful before
|
|
// daylight; a single-value `offset` (every existing theme's
|
|
// manifest) still means exactly what it always did, since
|
|
// `offset.get(1)` falls back to 0 — a bottom-anchored theme is
|
|
// the first to give this a real, nonzero bottom value instead.
|
|
let top = surf.offset.first().copied().unwrap_or(0.0) as i32;
|
|
let bottom = surf.offset.get(1).copied().unwrap_or(0.0) as i32;
|
|
window.set_margin(Edge::Top, top);
|
|
window.set_margin(Edge::Bottom, bottom);
|
|
}
|
|
other => eprintln!(
|
|
"breadbar: surfaces.{namespace}.anchor = \"{other}\" is not one of \
|
|
top_right|bottom_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)));
|
|
}
|