"""PeakBar — the live amplitude indicator (see backend/peaklevel.py), drawn as a narrow purple column *inside* the magenta volume meter it belongs to rather than as a separate bar above the card: one glance tells you both what the volume is set to and how much signal is actually reaching the mix, so a silent app/device reads differently from one that's just quiet (a stuck or mis-routed stream shows an empty inner column at 100% volume). Cairo-drawn instead of a Gtk.LevelBar because it has to line up with the Gtk.Scale trough underneath it: the fill has to grow bottom-to-top over the same span as the scale's highlight, minus the half-slider inset the scale reserves at each end (`inset` below). It also sidesteps this GTK build's reluctance to paint CSS backgrounds on non-drawing widgets. Owned by ui/meter.py, which stacks it over the scale in a Gtk.Overlay. """ from __future__ import annotations import math import gi gi.require_version("Gtk", "4.0") from gi.repository import Gtk # noqa: E402 # glow-violet, the suite's frame colour — deliberately *inside* the meter's # emitted-magenta fill, so "set to" and "actually playing" never read alike PEAK_COLOR = (0x8A / 255, 0x5C / 255, 0xFF / 255) class PeakBar: WIDTH = 9 # narrower than the meter trough, so magenta frames it MIN_VISIBLE = 0.01 def __init__(self, inset: int = 0) -> None: self._inset = inset self._level = 0.0 self.widget = Gtk.DrawingArea() self.widget.add_css_class("sb-peakbar") self.widget.set_size_request(self.WIDTH, -1) self.widget.set_halign(Gtk.Align.CENTER) self.widget.set_valign(Gtk.Align.FILL) self.widget.set_vexpand(True) # the scale underneath owns every click/drag in this area self.widget.set_can_target(False) self.widget.set_draw_func(self._draw) def set_level(self, level: float) -> None: level = max(0.0, min(1.0, level)) if abs(level - self._level) < 0.002: return self._level = level self.widget.queue_draw() # -- drawing ------------------------------------------------------------------ def _draw(self, _area, cr, width: int, height: int) -> None: if self._level < self.MIN_VISIBLE: return top = self._inset bottom = height - self._inset span = bottom - top if span <= 0: return w = min(width, self.WIDTH) x = (width - w) / 2.0 h = span * self._level y = bottom - h self._rounded(cr, x, y, w, h, w / 2.0) cr.set_source_rgba(*PEAK_COLOR, 0.95) cr.fill_preserve() cr.set_source_rgba(*PEAK_COLOR, 0.35) cr.set_line_width(2.0) cr.stroke() @staticmethod def _rounded(cr, x: float, y: float, w: float, h: float, r: float) -> None: r = min(r, w / 2.0, h / 2.0) cr.new_sub_path() cr.arc(x + w - r, y + r, r, -math.pi / 2, 0) cr.arc(x + w - r, y + h - r, r, 0, math.pi / 2) cr.arc(x + r, y + h - r, r, math.pi / 2, math.pi) cr.arc(x + r, y + r, r, math.pi, 3 * math.pi / 2) cr.close_path()