v0.31.1: one button look — crisper emboss, darken on hover

- The emboss hugs the edges: thin lit and shaded rims, a defined outline
  that's a touch darker along the bottom, and a nearly flat face — shape
  without depth.
- Hover darkens the whole button slightly instead of Qt's pale panel on
  top of it.
- One face for every button (theme.button_face_qss): regular buttons,
  Library and its ▾, the purple friend-mode buttons, and the transport
  boxes. Shuffle and bpm are boxes that are one button, so the whole box
  darkens (bpm's whole box is now its tap target); the play box's three
  tiles each darken on their own.
This commit is contained in:
2026-09-28 22:17:05 -07:00
parent f1f5f450ff
commit 2d0f077af0
6 changed files with 145 additions and 73 deletions
+60 -28
View File
@@ -166,44 +166,76 @@ def apply_theme(app, prefs):
def emboss_gradient(color: QColor, pressed: bool = False) -> str:
"""A barely-there top-lit vertical gradient around ``color``.
Computed from the live color rather than hard-coded, so it follows the
"""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."""
top, bottom = color.lighter(EMBOSS_LIGHTER), color.darker(EMBOSS_DARKER)
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."""
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:
top, bottom = bottom, top
# The color itself sits at the midpoint, so what the user picked is still
# exactly what the middle of the button shows.
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 {top.name()}, stop:0.5 {color.name()}, "
f"stop:1 {bottom.name()})")
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()})")
# How far the emboss strays from the base color (Qt's lighter/darker factor,
# 100 = unchanged). Kept small on purpose: "very slight, soft".
EMBOSS_LIGHTER = 106
EMBOSS_DARKER = 104
# 100 = unchanged). The face barely moves; the rims are where the shape is.
EMBOSS_LIGHTER = 103
EMBOSS_DARKER = 103
EMBOSS_RIM = 116
EMBOSS_RIM_WIDTH = 0.08 # fraction of the height each rim takes
HOVER_DARKER = 107 # hover: the whole button, slightly darker
PRESSED_DARKER = 112
def emboss_edges(color: QColor) -> str:
"""A defined outline: a touch darker at the bottom, like a lit shape."""
edge = color.darker(135).name()
bottom = color.darker(155).name()
return f"border: 1px solid {edge}; border-bottom-color: {bottom};"
def button_face_qss(selector: str, color: QColor, *, radius: int = 4,
extra: str = "", checked_color: QColor | None = None,
checked_text: str = "") -> str:
"""The one look every button shares: the emboss, the outline, a hover that
darkens the whole button a little, and a pressed state that sinks in.
``extra`` carries per-button layout (padding, margins, corners);
``checked_color`` is for toggles like the Library button."""
hover = color.darker(HOVER_DARKER)
down = color.darker(PRESSED_DARKER)
qss = (f"{selector} {{ {emboss_edges(color)} border-radius: {radius}px;"
f" background: {emboss_gradient(color)}; {extra} }}"
f"{selector}:hover {{ background: {emboss_gradient(hover)}; }}"
f"{selector}:pressed {{ background: {emboss_gradient(down, pressed=True)}; }}")
if checked_color is not None:
lit = checked_color
text = f" color: {checked_text};" if checked_text else ""
qss += (f"{selector}:checked {{ background: {emboss_gradient(lit, pressed=True)};"
f" {emboss_edges(lit)}{text} }}"
f"{selector}:checked:hover {{ background:"
f" {emboss_gradient(lit.darker(HOVER_DARKER), pressed=True)}; }}")
return qss
def button_emboss_qss(button: QColor) -> str:
"""Global rule for every non-flat QPushButton. Flat buttons (the version
button, the cancel-transfer ✕, the bpm label) opt out by being flat."""
hover = button.lighter(103)
return f"""
QPushButton:!flat {{
border: 1px solid palette(mid); border-radius: 4px;
padding: 3px 12px; min-height: 1.3em;
background: {emboss_gradient(button)};
}}
QPushButton:!flat:hover {{ background: {emboss_gradient(hover)}; }}
QPushButton:!flat:pressed, QPushButton:!flat:checked {{
background: {emboss_gradient(button, pressed=True)};
}}
QPushButton:!flat:default {{ border-color: palette(highlight); }}
QPushButton:!flat:disabled {{ color: palette(mid); }}
"""
button, the cancel-transfer ✕) opt out by being flat; the transport's bpm
button is flat inside a box that carries the look instead."""
return button_face_qss(
"QPushButton:!flat", button,
extra="padding: 3px 12px; min-height: 1.3em;",
checked_color=button.darker(PRESSED_DARKER)) + \
"QPushButton:!flat:default { border-color: palette(highlight); }" \
"QPushButton:!flat:disabled { color: palette(mid); }"
def installed_families() -> set[str]: