diff --git a/src/bar/workspaces.rs b/src/bar/workspaces.rs index 8bdf799..ef2d833 100644 --- a/src/bar/workspaces.rs +++ b/src/bar/workspaces.rs @@ -369,7 +369,7 @@ impl WorkspaceTrail { pub fn stretch(&self, from: Option<>k4::Button>, to: >k4::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<>k4::Button>) -> Option { + fn from_geom(&self, from: Option<>k4::Button>, to: >k4::Button) -> Option { 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, } } } diff --git a/src/main.rs b/src/main.rs index e78ff10..1e4f77a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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)(); } } }