Skip to content

Commit 84f86c7

Browse files
committed
perf(shelf): keep terminal out of spine layout animation
1 parent dfd40af commit 84f86c7

1 file changed

Lines changed: 65 additions & 5 deletions

File tree

supacode/Features/Shelf/Views/ShelfView.swift

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,27 +37,47 @@ struct ShelfView: View {
3737
books.firstIndex(where: { $0.id == id })
3838
}
3939

40+
ZStack(alignment: .leading) {
41+
spineFlow(books: books, openBookID: openBookID, openIndex: openIndex)
42+
openBookOverlay(books: books, openIndex: openIndex, state: state)
43+
}
44+
.frame(maxWidth: .infinity, maxHeight: .infinity)
45+
.background(Color(nsColor: .windowBackgroundColor).opacity(surfaceBackgroundOpacity))
46+
}
47+
48+
@ViewBuilder
49+
private func spineFlow(books: [ShelfBook], openBookID: ShelfBook.ID?, openIndex: Int?)
50+
-> some View
51+
{
4052
HStack(spacing: 0) {
4153
ForEach(Array(books.enumerated()), id: \.element.id) { index, book in
4254
spine(book: book, index: index, openIndex: openIndex)
4355
if book.id == openBookID {
44-
openBookArea(for: book, state: state)
45-
.transition(.opacity)
56+
terminalPlaceholder()
4657
}
4758
}
4859
if openBookID == nil {
49-
emptyOpenArea()
60+
terminalPlaceholder()
5061
}
5162
}
5263
.frame(maxWidth: .infinity, maxHeight: .infinity)
53-
.background(Color(nsColor: .windowBackgroundColor).opacity(surfaceBackgroundOpacity))
5464
// Animate on every openBookID change — covers both Shelf-originated
5565
// book switches (which also set their own TCA animation) and
5666
// left-nav-originated switches, so the spine flow is consistent
57-
// regardless of entry point.
67+
// regardless of entry point. The real terminal is rendered in a
68+
// non-layout overlay, so this animation only moves spines and the
69+
// lightweight placeholder.
5870
.animation(.easeInOut(duration: 0.2), value: openBookID)
5971
}
6072

73+
@ViewBuilder
74+
private func terminalPlaceholder() -> some View {
75+
Color.clear
76+
.frame(maxWidth: .infinity, maxHeight: .infinity)
77+
.layoutPriority(1)
78+
.accessibilityHidden(true)
79+
}
80+
6181
@ViewBuilder
6282
private func spine(book: ShelfBook, index: Int, openIndex: Int?) -> some View {
6383
let distance = openIndex.map { abs(index - $0) }
@@ -87,6 +107,46 @@ struct ShelfView: View {
87107
)
88108
}
89109

110+
@ViewBuilder
111+
private func openBookOverlay(
112+
books: [ShelfBook], openIndex: Int?, state: RepositoriesFeature.State
113+
) -> some View {
114+
GeometryReader { proxy in
115+
let frame = openBookOverlayFrame(in: proxy.size, bookCount: books.count, openIndex: openIndex)
116+
openBookOverlayContent(books: books, openIndex: openIndex, state: state)
117+
.frame(width: frame.width, height: proxy.size.height)
118+
.clipped()
119+
.offset(x: frame.minX)
120+
}
121+
}
122+
123+
private func openBookOverlayFrame(in size: CGSize, bookCount: Int, openIndex: Int?) -> (
124+
minX: CGFloat, width: CGFloat
125+
) {
126+
let leftSpineCount = openIndex.map { $0 + 1 } ?? bookCount
127+
let rightSpineCount = openIndex.map { bookCount - $0 - 1 } ?? 0
128+
let minX = CGFloat(leftSpineCount) * ShelfMetrics.spineWidth
129+
let occupiedWidth = CGFloat(leftSpineCount + rightSpineCount) * ShelfMetrics.spineWidth
130+
return (minX, max(0, size.width - occupiedWidth))
131+
}
132+
133+
@ViewBuilder
134+
private func openBookOverlayContent(
135+
books: [ShelfBook],
136+
openIndex: Int?,
137+
state: RepositoriesFeature.State
138+
) -> some View {
139+
if let openIndex {
140+
openBookArea(for: books[openIndex], state: state)
141+
.id(books[openIndex].id)
142+
.transition(.opacity.animation(.easeInOut(duration: 0.12)))
143+
} else {
144+
emptyOpenArea()
145+
.id("__empty__")
146+
.transition(.opacity.animation(.easeInOut(duration: 0.12)))
147+
}
148+
}
149+
90150
/// Dispatch the open-book action only when `book` isn't already the open
91151
/// one — idempotent helper for taps that imply a book change.
92152
///

0 commit comments

Comments
 (0)