Fix CI: ship panel.rs and the workspace trail helper
Some checks failed
dev release / build (push) Failing after 9s
Some checks failed
dev release / build (push) Failing after 9s
mod panel and stretch_geom_on were referenced on main but not committed, so --locked release builds failed.
This commit is contained in:
parent
1806c6f912
commit
96d666b3cb
4 changed files with 325 additions and 55 deletions
|
|
@ -230,49 +230,68 @@ impl WorkspaceTrail {
|
|||
}
|
||||
|
||||
pub fn place(&self, btn: >k4::Button) {
|
||||
self.cancel();
|
||||
if let Some(g) = button_geom(btn, &self.overlay) {
|
||||
self.apply(&g);
|
||||
return;
|
||||
}
|
||||
// First map: the button exists but has no allocation yet.
|
||||
let pill = self.pill.clone();
|
||||
let host = self.host.clone();
|
||||
let btn = btn.clone();
|
||||
let inner = self.inner.clone();
|
||||
let id = self.overlay.add_tick_callback(move |ov, _| {
|
||||
let Some(g) = button_geom(&btn, ov) else {
|
||||
return ControlFlow::Continue;
|
||||
};
|
||||
self.when_stable(btn, move |g| {
|
||||
apply_geom(&host, &pill, &inner, &g);
|
||||
inner.borrow_mut().tick = None;
|
||||
ControlFlow::Break
|
||||
});
|
||||
self.inner.borrow_mut().tick = Some(id);
|
||||
}
|
||||
|
||||
pub fn stretch(&self, from: Option<>k4::Button>, to: >k4::Button) {
|
||||
let from_g = self.from_geom(from);
|
||||
if let Some(to_g) = button_geom(to, &self.overlay) {
|
||||
self.stretch_geom(from_g, to_g);
|
||||
return;
|
||||
}
|
||||
// Destination just appeared (empty workspace becoming active) and
|
||||
// has no allocation yet. Wait one layout pass, then stretch from
|
||||
// the last pill — don't snap via place().
|
||||
self.cancel();
|
||||
let overlay = self.overlay.clone();
|
||||
let pill = self.pill.clone();
|
||||
let host = self.host.clone();
|
||||
let inner = self.inner.clone();
|
||||
let btn = to.clone();
|
||||
let dest = to.clone();
|
||||
self.when_stable(to, move |to_g| {
|
||||
stretch_geom_on(&overlay, &host, &pill, &inner, from_g, to_g, Some(dest));
|
||||
});
|
||||
}
|
||||
|
||||
/// New empty-workspace buttons first allocate at CSS `min-width` (32px)
|
||||
/// and only then grow to the padded label. Two stable frames of that
|
||||
/// placeholder is not enough — empty→empty used to shrink the pill to it.
|
||||
fn when_stable(&self, btn: >k4::Button, then: impl FnOnce(Geom) + 'static) {
|
||||
self.cancel();
|
||||
let btn = btn.clone();
|
||||
let inner = self.inner.clone();
|
||||
let last = std::cell::Cell::new(None::<Geom>);
|
||||
let same = std::cell::Cell::new(0u8);
|
||||
let frames = std::cell::Cell::new(0u8);
|
||||
let then = std::cell::Cell::new(Some(then));
|
||||
let id = self.overlay.add_tick_callback(move |ov, _| {
|
||||
let Some(to_g) = button_geom(&btn, ov) else {
|
||||
return ControlFlow::Continue;
|
||||
};
|
||||
frames.set(frames.get().saturating_add(1));
|
||||
let n = frames.get();
|
||||
let Some(g) = button_geom(&btn, ov) else {
|
||||
last.set(None);
|
||||
same.set(0);
|
||||
return if n > 24 {
|
||||
inner.borrow_mut().tick = None;
|
||||
stretch_geom_on(&overlay, &host, &pill, &inner, from_g, to_g);
|
||||
ControlFlow::Break
|
||||
} else {
|
||||
ControlFlow::Continue
|
||||
};
|
||||
};
|
||||
// Still sitting on the 32px min-width slot, or smaller than the
|
||||
// button's natural request — keep waiting for the real layout.
|
||||
if still_placeholder(&btn, &g) && n < 20 {
|
||||
last.set(None);
|
||||
same.set(0);
|
||||
return ControlFlow::Continue;
|
||||
}
|
||||
let stable = last.get().is_some_and(|p| geom_close(&p, &g));
|
||||
last.set(Some(g));
|
||||
same.set(if stable { same.get().saturating_add(1) } else { 0 });
|
||||
if same.get() >= 2 || n > 22 {
|
||||
inner.borrow_mut().tick = None;
|
||||
if let Some(f) = then.take() {
|
||||
f(g);
|
||||
}
|
||||
return ControlFlow::Break;
|
||||
}
|
||||
ControlFlow::Continue
|
||||
});
|
||||
self.inner.borrow_mut().tick = Some(id);
|
||||
}
|
||||
|
|
@ -286,20 +305,68 @@ impl WorkspaceTrail {
|
|||
from.and_then(|b| button_geom(b, &self.overlay))
|
||||
}
|
||||
|
||||
fn stretch_geom(&self, from_g: Option<Geom>, to_g: Geom) {
|
||||
stretch_geom_on(
|
||||
&self.overlay,
|
||||
&self.host,
|
||||
&self.pill,
|
||||
&self.inner,
|
||||
from_g,
|
||||
to_g,
|
||||
);
|
||||
}
|
||||
|
||||
fn stretch_geom_on(
|
||||
overlay: >k4::Overlay,
|
||||
host: >k4::Fixed,
|
||||
pill: >k4::Box,
|
||||
inner: &Rc<RefCell<TrailInner>>,
|
||||
from_g: Option<Geom>,
|
||||
to_g: Geom,
|
||||
dest: Option<gtk4::Button>,
|
||||
) {
|
||||
let Some(from_g) = from_g else {
|
||||
apply_geom(host, pill, inner, &to_g);
|
||||
return;
|
||||
};
|
||||
if (from_g.x - to_g.x).abs() < 0.5 && (from_g.w - to_g.w).abs() < 0.5 {
|
||||
apply_geom(host, pill, inner, &to_g);
|
||||
return;
|
||||
}
|
||||
|
||||
fn apply(&self, g: &Geom) {
|
||||
apply_geom(&self.host, &self.pill, &self.inner, g);
|
||||
let span_x = from_g.x.min(to_g.x);
|
||||
let span_w = (from_g.x + from_g.w).max(to_g.x + to_g.w) - span_x;
|
||||
let mid = Geom {
|
||||
x: span_x,
|
||||
y: to_g.y,
|
||||
w: span_w,
|
||||
h: to_g.h,
|
||||
};
|
||||
|
||||
if let Some(id) = inner.borrow_mut().tick.take() {
|
||||
id.remove();
|
||||
}
|
||||
let started = Instant::now();
|
||||
let pill = pill.clone();
|
||||
let host = host.clone();
|
||||
let inner_tick = inner.clone();
|
||||
let dest = dest.clone();
|
||||
let ov = overlay.clone();
|
||||
let id = overlay.add_tick_callback(move |_, _| {
|
||||
let elapsed = started.elapsed().as_secs_f64() * 1000.0;
|
||||
let (g, done) = if elapsed < STRETCH_MS {
|
||||
let t = ease(elapsed / STRETCH_MS);
|
||||
(lerp_geom(&from_g, &mid, t), false)
|
||||
} else if elapsed < STRETCH_MS + SNAP_MS {
|
||||
let t = ease_overshoot((elapsed - STRETCH_MS) / SNAP_MS);
|
||||
(lerp_geom(&mid, &to_g, t), false)
|
||||
} else {
|
||||
let end = dest
|
||||
.as_ref()
|
||||
.and_then(|b| button_geom(b, &ov))
|
||||
.unwrap_or(to_g);
|
||||
(end, true)
|
||||
};
|
||||
apply_geom(&host, &pill, &inner_tick, &g);
|
||||
if done {
|
||||
inner_tick.borrow_mut().tick = None;
|
||||
ControlFlow::Break
|
||||
} else {
|
||||
ControlFlow::Continue
|
||||
}
|
||||
});
|
||||
inner.borrow_mut().tick = Some(id);
|
||||
}
|
||||
|
||||
fn button_geom(btn: >k4::Button, overlay: >k4::Overlay) -> Option<Geom> {
|
||||
|
|
@ -324,11 +391,27 @@ fn apply_geom(host: >k4::Fixed, pill: >k4::Box, inner: &Rc<RefCell<TrailInne
|
|||
w: g.w,
|
||||
h: g.h,
|
||||
};
|
||||
pill.set_size_request(g.w.max(1.0).round() as i32, g.h.max(1.0).round() as i32);
|
||||
let w = g.w.max(1.0).round() as i32;
|
||||
let h = g.h.max(1.0).round() as i32;
|
||||
// Clearing first lets GTK shrink; size-request is a minimum.
|
||||
pill.set_size_request(-1, -1);
|
||||
pill.set_size_request(w, h);
|
||||
host.move_(pill, g.x, g.y);
|
||||
pill.set_visible(true);
|
||||
}
|
||||
|
||||
fn still_placeholder(btn: >k4::Button, g: &Geom) -> bool {
|
||||
let (min_w, nat_w, _, _) = btn.measure(gtk4::Orientation::Horizontal, -1);
|
||||
g.w <= f64::from(min_w) + 1.0 || g.w + 0.5 < f64::from(nat_w)
|
||||
}
|
||||
|
||||
fn geom_close(a: &Geom, b: &Geom) -> bool {
|
||||
(a.x - b.x).abs() < 0.5
|
||||
&& (a.y - b.y).abs() < 0.5
|
||||
&& (a.w - b.w).abs() < 0.5
|
||||
&& (a.h - b.h).abs() < 0.5
|
||||
}
|
||||
|
||||
fn lerp(a: f64, b: f64, t: f64) -> f64 {
|
||||
a + (b - a) * t
|
||||
}
|
||||
|
|
|
|||
58
src/main.rs
58
src/main.rs
|
|
@ -62,9 +62,11 @@ pub struct App {
|
|||
cpu_pair: gtk4::Box,
|
||||
mem_pair: gtk4::Box,
|
||||
pwr_pair: gtk4::Box,
|
||||
gpu_pair: gtk4::Box,
|
||||
cpu_lbl: gtk4::Label,
|
||||
mem_lbl: gtk4::Label,
|
||||
pwr_lbl: gtk4::Label,
|
||||
gpu_lbl: gtk4::Label,
|
||||
vol_lbl: gtk4::Label,
|
||||
bat_lbl: gtk4::Label,
|
||||
bat_img: gtk4::Image,
|
||||
|
|
@ -240,6 +242,7 @@ impl SimpleComponent for App {
|
|||
let cpu_lbl = stat_label();
|
||||
let mem_lbl = stat_label();
|
||||
let pwr_lbl = stat_label();
|
||||
let gpu_lbl = stat_label();
|
||||
let vol_lbl = stat_label();
|
||||
let bat_lbl = stat_label();
|
||||
|
||||
|
|
@ -380,11 +383,22 @@ impl SimpleComponent for App {
|
|||
let cpu_pair = stat_pair(asset!("CPU.svg"), &cpu_lbl);
|
||||
let mem_pair = stat_pair(asset!("RAM Usage.svg"), &mem_lbl);
|
||||
let pwr_pair = stat_pair(asset!("Power Draw.svg"), &pwr_lbl);
|
||||
let system_stats_box = gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
|
||||
system_stats_box.append(&cpu_pair);
|
||||
system_stats_box.append(&mem_pair);
|
||||
system_stats_box.append(&pwr_pair);
|
||||
system_stats_box.set_visible(false);
|
||||
let gpu_pair = stat_pair(asset!("GPU.svg"), &gpu_lbl);
|
||||
for pair in [&cpu_pair, &mem_pair, &pwr_pair, &gpu_pair] {
|
||||
pair.add_css_class("sys-stat");
|
||||
pair.set_hexpand(true);
|
||||
}
|
||||
gpu_pair.set_visible(false);
|
||||
let system_stats_box = gtk4::Box::new(gtk4::Orientation::Vertical, 4);
|
||||
system_stats_box.add_css_class("sys-grid");
|
||||
let sys_row1 = gtk4::Box::new(gtk4::Orientation::Horizontal, 8);
|
||||
sys_row1.append(&cpu_pair);
|
||||
sys_row1.append(&mem_pair);
|
||||
let sys_row2 = gtk4::Box::new(gtk4::Orientation::Horizontal, 8);
|
||||
sys_row2.append(&gpu_pair);
|
||||
sys_row2.append(&pwr_pair);
|
||||
system_stats_box.append(&sys_row1);
|
||||
system_stats_box.append(&sys_row2);
|
||||
let system_sep = gtk4::Separator::new(gtk4::Orientation::Vertical);
|
||||
system_sep.add_css_class("bar-sep");
|
||||
system_sep.set_visible(false);
|
||||
|
|
@ -507,6 +521,13 @@ impl SimpleComponent for App {
|
|||
let panel_bright_slider = bright_row.1.clone();
|
||||
panel_inner.append(&bright_row.0);
|
||||
|
||||
let sys_header = gtk4::Label::new(Some("SYSTEM"));
|
||||
sys_header.add_css_class("control-panel-header");
|
||||
sys_header.set_xalign(0.0);
|
||||
sys_header.set_margin_top(10);
|
||||
panel_inner.append(&sys_header);
|
||||
panel_inner.append(&system_stats_box);
|
||||
|
||||
let power_row = gtk4::Box::new(gtk4::Orientation::Horizontal, 6);
|
||||
power_row.add_css_class("power-row");
|
||||
power_row.set_halign(gtk4::Align::Center);
|
||||
|
|
@ -697,9 +718,11 @@ impl SimpleComponent for App {
|
|||
cpu_pair,
|
||||
mem_pair,
|
||||
pwr_pair,
|
||||
gpu_pair,
|
||||
cpu_lbl,
|
||||
mem_lbl,
|
||||
pwr_lbl,
|
||||
gpu_lbl,
|
||||
vol_lbl,
|
||||
bat_lbl,
|
||||
bat_img,
|
||||
|
|
@ -854,17 +877,24 @@ impl SimpleComponent for App {
|
|||
self.date_lbl.set_label(&bar::clock::date());
|
||||
}
|
||||
AppInput::StatsUpdate(stats) => {
|
||||
self.cpu_lbl.set_label(&stats.cpu);
|
||||
let cpu = match stats.cpu_temp {
|
||||
Some(t) => format!("{} · {:.0}°", stats.cpu, t),
|
||||
None => stats.cpu,
|
||||
};
|
||||
self.cpu_lbl.set_label(&cpu);
|
||||
self.mem_lbl.set_label(&stats.mem);
|
||||
self.pwr_lbl.set_label(&stats.power);
|
||||
|
||||
// Island bar never shows the system-stats trio — they live
|
||||
// in the control panel. Keep the widgets hidden so a later
|
||||
// re-parent cannot accidentally flash them.
|
||||
self.cpu_pair.set_visible(false);
|
||||
self.mem_pair.set_visible(false);
|
||||
self.pwr_pair.set_visible(false);
|
||||
self.system_stats_box.set_visible(false);
|
||||
match stats.gpu_usage {
|
||||
Some(g) => {
|
||||
let gpu = match stats.gpu_temp {
|
||||
Some(t) => format!("{g}% · {t:.0}°"),
|
||||
None => format!("{g}%"),
|
||||
};
|
||||
self.gpu_lbl.set_label(&gpu);
|
||||
self.gpu_pair.set_visible(true);
|
||||
}
|
||||
None => self.gpu_pair.set_visible(false),
|
||||
}
|
||||
self.system_sep.set_visible(false);
|
||||
|
||||
tick_label(&self.vol_lbl, &stats.volume_pct.to_string());
|
||||
|
|
|
|||
154
src/panel.rs
Normal file
154
src/panel.rs
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
//! Standalone layer-shell panels for wifi / control / media.
|
||||
//!
|
||||
//! GTK `Popover` is an xdg_popup child of the island, so it paints over the
|
||||
//! bar and Hyprland can only fade it. These are their own surfaces, parked
|
||||
//! *below* the exclusive zone, and Hyprland slides `breadbar-panel` in from
|
||||
//! the right.
|
||||
|
||||
use gtk4::gdk::Key;
|
||||
use gtk4::prelude::*;
|
||||
use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
|
||||
|
||||
use crate::{bind_layer_monitor, theme, BAR_HEIGHT, BAR_MARGIN_SIDES, BAR_MARGIN_TOP};
|
||||
|
||||
const BELOW_BAR: i32 = BAR_MARGIN_TOP + BAR_HEIGHT + 8;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PanelSet {
|
||||
pub connectivity: gtk4::Window,
|
||||
pub control: gtk4::Window,
|
||||
pub media: gtk4::Window,
|
||||
dismiss: gtk4::Window,
|
||||
}
|
||||
|
||||
impl PanelSet {
|
||||
pub fn new(
|
||||
monitor: &str,
|
||||
connectivity_child: &impl IsA<gtk4::Widget>,
|
||||
control_child: &impl IsA<gtk4::Widget>,
|
||||
media_child: &impl IsA<gtk4::Widget>,
|
||||
) -> Self {
|
||||
let connectivity = make_panel("wifi-popover", connectivity_child, monitor);
|
||||
let control = make_panel("control-panel", control_child, monitor);
|
||||
let media = make_panel("media-popover", media_child, monitor);
|
||||
let dismiss = make_dismiss(monitor);
|
||||
|
||||
let set = Self {
|
||||
connectivity,
|
||||
control,
|
||||
media,
|
||||
dismiss,
|
||||
};
|
||||
set.wire_dismiss();
|
||||
set.wire_escape();
|
||||
set
|
||||
}
|
||||
|
||||
pub fn toggle(&self, which: >k4::Window) {
|
||||
if which.is_visible() {
|
||||
self.hide_all();
|
||||
} else {
|
||||
self.show(which);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(&self, which: >k4::Window) {
|
||||
self.hide_panels();
|
||||
// Dismiss first so the panel maps above it (same Overlay layer).
|
||||
self.dismiss.set_visible(true);
|
||||
self.dismiss.present();
|
||||
which.set_visible(true);
|
||||
which.present();
|
||||
}
|
||||
|
||||
pub fn hide_all(&self) {
|
||||
self.hide_panels();
|
||||
self.dismiss.set_visible(false);
|
||||
}
|
||||
|
||||
fn hide_panels(&self) {
|
||||
self.connectivity.set_visible(false);
|
||||
self.control.set_visible(false);
|
||||
self.media.set_visible(false);
|
||||
}
|
||||
|
||||
fn wire_dismiss(&self) {
|
||||
let set = self.clone();
|
||||
let click = gtk4::GestureClick::new();
|
||||
click.set_button(0);
|
||||
click.connect_pressed(move |_, _, _, _| {
|
||||
set.hide_all();
|
||||
});
|
||||
if let Some(child) = self.dismiss.child() {
|
||||
child.add_controller(click);
|
||||
} else {
|
||||
self.dismiss.add_controller(click);
|
||||
}
|
||||
}
|
||||
|
||||
fn wire_escape(&self) {
|
||||
for win in [&self.connectivity, &self.control, &self.media] {
|
||||
let set = self.clone();
|
||||
let keys = gtk4::EventControllerKey::new();
|
||||
keys.connect_key_pressed(move |_, key, _, _| {
|
||||
if key == Key::Escape {
|
||||
set.hide_all();
|
||||
gtk4::glib::Propagation::Stop
|
||||
} else {
|
||||
gtk4::glib::Propagation::Proceed
|
||||
}
|
||||
});
|
||||
win.add_controller(keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_panel(class: &str, child: &impl IsA<gtk4::Widget>, monitor: &str) -> gtk4::Window {
|
||||
let window = gtk4::Window::new();
|
||||
window.add_css_class("breadbar-panel");
|
||||
window.add_css_class(class);
|
||||
window.set_decorated(false);
|
||||
window.set_resizable(false);
|
||||
window.init_layer_shell();
|
||||
window.set_namespace(Some("breadbar-panel"));
|
||||
window.set_layer(Layer::Overlay);
|
||||
window.set_anchor(Edge::Top, true);
|
||||
window.set_anchor(Edge::Right, true);
|
||||
window.set_margin(Edge::Top, BELOW_BAR);
|
||||
window.set_margin(Edge::Right, BAR_MARGIN_SIDES);
|
||||
window.set_exclusive_zone(-1);
|
||||
window.set_keyboard_mode(KeyboardMode::OnDemand);
|
||||
window.set_child(Some(child));
|
||||
bind_layer_monitor(&window, monitor);
|
||||
theme::bind_output(&window, monitor);
|
||||
window.set_visible(false);
|
||||
window
|
||||
}
|
||||
|
||||
fn make_dismiss(monitor: &str) -> gtk4::Window {
|
||||
let window = gtk4::Window::new();
|
||||
window.add_css_class("breadbar-dismiss");
|
||||
window.init_layer_shell();
|
||||
window.set_namespace(Some("breadbar-dismiss"));
|
||||
// Overlay with the panels, but mapped first so they sit above it.
|
||||
// Top margin keeps the island's chips clickable.
|
||||
window.set_layer(Layer::Overlay);
|
||||
window.set_anchor(Edge::Top, true);
|
||||
window.set_anchor(Edge::Bottom, true);
|
||||
window.set_anchor(Edge::Left, true);
|
||||
window.set_anchor(Edge::Right, true);
|
||||
window.set_margin(Edge::Top, BAR_MARGIN_TOP + BAR_HEIGHT);
|
||||
window.set_exclusive_zone(-1);
|
||||
window.set_keyboard_mode(KeyboardMode::None);
|
||||
// An empty window never maps a hit region. A filling child + a hair of
|
||||
// alpha is what actually receives the click-away.
|
||||
let hit = gtk4::Box::new(gtk4::Orientation::Vertical, 0);
|
||||
hit.add_css_class("breadbar-dismiss-hit");
|
||||
hit.set_hexpand(true);
|
||||
hit.set_vexpand(true);
|
||||
window.set_child(Some(&hit));
|
||||
bind_layer_monitor(&window, monitor);
|
||||
theme::bind_output(&window, monitor);
|
||||
window.set_visible(false);
|
||||
window
|
||||
}
|
||||
|
|
@ -190,7 +190,10 @@ fn load_css() -> String {
|
|||
.control-panel-btn:hover {{ opacity: 1; background: alpha(@on-bg, 0.10); }}\
|
||||
.control-panel-btn:active {{ background: alpha(@on-bg, 0.16); }}\
|
||||
.control-panel {{ }}\
|
||||
.control-panel-inner {{ min-width: 220px; padding: {pad}; }}\
|
||||
.control-panel-inner {{ min-width: 248px; padding: {pad}; }}\
|
||||
.sys-grid {{ margin: 2px 0 6px; }}\
|
||||
.sys-stat {{ padding: 4px 2px; background: transparent; }}\
|
||||
.sys-stat:hover {{ background: transparent; }}\
|
||||
.control-panel-header {{ font-size: 12px; font-weight: bold; letter-spacing: 0.12em;\
|
||||
opacity: 0.45; margin-bottom: 8px; }}\
|
||||
.control-panel-row {{ margin: 8px 0; }}\
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue