Size the board and the rail by proportion, not by pixel constants

"Some screens scroll sideways, some don't" had two causes, and both were a
number that was right on one monitor.

The lanes. _KanbanColumn set a 150px floor with a comment claiming seven of
them fit a 1280 window; measured, the row wanted 1242px where 1280 leaves 1091,
so a 1280 screen scrolled and a 1920 one did not — this machine has one of each.
The floor is gone. The seven lanes share the board through the layout's
stretch, which is a proportion of whatever width there is, and the only pixel
question left — how narrow before scrolling beats squeezing — is answered from
the font: eight characters of a title plus padding, so it holds at 125%/150%
scaling too. No horizontal scroll now at 1280×720 through 1936×1048, at three
rail widths each, including the rail dragged to its maximum.

Each lane also grew its own scrollbar: QListWidget's column hint runs 1-6px past
its viewport, and which lanes overflowed changed with the window width — 36 of
38 widths had at least one. Cards word-wrap, so there was never anything to
reach sideways; the bar is off.

The rail. Its 360px ceiling was a quarter of a 1440 screen and more than that of
a 1280 one. It is now 22% of the window, capped at 360 — and recomputed on
resize, which is its own bug fixed: read once at construction off a not-yet-sized
window, it sat at 162px on every monitor, so the handle barely moved.

check_kanban_scroll covers five screen sizes × three rail widths. A rail dragged
to maximum on a 1280 screen still scrolls the board — that is the user's own
trade, so it is reported rather than failed.

19/19 checkers pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nam Pham Dinh Thanh
2026-08-18 13:00:05 +09:00
co-authored by Claude Opus 5
parent a0cef768c9
commit 77976405b1
4 changed files with 178 additions and 16 deletions
+21 -5
View File
@@ -41,8 +41,14 @@ _NAV_COLLAPSED_WIDTH = 54
# something if the rail can actually take a width from it, so the expanded rail
# is a range rather than one number; long project and thread names in RECENTS
# are the reason someone would widen it.
#
# The ceiling is a SHARE of the window, not a pixel count: 360px is a quarter
# of a 1440 screen and more than a quarter of a 1280 one, where it left the
# seven Kanban lanes 920px of the 1067 they need. A share behaves the same on
# every monitor.
_NAV_MIN_WIDTH = 132
_NAV_MAX_WIDTH = 360
_NAV_MAX_SHARE = 0.22
_NAV_MAX_CEILING = 360
def app_icon() -> QIcon:
@@ -220,7 +226,7 @@ class MainWindow(QMainWindow):
self._nav_wrap = QWidget()
self._nav_wrap.setObjectName("navWrap")
self._nav_width = _NAV_EXPANDED_WIDTH # remembered across collapses
self._set_nav_width_range(_NAV_MIN_WIDTH, _NAV_MAX_WIDTH)
self._set_nav_width_range(_NAV_MIN_WIDTH, self._nav_max_width())
nvl = QVBoxLayout(self._nav_wrap)
nvl.setContentsMargins(0, 0, 0, 0)
nvl.setSpacing(0)
@@ -372,6 +378,11 @@ class MainWindow(QMainWindow):
def resizeEvent(self, event): # noqa: N802 - Qt override
super().resizeEvent(event)
# The rail's ceiling is a share of the window, so it moves with the
# window. Computed once at construction it was read off a not-yet-sized
# window and stuck at 162px on every monitor.
if getattr(self, "_nav_wrap", None) is not None and not self._nav_collapsed:
self._set_nav_width_range(_NAV_MIN_WIDTH, self._nav_max_width())
# Keep the floating Help assistant pinned to the bottom-right corner.
if getattr(self, "help_agent", None) is not None:
self.help_agent.reposition()
@@ -755,6 +766,11 @@ class MainWindow(QMainWindow):
def _nav_new_chat_enabled(self) -> bool:
return bool(self.workspace.project_choices())
def _nav_max_width(self) -> int:
"""The rail's ceiling for THIS window, as a share of it."""
return max(_NAV_MIN_WIDTH,
min(_NAV_MAX_CEILING, int(self.width() * _NAV_MAX_SHARE)))
def _set_nav_width_range(self, lo: int, hi: int) -> None:
"""setFixedWidth would leave the splitter handle inert — visible, and
doing nothing when dragged."""
@@ -764,19 +780,19 @@ class MainWindow(QMainWindow):
def _on_split_moved(self, _pos: int, _index: int) -> None:
if not self._nav_collapsed:
self._nav_width = max(_NAV_MIN_WIDTH,
min(_NAV_MAX_WIDTH, self._nav_wrap.width()))
min(self._nav_max_width(), self._nav_wrap.width()))
def _toggle_nav(self) -> None:
if not self._nav_collapsed:
self._nav_width = max(_NAV_MIN_WIDTH,
min(_NAV_MAX_WIDTH, self._nav_wrap.width()))
min(self._nav_max_width(), self._nav_wrap.width()))
self._nav_collapsed = not self._nav_collapsed
if self._nav_collapsed:
width = _NAV_COLLAPSED_WIDTH
self._set_nav_width_range(width, width)
else:
width = self._nav_width
self._set_nav_width_range(_NAV_MIN_WIDTH, _NAV_MAX_WIDTH)
self._set_nav_width_range(_NAV_MIN_WIDTH, self._nav_max_width())
self._apply_nav_labels()
# Same chevron convention as every other collapsible panel: right-
# pointing (fill-right) means "click to expand", left means "collapse".