Skip to content

Fix TextKit/UILabel sync: use CoreText for hit testing#74

Open
pannous wants to merge 3 commits intolixiang1994:TextKit=UILabelfrom
pannous:coretext-hit-testing
Open

Fix TextKit/UILabel sync: use CoreText for hit testing#74
pannous wants to merge 3 commits intolixiang1994:TextKit=UILabelfrom
pannous:coretext-hit-testing

Conversation

@pannous
Copy link
Copy Markdown

@pannous pannous commented Mar 17, 2026

Summary

Resolves #12 — the $100 bounty for fixing TextKit and UILabel content synchronization.

Problem

TextKit's NSLayoutManager calculates line heights differently from UILabel, causing misalignment when:

  • Mixed Chinese/English fonts with different line heights
  • numberOfLines truncation (e.g., "abc\n\n\ndefg" with numberOfLines = 2)
  • lineBreakMode = .byCharWrapping
  • Various paragraph style combinations

Root Cause

UILabel internally uses CoreText for text layout, while the hit testing code was building a separate TextKit layout. These two engines have different line height calculation strategies, making pixel-perfect matching extremely difficult.

Solution

Replace TextKit (NSLayoutManager/NSTextContainer/NSTextStorage) with CoreText (CTFramesetter/CTFrame/CTLine) for both hit testing and the debug overlay. Since UILabel itself uses CoreText, the line heights and glyph positions now match inherently.

Key changes:

  • matching(_ point:) uses CTFramesetterCreateFrame + CTLineGetStringIndexForPosition for hit testing
  • Uses UILabel.textRect(forBounds:limitedToNumberOfLines:) for accurate text positioning
  • Midpoint-based vertical line boundary splitting (no inter-line gaps where taps are missed)
  • Accounts for text alignment offset (origins[i].x) in horizontal hit testing
  • No more adaptation() hack — CoreText handles all lineBreakMode values natively
  • Debug overlay uses CTLineDraw for visual verification
  • Removed stray print(scaledMetrics) from UILabelLayoutManagerDelegate

What this fixes

  • Mixed Chinese/English font line height discrepancies
  • numberOfLines truncation calculation errors
  • lineBreakMode = .byCharWrapping garbled rendering
  • Multiple consecutive newlines with truncation (abc\n\n\ndefg)
  • Paragraph spacing edge cases

Tests (all pass)

  • testCoreTextHeightMatchesUILabelForMixedText — CoreText height within 2pt of UILabel textRect
  • testCoreTextLineCountMatchesUILabelWithNumberOfLines — correct line limiting
  • testCoreTextHandlesNewlinesWithNumberOfLines — the abc\n\n\ndefg bug verified fixed
  • testCoreTextHitTestingMixedFonts — accurate character index for mixed font text

Backward compatibility

  • UILabelLayoutManagerDelegate preserved but no longer used by matching() — safe to remove later
  • No API changes — all public interfaces remain identical

pannous added 3 commits March 17, 2026 15:53
TextKit's NSLayoutManager calculates line heights differently from UILabel,
causing misalignment with mixed Chinese/English fonts, numberOfLines truncation,
and byCharWrapping mode. Since UILabel internally uses CoreText for text layout,
switching to CoreText (CTFramesetter/CTFrame/CTLine) directly for hit testing
and debug overlay eliminates these discrepancies.

Key changes:
- Use CTFramesetter/CTFrame for text layout instead of NSLayoutManager
- Use UILabel.textRect(forBounds:limitedToNumberOfLines:) for accurate positioning
- Handle line truncation with CTLineCreateTruncatedLine for visual matching
- Remove lineBreakMode adaptation hack (CoreText handles all modes natively)
- Debug overlay now uses CTLineDraw for closer visual match with UILabel

Fixes lixiang1994#12
Use vertical midpoints between adjacent lines for tap detection instead
of strict ascent/descent bounds. This ensures taps in inter-line spacing
are correctly assigned to the nearest line.
UILabel uses CoreText internally, not TextKit. The previous approach
used NSLayoutManager which has known line height discrepancies with
UILabel, especially for mixed Chinese/English fonts and numberOfLines
truncation (e.g. abc\n\n\ndefg with numberOfLines=2).

This replaces the TextKit-based matching() with CoreText (CTFramesetter/
CTFrame/CTLine) which matches UILabel's actual rendering engine:
- Uses textRect(forBounds:limitedToNumberOfLines:) for positioning
- Midpoint-based vertical line boundary splitting (no inter-line gaps)
- Accounts for text alignment offset in horizontal hit testing
- CoreText handles lineBreakMode natively (no adaptation() workaround)

Fixes lixiang1994#12
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