diff --git a/lintunes/__init__.py b/lintunes/__init__.py index 2340247..1b24f9b 100644 --- a/lintunes/__init__.py +++ b/lintunes/__init__.py @@ -1,3 +1,3 @@ """LinTunes — iTunes-style music library manager and player for Linux.""" -__version__ = "0.31.1" +__version__ = "0.31.2" diff --git a/lintunes/gui/transport.py b/lintunes/gui/transport.py index e628dd0..afdaa39 100644 --- a/lintunes/gui/transport.py +++ b/lintunes/gui/transport.py @@ -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). diff --git a/lintunes/gui/visualizer.py b/lintunes/gui/visualizer.py index 446fff7..9fcd2e5 100644 --- a/lintunes/gui/visualizer.py +++ b/lintunes/gui/visualizer.py @@ -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 diff --git a/lintunes/theme.py b/lintunes/theme.py index 5adc071..5b48b3f 100644 --- a/lintunes/theme.py +++ b/lintunes/theme.py @@ -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: