Dotfiles/desktopenvs/hyprlua/audio-panel/ui/peakbar.py

85 lines
3.1 KiB
Python

"""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()