mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Rich-bubble text selection (post-V2 fix)
Design and plan, then implementation: include the details title in selectableTextItems, restore V2 text selection in the rich bubble, drop the InstantPageMultiTextAdapter init parameter-name split, and split LinkHighlightingNode modern-path groups on disjoint rects. Also includes the IP V2 inline-formula baseline alignment design doc.
This commit is contained in:
parent
3a22e7c81b
commit
8dbbf0f715
4 changed files with 53 additions and 19 deletions
|
|
@ -60,6 +60,17 @@ private func drawRectsImageContent(size: CGSize, context: CGContext, color: UICo
|
||||||
}
|
}
|
||||||
currentRects.removeAll()
|
currentRects.removeAll()
|
||||||
} else {
|
} else {
|
||||||
|
// The path-stitching loop below assumes consecutive rects in a group
|
||||||
|
// are adjacent (overlapping or sharing an edge). When they're disjoint
|
||||||
|
// — vertical gap, vertical inversion, or horizontal gap on the same
|
||||||
|
// line — it bridges them with a polygon perimeter that renders as a
|
||||||
|
// diagonal bridge across empty space. Split groups whenever the next
|
||||||
|
// rect doesn't intersect the last one (1pt slop in both axes matches
|
||||||
|
// the snap loop's adjacency test).
|
||||||
|
if let last = currentRects.last, !last.insetBy(dx: -1.0, dy: -1.0).intersects(rect) {
|
||||||
|
combinedRects.append(currentRects)
|
||||||
|
currentRects.removeAll()
|
||||||
|
}
|
||||||
currentRects.append(rect)
|
currentRects.append(rect)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,28 +5,38 @@ import Display
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
|
|
||||||
public final class InstantPageMultiTextAdapter: ASDisplayNode, TextNodeProtocol {
|
public final class InstantPageMultiTextAdapter: ASDisplayNode, TextNodeProtocol {
|
||||||
private struct Entry {
|
public struct Entry {
|
||||||
|
public let item: InstantPageTextItem
|
||||||
|
public let frameOrigin: CGPoint
|
||||||
|
|
||||||
|
public init(item: InstantPageTextItem, frameOrigin: CGPoint) {
|
||||||
|
self.item = item
|
||||||
|
self.frameOrigin = frameOrigin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct InternalEntry {
|
||||||
let item: InstantPageTextItem
|
let item: InstantPageTextItem
|
||||||
let charOffset: Int
|
let charOffset: Int
|
||||||
let frameOrigin: CGPoint
|
let frameOrigin: CGPoint
|
||||||
}
|
}
|
||||||
|
|
||||||
private let entries: [Entry]
|
private let entries: [InternalEntry]
|
||||||
private let combinedString: NSAttributedString
|
private let combinedString: NSAttributedString
|
||||||
|
|
||||||
public init(items: [InstantPageTextItem]) {
|
public init(entries: [Entry]) {
|
||||||
let separator = NSAttributedString(string: "\n\n")
|
let separator = NSAttributedString(string: "\n\n")
|
||||||
let combined = NSMutableAttributedString()
|
let combined = NSMutableAttributedString()
|
||||||
var entries: [Entry] = []
|
var internalEntries: [InternalEntry] = []
|
||||||
for (index, item) in items.enumerated() {
|
for (index, entry) in entries.enumerated() {
|
||||||
let charOffset = combined.length
|
let charOffset = combined.length
|
||||||
entries.append(Entry(item: item, charOffset: charOffset, frameOrigin: item.frame.origin))
|
internalEntries.append(InternalEntry(item: entry.item, charOffset: charOffset, frameOrigin: entry.frameOrigin))
|
||||||
combined.append(item.attributedString)
|
combined.append(entry.item.attributedString)
|
||||||
if index != items.count - 1 {
|
if index != entries.count - 1 {
|
||||||
combined.append(separator)
|
combined.append(separator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.entries = entries
|
self.entries = internalEntries
|
||||||
self.combinedString = combined
|
self.combinedString = combined
|
||||||
super.init()
|
super.init()
|
||||||
self.isUserInteractionEnabled = false
|
self.isUserInteractionEnabled = false
|
||||||
|
|
|
||||||
|
|
@ -1223,6 +1223,12 @@ private func collectSelectableTextItems(
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
case let .details(details):
|
case let .details(details):
|
||||||
|
if details.titleTextItem.selectable && !details.titleTextItem.attributedString.string.isEmpty {
|
||||||
|
result.append((details.titleTextItem, CGPoint(
|
||||||
|
x: accumulatedOffset.x + details.frame.minX + details.titleTextItem.frame.minX,
|
||||||
|
y: accumulatedOffset.y + details.frame.minY + details.titleTextItem.frame.minY
|
||||||
|
)))
|
||||||
|
}
|
||||||
if let inner = details.innerLayout {
|
if let inner = details.innerLayout {
|
||||||
let innerOffset = CGPoint(
|
let innerOffset = CGPoint(
|
||||||
x: accumulatedOffset.x + details.frame.minX,
|
x: accumulatedOffset.x + details.frame.minX,
|
||||||
|
|
|
||||||
|
|
@ -746,23 +746,30 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
|
||||||
}
|
}
|
||||||
|
|
||||||
override public func updateIsExtractedToContextPreview(_ value: Bool) {
|
override public func updateIsExtractedToContextPreview(_ value: Bool) {
|
||||||
guard value, self.textSelectionNode == nil, let messageItem = self.item, self.currentPageLayout?.layout != nil, let rootNode = messageItem.controllerInteraction.chatControllerNode() else {
|
guard value, self.textSelectionNode == nil, let messageItem = self.item, self.currentPageLayout?.layout != nil, let pageView = self.pageView, let rootNode = messageItem.controllerInteraction.chatControllerNode() else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// V0 spec: root-level selection only (cross-sub-layout selection deferred).
|
// pageView sits at (-1, 0) inside containerNode; the adapter is placed at
|
||||||
// Root-level items have parentOffset == item.frame.origin (no sub-layout descent).
|
// containerNode.bounds, so shift each item's page-space origin into
|
||||||
let items = (self.pageView?.selectableTextItems() ?? [])
|
// containerNode-local coords for the adapter to operate in.
|
||||||
.filter { entry in
|
let pageOrigin = pageView.frame.origin
|
||||||
entry.parentOffset == entry.item.frame.origin
|
let entries = pageView.selectableTextItems()
|
||||||
|
.filter { $0.item.selectable && !$0.item.attributedString.string.isEmpty }
|
||||||
|
.map { entry in
|
||||||
|
InstantPageMultiTextAdapter.Entry(
|
||||||
|
item: entry.item,
|
||||||
|
frameOrigin: CGPoint(
|
||||||
|
x: entry.parentOffset.x + pageOrigin.x,
|
||||||
|
y: entry.parentOffset.y + pageOrigin.y
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.map { $0.item }
|
guard !entries.isEmpty else {
|
||||||
.filter { $0.selectable && !$0.attributedString.string.isEmpty }
|
|
||||||
guard !items.isEmpty else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let adapter = InstantPageMultiTextAdapter(items: items)
|
let adapter = InstantPageMultiTextAdapter(entries: entries)
|
||||||
adapter.frame = self.containerNode.bounds
|
adapter.frame = self.containerNode.bounds
|
||||||
self.textSelectionAdapter = adapter
|
self.textSelectionAdapter = adapter
|
||||||
self.containerNode.addSubnode(adapter)
|
self.containerNode.addSubnode(adapter)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue