Skip to content

feat(capi): frontend-supplied text measurement (fixes #56) - #57

Merged
willwade merged 3 commits into
mainfrom
feat/capi-text-size-callback
Aug 22, 2026
Merged

feat(capi): frontend-supplied text measurement (fixes #56)#57
willwade merged 3 commits into
mainfrom
feat/capi-text-size-callback

Conversation

@willwade

@willwade willwade commented Aug 22, 2026

Copy link
Copy Markdown

Fixes #56.

What

Implements option 1 from the issue: frontends can supply real text metrics for the font they actually draw with, so the engine's label layout (the anti-overlap shunting in DasherViewSquare::DoDelayedText) stops estimating.

API

typedef int (*dasher_text_size_callback)(const char* text, int font_size,
                                         int* out_width, int* out_height, void* user_data);
void dasher_set_text_size_callback(dasher_ctx* ctx, dasher_text_size_callback callback, void* user_data);
void dasher_text_metrics_changed(dasher_ctx* ctx);   // invalidate cache after canvas font changes

Design points:

  • Caching: measurements cached per Label × font size, invalidated by a generation counter (dasher_text_metrics_changed or re-registering the callback). Steady-state frames make zero callbacks — verified by test.
  • Early registration: frontends wire callbacks before starting the engine; the ctx holds the pending callback and dasher_set_screen_size forwards it to the screen.
  • Failure semantics: a measurement returning non-zero (or negative dims) falls back to the built-in estimate without caching, so a not-yet-ready font system retries next frame.
  • Wrapped labels (lock/pause message) stay on the estimate — the callback contract has no wrap parameter; they're transient and not layout-critical.
  • Fallback fix: the built-in estimate now counts UTF-8 code points, not bytes — accented alphabets previously measured 2× their glyph count (issue defect 2).

Consumer contract (per frontend)

Measure with the same font used for opcode-5 text commands (SP_DASHER_FONT + per-command size); pixels; call on the dasher_frame thread (it's the UI thread everywhere today); call dasher_text_metrics_changed when the font picker changes the family.

Tests (tests/test_text_metrics.cpp, all via public C API)

  • callback consulted during frames; each (text, size) pair measured at most once per generation (new nodes may legitimately add first-time measurements)
  • invalidation re-measures, then caches again
  • registering before dasher_set_screen_size still takes effect
  • failing callback → engine runs on estimate, retries after recovery (failure not cached)
  • null callback → unchanged behaviour, frames render

Full suite: 39/39. clang-format clean vs main.

Follow-ups (not in this PR)

Frontend wiring: Dasher-Windows #28 (the affected user), Dasher-GTK (Cairo/Pango), Dasher-Android (Paint.measureText via JNI), dasher-web. Each is small: one callback + invalidate call on font change.

DCO signed.

Greptile Summary

The PR adds frontend-provided text measurement to improve label layout while retaining a UTF-8-aware fallback estimate.

  • Adds public callback registration and cache-invalidation APIs.
  • Caches successful measurements by label, font size, and metrics generation.
  • Supports callback registration before screen creation and retries failed measurements.
  • Adds public C API coverage for caching, invalidation, failure recovery, early registration, and fallback rendering.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
src/CAPI.cpp Implements callback forwarding, generation-based measurement caching, failure fallback, and UTF-8 code-point estimation.
src/dasher.h Defines and documents the public text-measurement callback and invalidation APIs.
tests/test_text_metrics.cpp Exercises callback use, caching, invalidation, early registration, failure recovery, and null-callback behavior; the previously ineffective invalidation assertion is corrected.
docs/C_API.md Documents callback semantics, threading, cache invalidation, wrapped-label behavior, and fallback estimation.
CMakeLists.txt Registers the new text-metrics test target.

Sequence Diagram

sequenceDiagram
    participant Frontend
    participant CAPI as dasher_ctx / C API
    participant Screen as CommandScreen
    participant Cache as Label measurement cache

    Frontend->>CAPI: dasher_set_text_size_callback(callback, user_data)
    CAPI->>Screen: Forward callback when screen exists or is created
    Frontend->>CAPI: dasher_frame(...)
    CAPI->>Screen: TextSize(label, font_size)
    Screen->>Cache: Lookup current generation
    alt Cached measurement exists
        Cache-->>Screen: width, height
    else Measurement required
        Screen->>Frontend: callback(text, font_size, outputs, user_data)
        alt Callback succeeds
            Frontend-->>Screen: width, height
            Screen->>Cache: Store measurement for generation
        else Callback fails
            Frontend-->>Screen: non-zero or invalid dimensions
            Screen->>Screen: Use UTF-8 code-point estimate without caching
        end
    end
    Frontend->>CAPI: dasher_text_metrics_changed()
    CAPI->>Screen: Increment metrics generation
Loading

Reviews (2): Last reviewed commit: "test: make the invalidation assertions e..." | Re-trigger Greptile

The command-buffer screen's TextSize estimated label width as
bytes x fontSize/2. Real fonts (e.g. Segoe UI wide glyphs) advance up to
2x that, and the error compounds down the label shunting chain in
DasherViewSquare::DoDelayedText - users saw deep-zoom text degenerate
into jumbled overlaps as sentences grew (Dasher-Windows #28; v5, with
real platform font metrics, never overlapped).

- dasher_set_text_size_callback: frontends measure text with the SAME
  font they draw opcode-5 commands with. Results cached per label and
  font size; steady-state frames do not re-measure. Registering before
  dasher_set_screen_size is fine (the callback is forwarded at screen
  creation).
- dasher_text_metrics_changed: invalidates cached measurements after the
  canvas font changes (SP_DASHER_FONT / font picker).
- Fallback estimate now counts UTF-8 code points, not bytes: accented
  text previously measured 2x its glyph count. A failing measurement
  falls back to the estimate without caching, so a later frame retries.
- Wrapped labels (lock message) stay on the estimate: the callback
  contract has no wrap parameter.

Tests: tests/test_text_metrics.cpp (callback consulted + per-pair cache,
invalidation re-measures, pre-screen registration, failure fallback,
null-callback path). Docs: dasher.h + C_API.md incl. Important Notes #7.

Signed-off-by: will wade <willwade@gmail.com>
CI formats whole files with clang-format 18, which indents
lambda-as-argument continuations differently from the local 21. Bind the
shared callback to a named function and split the structured-binding
loop so both versions agree.

Signed-off-by: will wade <willwade@gmail.com>
Comment thread tests/test_text_metrics.cpp Outdated
Review finding on #57: CHECK(st.calls > 0) was vacuous after warm-up —
calls were already positive, so a no-op invalidation would pass. Compare
before/after counts for both total calls and per-pair maxima, then
assert the fresh measurements are cached again.

Signed-off-by: will wade <willwade@gmail.com>
@willwade

Copy link
Copy Markdown
Author

Valid — fixed in 5a2670f. Both invalidation assertions now compare before/after (total calls and per-pair maxima, so a no-op invalidation fails on either axis), followed by an exact-equality check that the fresh measurements are cached again. Tests: 3/3, 12 assertions.

@willwade
willwade merged commit 8825d6c into main Aug 22, 2026
15 checks passed
@willwade
willwade deleted the feat/capi-text-size-callback branch August 22, 2026 08:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CommandScreen::TextSize estimate causes compounding label overlap (jumbled text) at deep zoom — regression vs v5 real font metrics

1 participant