InstantPage V2 table: flush frame, inset borders

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
isaac 2026-05-30 18:47:03 +02:00
parent e8de87dc7b
commit 79bcc7bc34
3 changed files with 39 additions and 16 deletions

View file

@ -1942,15 +1942,20 @@ final class InstantPageV2TableView: UIView, InstantPageItemView {
self.backgroundColor = .clear
self.scrollView.frame = self.bounds
self.scrollView.contentSize = item.contentSize
// Scrollable tables clip to the full width with no inset on the clip; the inset lives inside
// the scroll content width as a left margin only (see contentSize below).
self.scrollView.clipsToBounds = true
self.scrollView.contentSize = CGSize(width: item.contentSize.width + item.contentInset, height: item.contentSize.height)
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.alwaysBounceVertical = false
self.scrollView.showsHorizontalScrollIndicator = item.contentSize.width > item.frame.width
self.scrollView.showsHorizontalScrollIndicator = item.contentSize.width + item.contentInset > item.frame.width
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.disablesInteractiveTransitionGestureRecognizer = true
self.addSubview(self.scrollView)
self.contentView.frame = CGRect(origin: .zero, size: item.contentSize)
// Grid container shifted right by the inset so the borders start at the body-text inset.
// Its size stays the bare grid (item.contentSize); grid coords inside remain x=0-relative.
self.contentView.frame = CGRect(x: item.contentInset, y: 0.0, width: item.contentSize.width, height: item.contentSize.height)
self.scrollView.addSubview(self.contentView)
// Title sub-layout (above the grid, inside the scroll view's content).
@ -2025,9 +2030,9 @@ final class InstantPageV2TableView: UIView, InstantPageItemView {
self.item = item
self.scrollView.frame = CGRect(origin: .zero, size: item.frame.size)
self.scrollView.contentSize = item.contentSize
self.scrollView.showsHorizontalScrollIndicator = item.contentSize.width > item.frame.width
self.contentView.frame = CGRect(origin: .zero, size: item.contentSize)
self.scrollView.contentSize = CGSize(width: item.contentSize.width + item.contentInset, height: item.contentSize.height)
self.scrollView.showsHorizontalScrollIndicator = item.contentSize.width + item.contentInset > item.frame.width
self.contentView.frame = CGRect(x: item.contentInset, y: 0.0, width: item.contentSize.width, height: item.contentSize.height)
// Forward updates to nested V2 sub-layouts (title + each cell). Recursive update
// propagation. Cell-count or title-presence changes fall back to rebuild via the
@ -2145,7 +2150,7 @@ private func findTextItem(
}
case let .table(table):
for cell in table.cells {
let cellAbs = cell.frame.offsetBy(dx: f.minX, dy: f.minY)
let cellAbs = cell.frame.offsetBy(dx: f.minX + table.contentInset, dy: f.minY)
if !cellAbs.contains(point) { continue }
if let sub = cell.subLayout {
if let hit = findTextItem(in: sub, point: point,
@ -2155,7 +2160,7 @@ private func findTextItem(
}
}
if let titleLayout = table.titleSubLayout, let titleFrame = table.titleFrame {
let titleAbs = titleFrame.offsetBy(dx: f.minX, dy: f.minY)
let titleAbs = titleFrame.offsetBy(dx: f.minX + table.contentInset, dy: f.minY)
if titleAbs.contains(point) {
if let hit = findTextItem(in: titleLayout, point: point,
accumulatedOffset: CGPoint(x: titleAbs.minX, y: titleAbs.minY)) {
@ -2208,7 +2213,7 @@ private func collectSelectableTextItems(
case let .table(table):
if let titleLayout = table.titleSubLayout, let titleFrame = table.titleFrame {
let titleOffset = CGPoint(
x: accumulatedOffset.x + table.frame.minX + titleFrame.minX,
x: accumulatedOffset.x + table.frame.minX + table.contentInset + titleFrame.minX,
y: accumulatedOffset.y + table.frame.minY + titleFrame.minY
)
collectSelectableTextItems(in: titleLayout, accumulatedOffset: titleOffset, into: &result)
@ -2216,7 +2221,7 @@ private func collectSelectableTextItems(
for cell in table.cells {
if let sub = cell.subLayout {
let cellOffset = CGPoint(
x: accumulatedOffset.x + table.frame.minX + cell.frame.minX,
x: accumulatedOffset.x + table.frame.minX + table.contentInset + cell.frame.minX,
y: accumulatedOffset.y + table.frame.minY + cell.frame.minY
)
collectSelectableTextItems(in: sub, accumulatedOffset: cellOffset, into: &result)

View file

@ -320,6 +320,7 @@ public struct InstantPageV2TableItem {
public let titleSubLayout: InstantPageV2Layout?
public let titleFrame: CGRect?
public let contentSize: CGSize // grid intrinsic size; may exceed frame.width scroll
public let contentInset: CGFloat // left inset (= page horizontalInset) baked into the scroll content width by the renderer
public let cells: [InstantPageV2TableCell]
public let horizontalLines: [CGRect]
public let verticalLines: [CGRect]
@ -1067,8 +1068,12 @@ private func layoutTable(
setupStyleStack(styleStack, theme: context.theme, category: .paragraph, link: false)
let borderWidth = bordered ? v2TableBorderWidth : 0.0
// Size columns against the inset content width (mirrors V1's `boundingWidth - horizontalInset*2`),
// so a fitting table aligns with body text on both sides. The item frame stays full-width (flush)
// and the renderer bakes the inset back in as a left margin on the scroll content.
let contentBoundingWidth = boundingWidth - horizontalInset * 2.0
let totalCellPadding = v2TableCellInsets.left + v2TableCellInsets.right
let cellWidthLimit = boundingWidth - totalCellPadding
let cellWidthLimit = contentBoundingWidth - totalCellPadding
var tableRows: [V2TableRow] = []
var columnCount: Int = 0
@ -1153,7 +1158,7 @@ private func layoutTable(
}
// Aggregate column min/max across all rows.
let maxContentWidth = boundingWidth - borderWidth
let maxContentWidth = contentBoundingWidth - borderWidth
var availableWidth = maxContentWidth
var minColumnWidths: [Int: CGFloat] = [:]
var maxColumnWidths: [Int: CGFloat] = [:]
@ -1224,7 +1229,7 @@ private func layoutTable(
distributedWidth -= growth
finalColumnWidths[i] = width
}
totalWidth = boundingWidth
totalWidth = contentBoundingWidth
} else {
totalWidth += borderWidth
}
@ -1530,10 +1535,11 @@ private func layoutTable(
titleFrame = CGRect(x: 0.0, y: 0.0, width: totalWidth, height: titleHeight)
}
// The table item frame spans the full boundingWidth slot in the bubble;
// contentSize.width is the intrinsic grid width (may exceed frame.width horizontal scroll).
// The table item frame spans the full visible bubble interior (`boundingWidth`); the scroll
// viewport equals what is actually visible. contentSize.width is the intrinsic grid width
// (may exceed frame.width horizontal scroll); the renderer adds the left inset on top.
let tableFrame = CGRect(x: 0.0, y: 0.0,
width: boundingWidth + horizontalInset * 2.0,
width: boundingWidth,
height: totalHeight + (titleFrame?.height ?? 0.0))
let contentSize = CGSize(
width: totalWidth,
@ -1545,6 +1551,7 @@ private func layoutTable(
titleSubLayout: titleSubLayout,
titleFrame: titleFrame,
contentSize: contentSize,
contentInset: horizontalInset,
cells: finalizedCells,
horizontalLines: horizontalLines,
verticalLines: verticalLines,