workspaces: derive trail width only from a live button; wire the launcher hotkey

Two fixes.

The trail pill still grew when spamming between workspace 1 and an empty 6.
Switching to an empty workspace makes Hyprland create and destroy it, which
rebuilds the button row mid-animation and leaves the source button detached —
button_geom then returned None and the fallback handed the wide mid-stretch
span straight back in, reintroducing exactly the accumulation the previous
commit removed. Width now falls back to the destination button, which is
always live, and no path returns the live width any more.

Separately, AppInput::OpenLauncher's local arm only called grab_focus(). Focus
stopped opening the capsule when connect_enter's open_fn() call was removed
(that call was what opened the capsule during window construction), so the
keybind focused the entry and left the drawer shut. It now invokes
launcher_open_fn, which was added for this and never read — the dead-code
warning on that field is what surfaced it.
This commit is contained in:
Breadway 2026-08-26 11:45:18 +08:00
parent 42d5acb736
commit 1b523eab63
2 changed files with 26 additions and 5 deletions

View file

@ -369,7 +369,7 @@ impl WorkspaceTrail {
pub fn stretch(&self, from: Option<&gtk4::Button>, to: &gtk4::Button) {
self.cancel();
let Some(from_g) = self.from_geom(from) else {
let Some(from_g) = self.from_geom(from, to) else {
self.place(to);
return;
};
@ -411,7 +411,7 @@ impl WorkspaceTrail {
self.inner.borrow_mut().tick = Some(id);
}
fn from_geom(&self, from: Option<&gtk4::Button>) -> Option<Geom> {
fn from_geom(&self, from: Option<&gtk4::Button>, to: &gtk4::Button) -> Option<Geom> {
let st = self.inner.borrow();
let live = if self.pill.is_visible() && st.geom.w > 0.5 {
Some(st.geom)
@ -420,7 +420,17 @@ impl WorkspaceTrail {
};
drop(st);
let natural = from.and_then(|b| button_geom(b, &self.host).map(inset_pill));
// Width must always come from a button that is CURRENTLY IN THE ROW.
// Switching to an empty workspace makes Hyprland create and destroy it,
// which rebuilds the button row mid-animation and can leave `from`
// detached — `button_geom` then returns None. Falling back to the live
// geometry there handed the wide mid-stretch span straight back in,
// which is exactly the accumulation this function exists to prevent
// (reproduced by spamming between workspace 1 and an empty 6). The
// destination button is always live, so it is the correct fallback.
let natural = from
.and_then(|b| button_geom(b, &self.host).map(inset_pill))
.or_else(|| button_geom(to, &self.host).map(inset_pill));
// Continuity without accumulation.
//
@ -437,6 +447,10 @@ impl WorkspaceTrail {
// Taking position from the live geometry and width from the source
// button's natural size keeps the motion continuous while making width
// a pure function of which button we started from.
// `natural` is None only if BOTH buttons are detached, in which case
// there is no sane width to animate from and the caller falls back to
// an instant `place()`. There is deliberately no path that returns the
// live width.
match (live, natural) {
(Some(live), Some(natural)) => Some(Geom {
x: live.x,
@ -444,8 +458,8 @@ impl WorkspaceTrail {
w: natural.w,
h: natural.h,
}),
(Some(live), None) if live.w <= MAX_CHIP_W => Some(live),
(_, natural) => natural,
(None, natural) => natural,
(Some(_), None) => None,
}
}
}

View file

@ -1915,6 +1915,13 @@ impl SimpleComponent for App {
// below is a silent no-op.
self.launcher_entry.set_can_focus(true);
self.launcher_entry.grab_focus();
// ...and then actually OPEN it. Focus alone no longer
// opens the capsule: `connect_enter` used to call
// `open_fn()`, which is precisely what made the capsule
// open itself during window construction, so that path
// was deliberately removed. Without this call the
// keybind focuses the entry and leaves the drawer shut.
(self.launcher_open_fn)();
}
}
}