CJK: full-width punctuation renders as annotations, and half-width numbers/words get truncated to "…" #128
Description
Summary
In Chinese scripts, full-width punctuation (,。:「」!?()…) is rendered wrong, and half-width numbers or Latin words on the same line can be squeezed into … and disappear. Both come from the same root cause in MarqueeTextView.swift.
Environment
- Textream 1.7.1 (Homebrew cask), macOS 26.6.2, Apple Silicon
- Font: Sans, size LG, overlay: Pinned (notch width 410), mode: Word Tracking, speech locale: zh-TW
Steps to reproduce
- Paste this script:
那一年他 36 歲,搬到新的城市,而且換過三份工作。 - Start the prompter.
Expected: 那一年他 36 歲,搬到新的城市,而且換過三份工作。
Actual: 那一年他 ...歲,搬到新的城市,而且換過三份工作。 The 36 is replaced by an ellipsis, and every full-width punctuation mark is shown italic, dimmed, and preceded by an extra space.
Whether a number disappears depends on how full the line is, so it comes and goes when the notch width or font size changes.
Root cause
1. Full-width punctuation becomes a standalone "annotation" word.
Unicode.Scalar.isCJK does not cover CJK Symbols and Punctuation (U+3000–303F) or Halfwidth and Fullwidth Forms (U+FF00–FFEF). So splitTextIntoWords emits each punctuation mark as its own word, and isAnnotationWord flags it because it has no letters or numbers. It is then rendered with the annotation style (italic, cueUnreadOpacity).
Tokenizer output for the sample (〔〕 = flagged as annotation):
那|一|年|他|36|歲|〔,〕|搬|到|新|的|城|市|〔,〕|而|且|換|過|三|份|工|作|〔。〕
2. buildLines underestimates line width, so SwiftUI truncates a word.
buildLines measures each word with NSString.size(withAttributes:), but each word is rendered as a separate SwiftUI Text(word + " "). For a standalone full-width punctuation mark, the two disagree a lot. Measured on this machine with NSFont.systemFont(ofSize: 20, weight: .semibold):
| Word | NSString width (+ space) | SwiftUI Text width |
Difference |
|---|---|---|---|
CJK character, e.g. 年 |
23.72 | 24.00 | +0.28 |
36 |
29.71 | 30.00 | +0.29 |
, or 。 (standalone) |
14.32 | 25.00 | +10.68 |
| Whole sample line | 523.4 | 561.0 | +37.6 |
When the real width exceeds containerWidth, the HStack compresses its children. Single CJK characters can't shrink, so the only multi-character word on the line (36, or a Latin word like Claude) is truncated to …. I reproduced this outside the app by rendering the same HStack(spacing: 0) of Text(word + " ") views, with .lineLimit(1), at the width buildLines computed. The result matches what the prompter shows: 那一年他 ...歲,…
Suggested fix
- In
splitTextIntoWords, attach punctuation to the preceding CJK character (e.g.歲,) instead of emitting it as its own word. At minimum, add U+3000–303F and U+FF00–FFEF to the CJK handling, so these marks are never treated as annotations. - In
buildLines, measure words the same way they are rendered, or round each word width up (ceil) and keep a small safety margin, so accumulated per-word rounding can't overflow the line. - Optionally, add
.fixedSize()to the wordText, so an overflow wraps to the next line instead of silently hiding script text.
Activity
繁中:全形標點黏字、行寬量法修正、防止吃字成「…」
lllwwwyyy commentedon Oct 22, 2026
Thanks for the detailed report. I've reproduced the numeric truncation with Chinese scripts in 1.7.1, including decimal amounts such as
1234.56and dates such as2026-10-20.I've prepared a focused fix that measures the complete displayed word, including its trailing space, rounds each item's width up, and prevents words that fit the container from being compressed horizontally. It also fixes a separate cache collision that can retain old text after a middle-only edit.
The native layout and remote-renderer regression checks pass with the changes and fail on the base revision. The macOS app also builds for arm64 and x86_64.
The PR addresses truncation and stale text. It leaves the punctuation styling part of this issue open.
PR: #139