From 2de1052c3d17a414242a9f6a22f7b0ea608f62ce Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 14 Apr 2026 15:33:58 +0200 Subject: [PATCH] Cleanup --- .../plans/2026-04-14-character-rects.md | 410 ------------------ .../2026-04-14-character-rects-design.md | 76 ---- ...26-04-14-layoutForCharacterCount-design.md | 43 -- 3 files changed, 529 deletions(-) delete mode 100644 docs/superpowers/plans/2026-04-14-character-rects.md delete mode 100644 docs/superpowers/specs/2026-04-14-character-rects-design.md delete mode 100644 docs/superpowers/specs/2026-04-14-layoutForCharacterCount-design.md diff --git a/docs/superpowers/plans/2026-04-14-character-rects.md b/docs/superpowers/plans/2026-04-14-character-rects.md deleted file mode 100644 index 1b6b4563b5..0000000000 --- a/docs/superpowers/plans/2026-04-14-character-rects.md +++ /dev/null @@ -1,410 +0,0 @@ -# Character Rects Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Compute per-line glyph bounding rects during layout and store them on `InteractiveTextNodeLine`, enabling `computeRevealedLines` and reveal animations to use pre-computed geometry. - -**Architecture:** Replace `characterToGlyphMapping: [Int]?` on `InteractiveTextNodeLine` with `characterRects: [CGRect]?`. Add a `computeCharacterRects` flag to `InteractiveTextNodeLayoutArguments` that gates the computation. A new helper function computes actual glyph bounding boxes via `CTFontGetBoundingRectsForGlyphs`. Thread the flag through both `calculateLayout` and `calculateLayoutV2`, and update `computeRevealedLines` and `getCharacterToGlyphMapping` to consume the new data. - -**Tech Stack:** Swift, CoreText (CTRun, CTFont, CTLine) - -**File:** `submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift` - ---- - -### Task 1: Add `computeCharacterRects` flag to `InteractiveTextNodeLayoutArguments` - -**Files:** -- Modify: `submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift:281-362` - -- [ ] **Step 1: Add the property declaration** - -After line 299 (`public let expandedBlocks: Set`), add: - -```swift - public let computeCharacterRects: Bool -``` - -- [ ] **Step 2: Add the init parameter** - -In the `init` at line 301, after `expandedBlocks: Set = Set()`, add: - -```swift - computeCharacterRects: Bool = false -``` - -- [ ] **Step 3: Add the assignment** - -In the init body, after `self.expandedBlocks = expandedBlocks` (line 338), add: - -```swift - self.computeCharacterRects = computeCharacterRects -``` - -- [ ] **Step 4: Propagate through `withAttributedString`** - -In the `withAttributedString` method (line 341), add after `expandedBlocks: self.expandedBlocks` (line 360): - -```swift - computeCharacterRects: self.computeCharacterRects -``` - -- [ ] **Step 5: Commit** - -```bash -git add submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift -git commit -m "feat: add computeCharacterRects flag to InteractiveTextNodeLayoutArguments" -``` - ---- - -### Task 2: Replace `characterToGlyphMapping` with `characterRects` on `InteractiveTextNodeLine` - -**Files:** -- Modify: `submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift:113-151` - -- [ ] **Step 1: Replace the property declaration** - -Change line 130 from: - -```swift - let characterToGlyphMapping: [Int]? -``` - -to: - -```swift - let characterRects: [CGRect]? -``` - -- [ ] **Step 2: Update the init signature** - -In the init at line 132, replace `characterToGlyphMapping: [Int]?` with `characterRects: [CGRect]?`. - -- [ ] **Step 3: Update the init body** - -Change line 149 from: - -```swift - self.characterToGlyphMapping = characterToGlyphMapping -``` - -to: - -```swift - self.characterRects = characterRects -``` - -- [ ] **Step 4: Commit** - -```bash -git add submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift -git commit -m "feat: replace characterToGlyphMapping with characterRects on InteractiveTextNodeLine" -``` - ---- - -### Task 3: Add `computeCharacterRects` helper function - -**Files:** -- Modify: `submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift` (insert before the `InteractiveTextNodeLine` class or after `addAttachment`, around line 1270) - -- [ ] **Step 1: Write the helper function** - -Insert a private free function near the other free helper functions (after `addAttachment` at ~line 1270): - -```swift -private func computeCharacterRectsForLine(line: CTLine, lineRange: NSRange) -> [CGRect] { - var result = [CGRect](repeating: CGRect.zero, count: lineRange.length) - - let glyphRuns = CTLineGetGlyphRuns(line) as NSArray - for run in glyphRuns { - let run = run as! CTRun - let glyphCount = CTRunGetGlyphCount(run) - if glyphCount == 0 { - continue - } - - var glyphs = [CGGlyph](repeating: 0, count: glyphCount) - CTRunGetGlyphs(run, CFRangeMake(0, glyphCount), &glyphs) - - var positions = [CGPoint](repeating: CGPoint.zero, count: glyphCount) - CTRunGetPositions(run, CFRangeMake(0, glyphCount), &positions) - - var stringIndices = [CFIndex](repeating: 0, count: glyphCount) - CTRunGetStringIndices(run, CFRangeMake(0, glyphCount), &stringIndices) - - let attributes = CTRunGetAttributes(run) as NSDictionary - guard let font = attributes[kCTFontAttributeName] as! CTFont? else { - continue - } - - var boundingRects = [CGRect](repeating: CGRect.zero, count: glyphCount) - CTFontGetBoundingRectsForGlyphs(font, .default, &glyphs, &boundingRects, glyphCount) - - for i in 0 ..< glyphCount { - let charIndex = stringIndices[i] - lineRange.location - if charIndex >= 0 && charIndex < lineRange.length { - let pos = positions[i] - let bbox = boundingRects[i] - result[charIndex] = CGRect( - x: pos.x + bbox.origin.x, - y: pos.y + bbox.origin.y, - width: bbox.width, - height: bbox.height - ) - } - } - } - - return result -} -``` - -- [ ] **Step 2: Commit** - -```bash -git add submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift -git commit -m "feat: add computeCharacterRectsForLine helper" -``` - ---- - -### Task 4: Thread `computeCharacterRects` through `calculateLayout` and `calculateLayoutV2` - -**Files:** -- Modify: `submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift:1433-1452,2045-2051` - -- [ ] **Step 1: Add parameter to `calculateLayoutV2`** - -At line 1433, the signature is `private static func calculateLayoutV2(...)`. After the last parameter `expandedBlocks: Set` (line 1451), add: - -```swift - computeCharacterRects: Bool -``` - -- [ ] **Step 2: Add parameter to `calculateLayout`** - -At line 2045, the signature is `static func calculateLayout(...)`. After `expandedBlocks: Set`, add: - -```swift - computeCharacterRects: Bool -``` - -- [ ] **Step 3: Pass through in `calculateLayout` body** - -In the `calculateLayout` body, there are two calls to `calculateLayoutV2` (line 2050). Add `computeCharacterRects: computeCharacterRects` to both the early-return `InteractiveTextNodeLayout(...)` call (line 2047) — actually that one constructs a layout directly, not a call to V2, so skip it — and the `calculateLayoutV2(...)` call (line 2050). Add `computeCharacterRects: computeCharacterRects` as the last argument. - -- [ ] **Step 4: Update the two `asyncLayout` call sites** - -In `asyncLayout` (lines 2223 and 2226), there are two calls to `InteractiveTextNode.calculateLayout(...)`. Add `computeCharacterRects: arguments.computeCharacterRects` as the last argument to both. - -- [ ] **Step 5: Commit** - -```bash -git add submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift -git commit -m "feat: thread computeCharacterRects through calculateLayout and calculateLayoutV2" -``` - ---- - -### Task 5: Update all 4 `InteractiveTextNodeLine` construction sites - -**Files:** -- Modify: `submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift:1619-1636,1661-1678,1720-1737,1775-1792` - -- [ ] **Step 1: Title line construction (~line 1619)** - -After `additionalTrailingLine: nil` (line 1635), add: - -```swift - characterRects: nil -``` - -Title lines don't need character rects (they have `range: nil`). - -- [ ] **Step 2: Main line construction (~line 1661)** - -After `additionalTrailingLine: nil` (line 1677), add: - -```swift - characterRects: computeCharacterRects ? computeCharacterRectsForLine(line: line, lineRange: NSRange(location: currentLineStartIndex, length: lineCharacterCount)) : nil -``` - -Note: `computeCharacterRects` here refers to the parameter passed into `calculateLayoutV2`. The `line` variable is the `CTLine` created on line 1646. - -- [ ] **Step 3: Collapsed truncation construction (~line 1720)** - -After `additionalTrailingLine: (truncationToken, 0.0)` (line 1736), add: - -```swift - characterRects: computeCharacterRects ? computeCharacterRectsForLine(line: updatedLine, lineRange: lastLine.range ?? NSRange()) : nil -``` - -- [ ] **Step 4: Final truncation construction (~line 1775)** - -After `additionalTrailingLine: (truncationToken, truncationTokenWidth)` (line 1791), add: - -```swift - characterRects: computeCharacterRects ? computeCharacterRectsForLine(line: updatedLine, lineRange: lastLine.range ?? NSRange()) : nil -``` - -- [ ] **Step 5: Commit** - -```bash -git add submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift -git commit -m "feat: pass characterRects at all InteractiveTextNodeLine construction sites" -``` - ---- - -### Task 6: Implement `computeRevealedLines` - -**Files:** -- Modify: `submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift:2798-2852` - -- [ ] **Step 1: Replace the stub with working implementation** - -The function at line 2798 currently has a commented-out body and returns `[]`. Replace the body (keep the commented-out code above, don't delete it) so the full function reads: - -```swift - private func computeRevealedLines(lines: [InteractiveTextNodeLine], layerSize: CGSize, offset: CGPoint, characterLimit: Int) -> [RevealLineInfo] { - /*var result: [RevealLineInfo] = [] - ...existing commented code stays... - return result*/ - //TODO - var result: [RevealLineInfo] = [] - var remainingCharacters = characterLimit - - for i in 0 ..< lines.count { - let line = lines[i] - - var lineFrame = line.frame - lineFrame.origin.y += offset.y - - if line.isRTL { - lineFrame.origin.x = offset.x + floor(layerSize.width - lineFrame.width) - lineFrame = displayLineFrame(frame: lineFrame, isRTL: true, boundingRect: CGRect(origin: CGPoint(), size: layerSize), cutout: nil) - } else { - lineFrame.origin.x += offset.x - } - - let lineHeight = line.ascent + line.descent - - guard let characterRects = line.characterRects else { - result.append(RevealLineInfo(lineFrame: lineFrame, lineHeight: lineHeight, revealedWidth: remainingCharacters > 0 ? lineFrame.width : 0.0, isFull: remainingCharacters > 0, isRTL: line.isRTL)) - continue - } - - if remainingCharacters <= 0 { - result.append(RevealLineInfo(lineFrame: lineFrame, lineHeight: lineHeight, revealedWidth: 0.0, isFull: false, isRTL: line.isRTL)) - continue - } - - let revealCount = min(characterRects.count, remainingCharacters) - var revealedWidth: CGFloat = 0.0 - for j in 0 ..< revealCount { - let rect = characterRects[j] - if !rect.isEmpty { - revealedWidth = max(revealedWidth, rect.maxX) - } - } - revealedWidth = ceil(revealedWidth) - - remainingCharacters -= characterRects.count - let isFull = remainingCharacters >= 0 - - result.append(RevealLineInfo(lineFrame: lineFrame, lineHeight: lineHeight, revealedWidth: revealedWidth, isFull: isFull, isRTL: line.isRTL)) - } - - return result - } -``` - -- [ ] **Step 2: Commit** - -```bash -git add submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift -git commit -m "feat: implement computeRevealedLines using characterRects" -``` - ---- - -### Task 7: Update `getCharacterToGlyphMapping` - -**Files:** -- Modify: `submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift:2263-2270` - -- [ ] **Step 1: Implement the method** - -Replace the stub body of `getCharacterToGlyphMapping()` (lines 2263-2270) with: - -```swift - public func getCharacterToGlyphMapping() -> [Int] { - /*guard let cachedLayout = self.cachedLayout else { - return [] - } - return cachedLayout.getCharacterToGlyphMapping()*/ - guard let cachedLayout = self.cachedLayout else { - return [] - } - - var result: [Int] = [] - var cumulativeGlyphCount = 0 - - for segment in cachedLayout.segments { - for line in segment.lines { - if let characterRects = line.characterRects { - for rect in characterRects { - if !rect.isEmpty { - cumulativeGlyphCount += 1 - } - result.append(cumulativeGlyphCount) - } - } else { - let glyphRuns = CTLineGetGlyphRuns(line.line) as NSArray - for run in glyphRuns { - let run = run as! CTRun - let glyphCount = CTRunGetGlyphCount(run) - for _ in 0 ..< glyphCount { - cumulativeGlyphCount += 1 - result.append(cumulativeGlyphCount) - } - } - } - } - } - - return result - } -``` - -This preserves the API contract: returns an array where each entry is the cumulative glyph count up to that character. When `characterRects` is available, characters with `.zero` rects (ligature components) don't increment the glyph count. When `characterRects` is `nil` (flag was off), it falls back to walking CTRuns directly (1:1 glyph-to-entry mapping). - -- [ ] **Step 2: Commit** - -```bash -git add submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift -git commit -m "feat: implement getCharacterToGlyphMapping using characterRects" -``` - ---- - -### Task 8: Build verification - -- [ ] **Step 1: Run the full build** - -```bash -PATH=/opt/homebrew/opt/ruby/bin:`gem environment gemdir`/bin:$PATH python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build --configurationPath build-system/appstore-configuration.json --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber 1 --configuration debug_sim_arm64 -``` - -Expected: Build succeeds with no errors related to `InteractiveTextComponent.swift`. - -- [ ] **Step 2: Fix any compilation errors and re-run build if needed** - -- [ ] **Step 3: Final commit if any fixes were needed** - -```bash -git add submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift -git commit -m "fix: address build errors in character rects implementation" -``` diff --git a/docs/superpowers/specs/2026-04-14-character-rects-design.md b/docs/superpowers/specs/2026-04-14-character-rects-design.md deleted file mode 100644 index a0f5c55d89..0000000000 --- a/docs/superpowers/specs/2026-04-14-character-rects-design.md +++ /dev/null @@ -1,76 +0,0 @@ -# Character Rects in InteractiveTextNodeLine - -## Goal - -Compute per-line character bounding rects (actual glyph drawn bounds) during the layout pass and store them on `InteractiveTextNodeLine`. This enables `computeRevealedLines` and the reveal animation system to work with precise glyph geometry without recomputing CoreText data at render time. - -## File - -`submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift` - -## Changes - -### 1. InteractiveTextNodeLayoutArguments — new flag - -Add a `computeCharacterRects: Bool` property (default `false`) to `InteractiveTextNodeLayoutArguments`. - -- Add the property declaration, init parameter, and assignment. -- Propagate through `withAttributedString`. - -### 2. InteractiveTextNodeLine — replace characterToGlyphMapping with characterRects - -Replace: -```swift -let characterToGlyphMapping: [Int]? -``` - -With: -```swift -let characterRects: [CGRect]? -``` - -Update the init signature and all construction sites (4 total: title line at ~1619, main line at ~1661, collapsed truncation at ~1720, final truncation at ~1775) to pass `characterRects` instead of `characterToGlyphMapping`. - -- When `computeCharacterRects` is `false` in layout arguments, pass `nil`. -- When `true`, compute the array at construction time (see section 3). - -### 3. Glyph rect computation - -A helper function (or inline logic) that, given a `CTLine` and its `NSRange`, produces `[CGRect]`: - -1. Allocate an array of `CGRect.zero` with length equal to the line's string range length. -2. For each `CTRun` in the line: - a. Get glyphs via `CTRunGetGlyphs`. - b. Get positions via `CTRunGetPositions` (glyph origins relative to the line). - c. Get string indices via `CTRunGetStringIndices`. - d. Get the font from the run's attributes (`kCTFontAttributeName`). - e. Call `CTFontGetBoundingRectsForGlyphs(.default, glyphs, &boundingRects, glyphCount)` to get per-glyph bounding boxes relative to each glyph's origin. - f. For each glyph at index `i`: - - Compute the final rect: `CGRect(x: position.x + bbox.origin.x, y: position.y + bbox.origin.y, width: bbox.width, height: bbox.height)`. - - Map to the character index: `stringIndices[i] - lineRangeStart`. - - Store in the output array at that character index. -3. Characters not covered by any glyph (ligature components, zero-width joiners, etc.) retain `.zero`. - -### 4. computeRevealedLines — uncomment and adapt - -The existing commented-out implementation at ~line 2798 walks CTRuns and uses `CTRunGetAdvances` to compute `revealedWidth`. Update it to use `characterRects` from the line instead: - -- The function signature stays the same: `(lines: [InteractiveTextNodeLine], layerSize: CGSize, offset: CGPoint, characterLimit: Int) -> [RevealLineInfo]`. -- Instead of walking CTRuns at call time, consume the pre-computed `characterRects` on each line to determine how many characters are revealed and what width they span. -- `characterLimit` counts characters (one per entry in the `characterRects` arrays across all lines). Walk lines in order, decrementing the remaining character budget by each line's `characterRects.count`. For each line, the revealed width is the max x-extent of the revealed character rects within the budget. -- Lines beyond the budget get `revealedWidth: 0`. - -### 5. getCharacterToGlyphMapping — adapt - -The public `getCharacterToGlyphMapping() -> [Int]` on `InteractiveTextNode` (line 2263) currently returns `[]`. Update it to derive a flat character count from the per-line `characterRects`: - -- Walk all segments/lines, count the total number of character rects (including `.zero` entries for ligature components). -- Return an array of cumulative glyph counts (preserving the existing API contract used by `ChatMessageTextBubbleContentNode` for animation counting). - -The commented-out implementation on `InteractiveTextNodeLayout` (lines 1136-1190) stays as-is per user instruction. - -## Non-goals - -- Not changing `RevealLineInfo` struct shape. -- Not touching `ChatMessageTextBubbleContentNode` in this task. -- Not deleting any commented-out code. diff --git a/docs/superpowers/specs/2026-04-14-layoutForCharacterCount-design.md b/docs/superpowers/specs/2026-04-14-layoutForCharacterCount-design.md deleted file mode 100644 index fab481f19b..0000000000 --- a/docs/superpowers/specs/2026-04-14-layoutForCharacterCount-design.md +++ /dev/null @@ -1,43 +0,0 @@ -# layoutForCharacterCount Implementation - -## Goal - -Implement `layoutForCharacterCount` and `sizeForCharacterCount` on `InteractiveTextNodeLayout` so that `ChatMessageTextBubbleContentNode` can compute the text frame size and trailing line width during reveal animations. - -## File - -`submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift` - -## Changes - -### `layoutForCharacterCount(characterCount:) -> TextNodeLayout.LayoutInfo` - -Replace the `preconditionFailure()` stub (keep the commented-out code above it). - -Algorithm: - -1. Initialize `height` from the first line's `frame.maxY` (same as commented-out code). -2. Walk segments/lines, consuming the character budget. Each line's character count comes from `characterRects?.count` if available, otherwise from the line's `range?.length` (falling back to `CTLineGetStringRange`). -3. For each line: - - **Fully included** (remaining budget >= line's character count): use `line.frame.width` for that line's width contribution. Consume the line's characters from the budget. - - **Cut line** (remaining budget > 0 but < line's character count): compute the string index at the cut point as `lineRange.location + remainingCharacters`. Call `CTLineGetOffsetForStringIndex(line.line, cutStringIndex, nil)` for the line width. This is the last contributing line. - - **Beyond budget** (remaining budget <= 0): skip. -4. Track `height` as the max `line.frame.maxY` across all contributing lines. -5. Compute final values: - - `width`: if more than one contributing line, use `self.size.width`; otherwise `ceil(maxLineWidth) + insets.left + insets.right`. - - `height`: add `insets.top + insets.bottom + 2.0`. - - `trailingLineWidth`: last contributing line's width + `insets.left + insets.right + 4.0`. -6. Return `TextNodeLayout.LayoutInfo(size: CGSize(width: width, height: ceil(height)), trailingLineWidth: trailingLineWidth)`. - -### `sizeForCharacterCount(characterCount:) -> CGSize` - -Replace the `return CGSize()` stub with: - -```swift -return self.layoutForCharacterCount(characterCount: characterCount).size -``` - -## Non-goals - -- Not deleting any commented-out code. -- Not changing `ChatMessageTextBubbleContentNode`.