From cc3bac5ce7a82c70cda9e7d281aecd78ef8f2fd0 Mon Sep 17 00:00:00 2001 From: Makesesama Date: Tue, 5 May 2026 23:47:47 +0200 Subject: [PATCH] feat: player animations --- sims/modules/player.py | 93 +++++++++++++++++++++++++++++++++++++++--- sims/styles/bar.css | 1 - 2 files changed, 88 insertions(+), 6 deletions(-) diff --git a/sims/modules/player.py b/sims/modules/player.py index 9a083b2..d3b87c1 100644 --- a/sims/modules/player.py +++ b/sims/modules/player.py @@ -2,7 +2,7 @@ import os import urllib.parse import urllib.request import tempfile -from gi.repository import Gtk, GLib, Gio, Gdk +from gi.repository import Gtk, GLib, Gio, Gdk, Pango from fabric.widgets.box import Box from fabric.widgets.centerbox import CenterBox from fabric.widgets.label import Label @@ -578,6 +578,9 @@ class PlayerSmall(Box): max_chars_width=26, h_align="center", ) + self._width_tween_id = None + self._current_label_width_px = None + self._width_tween_duration_ms = 220 self.play_image = Image( name="compact-mpris-button-icon", @@ -633,6 +636,86 @@ class PlayerSmall(Box): self.mpris_manager.connect("player-appeared", self.on_player_appeared) self.mpris_manager.connect("player-vanished", self.on_player_vanished) + def _char_width_px(self): + metrics = self.mpris_label.get_pango_context().get_metrics(None, None) + return max(1.0, metrics.get_approximate_char_width() / Pango.SCALE) + + def _measure_text_width(self, text): + if not text: + return 0 + layout = self.mpris_label.create_pango_layout(text) + text_width_px, _ = layout.get_pixel_size() + max_px = int(self._char_width_px() * 26) + return min(text_width_px, max_px) + + def _apply_label_width(self, width_px): + if width_px <= 0: + self.mpris_label.set_size_request(0, -1) + if self.mpris_label.get_margin_start() != 0: + self.mpris_label.set_margin_start(0) + self.mpris_label.set_margin_end(0) + if self.mpris_label.get_visible(): + self.mpris_label.set_visible(False) + self._current_label_width_px = 0 + return + + if not self.mpris_label.get_visible(): + self.mpris_label.set_visible(True) + chars = max(1, int(round(width_px / self._char_width_px()))) + if chars != self.mpris_label.get_max_width_chars(): + self.mpris_label.set_max_width_chars(chars) + self.mpris_label.set_size_request(width_px, -1) + margin = min(6, width_px // 2) + if self.mpris_label.get_margin_start() != margin: + self.mpris_label.set_margin_start(margin) + self.mpris_label.set_margin_end(margin) + self._current_label_width_px = width_px + + def _set_label_text(self, text): + target_text = text or "" + target_px = self._measure_text_width(target_text) + + if self._width_tween_id is not None: + GLib.source_remove(self._width_tween_id) + self._width_tween_id = None + + # First call — snap to target without animation. + if self._current_label_width_px is None: + self.mpris_label.set_text(target_text) + self._apply_label_width(target_px) + return + + start_px = self._current_label_width_px + + # Growing from collapsed: set new text first so it's ready to reveal. + # Cross-fading between two non-empty texts: also swap text immediately. + # Shrinking to empty: keep old text visible while it shrinks, clear at end. + if target_px > 0: + self.mpris_label.set_text(target_text) + + if start_px == target_px: + self._apply_label_width(target_px) + return + + duration_ms = self._width_tween_duration_ms + start_time = GLib.get_monotonic_time() + + def tick(): + elapsed_ms = (GLib.get_monotonic_time() - start_time) / 1000.0 + progress = min(1.0, elapsed_ms / duration_ms) + t = 1 - (1 - progress) ** 3 # ease-out cubic + cur_px = int(start_px + (target_px - start_px) * t) + self._apply_label_width(cur_px) + if progress >= 1.0: + self._apply_label_width(target_px) + if target_px == 0: + self.mpris_label.set_text("") + self._width_tween_id = None + return False + return True + + self._width_tween_id = GLib.timeout_add(16, tick) + def _set_cover_from_path(self, image_path): if image_path and os.path.isfile(image_path): try: @@ -701,7 +784,7 @@ class PlayerSmall(Box): def _apply_mpris_properties(self): if not self.mpris_player: - self.mpris_label.set_text("Nothing Playing") + self._set_label_text("") self.play_image.set_property("icon-name", self.PLAY_ICON) self.cover_stack.set_visible_child(self.fallback_icon) self.center_stack.set_visible_child(self.mpris_label) @@ -713,10 +796,10 @@ class PlayerSmall(Box): self.update_play_pause_icon() if self._current_display == "title": - text = mp.title if mp.title and mp.title.strip() else "Nothing Playing" + text = mp.title.strip() if mp.title and mp.title.strip() else "" else: # "artist" - text = mp.artist if mp.artist and mp.artist.strip() else "Nothing Playing" - self.mpris_label.set_text(text) + text = mp.artist.strip() if mp.artist and mp.artist.strip() else "" + self._set_label_text(text) self.center_stack.set_visible_child(self.mpris_label) def _on_icon_button_press(self, widget, event): diff --git a/sims/styles/bar.css b/sims/styles/bar.css index 4a7d0b5..46175fd 100644 --- a/sims/styles/bar.css +++ b/sims/styles/bar.css @@ -95,7 +95,6 @@ #compact-mpris-label { color: var(--foreground); font-size: 13px; - margin: 0 6px; } #nixos-label {