Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 23 additions & 13 deletions Packages/SimbiKit/Sources/SimbiUI/TranscriptView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ struct TranscriptView: View {
/// bottom. Two prior mechanisms failed here: scroll-geometry
/// preferences go stale (macOS doesn't re-fire them during user
/// scrolling → the view yanked), and per-row onAppear/onDisappear
/// crashes outright (the LazyVStack cache reinserts rows during
/// crashes outright (the prior LazyVStack cache reinserts rows during
/// NSHostingView.layout, and AppearanceEffect.didReinsert then requests
/// a constraint update mid-layout-pass — AppKit throws).
@State private var bottomRow: Int?
Expand All @@ -107,20 +107,30 @@ struct TranscriptView: View {
let activeRow = activeRow(in: document.entries)
let speakerSlots = Design.speakerSlots(names: speakers(in: document.entries))
ScrollViewReader { proxy in
ScrollView {
LazyVStack(alignment: .leading, spacing: Design.rowGap) {
ForEach(Array(document.entries.enumerated()), id: \.offset) { index, entry in
entryView(
entry, row: index, isActive: index == activeRow,
speakerSlots: speakerSlots
)
.id(index)
}
// Native List avoids LazyVStack's estimated-height relayouts for long transcripts.
List {
ForEach(document.entries.indices, id: \.self) { index in
entryView(
document.entries[index], row: index, isActive: index == activeRow,
speakerSlots: speakerSlots
)
.id(index)
.listRowInsets(
EdgeInsets(
// List contributes 8 pt; these restore the pane's 16 pt inset.
top: Design.rowGap / 2, leading: 8,
bottom: Design.rowGap / 2, trailing: 8)
)
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
}
.scrollTargetLayout()
.padding(Design.paneInset)
.frame(maxWidth: .infinity, alignment: .leading)
}
.listStyle(.plain)
.scrollContentBackground(.hidden)
.contentMargins(
.vertical, Design.paneInset - Design.rowGap / 2,
for: .scrollContent
)
.scrollPosition(id: $bottomRow, anchor: .bottom)
// Tail-follow: only when the PREVIOUS last row was at the
// viewport bottom (slack of one row for a partially
Expand Down
35 changes: 16 additions & 19 deletions Packages/SimbiKit/Sources/SplitViewKit/Split.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public struct Split<P: View, D: SplitDivider, S: View>: View {
@State private var oldSize: CGSize?
/// The previous position as we drag the `splitter`
@State private var previousPosition: CGFloat?
@Environment(\.displayScale) private var displayScale

public var body: some View {
GeometryReader { geometry in
Expand All @@ -64,18 +65,28 @@ public struct Split<P: View, D: SplitDivider, S: View>: View {
let minPLength = length * ((hidePrimary ? 0 : minPFraction) ?? 0)
let minSLength = length * ((hideSecondary ? 0 : minSFraction) ?? 0)
let spacing = spacing()
let pixelScale = max(displayScale, 1)
let alignToPixel: (CGFloat) -> CGFloat = {
($0 * pixelScale).rounded() / pixelScale
}
// Local edit (see VENDORED.md): also cap the primary side so the
// secondary side's minimum always fits inside the geometry.
// Upstream only clamps the fraction during drags, so a fraction
// that became too large after the window shrank pushed
// `secondary` past the trailing edge, where .clipped() cut it
// off instead of it holding its minimum size.
let pLength = max(minPLength, min(length - minSLength - spacing / 2, pLength(in: size)))
let sLength = max(minSLength, sLength(in: size))
let pWidth = horizontal ? max(minPLength, min(width - spacing, pLength - spacing / 2)) : breadth
let pHeight = horizontal ? breadth : max(minPLength, min(height - spacing, pLength - spacing / 2))
let sWidth = horizontal ? max(minSLength, min(width - pLength, sLength - spacing / 2)) : breadth
let sHeight = horizontal ? breadth : max(minSLength, min(height - pLength, sLength - spacing / 2))
// Platform-backed children blur when a fractional split places their
// backing layers between pixels. Snap the primary edge, then give the
// secondary pane the exact remainder so the split still fills its bounds.
let pWidth =
horizontal
? alignToPixel(max(minPLength, min(width - spacing, pLength - spacing / 2))) : breadth
let pHeight =
horizontal
? breadth : alignToPixel(max(minPLength, min(height - spacing, pLength - spacing / 2)))
let sWidth = horizontal ? max(0, width - pWidth - spacing) : breadth
let sHeight = horizontal ? breadth : max(0, height - pHeight - spacing)
let sOffset =
horizontal ? CGSize(width: pWidth + spacing, height: 0) : CGSize(width: 0, height: pHeight + spacing)
let dCenter =
Expand Down Expand Up @@ -331,20 +342,6 @@ public struct Split<P: View, D: SplitDivider, S: View>: View {
}
}

/// The length of `secondary` in the `layout` direction, without regard to any inset for the Splitter
private func sLength(in size: CGSize) -> CGFloat {
let length = layout.isHorizontal ? size.width : size.height
if let side = hide.side {
return side.isPrimary ? length : 0
} else {
if let sideToHide = sideToHide() {
return sideToHide.isPrimary ? length : 0
} else {
return length - pLength(in: size)
}
}
}

//MARK: Modifiers

/// Return a new Split with the `splitter` set to the `splitter` passed-in.
Expand Down
Loading