85 lines
3.1 KiB
Python
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()
|