Skip to main content

max / shop

Put a cluster's marks back on the pen, and say why Reverts the placement half of 72854df, which was wrong. Verified in a window this time rather than reasoned about. That commit saw a combining glyph reporting a full cell of advance and concluded the pen must not move, or the mark would land in the next cell. The measurement it did not take is the ink: the acute's bitmap sits at left -5 for a 3px mark, the diaeresis at -7 for a 6px one. The font is built for a pen that has already stepped past the base, and the negative bearing is what brings the mark back over it, centred. Holding the pen at the cell origin drew every mark a cell to the LEFT instead, onto the previous character: `x́ q̈` came out as `´x ¨q`. The comment now carries the measured numbers and says not to change it back without looking at a window, since the wrong version is the one that reads as obviously correct.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-09 15:35 UTC
Signed with PGP, not checked
Commit: 40724f03e6e0c37f9ce898b5d66168148090411b
Parent: 097f524
1 file changed, +29 insertions, -18 deletions
@@ -396,27 +396,38 @@
396 396 push(slot, shaper, id, col, 0.0, 0.0, color);
397 397 }
398 398 CellText::Cluster(s) => {
399 - // Every glyph is drawn from the SAME cell origin, and the
400 - // pen deliberately does not advance between them.
399 + // The pen DOES advance across the cluster, and the marks
400 + // still land on the base. Measured on the bundled Iosevka:
401 + // a combining glyph carries a full cell of advance and its
402 + // ink hangs off the LEFT of its origin (acute -5px for a
403 + // 3px mark, diaeresis -7px for a 6px one). The font is
404 + // built for a pen that has already stepped past the base,
405 + // and the negative bearing is what pulls the mark back
406 + // over it, centred.
401 407 //
402 - // A cluster is one grapheme by construction — a base and
403 - // the zero-width marks after it — so everything past the
404 - // first glyph is a mark belonging on top of that base, in
405 - // this cell. Walking the pen by each glyph's advance is
406 - // what a LINE of text wants and is wrong here: a monospace
407 - // font gives its combining glyphs a full cell of advance
408 - // (measured on the bundled Iosevka: the acute reports the
409 - // cell width, not zero), which would put every mark one
410 - // cell right, on top of the neighbour.
408 + // Anchoring every glyph at the cell origin instead — which
409 + // looks right, and which this did briefly — draws each mark
410 + // a whole cell to the LEFT, over the previous character.
411 + // Do not "fix" it back without looking at a window first.
411 412 //
412 - // The shaper's own offsets are still honoured, so a font
413 - // positioning marks by GPOS gets what it asked for: those
414 - // offsets are relative to the cluster origin, which is
415 - // exactly what this anchors to. Where the shaper composes
416 - // the pair into one precomposed glyph instead (Iosevka
417 - // does for e + acute), there is nothing left to stack.
413 + // The two other shapes this can take are handled by the
414 + // same line: where the shaper composes the pair into one
415 + // precomposed glyph (Iosevka does for e + acute) there is
416 + // nothing after the base to place, and where a font
417 + // positions marks by GPOS the mark's advance is zero and
418 + // its offsets do the work.
419 + let mut pen_x = 0.0;
418 420 for g in shaper.shape(&s) {
419 - push(slot, shaper, g.id, col, g.x_offset, g.y_offset, color);
421 + push(
422 + slot,
423 + shaper,
424 + g.id,
425 + col,
426 + pen_x + g.x_offset,
427 + g.y_offset,
428 + color,
429 + );
430 + pen_x += g.advance;
420 431 }
421 432 }
422 433 }