"""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