Files
lintunes/tests/test_round80.py
trav d4cf3f95c2 v0.47.1: the no-art panel sits on the window, shaded like the visualizer
With no art, the sidebar's art widget moves out of the playlist list's
frame onto the window below it, so the panel is set into the gray like the
visualizer rather than into a white box. A tall debossed panel is now
shaded along its top and bottom exactly as the 62 px visualizer is, flat
in the visualizer's color between, instead of one gradient stretched over
its whole height, which read as gray. The scrollbar thumb is shaded the
same way, like a button.
2026-10-10 23:03:47 -07:00

177 lines
6.5 KiB
Python

"""Round 80: scrollbars lose their arrow buttons, and the thumb is a button.
- No arrows: the trough runs the bar's full length and the arrow rects are
empty, so nothing paints or hit-tests there.
- The thumb gets the buttons' corner radius and emboss, with rims as tall in
pixels as a button's, however long the thumb.
- The bar's thickness comes from the UI scale through the style, not QSS
(any QSS rule on a scrollbar part makes Qt draw the arrows itself).
"""
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QColor
from PyQt6.QtWidgets import QPushButton, QScrollBar, QStyle, QStyleOptionSlider
from lintunes import theme
from lintunes.gui.track_table import ClickToJumpScrollStyle
SC = QStyle.SubControl
def _option(bar):
opt = QStyleOptionSlider()
bar.initStyleOption(opt)
return opt
def _bar(qapp, orientation=Qt.Orientation.Vertical):
bar = QScrollBar(orientation)
bar.setStyle(ClickToJumpScrollStyle())
bar.setRange(0, 900)
bar.setPageStep(100)
bar.resize(14, 400) if orientation == Qt.Orientation.Vertical \
else bar.resize(400, 14)
return bar
def test_no_arrow_buttons(qapp):
bar = _bar(qapp)
style, opt = bar.style(), _option(bar)
cc = QStyle.ComplexControl.CC_ScrollBar
assert style.subControlRect(cc, opt, SC.SC_ScrollBarAddLine, bar).isEmpty()
assert style.subControlRect(cc, opt, SC.SC_ScrollBarSubLine, bar).isEmpty()
assert style.subControlRect(cc, opt, SC.SC_ScrollBarGroove, bar) == opt.rect
def test_thumb_spans_the_full_trough(qapp):
for orientation in (Qt.Orientation.Vertical, Qt.Orientation.Horizontal):
bar = _bar(qapp, orientation)
style, cc = bar.style(), QStyle.ComplexControl.CC_ScrollBar
bar.setValue(0)
top = style.subControlRect(cc, _option(bar), SC.SC_ScrollBarSlider, bar)
bar.setValue(900)
end = style.subControlRect(cc, _option(bar), SC.SC_ScrollBarSlider, bar)
if orientation == Qt.Orientation.Vertical:
assert top.top() == 0 and end.bottom() == 399
assert top.height() == 40 # 100 / (900 + 100) of 400
else:
assert top.left() == 0 and end.right() == 399
def test_tiny_page_keeps_a_grabbable_thumb(qapp):
bar = _bar(qapp)
bar.setRange(0, 100000)
rect = bar.style().subControlRect(QStyle.ComplexControl.CC_ScrollBar,
_option(bar), SC.SC_ScrollBarSlider, bar)
assert rect.height() == ClickToJumpScrollStyle.extent * 2
def test_thickness_follows_ui_scale(qapp):
class Prefs(dict):
def get(self, key, default=None):
return "large" if key == "ui_scale" else default
before = ClickToJumpScrollStyle.extent
try:
theme.apply_theme(qapp, Prefs())
assert ClickToJumpScrollStyle.extent == theme.UI_SCALES["large"]["scrollbar"]
assert "QScrollBar" not in qapp.styleSheet()
finally:
ClickToJumpScrollStyle.extent = before
def test_thumb_is_shaded_like_a_real_button(qapp):
qapp.setStyleSheet(theme.button_emboss_qss(QColor("#d0d0d0")))
try:
button = QPushButton("OK")
assert abs(theme.button_height_px(button.fontMetrics())
- button.sizeHint().height()) <= 2
finally:
qapp.setStyleSheet("")
def test_thumb_paints(qapp):
bar = _bar(qapp)
bar.setValue(450)
image = bar.grab().toImage()
thumb = bar.style().subControlRect(QStyle.ComplexControl.CC_ScrollBar,
_option(bar), SC.SC_ScrollBarSlider, bar)
trough = image.pixelColor(7, 5)
middle = image.pixelColor(7, thumb.center().y())
assert middle != trough
# --------------------------------------------------------------------------
# no album art: a debossed panel on the window, not a box in the list
# --------------------------------------------------------------------------
def _sidebar(qapp, tmp_path):
from lintunes.gui.main_window import MainWindow
from lintunes.library_manager import LibraryManager
from lintunes.models.library import Library
from lintunes.models.track import Track
from lintunes.preferences import Preferences
library = Library(tracks={1: Track(track_id=1, name="One")})
window = MainWindow(LibraryManager(library, tmp_path), Preferences(tmp_path))
window.resize(1000, 700)
window.show()
qapp.processEvents()
return window, window.sidebar
def test_no_art_panel_sits_below_the_frame(qapp, tmp_path):
from lintunes.gui.sidebar import NO_ART_GAP
window, sidebar = _sidebar(qapp, tmp_path)
art = sidebar.art
assert not art.has_art()
assert art.parentWidget() is sidebar
assert art.y() >= sidebar.list_area.geometry().bottom()
assert art.height() == art.width() + NO_ART_GAP
image = art.grab().toImage()
window_bg = art.palette().window().color()
# the gap above is bare window; the panel's middle is the visualizer's
# color, flat, not shaded gray over its whole height
assert image.pixelColor(art.width() // 2, 1) == window_bg
panel = art.palette().alternateBase().color()
for y in (art.height() // 3, art.height() // 2, 2 * art.height() // 3):
assert image.pixelColor(art.width() // 2, y) == panel
window.close()
def test_real_art_goes_inside_the_frame(qapp, tmp_path):
from PyQt6.QtGui import QPixmap
window, sidebar = _sidebar(qapp, tmp_path)
art = sidebar.art
pixmap = QPixmap(10, 10)
pixmap.fill(QColor("red"))
art._pixmap = pixmap
art.has_art_changed.emit(True)
art._fit_height()
assert art.parentWidget() is sidebar.list_area
assert art.height() == art.width()
art._pixmap = None
art.set_track(None)
assert art.parentWidget() is sidebar
window.close()
def test_tall_deboss_copies_a_short_panels_edges(qapp):
"""Shaded ``like`` a 62 px panel, a 400 px one matches it row for row
along the top, and is flat in the middle."""
from PyQt6.QtCore import QRectF
from PyQt6.QtGui import QImage, QPainter
color = QColor("#f2f2f2")
def paint(height, **kw):
image = QImage(100, height, QImage.Format.Format_ARGB32)
image.fill(0)
painter = QPainter(image)
theme.paint_deboss(painter, QRectF(0.5, 0.5, 99, height - 1),
color, 8, **kw)
painter.end()
return image
short, tall = paint(62), paint(400, like=62)
for y in (3, 10, 20):
assert abs(short.pixelColor(50, y).lightness()
- tall.pixelColor(50, y).lightness()) <= 1
assert tall.pixelColor(50, 200) == color