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:
+8
-1
@@ -191,4 +191,11 @@ def main() -> int:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
_rc = main()
|
||||
# Qt (WebEngine especially) crashes during interpreter teardown with
|
||||
# 0xC0000409 AFTER the work is done, which would mask the real result —
|
||||
# and check_probes_bite reads these exit codes to decide whether a probe
|
||||
# caught its mutation. Leave immediately with the verdict instead.
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
os._exit(_rc)
|
||||
|
||||
@@ -199,4 +199,11 @@ def main() -> int:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
_rc = main()
|
||||
# Qt (WebEngine especially) crashes during interpreter teardown with
|
||||
# 0xC0000409 AFTER the work is done, which would mask the real result —
|
||||
# and check_probes_bite reads these exit codes to decide whether a probe
|
||||
# caught its mutation. Leave immediately with the verdict instead.
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
os._exit(_rc)
|
||||
|
||||
@@ -353,4 +353,11 @@ def main() -> int:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
_rc = main()
|
||||
# Qt (WebEngine especially) crashes during interpreter teardown with
|
||||
# 0xC0000409 AFTER the work is done, which would mask the real result —
|
||||
# and check_probes_bite reads these exit codes to decide whether a probe
|
||||
# caught its mutation. Leave immediately with the verdict instead.
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
os._exit(_rc)
|
||||
|
||||
@@ -127,4 +127,11 @@ def main() -> int:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
_rc = main()
|
||||
# Qt (WebEngine especially) crashes during interpreter teardown with
|
||||
# 0xC0000409 AFTER the work is done, which would mask the real result —
|
||||
# and check_probes_bite reads these exit codes to decide whether a probe
|
||||
# caught its mutation. Leave immediately with the verdict instead.
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
os._exit(_rc)
|
||||
|
||||
@@ -191,4 +191,11 @@ def main() -> int:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
_rc = main()
|
||||
# Qt (WebEngine especially) crashes during interpreter teardown with
|
||||
# 0xC0000409 AFTER the work is done, which would mask the real result —
|
||||
# and check_probes_bite reads these exit codes to decide whether a probe
|
||||
# caught its mutation. Leave immediately with the verdict instead.
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
os._exit(_rc)
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Does the layout adapt across screen sizes AND display scalings?
|
||||
|
||||
Two things change between machines, and only one of them is width:
|
||||
|
||||
* the screen is bigger or smaller — more or fewer pixels to lay out in;
|
||||
* the display scale is 100/125/150% — the SAME number of logical pixels
|
||||
holds less, because every label and margin is taller.
|
||||
|
||||
A breakpoint written as a raw pixel number only holds on the machine it was
|
||||
tuned on. This walks a grid of (window size × font scale) and, for each cell,
|
||||
checks that no screen is clipped and that the panes folded when they had to.
|
||||
|
||||
Run: python tools/check_multi_screen.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO.parent))
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
from capture_screens import _freeze_schedulers, _isolate_home, _load_fonts # noqa: E402
|
||||
|
||||
# Real-world panels, from a small laptop up to 4K-at-150%-effective.
|
||||
SIZES = [(1280, 720), (1366, 768), (1600, 900), (1920, 1080), (2560, 1440)]
|
||||
# 9pt ≈ 100%, 11pt ≈ 125%, 14pt ≈ 150% of the design baseline.
|
||||
POINTS = [9, 11, 14]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
sandbox = _isolate_home()
|
||||
from PySide6.QtGui import QFont
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
app = QApplication([])
|
||||
_load_fonts()
|
||||
_freeze_schedulers()
|
||||
|
||||
from cowork_local.config import AppConfig, CONFIG_DIR
|
||||
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
||||
|
||||
from seed_demo_data import seed
|
||||
seed()
|
||||
|
||||
from cowork_local.app import MainWindow
|
||||
from cowork_local.i18n import set_language
|
||||
from cowork_local.state import AppContext
|
||||
from cowork_local.ui.widgets import ui_scale
|
||||
|
||||
set_language("vi")
|
||||
fails: list[str] = []
|
||||
print(f"{'co chu':>7} {'cua so':>11} {'thang do':>9} {'man bi bo':>10} panel da gap")
|
||||
print("-" * 86)
|
||||
|
||||
for pt in POINTS:
|
||||
f = QFont(app.font())
|
||||
f.setPointSize(pt)
|
||||
app.setFont(f)
|
||||
win = MainWindow(AppContext(AppConfig.load()), user_name="local")
|
||||
win.show()
|
||||
for _ in range(6):
|
||||
app.processEvents()
|
||||
ws = win.workspace
|
||||
dests = [("Project", win._ROW_WORKSPACE, ws._project_tab_idx),
|
||||
("Cowork", win._ROW_WORKSPACE, ws._cowork_tab_idx),
|
||||
("Co4E", win._ROW_WORKSPACE, ws._co4e_tab_idx),
|
||||
("Folder", win._ROW_WORKSPACE, ws._folder_tab_idx),
|
||||
("GraphRAG", win._ROW_WORKSPACE, ws._graphrag_tab_idx),
|
||||
("Schedule", win._ROW_SCHEDULE, None),
|
||||
("Dashboard", win._ROW_DASHBOARD, None),
|
||||
("Monitoring", win._ROW_MONITORING, None)]
|
||||
|
||||
for w, h in SIZES:
|
||||
win.resize(w, h)
|
||||
for _ in range(6):
|
||||
app.processEvents()
|
||||
clipped = []
|
||||
for name, page, sub in dests:
|
||||
win._goto(page, sub)
|
||||
for _ in range(4):
|
||||
app.processEvents()
|
||||
widget = win._page_widgets[page]
|
||||
if widget.minimumSizeHint().width() > widget.width() + 1:
|
||||
clipped.append(name)
|
||||
folded = []
|
||||
if getattr(ws, "_is_narrow", False):
|
||||
folded.append("pane Project/History")
|
||||
import cowork_local.ui.co4e_tab as co4e_mod
|
||||
c4 = win.findChildren(co4e_mod.Co4ETab)[0]
|
||||
if c4._config_collapsed:
|
||||
folded.append("panel cau hinh Co4E")
|
||||
scale = ui_scale(win)
|
||||
print(f"{pt:>5}pt {w:>5}x{h:<5} {scale:>8.2f} "
|
||||
f"{(', '.join(clipped) or 'khong'):>10} {', '.join(folded) or '-'}")
|
||||
if clipped:
|
||||
fails.append(f"{pt}pt {w}x{h}: bi bo — {clipped}")
|
||||
# The window must never demand more than the smallest panel we support.
|
||||
need = win.minimumSizeHint().width()
|
||||
if need > SIZES[0][0]:
|
||||
fails.append(f"{pt}pt: cua so doi toi thieu {need}px, "
|
||||
f"rong hon man nho nhat ({SIZES[0][0]}px)")
|
||||
print(f"{'':>7} {'':>11} {'':>9} cua so doi toi thieu: {need}px")
|
||||
win.close()
|
||||
del win
|
||||
for _ in range(3):
|
||||
app.processEvents()
|
||||
|
||||
print()
|
||||
if fails:
|
||||
print("*** KHONG THICH UNG DUOC ***")
|
||||
for x in fails:
|
||||
print(" " + x)
|
||||
return 1
|
||||
print("KET QUA: bo cuc thich ung o moi co man hinh va muc phong chu da thu")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_rc = main()
|
||||
# Qt (WebEngine especially) crashes during interpreter teardown with
|
||||
# 0xC0000409 AFTER the work is done, which would mask the real result —
|
||||
# and check_probes_bite reads these exit codes to decide whether a probe
|
||||
# caught its mutation. Leave immediately with the verdict instead.
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
os._exit(_rc)
|
||||
+8
-1
@@ -298,4 +298,11 @@ def main() -> int:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
_rc = main()
|
||||
# Qt (WebEngine especially) crashes during interpreter teardown with
|
||||
# 0xC0000409 AFTER the work is done, which would mask the real result —
|
||||
# and check_probes_bite reads these exit codes to decide whether a probe
|
||||
# caught its mutation. Leave immediately with the verdict instead.
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
os._exit(_rc)
|
||||
|
||||
@@ -113,4 +113,11 @@ def main() -> int:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
_rc = main()
|
||||
# Qt (WebEngine especially) crashes during interpreter teardown with
|
||||
# 0xC0000409 AFTER the work is done, which would mask the real result —
|
||||
# and check_probes_bite reads these exit codes to decide whether a probe
|
||||
# caught its mutation. Leave immediately with the verdict instead.
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
os._exit(_rc)
|
||||
|
||||
Reference in New Issue
Block a user