Clock rendering (DirectWrite)
Renderer API (clock-windows)
| Item | Where | Contract |
|---|---|---|
font_size_px(pt, dpi), scale_dip, padding_px | render.rs | The only DPI conversions. Direct2D runs at 96 DPI over physical pixels; fonts carry the scale (pt × dpi / 72). None for zero inputs. |
PixelSize::of(PhysicalRect) | render.rs | Signed physical rectangles are reduced to a size only at the render boundary. Positions (including negative monitor coordinates) never enter layout. |
WorstCase::for_format(&CompiledFormat) | render.rs | Every distinct line the format yields over 2026‑01‑01..2028‑12‑31 × five wall times: all month/day names, year boundaries, ISO week 53 (2026), leap day (2028), AM/PM. Content-addressed fingerprint(). Compute once per committed preferences. |
TextMeasurer, required_size, plan_fit → Fit::{Fits(Layout), Overflow(Overflow)} | render.rs | Pure and fully unit tested. Width = widest candidate + both paddings; height = per-line tallest candidate, summed. Overflow carries required/available and width_exceeded()/height_exceeded(). |
Renderer | render/native.rs | UI-thread factories (single-threaded D2D, shared DirectWrite) and the system UI font (lfMessageFont, Segoe UI fallback). refresh_system_font() after settings changes. !Send. |
ClockSurface::paint(&Renderer, &PaintRequest) -> Result<PaintOutcome, RenderError> | render/native.rs | Painted, Unchanged (identical text/palette/font/region on a live device) or Overflow. Overflow draws nothing: the runtime must hide that monitor’s overlay. |
ClockSurface::{mark_dirty, invalidate_device} | render/native.rs | Force a repaint; discard device-dependent objects (what D2DERR_RECREATE_TARGET does internally, with one automatic retry). |
theme::{resolve, Palette, SystemAppearance, AppearanceTracker} | theme.rs | High contrast (system COLOR_WINDOW/COLOR_WINDOWTEXT) always wins; otherwise explicit Light/Dark; System follows SystemUsesLightTheme. Opaque colors only; no pixel sampling. |
theme::{system_appearance, is_appearance_message} | theme.rs | Windows-only queries. The tracker yields the reducer’s ThemeChanged { revision } only when the resolved appearance really changed. |
How the runtime drives it
On CreateOverlay/SetBounds size the HWND to the verified clock rectangle and
call paint on Effect::Paint. Build PaintRequest from the reducer’s committed
preferences (font_size_pt, theme → resolve(theme, tracker.current())), the
overlay’s discovered dpi, the effect’s ClockText and a WorstCase computed from
preferences.format(). Hide the overlay on PaintOutcome::Overflow (and on Err)
and surface the Overflow values in Settings/diagnostics. Route
WM_SETTINGCHANGE/WM_THEMECHANGED/WM_SYSCOLORCHANGE
(is_appearance_message) to AppearanceTracker::observe, and call
Renderer::refresh_system_font. A WM_PAINT, WM_DPICHANGED or
WM_DISPLAYCHANGE must call mark_dirty (the overlay prototype shows the pattern:
the window procedure only bumps an epoch counter and validates the rectangle).
Behavior notes
- Text is single-line-per-format-line,
NO_WRAP, centered, no trimming; the exact layout that will be drawn is measured once more and reported as overflow if it exceeds the box. The region is never widened. - The render target has
D2D1_ALPHA_MODE_IGNOREand is cleared to the opaque palette background each paint, so no native text can show through. - Cached per surface: text format (by pixel size/font generation), solid brush (by palette), worst-case verdict (by font, region, padding, format fingerprint) and the last painted state. Resize reuses the target; failures drop it.
- The
overlay_evidencedevelopment example paints fixed black/white (its harness checks pure-black corners) unlessELEVENCLOCK_R07_THEME=system|light|darkis set, which uses the live theme/high-contrast resolution.
Resource ownership
Everything is owned by the UI thread that created it and is !Send: Renderer
(D2D/DWrite factories), each ClockSurface (ID2D1HwndRenderTarget, brush, text
format) and the HWND passed in, which the caller creates and destroys. All objects
are COM references released by Drop; there are no raw handles. Drop a surface
before destroying its window (the overlay prototype declares the surface field
first). Nothing native crosses a worker channel.
Tests
Portable tests cover DPI conversion at 100/125/150/200%, DIP rounding/saturation,
negative-coordinate regions, worst-case coverage (months, ISO week 53, year
boundary, leap day), width/height overflow, exact-fit boundaries, “future text
overflows though today fits”, invalid measurements and palette/high-contrast
resolution. tests/render_native.rs adds real DirectWrite/Direct2D checks (all
four scales, no-repaint/theme repaint, long-format overflow, future month overflow,
height overflow, resize, and a device-loss + surface-churn handle/GDI/USER count
check), run by the Windows CI job.