mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
InstantPage V2 thinking block: text-item sizing, overhang, inline emoji/images
Make the thinking block render consistently with a normal text item: - Size the `.thinking` item frame text-width at the page inset (`(horizontalInset, 0, textWidth, height)`), identical to a `.text` item, instead of a full-bleed `(0, 0, boundingWidth, height)` box, by laying the text out flush and moving the inset onto the block frame. The shimmer (sized to `item.frame.size`) now hugs the text. - Size the shimmer + its gradient mask to the clipping-inset-expanded frame (shifted `-inset`) so the mask no longer crops glyph ascenders, descenders and the last line's underline, mirroring how a `.text` view's frame is inset-expanded. - Visit the thinking block's shimmer-wrapped inner text view in updateInlineEmoji / updateInlineImages so its inline custom emoji and images get layers created, positioned and revealed. They were skipped because the page only iterated top-level InstantPageV2TextView items. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1120b9084e
commit
6408175725
2 changed files with 46 additions and 12 deletions
|
|
@ -307,7 +307,18 @@ public final class InstantPageV2View: UIView {
|
|||
var validIds: [InlineStickerItemLayer.Key] = []
|
||||
|
||||
for view in self.itemViews {
|
||||
guard let textView = view as? InstantPageV2TextView else { continue }
|
||||
// Top-level `.text` items host their emoji directly. The thinking block hosts emoji on
|
||||
// its shimmer-wrapped inner text view, which the page never sees as a top-level item —
|
||||
// so without this it is skipped and the emoji never get layers (invisible). Nested V2
|
||||
// sub-layouts (details bodies, table cells) instead run their own updateInlineEmoji.
|
||||
let textView: InstantPageV2TextView
|
||||
if let topLevelTextView = view as? InstantPageV2TextView {
|
||||
textView = topLevelTextView
|
||||
} else if let thinkingView = view as? InstantPageV2ThinkingView {
|
||||
textView = thinkingView.textView
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
let textItem = textView.item.textItem
|
||||
let boundsWidth = textItem.frame.size.width
|
||||
for line in textItem.lines {
|
||||
|
|
@ -390,7 +401,17 @@ public final class InstantPageV2View: UIView {
|
|||
var validKeys: Set<InlineImageKey> = []
|
||||
|
||||
for view in self.itemViews {
|
||||
guard let textView = view as? InstantPageV2TextView else { continue }
|
||||
// Same nesting as updateInlineEmoji: top-level `.text` items host their inline images
|
||||
// directly; the thinking block hosts them on its shimmer-wrapped inner text view, which
|
||||
// the page never sees as a top-level item. Nested V2 sub-layouts run their own pass.
|
||||
let textView: InstantPageV2TextView
|
||||
if let topLevelTextView = view as? InstantPageV2TextView {
|
||||
textView = topLevelTextView
|
||||
} else if let thinkingView = view as? InstantPageV2ThinkingView {
|
||||
textView = thinkingView.textView
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
let textItem = textView.item.textItem
|
||||
let boundsWidth = textItem.frame.size.width
|
||||
for line in textItem.lines {
|
||||
|
|
@ -1859,7 +1880,7 @@ final class InstantPageV2ThinkingView: UIView, InstantPageItemView {
|
|||
var itemFrame: CGRect { return self.item.frame }
|
||||
|
||||
private let shimmerView: ShimmeringMaskView
|
||||
private let textView: InstantPageV2TextView
|
||||
let textView: InstantPageV2TextView // exposed so the parent V2 view can host its inline emoji
|
||||
|
||||
init(item: InstantPageV2ThinkingItem, theme: InstantPageTheme) {
|
||||
self.item = item
|
||||
|
|
@ -1877,15 +1898,22 @@ final class InstantPageV2ThinkingView: UIView, InstantPageItemView {
|
|||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
/// Parent positions children (see CLAUDE.md "View frame ownership"): the shimmer covers the
|
||||
/// whole block; the inner text view sits at its block-local typographic frame (expanded by the
|
||||
/// text view's clipping inset, matching `InstantPageV2TextView.init`).
|
||||
/// Parent positions self at the item frame (the bare line box). The shimmer and its gradient
|
||||
/// mask are sized to the text view's clipping-inset-EXPANDED frame and shifted to
|
||||
/// `(-inset, -inset)`, so the mask doesn't crop the glyph overhang the inset reserves (tall
|
||||
/// ascenders, descenders, the last line's underline) — the symptom of sizing the mask to the
|
||||
/// bare line box. The inner text view fills the shimmer; its `+inset` render translate lands the
|
||||
/// glyphs back at self's origin, so the text position is unchanged. Mirrors how a `.text` view's
|
||||
/// frame is inset-expanded (`actualFrame` / `InstantPageV2TextView.init`).
|
||||
private func layoutContents() {
|
||||
self.shimmerView.frame = CGRect(origin: .zero, size: self.item.frame.size)
|
||||
self.textView.frame = self.item.textItem.frame.insetBy(dx: -v2TextViewClippingInset, dy: -v2TextViewClippingInset)
|
||||
let inset = v2TextViewClippingInset
|
||||
let expandedSize = CGSize(width: self.item.frame.size.width + inset * 2.0,
|
||||
height: self.item.frame.size.height + inset * 2.0)
|
||||
self.shimmerView.frame = CGRect(x: -inset, y: -inset, width: expandedSize.width, height: expandedSize.height)
|
||||
self.textView.frame = CGRect(origin: .zero, size: expandedSize)
|
||||
self.shimmerView.update(
|
||||
size: self.item.frame.size,
|
||||
containerWidth: self.item.frame.size.width,
|
||||
size: expandedSize,
|
||||
containerWidth: expandedSize.width,
|
||||
offsetX: 0.0,
|
||||
gradientWidth: 200.0,
|
||||
transition: .immediate
|
||||
|
|
|
|||
|
|
@ -2004,16 +2004,22 @@ private func layoutThinking(
|
|||
setupStyleStack(styleStack, theme: context.theme, attributes: dimmedAttributes)
|
||||
let attributedString = attributedStringForRichText(text, styleStack: styleStack)
|
||||
|
||||
// Mirror a normal `.text` item's sizing: lay the text out flush (offset .zero) and put the page
|
||||
// inset onto the BLOCK frame, so the `.thinking` item's frame == a `.text` item's frame
|
||||
// (`(horizontalInset, 0, textWidth, height)`) instead of a full-bleed `(0, 0, boundingWidth, …)`
|
||||
// box. The shimmer (sized to `item.frame.size`) then hugs the text rather than the whole page
|
||||
// width; the rendered text stays at the same place (`horizontalInset`) since the block carries
|
||||
// the inset.
|
||||
let (textItem, _, textSize) = layoutTextItem(
|
||||
attributedString,
|
||||
boundingWidth: boundingWidth - horizontalInset * 2.0,
|
||||
offset: CGPoint(x: horizontalInset, y: 0.0),
|
||||
offset: CGPoint(x: 0.0, y: 0.0),
|
||||
fitToWidth: context.fitToWidth,
|
||||
computeRevealCharacterRects: context.computeRevealCharacterRects
|
||||
)
|
||||
guard let textItem = textItem else { return [] }
|
||||
|
||||
let blockFrame = CGRect(x: 0.0, y: 0.0, width: boundingWidth, height: textSize.height)
|
||||
let blockFrame = CGRect(x: horizontalInset, y: 0.0, width: textSize.width, height: textSize.height)
|
||||
return [.thinking(InstantPageV2ThinkingItem(frame: blockFrame, textItem: textItem))]
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue