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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user