Skip to content

Text rendering fixes - #341

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

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

Conversation

@ilya-fedin

Copy link
Copy Markdown
Contributor

No description provided.

ilya-fedin and others added 7 commits August 17, 2026 20:03
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 and several glyphs - a consonant
with its matra, a conjunct, a base with combining marks - and the only places a
caret can be at are its boundaries. Three walks used to step a glyph at a time
and land inside one: a drag resolved to a character no selection could start at,
so half a letter could be highlighted; the middle elision left an orphan matra
behind and let the line run past the width it was given; and the ordinary one
replaced half a letter with the ellipsis.

Step by grapheme in all three, weighing each letter by the advances of the glyphs
it takes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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