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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 1.0.2

- Discover: the model list takes 40% of the area (at least 380 pt, detail at least 400) instead of whatever width the split view started with, which left it squeezed in large windows. The divider is still draggable and its position is remembered.
- Discover: "Runnable only" and the model count stay on one line in narrow windows.

## 1.0.1

- The window opens at 1280×820 on first launch instead of at its minimum width, which squeezed the Discover screen and the chat.
Expand Down
2 changes: 1 addition & 1 deletion Sources/MLXEngine/Version.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/// The MLX Runner release version. The release workflow refuses a tag that doesn't match it,
/// and scripts/bundle.sh writes it into the app's Info.plist.
public enum MLXRunnerVersion {
public static let current = "1.0.1"
public static let current = "1.0.2"
public static let repository = "BackendStack21/mlx-runner"
public static let homepage = "https://mlx-runner.21no.de"
}
68 changes: 62 additions & 6 deletions Sources/MLXRunner/DiscoverView.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import AppKit
import MLXEngine
import SwiftUI

Expand All @@ -11,17 +12,18 @@ struct DiscoverView: View {
@State private var error: String?
@State private var supportedOnly = true
@State private var searchTask: Task<Void, Never>?
/// Share of the Discover area the model list takes; the user can drag the divider.
@AppStorage("discoverListFraction") private var listFraction = 0.4

var body: some View {
HSplitView {
ResizableSplit(fraction: $listFraction, leadingMin: 380, trailingMin: 400) {
VStack(spacing: 0) {
header
if query.isEmpty { RecommendedStrip(selected: $selected) }
Divider()
list
}
.frame(minWidth: 340, idealWidth: 420)

} trailing: {
Group {
if let id = selected {
ModelDetailView(repo: id).id(id)
Expand All @@ -37,7 +39,7 @@ struct DiscoverView: View {
}
}
}
.frame(minWidth: 400, maxWidth: .infinity, maxHeight: .infinity)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.navigationTitle("Discover")
.task { await search() }
Expand Down Expand Up @@ -66,8 +68,9 @@ struct DiscoverView: View {
.onChange(of: sort) { _, _ in Task { await search() } }
Toggle("Runnable only", isOn: $supportedOnly)
.toggleStyle(.switch).controlSize(.mini)
Spacer()
Text("\(visible.count) models").font(.caption).foregroundStyle(.secondary)
.fixedSize()
Spacer(minLength: 8)
Text("\(visible.count) models").font(.caption).foregroundStyle(.secondary).fixedSize()
}
}
.padding(12)
Expand Down Expand Up @@ -305,3 +308,56 @@ struct ModelDetailView: View {
return s
}
}

/// Two panes with a draggable divider. Unlike HSplitView (NSSplitView), the split is a fraction of
/// the available width, so it doesn't depend on the size the window happened to open at and keeps
/// its proportion as the window resizes. Each pane keeps its minimum width.
struct ResizableSplit<Leading: View, Trailing: View>: View {
@Binding var fraction: Double
let leadingMin: CGFloat
let trailingMin: CGFloat
@ViewBuilder let leading: Leading
@ViewBuilder let trailing: Trailing
@State private var dragStart: CGFloat?

var body: some View {
GeometryReader { geo in
let total = geo.size.width
let width = leadingWidth(total)
HStack(spacing: 0) {
leading.frame(width: width)
Divider()
.overlay {
// A wider, invisible grab area centred on the 1 pt line.
Color.clear
.frame(width: 8)
.contentShape(Rectangle())
.onHover { inside in
if inside { NSCursor.resizeLeftRight.push() } else { NSCursor.pop() }
}
.gesture(DragGesture(minimumDistance: 1, coordinateSpace: .global)
.onChanged { g in
let start = dragStart ?? width
dragStart = start
let proposed = start + g.translation.width
if total > 0 { fraction = Double(proposed / total) }
}
.onEnded { _ in
dragStart = nil
// Store the clamped value, not wherever the pointer went.
if total > 0 { fraction = Double(leadingWidth(total) / total) }
})
}
trailing.frame(maxWidth: .infinity)
}
}
.frame(minWidth: leadingMin + trailingMin + 1)
}

private func leadingWidth(_ total: CGFloat) -> CGFloat {
// The stored fraction comes from user defaults: fall back to 40% if it isn't sane.
let f = fraction.isFinite && fraction > 0 && fraction < 1 ? fraction : 0.4
let upper = max(leadingMin, total - trailingMin - 1)
return min(max(total * f, leadingMin), upper)
}
}
2 changes: 1 addition & 1 deletion Sources/MLXRunner/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ enum WindowLayout {
static let sidebarMax: CGFloat = 340
/// The conversation column alone.
static let chatMin: CGFloat = 560
/// Chat with the inspector open (560 + 260); also fits Discover's two panes (340 + 400 + divider).
/// Chat with the inspector open (560 + 260); also fits Discover's two panes (380 + 400 + divider).
static let detailMin: CGFloat = chatMin + 260
static let windowMinWidth: CGFloat = sidebarMin + detailMin
}
Expand Down
Loading