Skip to content

Text rendering fixes - #341

Open
ilya-fedin wants to merge 9 commits into
desktop-app:masterfrom
ilya-fedin:text-rendering-fixes
Open

Text rendering fixes#341
ilya-fedin wants to merge 9 commits into
desktop-app:masterfrom
ilya-fedin:text-rendering-fixes

Conversation

@ilya-fedin

Copy link
Copy Markdown
Contributor

No description provided.

@ilya-fedin
ilya-fedin force-pushed the text-rendering-fixes branch 3 times, most recently from c0bae99 to e5042b7 Compare August 22, 2026 16:19
ilya-fedin and others added 9 commits August 23, 2026 14:30
None of these are referenced by the files that include them:

  text_renderer.cpp     <private/qharfbuzz_p.h>   no hb_ or HB_ symbols
  text_block.cpp        <private/qfontengine_p.h> no QFontEngine, no QGlyphLayout
  text_block.h          <private/qfixed_p.h>      no QFixed
  text_block_parser.cpp <private/qfixed_p.h>      no QFixed

Private headers carry no binary compatibility guarantee, so every one of
them is a rebuild risk for distribution packagers on a Qt point release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The line came from QTextEngine::itemize(), where Qt guards it:

    if (!qt_useHarfbuzzNG())
        analysis[i].script = hbscript_to_script(script_to_hbscript(...));

and qt_useHarfbuzzNG() is `qgetenv("QT_HARFBUZZ") != "old"`, so Qt 5.15 only
runs it for the legacy shaper nobody asks for. Our copy dropped the guard and
ran it always. Qt itself introduced the mapping in 2012 when it moved to
QChar::Script, to keep the behaviour of the old harfbuzz, which knew 29 scripts
and treated the rest - latin, Han, hiragana, katakana and everything Unicode
gained since - as Common; Qt 6 lost it in f0813484 ("Remove old harfbuzz
dependency from QTextEngine"), which deleted that shaper wholesale.

Collapsing scripts to Common made itemization group into one item what it
otherwise splits, so dropping it does not change what the text looks like: it
adopts what Qt 5 does by default and what Qt 6 does always.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…dle.

A middle elision fills each half of the line from its outer edge inwards, so it
has to keep the end of an item that faces the middle - and which end that is
depends on the item's direction, since glyphs are stored in logical order and the
visually left part of a right-to-left item is its last glyphs. Walking from the
first glyph regardless kept the wrong end as soon as a half spanned more than one
item: instead of the beginning and the end of the text, a right-to-left line
showed two stretches out of its middle, out of order.

An item covers at most kMaxItemLength characters, so this takes a longer text
than that - a Premium message of up to 8192, or an Instant View paragraph.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A cluster can map to several glyphs, and the walks that turn an x into a
character and a character range into a highlight took only the first one's
advance. Their running x then fell behind the text as it is drawn - by 117
pixels on a devanagari item here - so a drag resolved to a character left of the
pointer, and the highlight started left of the characters it covered. The gap
left at the end of a line by a selection that spans lines came from the same
place, at seven pixels of it.

Sum the advances of the cluster's glyphs instead, which makes the walks add up to
the item width they are laid out with.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
When a selection runs past the end of a line, the rest of the line is filled
separately, and that fill started where the line width minus what was left over
said the text ended. The items add up to a slightly different place - the
leftover counts the last word's right bearing, the items do not - so on a script
whose last glyph overhangs, the two rounded to different pixels and left a gap.
The devanagari sample showed it at one pixel; before the cluster width fix it
was seven.

Fill it after the items instead, where the end of the text is known exactly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A point outside a line is checked against the line's width, which counts the
last word's right bearing, while the items that take a point inside the line do
not count it - so up to a pixel between the last glyph and the line's width
belongs to neither check. A click there fell through to whatever the walk over
lines had left behind: the end of the line above, or the very start of the text
when there is no line above.

Resolve it like a point past the line, at the end of the line it is on, and let
the three places that resolve to an edge of a line share one helper.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A visible letter can take several characters - a consonant with its matra, a
conjunct, a base with combining marks - and the only places a caret can be at
are its boundaries, so that is where a click belongs and where a cut may fall.
Taking a glyph at a time put a caret inside a letter and left an orphan matra
behind an ellipsis.

Letters can also share a glyph, and then they have no width of their own: they
take an equal share of the one they are drawn as, the same way the engine maps
a point back to a position. A cut falls where a letter boundary is also a glyph
boundary, since half a glyph cannot be drawn, and a half of a middle elision
that fits no whole letter now draws none instead of one that hangs past the
width it was given.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ilya-fedin
ilya-fedin force-pushed the text-rendering-fixes branch from e5042b7 to 422bdda Compare August 23, 2026 11:53
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.

1 participant