Improve InstantPage V2 layout: true font-height line boxes + code-block style
- layoutTextItem now sizes a text item to the true font line height (A + D) instead of the cap box: the line stack starts at lineBoxTopInset (the ascender headroom, max(0, fontAscent - fontLineHeight)) and the returned height is padded by the last line's descender. Inter-line advance is unchanged and per-line frames stay the cap box, so the baseline draw, decorations, reveal mask, and inline attachments translate consistently; the page grows. - Size inline custom emoji to ~the font line height (font.ascender - font.descender + 4*pointSize/17) so they fit the taller line box instead of overflowing it; the line is not inflated. - Add a dedicated code-block text style for rich messages (monospace font + codeBlock theme attribute) threaded through the rich-data bubble and send preview; unify the bubble code-block/separator colors. - Adjust inter-block spacings and assorted V2 layout/divider details. - Document the true-font-height box and emoji sizing in CLAUDE.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5403ffa8b0
commit
1872832e28
8 changed files with 154 additions and 83 deletions
18
CLAUDE.md
18
CLAUDE.md
|
|
@ -92,7 +92,7 @@ The `ChatMessageDateAndStatusNode` mirrors TextBubble's placement, adapted to th
|
|||
|
||||
- **X is a fixed left edge, not the last line's `minX`.** Anchor x = `pageHorizontalInset` (10pt, the page layout's text inset; pageView sits at self-x 0). The status layout is measured with `boundingWidth - 2·pageHorizontalInset` (mirrors TextBubble's `boundingWidth - sideInsets`) so the right-aligned date lands at the right inset instead of off the bubble. Using `lastTextLineFrame.minX` (which is large for nested/indented last lines) shoved the date off to the right.
|
||||
- **Trail the last line only when the bottom-most item is text.** `lastTextLineFrameIfLastItemIsText(in:)` (in `InstantPageV2Layout.swift`) returns the last line frame *only* when the bottom-most top-level item (max `maxY`) is a `.text`; otherwise nil, so the date wraps below all content (anchored at `contentSize.height`). For tables/images/etc. the date must not trail text buried above the final item.
|
||||
- **InstantPage draws the baseline at the line frame's `maxY`** (`InstantPageRenderer` draws each line at `lineOrigin.y + lineFrame.height`), so the visible text of a plain line sits ~5pt below `maxY`. A date that **trails** on the line (`statusHeight == 0`) adds `trailingBottomPadding` (5pt) to align with the text; a date that **wraps** onto its own line below (`statusHeight > 0`) sits at the bare `maxY`. The pad is 0 for lines taller than their font line height (an inline animated emoji, ~`pointSize·24/17`, already pushes `maxY` down). `lastTextLineFrameIfLastItemIsText` returns `(frame, trailingBottomPadding)`; the bubble applies the pad only in the trailing case.
|
||||
- **InstantPage draws the baseline at the line frame's `maxY`** (`InstantPageRenderer` draws each line at `lineOrigin.y + lineFrame.height`), so the visible text of a plain line sits ~5pt below `maxY`. A date that **trails** on the line (`statusHeight == 0`) adds `trailingBottomPadding` (5pt) to align with the text; a date that **wraps** onto its own line below (`statusHeight > 0`) sits at the bare `maxY`. The pad is 0 for lines taller than their font line height (a tall inline attachment, e.g. a formula, already pushes `maxY` down). `lastTextLineFrameIfLastItemIsText` returns `(frame, trailingBottomPadding)`; the bubble applies the pad only in the trailing case.
|
||||
- **Bubble height leaves ~6pt below the date.** One unified formula for all cases: `boundingSize.height = max(boundingSize.height, statusBottomEdge + 6.0)`, where `statusBottomEdge = statusAnchorY + max(1, statusHeight)`. The `statusAnchorY` in the measure (`continue`) closure must mirror the `statusFrameY` in the apply closure exactly, or the date will be clipped/misplaced. (`streamingHeaderOffset` is `0.0` — there is no header offset to add.) 6pt matches TextBubble's bottom bubble inset.
|
||||
- **`hasDraft` adds the same 6pt at the streaming site.** The status max() above is gated by `!hasDraft`, so during streaming (status hidden, alpha=0) it can't supply the bubble's bottom inset. A separate `boundingSize.height += 6.0` inside `if hasDraft` in the SizeBlock closure does it instead — same 6pt, so the streaming bubble's bottom breathing room matches its post-stream height and there's no 6pt grow-pop when the status node fades in at finalize. The `hadDraft && !hasDraft` finalize pass doesn't need it because `!hasDraft` re-enables the status max(). If you ever refactor the `+6.0` constant out of the status max() into a `bottomInset` (TextBubble's pattern), kill this separate term at the same time — they're two ends of the same invariant.
|
||||
|
||||
|
|
@ -109,6 +109,18 @@ A V2 `.table` block's item frame is **full-width / flush** with the bubble inter
|
|||
- **The 10pt rounded outer border is `contentView.layer`'s own border, NOT sublayers.** `v2TableCornerRadius = 10.0` (`InstantPageV2Layout.swift`). The renderer sets `contentView.layer.cornerRadius`/`borderColor`/`borderWidth = bordered ? v2TableBorderWidth : 0.0` in BOTH `init` and `update` (the four straight outer-edge rect layers were removed; `lineLayers` now holds only inner grid lines). **Border-only — deliberately no `masksToBounds`:** `cornerRadius` rounds the layer's border without clipping contents (filled corner cells round their own fills separately — see next bullet), and there is **zero interaction with the streaming reveal mask** (`contentView.layer.mask`, set only during AI streaming) — the border reveals row-by-row with the rows and is part of the masked layer. The rounded card belongs to the grid (scrolls with it). For a non-empty-title table (never produced by markdown/AI), the border wraps title+grid since `contentView` includes the title region — an accepted, approved nuance.
|
||||
- **Filled corner cells round their own fills to match the border.** A header/striped cell's background is a stripe `CALayer`; `tableStripeCornerMask(cellFrame:gridWidth:gridHeight:effectiveBorderWidth:)` detects which grid corners the cell's (grid-local) frame touches — `firstCol/firstRow` via `frame.min{X,Y} <= effectiveBorderWidth/2 + 0.5`, `lastCol/lastRow` via `frame.max{X,Y} >= grid{Width,Height} - …` (gridWidth = `item.contentSize.width`, gridHeight = `item.contentSize.height - gridOffsetY`) — and rounds only those corners: `stripe.cornerRadius = max(0, v2TableCornerRadius - effectiveBorderWidth)` (the `-borderWidth` leaves an even border ring; borderless → full radius) + `stripe.maskedCorners`, in BOTH `init` and `update`. A `CALayer`'s `backgroundColor` honors `cornerRadius`+`maskedCorners` with no `masksToBounds`. A full-width (colspan) header rounds both top corners; a one-row filled table rounds all four; bottom corners round only when the last row is filled. The empty-mask branch resets `cornerRadius = 0` **and** `maskedCorners = []` so reused stripes (persist across streaming chunks) don't keep stale rounding. Detection is grid-local, so it's independent of the `contentInset` shift / horizontal scroll.
|
||||
|
||||
## InstantPage V2 text item height (true font line box)
|
||||
|
||||
`layoutTextItem` (`InstantPageV2Layout.swift`) sizes a `.text` item to the **true font line height**, not the cap box. A single-line item measures exactly `fontAscent + fontDescentBelowBaseline` (`A + D`); the old behavior was the cap box `fontLineHeight = floor(fontAscent + fontDescent)` (`A − D`).
|
||||
|
||||
### Non-obvious invariants
|
||||
|
||||
- **Two edits in `layoutTextItem`:** the line stack starts at `lineBoxTopInset = max(0, fontAscent − fontLineHeight)` (was `0`), and the returned height is `lines.last.frame.maxY + extraDescent + fontDescentBelowBaseline` (the `+ fontDescentBelowBaseline` contains the last line's descender). Net: every text item grows ~`(A − L) + D` (~8pt @17pt) and its glyphs draw ~`lineBoxTopInset` (~4pt) lower within their box; the page grows.
|
||||
- **Per-line frames stay the cap box** (`height = lineAscent = fontLineHeight`). Only the stack's starting origin moves and the total is padded — so the baseline is still drawn at each line frame's `maxY`, inter-line advance (`lineAscent + fontLineSpacing + extraDescent`) is unchanged, and decorations / inline attachments / `characterRect` / the reveal mask (all line-frame-relative) translate consistently.
|
||||
- **`lineBoxTopInset` is exact, NOT pixel-snapped** — it is an intra-item line offset; crispness rides on the item's own pixel-snapped frame origin (intra-item line positions may already be fractional, e.g. after a non-integral `extraDescent`).
|
||||
- **Formulas / tall inline content still inflate** via `lineAscent`/`extraDescent`; the `"\u{200b}"`+anchors `height = 0` case is preserved.
|
||||
- **Inline custom emoji are sized to ≈ the line box** so they fit the taller box rather than overflowing it (see "Inline custom emoji").
|
||||
|
||||
## Inline custom emoji (RichText.textCustomEmoji)
|
||||
|
||||
`RichText.textCustomEmoji(fileId:alt:)` renders an inline **animated** custom emoji inside rich-data bubbles. Covers API parsing, Postbox + FlatBuffers serialization, and display in the InstantPage V2 renderer; the emoji participates in the streaming reveal above. (The **send / edit / copy / paste** round-trip that produces `.textCustomEmoji` from typed markdown is a separate section below: "Custom emoji in markdown messages".)
|
||||
|
|
@ -120,7 +132,7 @@ A V2 `.table` block's item frame is **full-width / flush** with the bubble inter
|
|||
| `submodules/TelegramCore/Sources/SyncCore/SyncCore_RichText.swift` | Enum case `textCustomEmoji(fileId: Int64, alt: String)` + Postbox coding (discriminator 17, keys `ce.f`/`ce.a`), `==`, `plainText` (returns `alt`), and FlatBuffers codec. |
|
||||
| `submodules/TelegramCore/FlatSerialization/Models/RichText.fbs` | FlatBuffers schema — `RichText_CustomEmoji` union member + table. **Source of truth**; the Bazel `flatc` genrule regenerates `*_generated.swift` at build time (the checked-in `Sources/*_generated.swift` is stale). |
|
||||
| `submodules/TelegramCore/Sources/ApiUtils/RichText.swift` | `Api.RichText.textCustomEmoji` ⇄ Swift, lossless both ways. |
|
||||
| `submodules/InstantPageUI/Sources/InstantPageTextItem.swift` (`attributedStringForRichText`) | Emits a single placeholder char carrying `ChatTextInputAttributes.customEmoji` (a `ChatTextInputTextCustomEmojiAttribute`) + a `CTRunDelegate` sized `font.pointSize · 24/17`. |
|
||||
| `submodules/InstantPageUI/Sources/InstantPageTextItem.swift` (`attributedStringForRichText`) | Emits a single placeholder char carrying `ChatTextInputAttributes.customEmoji` (a `ChatTextInputTextCustomEmojiAttribute`) + a `CTRunDelegate` sized to the font line height (`font.ascender − font.descender + 4·pointSize/17` ≈ 24pt @17pt). |
|
||||
| `submodules/InstantPageUI/Sources/InstantPageV2Layout.swift` (line-breaker) | Collects per-line `InstantPageTextLine.emojiItems`; overwrites each placeholder char's `characterRect` with a full cell (`width = itemSize`) so it feeds the reveal cost map. |
|
||||
| `submodules/InstantPageUI/Sources/InstantPageRenderer.swift` (`InstantPageV2View`) | Owns the `InlineStickerItemLayer`s: `updateInlineEmoji` (create/reuse/remove/position), `updateEmojiReveal` (reveal-driven pop-in), `updateEmojiVisibility` + `propagateVisibilityRect`. Layers attach to each text view's `emojiContainerView`. |
|
||||
|
||||
|
|
@ -129,7 +141,7 @@ A V2 `.table` block's item frame is **full-width / flush** with the bubble inter
|
|||
- **flatc casing/`required` gotchas.** Edit `RichText.fbs`, not the generated Swift. Scalars (`long`) cannot be `(required)` — only strings/tables can. A union member `RichText_CustomEmoji` generates the Swift enum case `.richtextCustomemoji` (everything after the suffix's first letter is lowercased); the table type stays `TelegramCore_RichText_CustomEmoji` and field accessors keep `.fbs` casing (`value.fileId`). See the `flatbuffers-codegen` memory.
|
||||
- **`ChatTextInputTextCustomEmojiAttribute` is reused end-to-end** (display layer ⇄ layout model). The attribute is written to the placeholder in `attributedStringForRichText` and read back by the V2 line-breaker under the SAME key (`ChatTextInputAttributes.customEmoji`); `InlineStickerItemLayer.init` consumes it directly and resolves the file lazily from `fileId`.
|
||||
- **Emoji participates in the streaming reveal.** Its placeholder char's `characterRect` is overwritten to a full cell (width = `itemSize`), so the width-based cost map charges it like other content. `updateEmojiReveal` pops the layer in (alpha 0→1 + scale) when `charIndexInItem < currentRevealCharacterCount`; unrevealed → opacity 0.
|
||||
- **Inline emoji/images are CENTERED on the font line box, NOT baseline-aligned, and do NOT inflate the line.** The line-breaker keeps `lineAscent = fontLineHeight` (only formulas grow it) and places each attachment at `baselineY − fontLineHeight/2 − size/2`, so a 24pt emoji on a ~17pt line bleeds symmetrically instead of doubling the line height and shoving the text baseline down (the prior `lineAscent = emoji.size` behavior was a regression from V1 `layoutTextItemWithString`, which centers via `(fontLineHeight − imageHeight)/2`). Mirrors the chat `InteractiveTextComponent`. The cell's `characterRect` is centered the same way (`y = fontLineHeight/2 − size/2`) so the reveal mask (`renderer: y = minY + lineAscent − rect.maxY`) tracks it; a tall attachment grows `extraDescent` so the next line isn't overlapped. Three things must stay in lockstep: the display frame, the `characterRect`, and `extraDescent`.
|
||||
- **Inline emoji/images are CENTERED on the font line box, NOT baseline-aligned, and do NOT inflate the line.** The line-breaker keeps `lineAscent = fontLineHeight` (only formulas grow it) and places each attachment at `baselineY − fontLineHeight/2 − size/2`, so it bleeds symmetrically about the line box instead of doubling the line height and shoving the text baseline down (the prior `lineAscent = emoji.size` behavior was a regression from V1 `layoutTextItemWithString`, which centers via `(fontLineHeight − imageHeight)/2`). Custom emoji are sized to ≈ the line box (`size = font.ascender − font.descender + 4·pointSize/17`) so they fit the true-font-height item box (see "InstantPage V2 text item height") with minimal bleed. Mirrors the chat `InteractiveTextComponent`. The cell's `characterRect` is centered the same way (`y = fontLineHeight/2 − size/2`) so the reveal mask (`renderer: y = minY + lineAscent − rect.maxY`) tracks it; a tall attachment grows `extraDescent` so the next line isn't overlapped. Three things must stay in lockstep: the display frame, the `characterRect`, and `extraDescent`.
|
||||
- **Layers sit ABOVE the reveal mask.** They attach to `InstantPageV2TextView.emojiContainerView` (a sibling above `renderContainer`), NOT inside it — so the reveal mask wipes glyphs while emoji pop in independently. Adding a CTRunDelegate-glyph to the mask would clip-wipe them instead.
|
||||
- **Layers are owned by `InstantPageV2View`, not the text view.** Keyed by `InlineStickerItemLayer.Key(id: fileId, index: occurrence)`. The pageView is now REUSED across `stableVersion` bumps (see streaming section), so the inline-emoji dict PERSISTS across chunks; `updateInlineEmoji` prunes stale keys (emoji whose blocks have been removed) and creates/repositions layers for new or unchanged emoji each update pass.
|
||||
- **`visibilityRect` gates looping; `nil` means "not visible".** The bubble's `visibility` override pushes a full-width sub-rect to the root `pageView.visibilityRect`, re-pushed in the apply closure after `pageView.frame` is set. `propagateVisibilityRect` converts the rect into each nested V2View's coordinate space (`self.convert(_:to:)`) for details bodies / table cells+title, fanning out via each child's `didSet`.
|
||||
|
|
|
|||
|
|
@ -35,10 +35,12 @@ private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantP
|
|||
stack.push(.linkColor(theme.linkColor))
|
||||
stack.push(.linkMarkerColor(theme.linkHighlightColor))
|
||||
switch attributes.font.style {
|
||||
case .sans:
|
||||
stack.push(.fontSerif(false))
|
||||
case .serif:
|
||||
stack.push(.fontSerif(true))
|
||||
case .sans:
|
||||
stack.push(.fontSerif(false))
|
||||
case .serif:
|
||||
stack.push(.fontSerif(true))
|
||||
case .monospace:
|
||||
stack.push(.fontFixed(true))
|
||||
}
|
||||
stack.push(.fontSize(attributes.font.size))
|
||||
stack.push(.lineSpacingFactor(attributes.font.lineSpacingFactor))
|
||||
|
|
@ -85,6 +87,16 @@ private func instantPageFont(style: InstantPageTextAttributes, bold: Bool = fals
|
|||
} else {
|
||||
return Font.regular(size)
|
||||
}
|
||||
case .monospace:
|
||||
if bold && italic {
|
||||
return Font.semiboldItalicMonospace(size)
|
||||
} else if bold {
|
||||
return Font.semiboldMonospace(size)
|
||||
} else if italic {
|
||||
return Font.italicMonospace(size)
|
||||
} else {
|
||||
return Font.monospace(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?, fi
|
|||
return 0.0
|
||||
case (.divider, _), (_, .divider):
|
||||
if fitToWidth {
|
||||
return 20.0
|
||||
return 21.0
|
||||
} else {
|
||||
return 25.0
|
||||
}
|
||||
|
|
@ -54,18 +54,14 @@ func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?, fi
|
|||
} else {
|
||||
return 31.0
|
||||
}
|
||||
case (.preformatted, .paragraph):
|
||||
return 19.0
|
||||
case (.formula, .paragraph):
|
||||
return 19.0
|
||||
case (.paragraph, .paragraph):
|
||||
if fitToWidth {
|
||||
return 10.0
|
||||
return 2.0
|
||||
} else {
|
||||
return 25.0
|
||||
}
|
||||
case (_, .paragraph):
|
||||
return 20.0
|
||||
case (.title, .formula), (.authorDate, .formula):
|
||||
return 34.0
|
||||
case (.header, .formula), (.subheader, .formula), (.heading, .formula):
|
||||
|
|
@ -76,8 +72,6 @@ func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?, fi
|
|||
}
|
||||
case (.list, .formula):
|
||||
return 31.0
|
||||
case (.preformatted, .formula):
|
||||
return 19.0
|
||||
case (.paragraph, .formula):
|
||||
return 19.0
|
||||
case (_, .formula):
|
||||
|
|
@ -86,8 +80,12 @@ func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?, fi
|
|||
return 34.0
|
||||
case (.header, .list), (.subheader, .list), (.heading, .list):
|
||||
return 31.0
|
||||
case (.preformatted, .list):
|
||||
return 19.0
|
||||
case (.preformatted, _), (_, .preformatted):
|
||||
if fitToWidth {
|
||||
return 12.0
|
||||
} else {
|
||||
return 19.0
|
||||
}
|
||||
case (.formula, .list):
|
||||
if fitToWidth {
|
||||
return 10.0
|
||||
|
|
@ -100,12 +98,6 @@ func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?, fi
|
|||
} else {
|
||||
return 25.0
|
||||
}
|
||||
case (.paragraph, .preformatted):
|
||||
return 19.0
|
||||
case (.formula, .preformatted):
|
||||
return 19.0
|
||||
case (_, .preformatted):
|
||||
return 20.0
|
||||
case (_, .header), (_, .subheader), (_, .heading):
|
||||
return 32.0
|
||||
default:
|
||||
|
|
@ -121,9 +113,11 @@ func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?, fi
|
|||
case .topLevel:
|
||||
switch lower {
|
||||
case .heading:
|
||||
return 13.0
|
||||
default:
|
||||
return 6.0
|
||||
case .table:
|
||||
return 10.0
|
||||
default:
|
||||
return 5.0
|
||||
}
|
||||
case .cell:
|
||||
return 0.0
|
||||
|
|
@ -139,6 +133,8 @@ func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?, fi
|
|||
case .topLevel:
|
||||
if case .relatedArticles = upper {
|
||||
return 0.0
|
||||
} else if case .thinking = upper {
|
||||
return 2.0
|
||||
} else {
|
||||
if fitToWidth {
|
||||
return 5.0
|
||||
|
|
|
|||
|
|
@ -960,7 +960,11 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt
|
|||
}
|
||||
let attributes = styleStack.textAttributes()
|
||||
let font = (attributes[NSAttributedString.Key.font] as? UIFont) ?? UIFont.systemFont(ofSize: 17.0)
|
||||
let itemSize = font.pointSize * 24.0 / 17.0
|
||||
// Size the inline emoji to the font's line height (A + D) plus a 4pt bump at the 17pt
|
||||
// body font (scaled proportionally). Must match the V2 layout's emoji cell size
|
||||
// (InstantPageV2Layout.swift). The run delegate still reports the font's own
|
||||
// ascent/descent (below), so the line height is unchanged — only the emoji width changes.
|
||||
let itemSize = font.ascender - font.descender + 4.0 * font.pointSize / 17.0
|
||||
let extentBuffer = UnsafeMutablePointer<RunStruct>.allocate(capacity: 1)
|
||||
extentBuffer.initialize(to: RunStruct(ascent: font.ascender, descent: font.descender, width: itemSize))
|
||||
var callbacks = CTRunDelegateCallbacks(version: kCTRunDelegateVersion1, dealloc: { pointer in
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import TelegramUIPreferences
|
|||
public enum InstantPageFontStyle {
|
||||
case sans
|
||||
case serif
|
||||
case monospace
|
||||
}
|
||||
|
||||
public struct InstantPageFont {
|
||||
|
|
@ -50,6 +51,7 @@ enum InstantPageTextCategoryType {
|
|||
case credit
|
||||
case table
|
||||
case article
|
||||
case codeBlock
|
||||
}
|
||||
|
||||
public struct InstantPageTextCategories {
|
||||
|
|
@ -61,8 +63,9 @@ public struct InstantPageTextCategories {
|
|||
let credit: InstantPageTextAttributes
|
||||
let table: InstantPageTextAttributes
|
||||
let article: InstantPageTextAttributes
|
||||
let codeBlock: InstantPageTextAttributes
|
||||
|
||||
public init(kicker: InstantPageTextAttributes, header: InstantPageTextAttributes, subheader: InstantPageTextAttributes, paragraph: InstantPageTextAttributes, caption: InstantPageTextAttributes, credit: InstantPageTextAttributes, table: InstantPageTextAttributes, article: InstantPageTextAttributes) {
|
||||
public init(kicker: InstantPageTextAttributes, header: InstantPageTextAttributes, subheader: InstantPageTextAttributes, paragraph: InstantPageTextAttributes, caption: InstantPageTextAttributes, credit: InstantPageTextAttributes, table: InstantPageTextAttributes, article: InstantPageTextAttributes, codeBlock: InstantPageTextAttributes) {
|
||||
self.kicker = kicker
|
||||
self.header = header
|
||||
self.subheader = subheader
|
||||
|
|
@ -71,26 +74,29 @@ public struct InstantPageTextCategories {
|
|||
self.credit = credit
|
||||
self.table = table
|
||||
self.article = article
|
||||
self.codeBlock = codeBlock
|
||||
}
|
||||
|
||||
func attributes(type: InstantPageTextCategoryType, link: Bool) -> InstantPageTextAttributes {
|
||||
switch type {
|
||||
case .kicker:
|
||||
return self.kicker.withUnderline(link)
|
||||
case .header:
|
||||
return self.header.withUnderline(link)
|
||||
case .subheader:
|
||||
return self.subheader.withUnderline(link)
|
||||
case .paragraph:
|
||||
return self.paragraph.withUnderline(link)
|
||||
case .caption:
|
||||
return self.caption.withUnderline(link)
|
||||
case .credit:
|
||||
return self.credit.withUnderline(link)
|
||||
case .table:
|
||||
return self.table.withUnderline(link)
|
||||
case .article:
|
||||
return self.article.withUnderline(link)
|
||||
case .kicker:
|
||||
return self.kicker.withUnderline(link)
|
||||
case .header:
|
||||
return self.header.withUnderline(link)
|
||||
case .subheader:
|
||||
return self.subheader.withUnderline(link)
|
||||
case .paragraph:
|
||||
return self.paragraph.withUnderline(link)
|
||||
case .caption:
|
||||
return self.caption.withUnderline(link)
|
||||
case .credit:
|
||||
return self.credit.withUnderline(link)
|
||||
case .table:
|
||||
return self.table.withUnderline(link)
|
||||
case .article:
|
||||
return self.article.withUnderline(link)
|
||||
case .codeBlock:
|
||||
return self.codeBlock.withUnderline(link)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -103,7 +109,8 @@ public struct InstantPageTextCategories {
|
|||
caption: self.caption.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, lineSpacingFactor: lineSpacingFactor, forceSerif: forceSerif),
|
||||
credit: self.credit.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, lineSpacingFactor: lineSpacingFactor, forceSerif: forceSerif),
|
||||
table: self.table.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, lineSpacingFactor: lineSpacingFactor, forceSerif: forceSerif),
|
||||
article: self.article.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, lineSpacingFactor: lineSpacingFactor, forceSerif: forceSerif)
|
||||
article: self.article.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, lineSpacingFactor: lineSpacingFactor, forceSerif: forceSerif),
|
||||
codeBlock: self.codeBlock.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, lineSpacingFactor: lineSpacingFactor, forceSerif: forceSerif)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -214,7 +221,8 @@ private let lightTheme = InstantPageTheme(
|
|||
caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x79828b)),
|
||||
credit: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 13.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x79828b)),
|
||||
table: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: .black),
|
||||
article: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 18.0, lineSpacingFactor: 1.0), color: .black)
|
||||
article: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 18.0, lineSpacingFactor: 1.0), color: .black),
|
||||
codeBlock: InstantPageTextAttributes(font: InstantPageFont(style: .monospace, size: 14.0, lineSpacingFactor: 1.0), color: .black)
|
||||
),
|
||||
serif: false,
|
||||
codeBlockBackgroundColor: UIColor(rgb: 0xf5f8fc),
|
||||
|
|
@ -247,7 +255,8 @@ private let sepiaTheme = InstantPageTheme(
|
|||
caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x927e6b)),
|
||||
credit: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 13.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x927e6b)),
|
||||
table: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x4f321d)),
|
||||
article: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 18.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x4f321d))
|
||||
article: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 18.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x4f321d)),
|
||||
codeBlock: InstantPageTextAttributes(font: InstantPageFont(style: .monospace, size: 14.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x4f321d))
|
||||
),
|
||||
serif: false,
|
||||
codeBlockBackgroundColor: UIColor(rgb: 0xefe7d6),
|
||||
|
|
@ -280,7 +289,8 @@ private let grayTheme = InstantPageTheme(
|
|||
caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xa0a0a0)),
|
||||
credit: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 13.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xa0a0a0)),
|
||||
table: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xcecece)),
|
||||
article: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 18.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xcecece))
|
||||
article: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 18.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xcecece)),
|
||||
codeBlock: InstantPageTextAttributes(font: InstantPageFont(style: .monospace, size: 14.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xcecece))
|
||||
),
|
||||
serif: false,
|
||||
codeBlockBackgroundColor: UIColor(rgb: 0x555556),
|
||||
|
|
@ -313,7 +323,8 @@ private let darkTheme = InstantPageTheme(
|
|||
caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x6a6a6a)),
|
||||
credit: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 13.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x6a6a6a)),
|
||||
table: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xb0b0b0)),
|
||||
article: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 18.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xb0b0b0))
|
||||
article: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 18.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xb0b0b0)),
|
||||
codeBlock: InstantPageTextAttributes(font: InstantPageFont(style: .monospace, size: 14.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xb0b0b0))
|
||||
),
|
||||
serif: false,
|
||||
codeBlockBackgroundColor: UIColor(rgb: 0x131313),
|
||||
|
|
|
|||
|
|
@ -440,7 +440,7 @@ public func lastTextLineFrame(in layout: InstantPageV2Layout) -> CGRect? {
|
|||
/// Also returns `trailingBottomPadding`: the renderer draws the baseline at the line frame's maxY,
|
||||
/// so the visible text of a plain line sits ~5pt below it. A status that *trails on the line* should
|
||||
/// anchor at `maxY + trailingBottomPadding` to align with where the text actually renders. The pad
|
||||
/// is 0 when the line is taller than its font line height (an inline animated emoji, ~pointSize·24/17,
|
||||
/// is 0 when the line is taller than its font line height (a tall inline attachment, e.g. a formula,
|
||||
/// already pushes maxY down to the right spot). Callers should NOT apply the pad when the status
|
||||
/// wraps onto its own line below the text — there it should sit at the bare maxY.
|
||||
public func lastTextLineFrameIfLastItemIsText(in layout: InstantPageV2Layout) -> (frame: CGRect, trailingBottomPadding: CGFloat)? {
|
||||
|
|
@ -1899,17 +1899,14 @@ private func layoutDivider(
|
|||
boundingWidth: CGFloat,
|
||||
context: LayoutContext
|
||||
) -> [InstantPageV2LaidOutItem] {
|
||||
// Geometry matches V1 InstantPageLayout.swift lines 361–363:
|
||||
// lineWidth = floor(boundingWidth / 2.0), x = floor((boundingWidth - lineWidth) / 2.0), h = 1pt.
|
||||
// Color matches V1: theme.textCategories.caption.color.
|
||||
let lineWidth = floor(boundingWidth / 2.0)
|
||||
let frame = CGRect(
|
||||
x: floor((boundingWidth - lineWidth) / 2.0),
|
||||
y: 0.0,
|
||||
width: lineWidth,
|
||||
height: 1.0
|
||||
height: UIScreenPixel
|
||||
)
|
||||
return [.divider(InstantPageV2DividerItem(frame: frame, color: context.theme.textCategories.caption.color))]
|
||||
return [.divider(InstantPageV2DividerItem(frame: frame, color: context.theme.separatorColor))]
|
||||
}
|
||||
|
||||
// MARK: - Code block layout (ported from V1 InstantPageLayout.swift lines 329–351)
|
||||
|
|
@ -1921,11 +1918,8 @@ private func layoutCodeBlock(
|
|||
horizontalInset: CGFloat,
|
||||
context: inout LayoutContext
|
||||
) -> [InstantPageV2LaidOutItem] {
|
||||
// V1 InstantPageLayout.swift line 330: backgroundInset = 14.0 (top + bottom padding).
|
||||
let backgroundInset: CGFloat = 14.0
|
||||
// V1 line 342: text x offset is 17.0 (hardcoded, not backgroundInset).
|
||||
let textXOffset: CGFloat = 17.0
|
||||
// V1 line 348: shape is .rect — no corner radius.
|
||||
let backgroundInset: CGFloat = 15.0
|
||||
let textXOffset: CGFloat = 11.0
|
||||
let cornerRadius: CGFloat = 0.0
|
||||
|
||||
let attributedString: NSAttributedString
|
||||
|
|
@ -1940,7 +1934,7 @@ private func layoutCodeBlock(
|
|||
} else {
|
||||
// V1 lines 335–338: fall back to plain paragraph style when no language.
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: context.theme, category: .paragraph, link: false)
|
||||
setupStyleStack(styleStack, theme: context.theme, category: .codeBlock, link: false)
|
||||
attributedString = attributedStringForRichText(text, styleStack: styleStack)
|
||||
}
|
||||
|
||||
|
|
@ -2528,10 +2522,12 @@ private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantP
|
|||
stack.push(.linkColor(theme.linkColor))
|
||||
stack.push(.linkMarkerColor(theme.linkHighlightColor))
|
||||
switch attributes.font.style {
|
||||
case .sans:
|
||||
stack.push(.fontSerif(false))
|
||||
case .serif:
|
||||
stack.push(.fontSerif(true))
|
||||
case .sans:
|
||||
stack.push(.fontSerif(false))
|
||||
case .serif:
|
||||
stack.push(.fontSerif(true))
|
||||
case .monospace:
|
||||
stack.push(.fontFixed(true))
|
||||
}
|
||||
stack.push(.fontSize(attributes.font.size))
|
||||
stack.push(.lineSpacingFactor(attributes.font.lineSpacingFactor))
|
||||
|
|
@ -2578,6 +2574,16 @@ private func instantPageFont(style: InstantPageTextAttributes, bold: Bool = fals
|
|||
} else {
|
||||
return Font.regular(size)
|
||||
}
|
||||
case .monospace:
|
||||
if bold && italic {
|
||||
return Font.semiboldItalicMonospace(size)
|
||||
} else if bold {
|
||||
return Font.semiboldMonospace(size)
|
||||
} else if italic {
|
||||
return Font.italicMonospace(size)
|
||||
} else {
|
||||
return Font.monospace(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2745,10 +2751,18 @@ func layoutTextItem(
|
|||
let fontLineHeight = floor(fontAscent + fontDescent)
|
||||
let fontLineSpacing = floor(fontLineHeight * lineSpacingFactor)
|
||||
let fontDescentBelowBaseline = max(0.0, -fontDescent)
|
||||
// True font-height line box: shift the whole line stack down by the ascender headroom above
|
||||
// the cap line (A − L) and pad the final height by the descender (D) below the last baseline,
|
||||
// so a single-line item measures exactly A + D. Exact (not pixel-snapped): this is an
|
||||
// intra-item line offset; crispness rides on the item's own pixel-snapped frame origin, and
|
||||
// intra-item line positions may already be fractional (e.g. after a non-integral extraDescent).
|
||||
// Inter-line advance is unchanged. (Named `lineBoxTopInset` to avoid colliding with the
|
||||
// formula-bleed `topInset` local near the end of this function.)
|
||||
let lineBoxTopInset = max(0.0, fontAscent - fontLineHeight)
|
||||
let baselineToNextTopSlack = max(0.0, fontLineSpacing - 4.0)
|
||||
|
||||
var lastIndex: CFIndex = 0
|
||||
var currentLineOrigin = CGPoint()
|
||||
var currentLineOrigin = CGPoint(x: 0.0, y: lineBoxTopInset)
|
||||
|
||||
var hasAnchors = false
|
||||
var maxLineWidth: CGFloat = 0.0
|
||||
|
|
@ -2831,7 +2845,12 @@ func layoutTextItem(
|
|||
} else if let emoji = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
|
||||
let xOffset = CTLineGetOffsetForStringIndex(line, range.location, nil)
|
||||
let font = (attributes[NSAttributedString.Key.font] as? UIFont) ?? UIFont.systemFont(ofSize: 17.0)
|
||||
let itemSize = font.pointSize * 24.0 / 17.0
|
||||
// Size the inline emoji to the font's line height (A + D = the true
|
||||
// line-box height) plus a 4pt bump at the 17pt body font (scaled
|
||||
// proportionally) so it reads a touch larger than the bare line box.
|
||||
// The line is NOT inflated (lineAscent stays fontLineHeight). Must match
|
||||
// the run-delegate width in attributedStringForRichText (InstantPageTextItem.swift).
|
||||
let itemSize = font.ascender - font.descender + 4.0 * font.pointSize / 17.0
|
||||
pendingEmoji.append(PendingV2EmojiAttachment(xOffset: xOffset, range: range, emoji: emoji, size: itemSize))
|
||||
}
|
||||
}
|
||||
|
|
@ -2897,7 +2916,7 @@ func layoutTextItem(
|
|||
extraDescent = max(0.0, lineDescent - baselineToNextTopSlack)
|
||||
// A centered attachment taller than the line bleeds below the baseline; grow the
|
||||
// descent so the following line isn't overlapped (mirrors V1's extraDescent handling).
|
||||
// Emoji at the default 24/17 ratio stay within the line slack and contribute nothing.
|
||||
// Emoji sized to the font line height (A + D) fit the line box, so they contribute nothing.
|
||||
for imageItem in lineImageItems {
|
||||
extraDescent = max(extraDescent, imageItem.frame.maxY - (baselineY + baselineToNextTopSlack))
|
||||
}
|
||||
|
|
@ -3088,7 +3107,9 @@ func layoutTextItem(
|
|||
|
||||
var height: CGFloat = 0.0
|
||||
if !lines.isEmpty && !(string.string == "\u{200b}" && hasAnchors) {
|
||||
height = lines.last!.frame.maxY + extraDescent
|
||||
// + fontDescentBelowBaseline: contain the last line's descender below its baseline, so
|
||||
// (with the topInset shift) a single-line item measures exactly A + D = true font height.
|
||||
height = lines.last!.frame.maxY + extraDescent + fontDescentBelowBaseline
|
||||
}
|
||||
|
||||
var textWidth = boundingWidth
|
||||
|
|
|
|||
|
|
@ -228,9 +228,9 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
|
|||
nameColors = nil
|
||||
}
|
||||
|
||||
let codeBlockBackgroundColor: UIColor
|
||||
let codeBlockTitleColor: UIColor
|
||||
let codeBlockAccentColor: UIColor
|
||||
let codeBlockBackgroundColor: UIColor
|
||||
if !isIncoming {
|
||||
mainColor = messageTheme.accentTextColor
|
||||
if let _ = nameColors?.secondary {
|
||||
|
|
@ -243,12 +243,12 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
|
|||
if item.presentationData.theme.theme.overallDarkAppearance {
|
||||
codeBlockTitleColor = .white
|
||||
codeBlockAccentColor = UIColor(white: 1.0, alpha: 0.5)
|
||||
codeBlockBackgroundColor = UIColor(white: 0.0, alpha: 0.25)
|
||||
} else {
|
||||
codeBlockTitleColor = mainColor
|
||||
codeBlockAccentColor = mainColor
|
||||
codeBlockBackgroundColor = mainColor.withMultipliedAlpha(0.1)
|
||||
}
|
||||
|
||||
codeBlockBackgroundColor = mainColor.withMultipliedAlpha(0.1)
|
||||
} else {
|
||||
let authorNameColor = nameColors?.main
|
||||
secondaryColor = nameColors?.secondary
|
||||
|
|
@ -263,11 +263,7 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
|
|||
codeBlockTitleColor = mainColor
|
||||
codeBlockAccentColor = mainColor
|
||||
|
||||
if item.presentationData.theme.theme.overallDarkAppearance {
|
||||
codeBlockBackgroundColor = UIColor(white: 0.0, alpha: 0.65)
|
||||
} else {
|
||||
codeBlockBackgroundColor = UIColor(white: 0.0, alpha: 0.05)
|
||||
}
|
||||
codeBlockBackgroundColor = mainColor.withMultipliedAlpha(0.1)
|
||||
}
|
||||
|
||||
let _ = secondaryColor
|
||||
|
|
@ -284,7 +280,8 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
|
|||
caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: messageTheme.secondaryTextColor),
|
||||
credit: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 13.0, lineSpacingFactor: 1.0), color: messageTheme.secondaryTextColor),
|
||||
table: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: messageTheme.primaryTextColor),
|
||||
article: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 18.0, lineSpacingFactor: 1.0), color: messageTheme.primaryTextColor)
|
||||
article: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 18.0, lineSpacingFactor: 1.0), color: messageTheme.primaryTextColor),
|
||||
codeBlock: InstantPageTextAttributes(font: InstantPageFont(style: .monospace, size: 14.0, lineSpacingFactor: 1.0), color: messageTheme.primaryTextColor),
|
||||
)
|
||||
let pageTheme = InstantPageTheme(
|
||||
type: isDark ? .dark : .light,
|
||||
|
|
@ -306,8 +303,8 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
|
|||
controlColor: messageTheme.accentControlColor,
|
||||
imageTintColor: nil,
|
||||
overlayPanelColor: isDark ? UIColor(white: 0.0, alpha: 0.13) : UIColor(white: 1.0, alpha: 0.13),
|
||||
separatorColor: isIncoming ? UIColor(white: 0.0, alpha: 0.25): messageTheme.accentControlColor.withMultipliedAlpha(0.25),
|
||||
secondaryControlColor: messageTheme.secondaryTextColor
|
||||
separatorColor: messageTheme.secondaryTextColor.mixedWith(mainColor.withMultipliedAlpha(0.2), alpha: 0.3),
|
||||
secondaryControlColor: messageTheme.secondaryTextColor.mixedWith(mainColor.withMultipliedAlpha(0.2), alpha: 0.3)
|
||||
)
|
||||
|
||||
var hasDraft = false
|
||||
|
|
@ -320,6 +317,15 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
|
|||
}
|
||||
|
||||
if let attribute = item.message.richText {
|
||||
#if DEBUG && false
|
||||
let instantPage = InstantPage(blocks: [.thinking(.concat([
|
||||
.textCustomEmoji(fileId: 5384559872899555845, alt: "a"),
|
||||
.plain("Thinking...")
|
||||
]))], media: [:], isComplete: true, rtl: false, url: "", views: nil)
|
||||
#else
|
||||
let instantPage = attribute.instantPage
|
||||
#endif
|
||||
|
||||
let webpage = TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(
|
||||
url: "",
|
||||
displayUrl: "",
|
||||
|
|
@ -339,7 +345,7 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
|
|||
file: nil,
|
||||
story: nil,
|
||||
attributes: [],
|
||||
instantPage: attribute.instantPage
|
||||
instantPage: instantPage
|
||||
)))
|
||||
pageWebpage = webpage
|
||||
|
||||
|
|
@ -352,9 +358,17 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
|
|||
current.messageStableVersion == currentMessageStableVersion {
|
||||
pageLayout = current.layout
|
||||
} else {
|
||||
#if DEBUG && false
|
||||
let instantPage = InstantPage(blocks: [.thinking(.concat([
|
||||
.textCustomEmoji(fileId: 5384559872899555845, alt: "a"),
|
||||
.plain("Thinking...")
|
||||
]))], media: [:], isComplete: true, rtl: false, url: "", views: nil)
|
||||
#else
|
||||
let instantPage = attribute.instantPage
|
||||
#endif
|
||||
pageLayout = layoutInstantPageV2(
|
||||
webpage: webpage,
|
||||
instantPage: attribute.instantPage,
|
||||
instantPage: instantPage,
|
||||
userLocation: .other,
|
||||
boundingWidth: suggestedBoundingWidth - 2.0,
|
||||
horizontalInset: pageHorizontalInset,
|
||||
|
|
|
|||
|
|
@ -96,7 +96,8 @@ final class ChatSendMessageRichTextPreview: ChatSendMessageContextScreenRichText
|
|||
caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: messageTheme.secondaryTextColor),
|
||||
credit: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 13.0, lineSpacingFactor: 1.0), color: messageTheme.secondaryTextColor),
|
||||
table: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: messageTheme.primaryTextColor),
|
||||
article: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 18.0, lineSpacingFactor: 1.0), color: messageTheme.primaryTextColor)
|
||||
article: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 18.0, lineSpacingFactor: 1.0), color: messageTheme.primaryTextColor),
|
||||
codeBlock: InstantPageTextAttributes(font: InstantPageFont(style: .monospace, size: 14.0, lineSpacingFactor: 1.0), color: messageTheme.primaryTextColor)
|
||||
)
|
||||
let pageTheme = InstantPageTheme(
|
||||
type: isDark ? .dark : .light,
|
||||
|
|
@ -119,7 +120,7 @@ final class ChatSendMessageRichTextPreview: ChatSendMessageContextScreenRichText
|
|||
tableHeaderColor: messageTheme.accentControlColor.withMultipliedAlpha(0.1),
|
||||
controlColor: messageTheme.accentControlColor,
|
||||
imageTintColor: nil,
|
||||
overlayPanelColor: isDark ? UIColor(white: 0.0, alpha: 0.13) : UIColor(white: 1.0, alpha: 0.13),
|
||||
overlayPanelColor: messageTheme.accentControlColor.withMultipliedAlpha(0.25),
|
||||
separatorColor: messageTheme.accentControlColor.withMultipliedAlpha(0.25),
|
||||
secondaryControlColor: messageTheme.secondaryTextColor
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue