Make the panes fill the window: replace VSplitView with an explicit split

The tree kept rendering as a ~628pt column centred in a wide window. That number
was exactly the sum of the row's column minimums, which gave it away: VSplitView
proposes its children their *ideal* width, so the List settled at the narrowest
size its content allowed instead of filling.

SplitPane hands each pane geo.size.width explicitly, removing the ambiguity, and
its divider is draggable with the position remembered across launches. With the
width no longer ambiguous the two name columns share it evenly, so each side gets
half the window; their floor dropped to 120pt since a large minimum is what
becomes the ideal width in the first place.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfhDYRLTpGJSKGP1m3LVJN
This commit is contained in:
alexandrev-tibco
2026-07-27 17:44:40 +02:00
parent a4efef9664
commit e0bff88225
3 changed files with 74 additions and 6 deletions
+8 -3
View File
@@ -4,6 +4,8 @@ import KotejEngine
struct ContentView: View {
@Bindable var model: ComparisonModel
/// How much of the height the tree takes; dragging the divider keeps it.
@AppStorage("kotej.splitFraction") private var splitFraction: Double = 0.45
var body: some View {
VStack(spacing: 0) {
@@ -23,11 +25,10 @@ struct ContentView: View {
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if model.root != nil {
VSplitView {
SplitPane(fraction: splitBinding) {
DiffTreeView(model: model)
.frame(minHeight: 200, idealHeight: 320)
} bottom: {
FileDiffView(model: model)
.frame(minHeight: 180)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
Divider()
@@ -39,6 +40,10 @@ struct ContentView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
private var splitBinding: Binding<CGFloat> {
Binding(get: { CGFloat(splitFraction) }, set: { splitFraction = Double($0) })
}
private func banner(_ text: String) -> some View {
Label(text, systemImage: "exclamationmark.triangle.fill")
.font(.callout)
+4 -3
View File
@@ -37,9 +37,10 @@ struct DisplayNode: Identifiable {
struct DiffTreeView: View {
@Bindable var model: ComparisonModel
/// Columns are sized explicitly: flexible columns with no floor let SwiftUI
/// crush the whole list to its minimum width, which truncates every name.
private let nameMinWidth: CGFloat = 220
/// The two name columns split whatever is left evenly, so each side gets half
/// the window. They carry only a small floor a large one would become the
/// list's ideal width and stop it from filling.
private let nameMinWidth: CGFloat = 120
private let sizeWidth: CGFloat = 84
private let statusWidth: CGFloat = 104
+62
View File
@@ -0,0 +1,62 @@
import SwiftUI
/// A vertical split with an explicit width for both panes.
///
/// `VSplitView` proposes its children their *ideal* width, so a `List` inside it
/// settles at the sum of its columns' minimums and sits centred with empty space
/// on both sides. Handing each pane `geo.size.width` removes the ambiguity, and
/// the divider stays draggable.
struct SplitPane<Top: View, Bottom: View>: View {
@Binding var fraction: CGFloat
let minimumFraction: CGFloat
@ViewBuilder var top: () -> Top
@ViewBuilder var bottom: () -> Bottom
private let dividerHeight: CGFloat = 6
init(fraction: Binding<CGFloat>,
minimumFraction: CGFloat = 0.15,
@ViewBuilder top: @escaping () -> Top,
@ViewBuilder bottom: @escaping () -> Bottom) {
self._fraction = fraction
self.minimumFraction = minimumFraction
self.top = top
self.bottom = bottom
}
var body: some View {
GeometryReader { geo in
let available = max(geo.size.height - dividerHeight, 1)
let topHeight = (available * fraction).rounded()
VStack(spacing: 0) {
top()
.frame(width: geo.size.width, height: topHeight)
divider(totalHeight: available)
bottom()
.frame(width: geo.size.width, height: available - topHeight)
}
}
}
private func divider(totalHeight: CGFloat) -> some View {
ZStack {
Rectangle().fill(Color(nsColor: .separatorColor))
.frame(height: 1)
Rectangle().fill(Color.clear)
.frame(height: dividerHeight)
.contentShape(Rectangle())
}
.frame(height: dividerHeight)
.onHover { inside in
if inside { NSCursor.resizeUpDown.push() } else { NSCursor.pop() }
}
.gesture(
DragGesture()
.onChanged { value in
let delta = value.translation.height / totalHeight
fraction = min(max(fraction + delta * 0.35, minimumFraction), 1 - minimumFraction)
}
)
}
}