Skip to content

Commit 556366e

Browse files
committed
docs(adr): record color theme architecture decision
1 parent d8a2d30 commit 556366e

1 file changed

Lines changed: 72 additions & 0 deletions

File tree

docs/adr/0005-color-theme.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Color theme
2+
3+
labelme exposes a `color_theme` Setting with three values — `system` (default),
4+
`light`, and `dark` — surfaced at the top of the "General" section of the Settings
5+
dialog as an enum control labelled "System default" / "Light" / "Dark" (mirroring
6+
the language picker's "System default"). It is applied by calling
7+
`QStyleHints.setColorScheme(...)`, which lets the pinned Fusion style generate the
8+
matching palette itself (`Unknown` follows the OS, `Light`/`Dark` force a scheme
9+
regardless of the OS). This replaces the previous unconditional force-light
10+
override in `__main__.py`.
11+
12+
Qt swaps the palette, but cached `QIcon` pixmaps do not follow it. The monochrome
13+
toolbar icons are therefore authored as `fill="currentColor"` and tinted to the
14+
active palette's `WindowText` color by a `QIconEngine` that reads the palette at
15+
paint time, so they re-tint live when `colorSchemeChanged` fires (the single
16+
re-theme hook for both user changes and OS flips). Selection is by content, not an
17+
allowlist: `new_icon` tints any SVG containing the `currentColor` token, so a new
18+
monochrome icon authored with `fill="currentColor"` is theme-aware automatically.
19+
The five semantic accent icons (blue `floppy-disk`/`folders`/`folder-open`, red
20+
`trash`/`file-x`) keep their fixed colors, since their color carries meaning and is
21+
legible on both backgrounds; the other 26 are tinted.
22+
23+
Theming is scoped to *chrome*, not annotation *data*. Chrome colors must follow
24+
the palette: the toolbar icons, the AI-button highlight (previously a hardcoded
25+
`#FFFFCC`/`#E6E6A0` in `_app.py`), and the canvas crosshair (previously a
26+
hardcoded `QColor(0, 0, 0)` in `canvas.py`, which is invisible against the dark
27+
margin in out-of-bounds mode). Annotation colors drawn over the image — the shape
28+
line/fill colors and white vertex fills in `_shape_render.py` — are data, not
29+
chrome, and stay fixed across themes. Adding a chrome color must go through a
30+
palette role, not a literal; the audit is a sweep of `_widgets/` and `_app.py`
31+
for color literals, triaged chrome-vs-data, not a single fix.
32+
33+
## Considered options
34+
35+
- **App-owned light/dark `QPalette`s** — rejected: Qt 6.8's `setColorScheme`
36+
makes Fusion produce good light and dark palettes for free, so hand-maintaining
37+
~12 role colors per theme is redundant. The cost is that "System" dark adopts
38+
the platform palette and so varies slightly per OS; this is an acceptable trade
39+
for not owning palette tables.
40+
- **Two icon sets (separate light/dark SVGs)** — rejected: doubles the icon
41+
assets and every future icon, and does not scale to a third theme. Tinting one
42+
monochrome source (the template-image pattern) is the standard approach.
43+
- **Restart-required theme change** — rejected: it would be the only Setting that
44+
is not immediate-apply (see ADR-0001) and would make "follow system" sample the
45+
OS only at launch instead of tracking `colorSchemeChanged` live.
46+
- **A Qt theming library (`qdarktheme`/PyQtDarkTheme, `qdarkstyle`, `qt-material`)**
47+
— rejected: these are the pre-6.8 way to get dark Qt, each shipping a full QSS
48+
stylesheet that would fight the pinned Fusion style. `setColorScheme` makes the
49+
native style produce both palettes with no dependency and no stylesheet to
50+
maintain, so the library's core value no longer applies. (Recorded here so the
51+
dependency is not reintroduced later as an apparent simplification.)
52+
53+
## Consequences
54+
55+
- The PySide6 floor moves from `>=6.5` to `>=6.8`, since `setColorScheme` and
56+
`Qt.ColorScheme` are 6.8 APIs.
57+
- The default changes observable behavior on upgrade: users on a dark-themed OS,
58+
who were previously forced to light, now get dark labelme. With the icon tinting
59+
in place this is the intended result, not a regression.
60+
- New icons must be authored with `fill="currentColor"` to participate in
61+
theming; an icon shipped with a hardcoded dark fill will be invisible on a dark
62+
background.
63+
- `colorSchemeChanged` is the one place the running app re-themes. Two things do
64+
not follow Qt's live palette swap on their own and the handler fixes both:
65+
cached `QIcon` pixmaps (cleared via `QPixmapCache`), and any widget carrying a
66+
stylesheet — `QStyleSheetStyle` resolves `palette(...)` references and pins the
67+
widget's palette at polish time, so a styled toolbar (the vertical toolbars use
68+
`QToolBar::separator { background: palette(mid) }`) stays stuck on the old
69+
scheme until its stylesheet is re-applied. The handler re-applies each widget's
70+
stylesheet and repaints all widgets. (`grab()`-based screenshots cannot catch
71+
this: grab re-renders the tree fresh, so it always shows the new palette even
72+
when the on-screen widget never repainted.)

0 commit comments

Comments
 (0)