zapretgui/tests/test_stable_center.py
loop-uh 0fdbb421aa
All checks were successful
Zapret source guards / validate (push) Successful in 10s
Windows-сборка / Dev Dev 21.1.7.204 опубликован
Presets list: scroll without redrawing every tile, only over a flat background
The list is translucent, so Qt cannot reuse the pixels it already drew and
repainted every visible tile (70-140 Python paint calls) on each scroll step.
While a scroll is going on (and only then) the viewport is now declared opaque, so
Qt shifts the picture and asks for just the exposed strip; the strip gets the real
backdrop first. That is allowed only when the backdrop is flat, which is measured, not
assumed: Qt draws the window without the list and six points behind it must be the
same colour (a background image, another animation over the list or a hidden/minimised
window keep the old path). When the scroll stops (140 ms) the list is repainted once
the normal way. Measured on the real window: tiles drawn per scroll 3788 -> 688 (slow
notches) and 840 -> 60 (fast back and forth), CPU about 3x and 5x lower.

The check against a fresh render found a bug that was already there: QRect.center()
rounds toward zero in C++, so a tile whose top is above the list (negative y) drew its
icon and text 1 px lower than when fully visible and jumped 1 px as it crossed the top
edge. The centre is now computed by floor (ui/widgets/stable_center.py), identical for
every tile that is fully visible, used for tiles, folder headers and the marker.
After that, 384 random scroll steps with mouse movement (dark, light, tinted) match a fresh
render with no pixel off by more than 2 levels of 255.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 18:46:18 -04:00

90 lines
4.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import os
import unittest
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtCore import QRect
from PyQt6.QtGui import QColor, QImage, QPainter
from PyQt6.QtWidgets import QApplication, QStyleOptionViewItem
from ui.presets_menu.delegate import PresetListDelegate
from ui.presets_menu.model import PresetListModel
from ui.presets_menu.view import LinkedWheelListView
from ui.widgets.stable_center import center_point, center_x, center_y
class StableCenterTests(unittest.TestCase):
def test_it_equals_qrect_center_for_every_rectangle_that_is_not_above_or_left_of_the_origin(self) -> None:
for left in (0, 1, 7, 100):
for top in (0, 1, 2, 13, 250):
for width in (1, 2, 3, 18, 209, 840):
for height in (1, 2, 3, 24, 28, 31, 32):
rect = QRect(left, top, width, height)
self.assertEqual((center_x(rect), center_y(rect)), (rect.center().x(), rect.center().y()), rect)
self.assertEqual(center_point(rect), rect.center())
def test_it_does_not_depend_on_where_the_rectangle_is(self) -> None:
# У QRect.center() для отрицательного верха центр выходит на точку ниже.
offsets = {center_y(QRect(0, top, 10, 28)) - top for top in range(-60, 60)}
self.assertEqual(offsets, {13})
self.assertNotEqual({QRect(0, top, 10, 28).center().y() - top for top in range(-60, 60)}, {13})
def test_the_centre_is_inside_the_rectangle(self) -> None:
for top in (-40, -17, -1, 0, 9):
rect = QRect(-5, top, 21, 28)
self.assertTrue(rect.contains(center_point(rect)))
class TileAtTheTopEdgeTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls._app = QApplication.instance() or QApplication([])
def setUp(self) -> None:
self.view = LinkedWheelListView()
self.addCleanup(self.view.deleteLater)
self.model = PresetListModel(self.view)
rows = [{"kind": "folder", "folder_key": "a", "name": "Группа", "is_collapsed": False, "count": 3}]
rows += [{"kind": "preset", "name": f"general ALT{n} (game filter) gjpqy", "file_name": f"p{n}.txt", "folder_key": "a", "depth": 1,
"icon_color": "#e0a030", "is_pinned": n == 1, "rating": 3 if n == 2 else 0} for n in range(3)]
self.model.set_rows(rows)
self.view.setModel(self.model)
self.delegate = PresetListDelegate(self.view)
self.view.setItemDelegate(self.delegate)
def _paint(self, row: int, top: int) -> QImage:
image = QImage(260, 140, QImage.Format.Format_RGB32)
image.fill(QColor(31, 41, 59))
painter = QPainter(image)
option = QStyleOptionViewItem()
option.rect = QRect(5, top, 209, 32)
self.delegate.paint(painter, option, self.model.index(row, 0))
painter.end()
return image
def test_a_tile_looks_the_same_whether_or_not_its_top_is_above_the_list(self) -> None:
# Раньше значок и имя плитки с отрицательным верхом рисовались на пиксель
# ниже и «прыгали» при прокрутке, когда верх пересекал верхний край.
for row in (1, 2, 3):
full = self._paint(row, 60)
for top in (-19, -17, -13, -10, -7, -4, -1):
cut = self._paint(row, top)
shift = 60 - top
for y in range(0, 140 - shift):
for x in range(260):
self.assertEqual(cut.pixel(x, y), full.pixel(x, y + shift), (row, top, x, y))
def test_the_folder_header_looks_the_same_at_any_position_too(self) -> None:
full = self._paint(0, 60)
for top in (-23, -14, -9, -3):
cut = self._paint(0, top)
shift = 60 - top
for y in range(0, 140 - shift):
for x in range(260):
self.assertEqual(cut.pixel(x, y), full.pixel(x, y + shift), (top, x, y))
if __name__ == "__main__":
unittest.main()