Daylight bar chrome + embedded launcher, v0.7.5 pins #5
2 changed files with 216 additions and 7 deletions
106
src/main.rs
106
src/main.rs
|
|
@ -1060,6 +1060,7 @@ impl SimpleComponent for App {
|
||||||
let panels = panels.clone();
|
let panels = panels.clone();
|
||||||
let idle_width = launcher_cfg.width;
|
let idle_width = launcher_cfg.width;
|
||||||
let search_width = launcher_cfg.search_width;
|
let search_width = launcher_cfg.search_width;
|
||||||
|
let monitor_name_for_dismiss = monitor_name.clone();
|
||||||
move || {
|
move || {
|
||||||
if !launcher_open.get() {
|
if !launcher_open.get() {
|
||||||
launcher_open.set(true);
|
launcher_open.set(true);
|
||||||
|
|
@ -1081,8 +1082,20 @@ impl SimpleComponent for App {
|
||||||
// offset (`capsule_dismiss_margin`), never the live
|
// offset (`capsule_dismiss_margin`), never the live
|
||||||
// drawer height — see that constant's own doc comment
|
// drawer height — see that constant's own doc comment
|
||||||
// for why a live-tracking offset would risk swallowing
|
// for why a live-tracking offset would risk swallowing
|
||||||
// clicks meant for a result row.
|
// clicks meant for a result row. `hole` scopes that
|
||||||
panels.show_capsule_dismiss(capsule_dismiss_margin);
|
// dead zone to the capsule's own column instead of the
|
||||||
|
// full screen width ("it only sometimes is dismissed
|
||||||
|
// when you click somewhere else") — see
|
||||||
|
// `capsule_dismiss_hole`'s doc comment. Falls back to
|
||||||
|
// the old full-width dead zone if the live geometry
|
||||||
|
// query fails for any reason.
|
||||||
|
let hole = hypr_capsule_center_x(&monitor_name_for_dismiss).and_then(
|
||||||
|
|center_x| {
|
||||||
|
let (origin_x, _) = hypr_monitor_origin(&monitor_name_for_dismiss)?;
|
||||||
|
Some(capsule_dismiss_hole(center_x, origin_x, search_width))
|
||||||
|
},
|
||||||
|
);
|
||||||
|
panels.show_capsule_dismiss(capsule_dismiss_margin, hole);
|
||||||
}
|
}
|
||||||
let target = drawer_target_height(&drawer_box);
|
let target = drawer_target_height(&drawer_box);
|
||||||
let current = drawer_box.size_request().1;
|
let current = drawer_box.size_request().1;
|
||||||
|
|
@ -2975,6 +2988,64 @@ fn hypr_monitor_origin(name: &str) -> Option<(i32, i32)> {
|
||||||
.map(|m| (m.x, m.y))
|
.map(|m| (m.x, m.y))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The bar/capsule's own layer-surface geometry for `monitor`, straight
|
||||||
|
/// from the compositor (`hyprctl layers -j`, ground truth — not derived
|
||||||
|
/// from anything GTK/gtk4-layer-shell reports client-side, since the
|
||||||
|
/// wlr-layer-shell protocol never hands a client its own assigned x/y back;
|
||||||
|
/// only width/height come through `configure`). Matched by `namespace`
|
||||||
|
/// ("breadbar", set via `root.set_namespace` above), which is unique per
|
||||||
|
/// output since each monitor gets its own bound `App` instance/window.
|
||||||
|
/// Returns the surface's horizontal center in Hyprland's global coordinate
|
||||||
|
/// space. `None` on any parse/lookup failure — callers must fall back to
|
||||||
|
/// the pre-existing, safe-but-broader dead-zone behaviour rather than
|
||||||
|
/// guess.
|
||||||
|
fn hypr_capsule_center_x(monitor: &str) -> Option<i32> {
|
||||||
|
let output = std::process::Command::new("hyprctl")
|
||||||
|
.args(["layers", "-j"])
|
||||||
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let root: serde_json::Value = serde_json::from_slice(&output.stdout).ok()?;
|
||||||
|
let levels = root.get(monitor)?.get("levels")?.as_object()?;
|
||||||
|
for arr in levels.values() {
|
||||||
|
let Some(items) = arr.as_array() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
for item in items {
|
||||||
|
if item.get("namespace").and_then(|v| v.as_str()) != Some("breadbar") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let x = item.get("x")?.as_i64()? as i32;
|
||||||
|
let w = item.get("w")?.as_i64()? as i32;
|
||||||
|
return Some(x + w / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The click-away scrim's capsule-column hole, in coordinates local to the
|
||||||
|
/// `breadbar-dismiss` surface (see `PanelSet::show_capsule_dismiss`) —
|
||||||
|
/// pure arithmetic, split out for unit testing. `capsule_center_global` and
|
||||||
|
/// `monitor_origin_x` are both in Hyprland's global compositor space
|
||||||
|
/// (`hypr_capsule_center_x`/`hypr_monitor_origin`); `column_width` is the
|
||||||
|
/// capsule's own *configured* search-state width
|
||||||
|
/// (`[launcher].search_width`), not a live-queried one — this fires right
|
||||||
|
/// as `open_fn` starts the width-animation from idle to search width, so a
|
||||||
|
/// live query at that exact instant would catch it mid-transition. Using
|
||||||
|
/// the wider, settled target here (like `DRAWER_MAX_HEIGHT_PX` already does
|
||||||
|
/// for the vertical bound) means the hole is never narrower than the
|
||||||
|
/// capsule ever actually gets while the scrim is showing.
|
||||||
|
fn capsule_dismiss_hole(
|
||||||
|
capsule_center_global: i32,
|
||||||
|
monitor_origin_x: i32,
|
||||||
|
column_width: i32,
|
||||||
|
) -> (i32, i32) {
|
||||||
|
let local_center = capsule_center_global - monitor_origin_x;
|
||||||
|
(local_center - column_width / 2, column_width)
|
||||||
|
}
|
||||||
|
|
||||||
/// Hyprland connector names and GDK connector names can disagree after a
|
/// Hyprland connector names and GDK connector names can disagree after a
|
||||||
/// hotplug (`DVI-I-1` vs `DVI-I-2`). Match the connector first, then the
|
/// hotplug (`DVI-I-1` vs `DVI-I-2`). Match the connector first, then the
|
||||||
/// output's origin — transform swaps width/height so size is not reliable.
|
/// output's origin — transform swaps width/height so size is not reliable.
|
||||||
|
|
@ -3091,6 +3162,37 @@ mod launcher_route_tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod capsule_dismiss_hole_tests {
|
||||||
|
use super::capsule_dismiss_hole;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn centered_capsule_on_primary_monitor_at_origin() {
|
||||||
|
// A 520px-wide capsule centered on a 1920px-wide monitor at global
|
||||||
|
// origin (0,0): global center x = 960, monitor origin x = 0.
|
||||||
|
let (x, w) = capsule_dismiss_hole(960, 0, 520);
|
||||||
|
assert_eq!((x, w), (960 - 260, 520));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negative_origin_secondary_monitor_converts_to_local() {
|
||||||
|
// This machine's own DVI-I-1 (`hyprctl layers -j`, quoted in this
|
||||||
|
// module's doc comments): monitor origin x = -1080. A capsule
|
||||||
|
// centered on that output's own 1080px-wide span sits at global
|
||||||
|
// center x = -1080 + 540 = -540.
|
||||||
|
let (x, w) = capsule_dismiss_hole(-540, -1080, 520);
|
||||||
|
// Local center is 540 (origin subtracted back out); hole starts
|
||||||
|
// 260px to either side of it, independent of the monitor's sign.
|
||||||
|
assert_eq!((x, w), (540 - 260, 520));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hole_width_always_matches_requested_column_width() {
|
||||||
|
let (_, w) = capsule_dismiss_hole(100, 0, 480);
|
||||||
|
assert_eq!(w, 480);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod url_open_target_tests {
|
mod url_open_target_tests {
|
||||||
use super::url_open_target;
|
use super::url_open_target;
|
||||||
|
|
|
||||||
117
src/panel.rs
117
src/panel.rs
|
|
@ -5,7 +5,7 @@
|
||||||
//! *below* the exclusive zone, and Hyprland slides `breadbar-panel` in from
|
//! *below* the exclusive zone, and Hyprland slides `breadbar-panel` in from
|
||||||
//! the right.
|
//! the right.
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::{Cell, RefCell};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gtk4::gdk::Key;
|
use gtk4::gdk::Key;
|
||||||
|
|
@ -14,10 +14,26 @@ use gtk4_layer_shell::{Edge, KeyboardMode, LayerShell};
|
||||||
|
|
||||||
use crate::{bind_layer_monitor, theme};
|
use crate::{bind_layer_monitor, theme};
|
||||||
|
|
||||||
|
/// Outside this rectangle's local x/y span, the dismiss window's own real
|
||||||
|
/// size (Wayland clips an input region to the surface's actual bounds, same
|
||||||
|
/// as `surface::click_through`'s empty-region trick) — big enough to cover
|
||||||
|
/// any realistic monitor layout, including a negative-origin secondary
|
||||||
|
/// output (`hyprctl layers -j` reported `x: -1080` for this machine's own
|
||||||
|
/// DVI-I-1). Centered on the origin so it's safe regardless of which way a
|
||||||
|
/// hole's coordinates end up signed.
|
||||||
|
const HOLE_CANVAS_SPAN: i32 = 20_000;
|
||||||
|
|
||||||
/// A boxed, ref-counted, optionally-unset click-away callback — see
|
/// A boxed, ref-counted, optionally-unset click-away callback — see
|
||||||
/// `PanelSet::on_dismiss`'s own doc comment.
|
/// `PanelSet::on_dismiss`'s own doc comment.
|
||||||
type DismissCallback = Rc<RefCell<Option<Rc<dyn Fn()>>>>;
|
type DismissCallback = Rc<RefCell<Option<Rc<dyn Fn()>>>>;
|
||||||
|
|
||||||
|
/// The capsule-column hole punched in the dismiss scrim's input region —
|
||||||
|
/// local x, y, width, height — see `PanelSet::dismiss_hole`'s own doc
|
||||||
|
/// comment. Shared (not just passed by value) so `make_dismiss`'s
|
||||||
|
/// `connect_map` hook and every `show_capsule_dismiss`/`reset_dismiss_margin`
|
||||||
|
/// call agree on the current value.
|
||||||
|
type DismissHole = Rc<Cell<Option<(i32, i32, i32, i32)>>>;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PanelSet {
|
pub struct PanelSet {
|
||||||
pub connectivity: gtk4::Window,
|
pub connectivity: gtk4::Window,
|
||||||
|
|
@ -30,6 +46,16 @@ pub struct PanelSet {
|
||||||
// capsule's drawer — see `show_capsule_dismiss`/`hide_dismiss` and
|
// capsule's drawer — see `show_capsule_dismiss`/`hide_dismiss` and
|
||||||
// `set_on_dismiss`. `None` under every other theme (never set).
|
// `set_on_dismiss`. `None` under every other theme (never set).
|
||||||
on_dismiss: DismissCallback,
|
on_dismiss: DismissCallback,
|
||||||
|
// The rectangle (local to `dismiss`'s own surface coordinates) that
|
||||||
|
// should stay click-through even while the scrim otherwise covers the
|
||||||
|
// screen — `show_capsule_dismiss`'s own doc comment explains why this
|
||||||
|
// exists and how it's computed. `None` = no hole, the plain
|
||||||
|
// margin-based popover behaviour applies instead. Read inside
|
||||||
|
// `dismiss`'s own `connect_map` (the input region can only be set once
|
||||||
|
// the surface is real — see `surface::click_through`'s doc comment for
|
||||||
|
// the same constraint) and, for the case where `dismiss` is already
|
||||||
|
// mapped from a prior show, applied immediately too.
|
||||||
|
dismiss_hole: DismissHole,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PanelSet {
|
impl PanelSet {
|
||||||
|
|
@ -42,7 +68,8 @@ impl PanelSet {
|
||||||
let connectivity = make_panel("wifi-popover", connectivity_child, monitor);
|
let connectivity = make_panel("wifi-popover", connectivity_child, monitor);
|
||||||
let control = make_panel("control-panel", control_child, monitor);
|
let control = make_panel("control-panel", control_child, monitor);
|
||||||
let media = make_panel("media-popover", media_child, monitor);
|
let media = make_panel("media-popover", media_child, monitor);
|
||||||
let dismiss = make_dismiss(monitor);
|
let dismiss_hole: DismissHole = Rc::new(Cell::new(None));
|
||||||
|
let dismiss = make_dismiss(monitor, &dismiss_hole);
|
||||||
|
|
||||||
let set = Self {
|
let set = Self {
|
||||||
connectivity,
|
connectivity,
|
||||||
|
|
@ -50,6 +77,7 @@ impl PanelSet {
|
||||||
media,
|
media,
|
||||||
dismiss,
|
dismiss,
|
||||||
on_dismiss: Rc::new(RefCell::new(None)),
|
on_dismiss: Rc::new(RefCell::new(None)),
|
||||||
|
dismiss_hole,
|
||||||
};
|
};
|
||||||
set.wire_dismiss();
|
set.wire_dismiss();
|
||||||
set.wire_escape();
|
set.wire_escape();
|
||||||
|
|
@ -106,8 +134,31 @@ impl PanelSet {
|
||||||
/// (`top`), so if its clickable region ever reached up into where the
|
/// (`top`), so if its clickable region ever reached up into where the
|
||||||
/// drawer is actually drawn, it would swallow clicks meant for a
|
/// drawer is actually drawn, it would swallow clicks meant for a
|
||||||
/// result row instead of forwarding them.
|
/// result row instead of forwarding them.
|
||||||
pub fn show_capsule_dismiss(&self, top_margin: i32) {
|
///
|
||||||
self.dismiss.set_margin(Edge::Top, top_margin);
|
/// Before this fix, that safety was bought with a `set_margin` that
|
||||||
|
/// pushed the scrim's *entire width* down by `top_margin` — leaving a
|
||||||
|
/// full-screen-wide dead band above it (up to ~470px on a 1200px-tall
|
||||||
|
/// display) where a click neither dismissed nor hit anything else, the
|
||||||
|
/// "it only sometimes is dismissed when you click somewhere else"
|
||||||
|
/// report. `hole`, when known (local-to-this-surface x-start/width, in
|
||||||
|
/// `main.rs`'s `capsule_dismiss_hole`), keeps exactly the same
|
||||||
|
/// vertical safety margin but scopes the dead band to the capsule's
|
||||||
|
/// own column instead of the full width, so everywhere else in that
|
||||||
|
/// band is dismiss-clickable too. `None` (geometry unavailable, e.g.
|
||||||
|
/// `hyprctl` failed) falls back to the old full-width behaviour rather
|
||||||
|
/// than risk a hole in the wrong place.
|
||||||
|
pub fn show_capsule_dismiss(&self, top_margin: i32, hole: Option<(i32, i32)>) {
|
||||||
|
match hole {
|
||||||
|
Some((x, w)) if w > 0 => {
|
||||||
|
self.dismiss.set_margin(Edge::Top, 0);
|
||||||
|
self.dismiss_hole.set(Some((x, 0, w, top_margin)));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.dismiss.set_margin(Edge::Top, top_margin);
|
||||||
|
self.dismiss_hole.set(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apply_dismiss_hole(&self.dismiss, &self.dismiss_hole);
|
||||||
self.dismiss.set_visible(true);
|
self.dismiss.set_visible(true);
|
||||||
self.dismiss.present();
|
self.dismiss.present();
|
||||||
}
|
}
|
||||||
|
|
@ -126,6 +177,10 @@ impl PanelSet {
|
||||||
let top = surf.offset.first().copied().unwrap_or(0.0) as i32;
|
let top = surf.offset.first().copied().unwrap_or(0.0) as i32;
|
||||||
self.dismiss.set_margin(Edge::Top, top);
|
self.dismiss.set_margin(Edge::Top, top);
|
||||||
}
|
}
|
||||||
|
// A stale capsule-shaped hole must not leak into a popover's own
|
||||||
|
// full-width dead zone.
|
||||||
|
self.dismiss_hole.set(None);
|
||||||
|
apply_dismiss_hole(&self.dismiss, &self.dismiss_hole);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wire_dismiss(&self) {
|
fn wire_dismiss(&self) {
|
||||||
|
|
@ -180,7 +235,7 @@ fn make_panel(class: &str, child: &impl IsA<gtk4::Widget>, monitor: &str) -> gtk
|
||||||
window
|
window
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_dismiss(monitor: &str) -> gtk4::Window {
|
fn make_dismiss(monitor: &str, hole: &DismissHole) -> gtk4::Window {
|
||||||
let window = gtk4::Window::new();
|
let window = gtk4::Window::new();
|
||||||
window.add_css_class("breadbar-dismiss");
|
window.add_css_class("breadbar-dismiss");
|
||||||
window.init_layer_shell();
|
window.init_layer_shell();
|
||||||
|
|
@ -206,5 +261,57 @@ fn make_dismiss(monitor: &str) -> gtk4::Window {
|
||||||
bind_layer_monitor(&window, monitor);
|
bind_layer_monitor(&window, monitor);
|
||||||
theme::bind_output(&window, monitor);
|
theme::bind_output(&window, monitor);
|
||||||
window.set_visible(false);
|
window.set_visible(false);
|
||||||
|
// The underlying `GdkSurface` (and therefore `window.surface()`, which
|
||||||
|
// `apply_dismiss_hole` needs) doesn't exist until the window is mapped
|
||||||
|
// — same constraint `surface::click_through` documents. This surface
|
||||||
|
// gets hidden/shown repeatedly (every popover open/close, every
|
||||||
|
// capsule search), and GTK4 unmaps-then-remaps a toplevel each time
|
||||||
|
// its visibility toggles off then on, so re-applying here on every
|
||||||
|
// `map` (not just the first) is what keeps a freshly (re)shown surface
|
||||||
|
// honouring whatever hole was set before this particular `present()`.
|
||||||
|
{
|
||||||
|
let hole = Rc::clone(hole);
|
||||||
|
window.connect_map(move |win| apply_dismiss_hole(win, &hole));
|
||||||
|
}
|
||||||
window
|
window
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets `dismiss`'s click-away input region to "everywhere" minus `hole`
|
||||||
|
/// (if any) — see `PanelSet::show_capsule_dismiss`'s doc comment for why.
|
||||||
|
/// Only takes effect once `dismiss.surface()` is real, i.e. the window is
|
||||||
|
/// currently mapped; harmlessly no-ops otherwise (the `connect_map` hook in
|
||||||
|
/// `make_dismiss` re-runs this the moment that stops being true).
|
||||||
|
fn apply_dismiss_hole(dismiss: >k4::Window, hole: &DismissHole) {
|
||||||
|
let Some(surface) = dismiss.surface() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match hole.get() {
|
||||||
|
Some((x, y, w, h)) => {
|
||||||
|
let canvas = gtk4::cairo::RectangleInt::new(
|
||||||
|
-HOLE_CANVAS_SPAN,
|
||||||
|
-HOLE_CANVAS_SPAN,
|
||||||
|
HOLE_CANVAS_SPAN * 2,
|
||||||
|
HOLE_CANVAS_SPAN * 2,
|
||||||
|
);
|
||||||
|
let region = gtk4::cairo::Region::create_rectangle(&canvas);
|
||||||
|
let punch = gtk4::cairo::RectangleInt::new(x, y, w, h);
|
||||||
|
if region.subtract_rectangle(&punch).is_ok() {
|
||||||
|
surface.set_input_region(Some(®ion));
|
||||||
|
} else {
|
||||||
|
// Punching the hole failed for some reason (an invalid
|
||||||
|
// cairo status on a plain rectangle op, effectively
|
||||||
|
// unreachable in practice) — falling back to `None` (the
|
||||||
|
// protocol's documented "no input region set: whole
|
||||||
|
// surface hits") is still safer than leaving whatever
|
||||||
|
// region predates this call in place, which could be
|
||||||
|
// stale from a completely different mode (e.g. an old
|
||||||
|
// popover-shaped margin-only region with no hole at all).
|
||||||
|
eprintln!(
|
||||||
|
"breadbar: could not punch capsule hole in dismiss scrim's input region"
|
||||||
|
);
|
||||||
|
surface.set_input_region(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => surface.set_input_region(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue