feat(ui): adapt to the screen and its scaling, not to fixed pixels

Two things differ between machines and only one of them is width: a 4K panel
has more pixels, while a 125%/150% display has the same logical pixels holding
LESS, because every label and margin is taller. Breakpoints written as raw
pixels only hold on the machine they were tuned on.

  * ui_scale() derives a factor from font height (1.0 at the 15px line the
    layouts were measured against) and every narrow-guard threshold is
    multiplied by it, so panes fold when the content is cramped rather than
    when a number is crossed.
  * The window takes a share of the available screen (80% × 85%) with the old
    1180×760 as the floor, instead of opening at that size on any monitor.
  * Moving the window to another screen re-pins the assistant and re-decides
    the fold, since the new screen's work area and scaling may differ.

Found by tools/check_multi_screen.py, which walks 5 window sizes × 3 font
scales:

  * At 150%, Schedule was clipped on 1280 and 1366 screens and the window's
    own minimum grew to 1459px — wider than a 1280 laptop, so the app could
    not fit at all. The cause was not the lanes: the one-line lane-count
    summary in the header reported a sizeHint wide enough to set the minimum
    width of the entire window. It now yields first (its text stays in the
    tooltip); the window minimum drops 1459 → 752 and holds there at every
    scale.

Also: these checkers exited 0xC0000409 from a Qt teardown crash AFTER printing
their verdict. check_probes_bite decides whether a probe caught its mutation by
reading exit codes, so a crash would have counted as "caught" — the round could
have passed while proving nothing. They now flush and os._exit with the real
verdict, and round 5 still catches all six mutations.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
NamPDT
2026-08-17 14:10:19 +09:00
co-authored by Claude Opus 5
parent f381fd298a
commit b6dee044c9
12 changed files with 253 additions and 15 deletions
+30 -2
View File
@@ -1013,6 +1013,11 @@ class MainWindow(QMainWindow):
self.help_agent.apply_theme() # chat body follows theme (header stays fixed)
# ---- sizing ------------------------------------------------------
# Share of the available screen the window takes when it has room to. Fixed
# pixels do not travel: 1180×760 fills a laptop and looks lost on a 4K
# panel. `want_*` stays the floor so a small screen behaves as before.
_SCREEN_SHARE_W, _SCREEN_SHARE_H = 0.80, 0.85
def _fit_to_screen(self, want_w: int, want_h: int) -> None:
screen = self.screen() or QGuiApplication.primaryScreen()
avail = screen.availableGeometry() if screen else None
@@ -1020,8 +1025,12 @@ class MainWindow(QMainWindow):
self.resize(want_w, want_h)
return
margin = 60
w = min(want_w, avail.width() - margin)
h = min(want_h, avail.height() - margin)
# Take a share of the screen, never less than the asked-for size and
# never more than the screen can show.
w = min(max(want_w, int(avail.width() * self._SCREEN_SHARE_W)),
avail.width() - margin)
h = min(max(want_h, int(avail.height() * self._SCREEN_SHARE_H)),
avail.height() - margin)
# minimum must never exceed what the screen can show
self.setMinimumSize(min(820, avail.width() - margin), min(520, avail.height() - margin))
self.resize(max(w, 1), max(h, 1))
@@ -1029,6 +1038,25 @@ class MainWindow(QMainWindow):
frame.moveCenter(avail.center())
self.move(frame.topLeft())
def moveEvent(self, event): # noqa: N802 - Qt override
super().moveEvent(event)
# Dragged to another monitor: its work area (and scaling) may differ, so
# the floating assistant re-pins and the panes re-decide if they fit.
self._on_screen_maybe_changed()
def _on_screen_maybe_changed(self) -> None:
screen = self.screen()
if screen is getattr(self, "_last_screen", None):
return
self._last_screen = screen
avail = screen.availableGeometry() if screen else None
if avail is not None:
self.setMinimumSize(min(820, avail.width() - 60),
min(520, avail.height() - 60))
if getattr(self, "help_agent", None) is not None:
self._update_dock_guard()
self.help_agent.reposition()
# ---- lifecycle ---------------------------------------------------
def closeEvent(self, event) -> None: # noqa: N802
keep = (self.tray is not None