Skip to content

CJK: full-width punctuation renders as annotations, and half-width numbers/words get truncated to "…" #128

Open

Description

@wakichuang

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

  1. Paste this script: 那一年他 36 歲,搬到新的城市,而且換過三份工作。
  2. 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 word Text, so an overflow wraps to the next line instead of silently hiding script text.

Related: #60, #95 (other Chinese support issues).

Activity

  1. lllwwwyyy commented on Oct 22, 2026

    @lllwwwyyy

    Thanks for the detailed report. I've reproduced the numeric truncation with Chinese scripts in 1.7.1, including decimal amounts such as 1234.56 and dates such as 2026-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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions