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()
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,28 +5,38 @@ import Display
|
|||
import TelegramCore
|
||||
|
||||
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 charOffset: Int
|
||||
let frameOrigin: CGPoint
|
||||
}
|
||||
|
||||
private let entries: [Entry]
|
||||
private let entries: [InternalEntry]
|
||||
private let combinedString: NSAttributedString
|
||||
|
||||
public init(items: [InstantPageTextItem]) {
|
||||
public init(entries: [Entry]) {
|
||||
let separator = NSAttributedString(string: "\n\n")
|
||||
let combined = NSMutableAttributedString()
|
||||
var entries: [Entry] = []
|
||||
for (index, item) in items.enumerated() {
|
||||
var internalEntries: [InternalEntry] = []
|
||||
for (index, entry) in entries.enumerated() {
|
||||
let charOffset = combined.length
|
||||
entries.append(Entry(item: item, charOffset: charOffset, frameOrigin: item.frame.origin))
|
||||
combined.append(item.attributedString)
|
||||
if index != items.count - 1 {
|
||||
internalEntries.append(InternalEntry(item: entry.item, charOffset: charOffset, frameOrigin: entry.frameOrigin))
|
||||
combined.append(entry.item.attributedString)
|
||||
if index != entries.count - 1 {
|
||||
combined.append(separator)
|
||||
}
|
||||
}
|
||||
self.entries = entries
|
||||
self.entries = internalEntries
|
||||
self.combinedString = combined
|
||||
super.init()
|
||||
self.isUserInteractionEnabled = false
|
||||
|
|
|
|||
|
|
@ -1223,6 +1223,12 @@ private func collectSelectableTextItems(
|
|||
)))
|
||||
}
|
||||
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 {
|
||||
let innerOffset = CGPoint(
|
||||
x: accumulatedOffset.x + details.frame.minX,
|
||||
|
|
|
|||
|
|
@ -746,23 +746,30 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// V0 spec: root-level selection only (cross-sub-layout selection deferred).
|
||||
// Root-level items have parentOffset == item.frame.origin (no sub-layout descent).
|
||||
let items = (self.pageView?.selectableTextItems() ?? [])
|
||||
.filter { entry in
|
||||
entry.parentOffset == entry.item.frame.origin
|
||||
// pageView sits at (-1, 0) inside containerNode; the adapter is placed at
|
||||
// containerNode.bounds, so shift each item's page-space origin into
|
||||
// containerNode-local coords for the adapter to operate in.
|
||||
let pageOrigin = pageView.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 }
|
||||
.filter { $0.selectable && !$0.attributedString.string.isEmpty }
|
||||
guard !items.isEmpty else {
|
||||
guard !entries.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
let adapter = InstantPageMultiTextAdapter(items: items)
|
||||
let adapter = InstantPageMultiTextAdapter(entries: entries)
|
||||
adapter.frame = self.containerNode.bounds
|
||||
self.textSelectionAdapter = adapter
|
||||
self.containerNode.addSubnode(adapter)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue