v0.31.2: panels pushed in, buttons pushed out; black bpm text

- The now-playing panel and the visualizer are debossed by exactly as
  much as the buttons are embossed: the same rims and outline, flipped
  (theme.deboss_qss / theme.paint_deboss), so things you press stand out
  and things that show you something sit in.
- The bpm button's text is black, enabled or not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 22:23:34 -07:00
co-authored by Claude Opus 5.5
parent c8c90b830a
commit 8797e63bd8
4 changed files with 65 additions and 27 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
"""LinTunes — iTunes-style music library manager and player for Linux."""
__version__ = "0.31.1"
__version__ = "0.31.2"
+5 -6
View File
@@ -51,6 +51,9 @@ TILE_STYLE = """
QFrame#transportBox QToolButton, QFrame#transportBox QPushButton {
border: none; background: transparent; border-radius: 6px;
}
QFrame#transportBox QPushButton, QFrame#transportBox QPushButton:disabled {
color: black;
}
QFrame#transportBox[tiles="true"] QToolButton:hover {
background: rgba(0, 0, 0, 22);
}
@@ -61,12 +64,8 @@ QFrame#transportBox[tiles="true"] QToolButton:pressed {
def _nowplaying_style(bg: QColor) -> str:
return f"""
QFrame#nowPlayingBox {{
background: {bg.name()};
border-radius: 8px;
}}
"""
"""Debossed: pushed in as far as the buttons stand out."""
return theme.deboss_qss("QFrame#nowPlayingBox", bg, radius=8)
# Title sits this many points above the artist/album line, both following the
# UI-scale app font (see theme.UI_SCALES).
+4 -6
View File
@@ -211,13 +211,11 @@ class VisualizerWidget(QWidget):
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
# Rounded gray panel + 1px border, matching the transport button boxes.
# A rounded panel debossed as far as the transport buttons stand out:
# it shows things, it isn't pressed.
rect = QRectF(self.rect()).adjusted(0.5, 0.5, -0.5, -0.5)
panel = QPainterPath()
panel.addRoundedRect(rect, RADIUS, RADIUS)
painter.fillPath(panel, self.palette().alternateBase().color())
painter.setPen(QPen(self.palette().mid().color(), 1))
painter.drawPath(panel)
panel = theme.paint_deboss(painter, rect,
self.palette().alternateBase().color(), RADIUS)
painter.setClipPath(panel) # keep bars inside the rounded shape
if self._no_pcm:
# Casting: the audio never passes through this machine, so show
+55 -14
View File
@@ -165,25 +165,27 @@ def apply_theme(app, prefs):
""")
def emboss_gradient(color: QColor, pressed: bool = False) -> str:
"""A shallow emboss around ``color`` that hugs the edges: a crisp lit rim
along the top and a crisp shaded rim along the bottom, with the face
itself nearly flat. Computed from the live color, so it follows the
palette (light or dark) and the user's grayscale overrides. ``pressed``
flips it, which reads as the button sinking in.
The color itself sits at the midpoint, so what the user picked is still
exactly what the middle of the button shows."""
def emboss_stops(color: QColor, pressed: bool = False) -> list:
"""[(position, QColor)] of the emboss: a crisp lit rim along the top and
a crisp shaded rim along the bottom, with the face itself nearly flat.
``pressed`` flips it — a button sinking in, or a debossed panel. The
color itself sits at the midpoint, so what the user picked is still
exactly what the middle shows."""
rim_top, face_top = color.lighter(EMBOSS_RIM), color.lighter(EMBOSS_LIGHTER)
face_bottom, rim_bottom = color.darker(EMBOSS_DARKER), color.darker(EMBOSS_RIM)
if pressed:
rim_top, face_top, face_bottom, rim_bottom = (
rim_bottom, face_bottom, face_top, rim_top)
return (f"qlineargradient(x1:0, y1:0, x2:0, y2:1, "
f"stop:0 {rim_top.name()}, stop:{EMBOSS_RIM_WIDTH} {face_top.name()}, "
f"stop:0.5 {color.name()}, "
f"stop:{1 - EMBOSS_RIM_WIDTH:.2f} {face_bottom.name()}, "
f"stop:1 {rim_bottom.name()})")
return [(0, rim_top), (EMBOSS_RIM_WIDTH, face_top), (0.5, color),
(round(1 - EMBOSS_RIM_WIDTH, 2), face_bottom), (1, rim_bottom)]
def emboss_gradient(color: QColor, pressed: bool = False) -> str:
"""``emboss_stops`` as a style-sheet gradient. Computed from the live
color, so it follows the palette and the user's grayscale overrides."""
stops = ", ".join(f"stop:{pos} {c.name()}"
for pos, c in emboss_stops(color, pressed))
return f"qlineargradient(x1:0, y1:0, x2:0, y2:1, {stops})"
# How far the emboss strays from the base color (Qt's lighter/darker factor,
@@ -203,6 +205,45 @@ def emboss_edges(color: QColor) -> str:
return f"border: 1px solid {edge}; border-bottom-color: {bottom};"
def deboss_edges(color: QColor) -> str:
"""The emboss outline turned over: darker along the top, as a panel set
into the surface catches shadow there."""
edge = color.darker(135).name()
top = color.darker(155).name()
return f"border: 1px solid {edge}; border-top-color: {top};"
def deboss_qss(selector: str, color: QColor, *, radius: int = 8,
extra: str = "") -> str:
"""Pushed in, by exactly as much as a button is pushed out: the same
rims and outline, flipped. For panels that show things rather than do
things (the now-playing panel, the visualizer)."""
return (f"{selector} {{ {deboss_edges(color)} border-radius: {radius}px;"
f" background: {emboss_gradient(color, pressed=True)}; {extra} }}")
def paint_deboss(painter, rect, color: QColor, radius: float):
"""``deboss_qss`` for a widget that paints itself: fills ``rect`` (a
QRectF) as a debossed rounded panel and returns its path, for clipping."""
from PyQt6.QtCore import QPointF
from PyQt6.QtGui import QLinearGradient, QPainterPath, QPen
path = QPainterPath()
path.addRoundedRect(rect, radius, radius)
gradient = QLinearGradient(QPointF(0, rect.top()), QPointF(0, rect.bottom()))
for pos, stop in emboss_stops(color, pressed=True):
gradient.setColorAt(pos, stop)
painter.fillPath(path, gradient)
painter.setPen(QPen(color.darker(135), 1))
painter.drawPath(path)
# the darker top edge
painter.save()
painter.setClipRect(rect.adjusted(-1, -1, 1, -rect.height() / 2))
painter.setPen(QPen(color.darker(155), 1))
painter.drawPath(path)
painter.restore()
return path
def button_face_qss(selector: str, color: QColor, *, radius: int = 4,
extra: str = "", checked_color: QColor | None = None,
checked_text: str = "") -> str: