User-visible symptom (Dasher-Windows v0.1.14 → v0.1.15, user reports Aug 17 + Aug 21 2026)
"When I type a long sentence, the next letters to form a word get all jumbled up. Sometimes it gets so bad that I can't type any further."
"As a sentence grows, the letters and predictive boxes are still getting completely squished, jumbled, and cut off right along the right-hand border of the canvas."
The same user runs Dasher v5 on Windows without any problem — this is a v6 regression. She uses Direct Entry as her primary mode (power user, long sentences = deepest exposure).
Root cause
All C-API frontends render the command buffer, but label layout decisions happen engine-side using CDasherScreen::TextSize(). The CAPI implementation is a crude estimate:
src/CAPI.cpp:221
std::pair<screenint, screenint> TextSize(Label* label, unsigned int iFontSize) override {
return {label->m_strText.size() * iFontSize / 2, iFontSize};
}
Two defects:
- Per-glyph advance underestimate.
size/2 assumes every glyph advances 0.5em. Segoe UI / common UI fonts: 'w'/'m' ≈ 0.75–0.85em, uppercase ≈ 0.65–1.0em, digits ≈ 0.55em. Deep-zoom labels are single characters, so the worst glyphs are ~2× wider than estimated.
- Bytes, not code points.
m_strText.size() counts UTF-8 bytes: 'é' counts twice (over-shove), fine only for pure ASCII.
The estimate feeds the label anti-overlap "shunting" in DasherViewSquare.cpp DoDelayedText():
const screenint iRight = x + textDims.first; // estimated right edge
...
pChild->m_ix = std::max(pChild->m_ix, iRight); // shove child past parent
DoDelayedText(pChild, ...); // child shoves ITS children...
Every generation shifts only by the parent's estimated width, so errors compound along the ancestor label chain. As the user writes more context, the visible subtree deepens, the label chains lengthen, and the right-hand frontier (where the next-letter boxes live) degenerates into overlapping text — matching "as a sentence grows … so bad I can't type any further".
Why v5 was fine
v5's platform screens implemented TextSize with real font metrics (GTK: Pango; Win32: GetTextExtentPoint32; macOS: NSString size). The rewrite moved rendering behind the command buffer and replaced metrics with the estimate — documented as a known limitation in docs/C_API.md ("No font rendering — text width is estimated as characters × fontSize / 2") but never connected to this user-visible failure.
Impact
| Frontend |
Affected? |
| Dasher-Windows (Avalonia) |
Yes — reported (Segoe UI) |
| Dasher-GTK (Cairo) |
Yes in principle (shared estimate; severity depends on font/glyph mix) |
| Dasher-Android (Compose) |
Yes in principle |
| dasher-web |
Yes in principle |
| v5 |
No — real metrics |
Fix options
- Frontend-supplied metrics (proper fix, matches v5): add a C API for the frontend to provide text measurement — e.g.
dasher_set_text_size_callback(ctx, int (*measure)(const char* text, int font_size, int* width, int* height)) — so the engine's layout decisions use the actual font the canvas will draw with. One callback, called only when labels change size bucket (cacheable in CDasherScreen::Label, which already exists for exactly this purpose).
- Better constant (band-aid): count UTF-8 code points and raise the factor to ~0.6, with a wider multiplier for uppercase/wide glyphs. Reduces but cannot eliminate overlap (real fonts vary ±30% per glyph).
- Strand 2 (RFC 0013): frontends that take over rendering entirely (
dasher_get_visible_nodes) can measure text themselves — but the default command-buffer path still needs 1 or 2.
Suggested: option 1. CDasherScreen::Label was designed as the metrics cache seam; the C-API layer just never wired it to the frontend.
Repro (any command-buffer frontend)
- Alphabet: English with limited punctuation; font size default (22).
- Write a long sentence (30+ chars) continuously without pausing.
- Watch the right-hand frontier: descendant labels progressively overlap until individual next-letter boxes are illegible.
Severity: high for power users (primary text-entry path); cosmetic at short text.
User-visible symptom (Dasher-Windows v0.1.14 → v0.1.15, user reports Aug 17 + Aug 21 2026)
The same user runs Dasher v5 on Windows without any problem — this is a v6 regression. She uses Direct Entry as her primary mode (power user, long sentences = deepest exposure).
Root cause
All C-API frontends render the command buffer, but label layout decisions happen engine-side using
CDasherScreen::TextSize(). The CAPI implementation is a crude estimate:src/CAPI.cpp:221Two defects:
size/2assumes every glyph advances 0.5em. Segoe UI / common UI fonts: 'w'/'m' ≈ 0.75–0.85em, uppercase ≈ 0.65–1.0em, digits ≈ 0.55em. Deep-zoom labels are single characters, so the worst glyphs are ~2× wider than estimated.m_strText.size()counts UTF-8 bytes: 'é' counts twice (over-shove), fine only for pure ASCII.The estimate feeds the label anti-overlap "shunting" in
DasherViewSquare.cppDoDelayedText():Every generation shifts only by the parent's estimated width, so errors compound along the ancestor label chain. As the user writes more context, the visible subtree deepens, the label chains lengthen, and the right-hand frontier (where the next-letter boxes live) degenerates into overlapping text — matching "as a sentence grows … so bad I can't type any further".
Why v5 was fine
v5's platform screens implemented
TextSizewith real font metrics (GTK: Pango; Win32:GetTextExtentPoint32; macOS: NSString size). The rewrite moved rendering behind the command buffer and replaced metrics with the estimate — documented as a known limitation indocs/C_API.md("No font rendering — text width is estimated as characters × fontSize / 2") but never connected to this user-visible failure.Impact
Fix options
dasher_set_text_size_callback(ctx, int (*measure)(const char* text, int font_size, int* width, int* height))— so the engine's layout decisions use the actual font the canvas will draw with. One callback, called only when labels change size bucket (cacheable inCDasherScreen::Label, which already exists for exactly this purpose).dasher_get_visible_nodes) can measure text themselves — but the default command-buffer path still needs 1 or 2.Suggested: option 1.
CDasherScreen::Labelwas designed as the metrics cache seam; the C-API layer just never wired it to the frontend.Repro (any command-buffer frontend)
Severity: high for power users (primary text-entry path); cosmetic at short text.