fix(hyprdrive/station-bar): wall-clock safety net so the holo intro can't stall

The materialise intro was driven only by the frame-clock tick, which stops
advancing when the compositor withholds frame callbacks (e.g. the bar restarted
into an idle compositor) — freezing it at t=0 with content stuck at opacity 0,
i.e. permanent static. Arm a GLib.timeout alongside start_intro() that
force-resolves the intro regardless, so content always appears.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
main
Amir Alexander Abdelbaki 2026-07-19 16:21:28 +02:00
parent e1126e9613
commit 837bc2823a
1 changed files with 35 additions and 4 deletions

View File

@ -19,7 +19,7 @@ import cairo
import gi import gi
gi.require_version("Gtk", "4.0") gi.require_version("Gtk", "4.0")
from gi.repository import Gtk # noqa: E402 from gi.repository import GLib, Gtk # noqa: E402
# Same CyberQueer violet/magenta/red combo as the rest of the suite's hologram. # Same CyberQueer violet/magenta/red combo as the rest of the suite's hologram.
_VIOLET = (0x50 / 255, 0x18 / 255, 0xDD / 255) _VIOLET = (0x50 / 255, 0x18 / 255, 0xDD / 255)
@ -58,6 +58,13 @@ class HologramOverlay:
self._intro_t: float | None = None # >=0 while the materialise intro plays self._intro_t: float | None = None # >=0 while the materialise intro plays
self._outro_t: float | None = None # >=0 while the closing dissolve plays self._outro_t: float | None = None # >=0 while the closing dissolve plays
self._outro_done = None # callback fired when the dissolve finishes self._outro_done = None # callback fired when the dissolve finishes
# Wall-clock safety net for the intro: the frame-clock tick that normally
# advances the intro only fires while the compositor sends frame callbacks.
# When the bar is (re)started into an otherwise-idle compositor those can
# stall, freezing the intro at t=0 — i.e. the bar sits in permanent static
# with content stuck at opacity 0. This timeout force-resolves the intro on
# real time regardless, so content always appears.
self._intro_deadline_id: int | None = None
self._mask_cache: tuple | None = None # (w, h, pattern) edge-fade mask self._mask_cache: tuple | None = None # (w, h, pattern) edge-fade mask
self.widget = Gtk.DrawingArea() self.widget = Gtk.DrawingArea()
@ -76,9 +83,7 @@ class HologramOverlay:
if self._intro_t is not None: if self._intro_t is not None:
self._intro_t += dt self._intro_t += dt
if self._intro_t >= self.INTRO_DURATION: if self._intro_t >= self.INTRO_DURATION:
self._intro_t = None self._finish_intro()
if self._fade_widget is not None:
self._fade_widget.set_opacity(1.0)
elif self._fade_widget is not None: elif self._fade_widget is not None:
p = self._intro_t / self.INTRO_DURATION p = self._intro_t / self.INTRO_DURATION
self._fade_widget.set_opacity(p * p * (3 - 2 * p)) # smooth ramp 0->1 self._fade_widget.set_opacity(p * p * (3 - 2 * p)) # smooth ramp 0->1
@ -95,6 +100,17 @@ class HologramOverlay:
done() done()
self.widget.queue_draw() self.widget.queue_draw()
def _finish_intro(self) -> None:
"""End the intro: clear its state, cancel the safety timeout, and make the
content fully opaque. Safe to call from either the tick or the timeout."""
self._intro_t = None
if self._intro_deadline_id is not None:
GLib.source_remove(self._intro_deadline_id)
self._intro_deadline_id = None
if self._fade_widget is not None:
self._fade_widget.set_opacity(1.0)
self.widget.queue_draw() # in case the frame clock has stalled
def start_intro(self) -> None: def start_intro(self) -> None:
"""Kick off the 'hologram materialising out of static' opening effect.""" """Kick off the 'hologram materialising out of static' opening effect."""
if self.enabled: if self.enabled:
@ -103,6 +119,18 @@ class HologramOverlay:
self._intro_t = 0.0 self._intro_t = 0.0
if self._fade_widget is not None: if self._fade_widget is not None:
self._fade_widget.set_opacity(0.0) # start hidden; tick() ramps it up self._fade_widget.set_opacity(0.0) # start hidden; tick() ramps it up
# Wall-clock backstop: if frame callbacks stall, force the intro done a
# little past its nominal length so content can never get stuck hidden.
if self._intro_deadline_id is not None:
GLib.source_remove(self._intro_deadline_id)
self._intro_deadline_id = GLib.timeout_add(
int(self.INTRO_DURATION * 1000) + 150, self._on_intro_deadline)
def _on_intro_deadline(self) -> bool:
self._intro_deadline_id = None
if self._intro_t is not None:
self._finish_intro()
return False # one-shot
def start_outro(self, on_done) -> None: def start_outro(self, on_done) -> None:
"""Play a quick reverse of the intro (content dissolving back into static), """Play a quick reverse of the intro (content dissolving back into static),
@ -111,6 +139,9 @@ class HologramOverlay:
on_done() on_done()
return return
self._intro_t = None # cancel any in-flight opening intro self._intro_t = None # cancel any in-flight opening intro
if self._intro_deadline_id is not None:
GLib.source_remove(self._intro_deadline_id)
self._intro_deadline_id = None
self._outro_t = 0.0 self._outro_t = 0.0
self._outro_done = on_done self._outro_done = on_done