v0.1.2: transport buttons fill their bubble as equal tap targets
Prev/play/next (and shuffle) used to hug their ~20px glyphs in the middle of the 62px rounded box, so most of the bubble was dead space. _box() now takes split=True: the buttons tile the bubble interior in equal shares, expanding to the (tightened) margins with the glyph centered in each. Minimum button widths keep the bubbles at their old footprint — no bigger, no smaller. Round 21 tests cover the equal split, vertical fill, edge spans, and the preserved footprint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
12
TASKS.md
12
TASKS.md
@ -3,7 +3,17 @@
|
|||||||
Legend: `[ ]` todo · `[~]` in progress · `[x]` done.
|
Legend: `[ ]` todo · `[~]` in progress · `[x]` done.
|
||||||
When a round closes, move its finished items to `tasks-done.md`.
|
When a round closes, move its finished items to `tasks-done.md`.
|
||||||
|
|
||||||
## Round 20 (current) — Ctrl+I save hang on the Debian machine (v0.1.1)
|
## Round 21 — transport buttons fill their bubble (v0.1.2)
|
||||||
|
|
||||||
|
Plan reference: `~/.claude/plans/tap-targets-on-the-snazzy-minsky.md`.
|
||||||
|
Tests in `tests/test_round21.py`. Fix-only round → patch bump **0.1.2**.
|
||||||
|
|
||||||
|
- [x] Prev/play/next (and shuffle) tap targets now tile their rounded box:
|
||||||
|
`_box(split=True)` in `gui/transport.py` gives each button an equal,
|
||||||
|
full-height share of the bubble with the glyph centered; minimum
|
||||||
|
button widths keep the bubbles at their old footprint (no bigger).
|
||||||
|
|
||||||
|
## Round 20 — Ctrl+I save hang on the Debian machine (v0.1.1)
|
||||||
|
|
||||||
Plan reference: `~/.claude/plans/i-m-having-some-trouble-eager-gosling.md`.
|
Plan reference: `~/.claude/plans/i-m-having-some-trouble-eager-gosling.md`.
|
||||||
Tests in `tests/test_round20.py`. Fix-only round → patch bump **0.1.1**.
|
Tests in `tests/test_round20.py`. Fix-only round → patch bump **0.1.1**.
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
"""LinTunes — iTunes-style music library manager and player for Linux."""
|
"""LinTunes — iTunes-style music library manager and player for Linux."""
|
||||||
|
|
||||||
__version__ = "0.1.1"
|
__version__ = "0.1.2"
|
||||||
|
|||||||
@ -90,22 +90,33 @@ class FlashButton(QToolButton):
|
|||||||
|
|
||||||
|
|
||||||
def _box(*widgets, bg: QColor, margins=(10, 3, 10, 3), hug=False,
|
def _box(*widgets, bg: QColor, margins=(10, 3, 10, 3), hug=False,
|
||||||
height=None) -> QFrame:
|
height=None, split=False) -> QFrame:
|
||||||
"""A small rounded frame with its content centered inside.
|
"""A small rounded frame with its content centered inside.
|
||||||
|
|
||||||
With hug=True the frame stays at its size hint instead of stretching
|
With hug=True the frame stays at its size hint instead of stretching
|
||||||
to fill the row it sits in. height pins the frame's height (width is
|
to fill the row it sits in. height pins the frame's height (width is
|
||||||
left untouched), so several boxes can be lined up to the same height.
|
left untouched), so several boxes can be lined up to the same height.
|
||||||
bg is the rounded-rectangle fill (a user-tunable grayscale)."""
|
bg is the rounded-rectangle fill (a user-tunable grayscale).
|
||||||
|
|
||||||
|
With split=True the widgets tile the frame's interior in equal shares
|
||||||
|
instead of hugging their size hints in the middle — each widget becomes
|
||||||
|
a full-bleed tap target with its content centered in its share."""
|
||||||
frame = QFrame()
|
frame = QFrame()
|
||||||
frame.setObjectName("transportBox")
|
frame.setObjectName("transportBox")
|
||||||
frame.setStyleSheet(_box_style(bg))
|
frame.setStyleSheet(_box_style(bg))
|
||||||
inner = QHBoxLayout(frame)
|
inner = QHBoxLayout(frame)
|
||||||
inner.setContentsMargins(*margins)
|
inner.setContentsMargins(*margins)
|
||||||
inner.addStretch()
|
if split:
|
||||||
for widget in widgets:
|
inner.setSpacing(0)
|
||||||
inner.addWidget(widget)
|
for widget in widgets:
|
||||||
inner.addStretch()
|
widget.setSizePolicy(QSizePolicy.Policy.Expanding,
|
||||||
|
QSizePolicy.Policy.Expanding)
|
||||||
|
inner.addWidget(widget, stretch=1)
|
||||||
|
else:
|
||||||
|
inner.addStretch()
|
||||||
|
for widget in widgets:
|
||||||
|
inner.addWidget(widget)
|
||||||
|
inner.addStretch()
|
||||||
if height is not None:
|
if height is not None:
|
||||||
frame.setFixedHeight(height)
|
frame.setFixedHeight(height)
|
||||||
if hug:
|
if hug:
|
||||||
@ -242,15 +253,23 @@ class TransportBar(QWidget):
|
|||||||
self._prev_btn = FlashButton("previous")
|
self._prev_btn = FlashButton("previous")
|
||||||
self._play_btn = FlashButton("play")
|
self._play_btn = FlashButton("play")
|
||||||
self._next_btn = FlashButton("next")
|
self._next_btn = FlashButton("next")
|
||||||
|
# split=True turns each button into a full-bleed tap target tiling the
|
||||||
|
# bubble; the minimum widths keep the bubbles at their pre-split
|
||||||
|
# footprint (the centering stretches/margins used to pad them out).
|
||||||
|
for btn in (self._prev_btn, self._play_btn, self._next_btn):
|
||||||
|
btn.setMinimumWidth(36)
|
||||||
transport_box = _box(self._prev_btn, self._play_btn, self._next_btn,
|
transport_box = _box(self._prev_btn, self._play_btn, self._next_btn,
|
||||||
bg=box_bg, height=CONTROL_HEIGHT)
|
bg=box_bg, margins=(3, 3, 3, 3),
|
||||||
|
height=CONTROL_HEIGHT, split=True)
|
||||||
self._boxes.append(transport_box)
|
self._boxes.append(transport_box)
|
||||||
layout.addWidget(transport_box)
|
layout.addWidget(transport_box)
|
||||||
|
|
||||||
self._shuffle_btn = FlashButton("shuffle")
|
self._shuffle_btn = FlashButton("shuffle")
|
||||||
|
self._shuffle_btn.setMinimumWidth(50)
|
||||||
self._shuffle_btn.setCheckable(True)
|
self._shuffle_btn.setCheckable(True)
|
||||||
self._shuffle_btn.setToolTip("Shuffle (doesn't change playlist order)")
|
self._shuffle_btn.setToolTip("Shuffle (doesn't change playlist order)")
|
||||||
shuffle_box = _box(self._shuffle_btn, bg=box_bg, height=CONTROL_HEIGHT)
|
shuffle_box = _box(self._shuffle_btn, bg=box_bg, margins=(3, 3, 3, 3),
|
||||||
|
height=CONTROL_HEIGHT, split=True)
|
||||||
self._boxes.append(shuffle_box)
|
self._boxes.append(shuffle_box)
|
||||||
layout.addWidget(shuffle_box)
|
layout.addWidget(shuffle_box)
|
||||||
|
|
||||||
|
|||||||
75
tests/test_round21.py
Normal file
75
tests/test_round21.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
"""Round 21: transport buttons fill their bubble as equal tap targets.
|
||||||
|
|
||||||
|
The prev/play/next FlashButtons used to hug their ~20px icons in the middle
|
||||||
|
of the 62px-tall rounded box, leaving most of the bubble unclickable. Now
|
||||||
|
_box(split=True) tiles the bubble interior with the buttons: each gets an
|
||||||
|
equal third, expands vertically to the margins, and keeps its glyph centered.
|
||||||
|
The bubble itself keeps its pre-split footprint (no bigger, no smaller).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from lintunes.models import Library, Track
|
||||||
|
from lintunes.library_manager import LibraryManager
|
||||||
|
from lintunes.preferences import Preferences
|
||||||
|
from lintunes import player as player_module
|
||||||
|
from lintunes.player import Player
|
||||||
|
from lintunes.gui.transport import TransportBar, CONTROL_HEIGHT
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_player():
|
||||||
|
"""A real Player with the Qt multimedia backend stubbed out (it blocks on
|
||||||
|
init headless — see tests/test_player.py)."""
|
||||||
|
library = Library(tracks={1: Track(track_id=1, name="One")})
|
||||||
|
with patch.multiple(
|
||||||
|
player_module,
|
||||||
|
QMediaPlayer=MagicMock(),
|
||||||
|
QAudioOutput=MagicMock(),
|
||||||
|
QAudioBufferOutput=MagicMock(),
|
||||||
|
QMediaDevices=MagicMock(),
|
||||||
|
):
|
||||||
|
return Player(MagicMock(library=library))
|
||||||
|
|
||||||
|
|
||||||
|
def _build_bar(qapp, tmp_path):
|
||||||
|
prefs = Preferences(tmp_path)
|
||||||
|
manager = LibraryManager(
|
||||||
|
Library(tracks={1: Track(track_id=1, name="One")}), tmp_path)
|
||||||
|
bar = TransportBar(_mock_player(), manager, prefs)
|
||||||
|
bar.resize(1200, 90)
|
||||||
|
bar.show()
|
||||||
|
qapp.processEvents()
|
||||||
|
return bar
|
||||||
|
|
||||||
|
|
||||||
|
class TestTransportTapTargets:
|
||||||
|
def test_buttons_split_the_bubble_equally(self, qapp, tmp_path):
|
||||||
|
bar = _build_bar(qapp, tmp_path)
|
||||||
|
widths = [b.width() for b in
|
||||||
|
(bar._prev_btn, bar._play_btn, bar._next_btn)]
|
||||||
|
assert max(widths) - min(widths) <= 1
|
||||||
|
|
||||||
|
def test_buttons_fill_the_bubble_vertically(self, qapp, tmp_path):
|
||||||
|
bar = _build_bar(qapp, tmp_path)
|
||||||
|
for btn in (bar._prev_btn, bar._play_btn, bar._next_btn,
|
||||||
|
bar._shuffle_btn):
|
||||||
|
# Bubble height minus margins+border (4px each side).
|
||||||
|
assert btn.height() >= CONTROL_HEIGHT - 10
|
||||||
|
|
||||||
|
def test_buttons_span_the_bubble_horizontally(self, qapp, tmp_path):
|
||||||
|
bar = _build_bar(qapp, tmp_path)
|
||||||
|
box = bar._prev_btn.parentWidget()
|
||||||
|
assert bar._prev_btn.x() <= 5 # flush against the left margin
|
||||||
|
right_edge = bar._next_btn.x() + bar._next_btn.width()
|
||||||
|
assert right_edge >= box.width() - 5 # ...and the right one
|
||||||
|
# No stretch gaps between the buttons.
|
||||||
|
assert bar._play_btn.x() == bar._prev_btn.x() + bar._prev_btn.width()
|
||||||
|
assert bar._next_btn.x() == bar._play_btn.x() + bar._play_btn.width()
|
||||||
|
|
||||||
|
def test_bubble_keeps_its_footprint(self, qapp, tmp_path):
|
||||||
|
"""The bubble must not grow (trav's constraint) nor shrink (the
|
||||||
|
minimum button widths preserve the old stretch-padded size)."""
|
||||||
|
bar = _build_bar(qapp, tmp_path)
|
||||||
|
box = bar._prev_btn.parentWidget()
|
||||||
|
assert box.height() == CONTROL_HEIGHT
|
||||||
|
assert 106 <= box.width() <= 126 # was 116 pre-split
|
||||||
Reference in New Issue
Block a user