Skip to content

Commit d9aca09

Browse files
committed
labels adjust to light/dark mode
1 parent c17be31 commit d9aca09

3 files changed

Lines changed: 75 additions & 4 deletions

File tree

webgpu/engine/engine.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const LIGHT_CLEAR_COLOR = Object.freeze({ r: 1.0, g: 1.0, b: 1.0, a: 1.0 });
88
const DARK_CLEAR_COLOR = Object.freeze({ r: 0.8078, g: 0.8392, b: 0.8667, a: 1.0 });
99
const LIGHT_CANVAS_BG = '#ffffff';
1010
const DARK_CANVAS_BG = '#ced6dd';
11+
const LIGHT_LABEL_COLOR = Object.freeze([0.0, 0.0, 0.0]);
12+
const DARK_LABEL_COLOR = Object.freeze([0.9, 0.9, 0.9]);
1113
const DEPTH_FORMAT = 'depth24plus';
1214
// Object-id / pick texture format. Matches Python canvas.select_format. The
1315
// select pass renders into a host-owned texture of this format (non-MSAA).
@@ -1414,6 +1416,16 @@ class RenderEngine {
14141416
this.device.queue.writeBuffer(buf, 16, new Float32Array([c.r, c.g, c.b]));
14151417
}
14161418
}
1419+
if (this.scene && this.scene.theme && Array.isArray(this.scene.theme.label_buffers)) {
1420+
const c = this.clearColor;
1421+
const lum = 0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b;
1422+
const labelColor = Float32Array.from(lum < 0.5 ? DARK_LABEL_COLOR : LIGHT_LABEL_COLOR);
1423+
for (const lb of this.scene.theme.label_buffers) {
1424+
if (!lb || !lb.buffer_id) continue;
1425+
const buf = this.buffers && this.buffers.get(lb.buffer_id);
1426+
if (buf) this.device.queue.writeBuffer(buf, lb.offset | 0, labelColor);
1427+
}
1428+
}
14171429
}
14181430

14191431
_setupThemeObserver() {

webgpu/export/capture.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,27 @@ def _capture_renderer(obj, options, registry, render_passes, compute_passes):
116116

117117

118118
def _export_theme(scene, registry) -> dict:
119-
"""Find any renderer's buffer for theme color updates."""
120-
from ..renderer import Renderer
119+
"""Find renderer buffers that the engine should update on theme changes.
120+
121+
``buffer_id`` is the (optional) colorbar background buffer. ``label_buffers``
122+
lists font-color targets for labels that left their color at the default, so
123+
the engine can pick a theme-appropriate text color (black on a light canvas,
124+
off-white on a dark one).
125+
"""
126+
theme = {}
121127
for obj in scene.render_objects:
122128
buf_id = _find_theme_buffer(obj, registry)
123129
if buf_id:
124-
return {"buffer_id": buf_id}
125-
return {}
130+
theme["buffer_id"] = buf_id
131+
break
132+
133+
label_targets = []
134+
seen = set()
135+
for obj in scene.render_objects:
136+
_collect_theme_label_targets(obj, registry, label_targets, seen)
137+
if label_targets:
138+
theme["label_buffers"] = label_targets
139+
return theme
126140

127141

128142
def _find_theme_buffer(obj, registry) -> str | None:
@@ -137,6 +151,24 @@ def _find_theme_buffer(obj, registry) -> str | None:
137151
return None
138152

139153

154+
def _collect_theme_label_targets(obj, registry, out, seen):
155+
"""Recursively gather font-color targets for theme-defaulted labels."""
156+
if obj is None or id(obj) in seen:
157+
return
158+
seen.add(id(obj))
159+
if hasattr(obj, 'get_theme_label_target'):
160+
target = obj.get_theme_label_target(registry)
161+
if target and target["buffer_id"] not in {t["buffer_id"] for t in out}:
162+
out.append(target)
163+
if hasattr(obj, 'render_objects'):
164+
for child in obj.render_objects:
165+
_collect_theme_label_targets(child, registry, out, seen)
166+
if hasattr(obj, 'gpu_objects'):
167+
for child in obj.gpu_objects:
168+
if child is not obj:
169+
_collect_theme_label_targets(child, registry, out, seen)
170+
171+
140172
def _export_camera(camera, options, registry) -> dict:
141173
"""Export camera state and identify the camera buffer."""
142174
t = camera.transform

webgpu/labels.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,33 @@ def text_color(self, value):
6868
if self.font is not None and self._text_color is not None:
6969
self.font.set_color(self._text_color)
7070

71+
def get_theme_label_target(self, registry):
72+
"""Expose the font color uniform for theme-dependent defaulting.
73+
74+
When the app left the label color at the default (neither per-label
75+
``colors`` nor ``text_color`` were set), the engine should pick a color
76+
based on the canvas background so the text stays legible: black on a
77+
light background, off-white on a dark one. Returns the buffer id and
78+
byte offset of the color field, or ``None`` when a color was set
79+
explicitly (in which case the app's choice is respected).
80+
"""
81+
if self.colors is not None or self._text_color is not None:
82+
return None
83+
if self.font is None or self.font.uniforms is None:
84+
return None
85+
buf = self.font.uniforms._buffer
86+
if buf is None:
87+
return None
88+
key = id(buf)
89+
if key not in registry._buffers:
90+
return None
91+
from .font import FontUniforms
92+
93+
return {
94+
"buffer_id": registry._buffers[key][0],
95+
"offset": FontUniforms.color.offset,
96+
}
97+
7198
def update(self, options: RenderOptions):
7299
n_chars = sum(len(label) for label in self.labels)
73100
n_labels = len(self.labels)

0 commit comments

Comments
 (0)