Skip to content

Commit 06ce6e5

Browse files
pcuencacyrilzakka
andauthored
macOS UI (huggingface#12)
Native macOS UI Co-authored-by: Cyril Zakka <1841186+cyrilzakka@users.noreply.github.com>
1 parent 2bdfcc5 commit 06ce6e5

31 files changed

+861
-62
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
.DS_Store
2+
xcuserdata/

Diffusion-macOS/ContentView.swift

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
//
2+
// ContentView.swift
3+
// Diffusion-macOS
4+
//
5+
// Created by Cyril Zakka on 1/12/23.
6+
// See LICENSE at https://github.com/huggingface/swift-coreml-diffusers/LICENSE
7+
//
8+
9+
import SwiftUI
10+
import ImageIO
11+
12+
13+
// AppKit version that uses NSImage, NSSavePanel
14+
struct ShareButtons: View {
15+
var image: CGImage
16+
var name: String
17+
18+
var filename: String {
19+
name.replacingOccurrences(of: " ", with: "_")
20+
}
21+
22+
func showSavePanel() -> URL? {
23+
let savePanel = NSSavePanel()
24+
savePanel.allowedContentTypes = [.png]
25+
savePanel.canCreateDirectories = true
26+
savePanel.isExtensionHidden = false
27+
savePanel.title = "Save your image"
28+
savePanel.message = "Choose a folder and a name to store the image."
29+
savePanel.nameFieldLabel = "File name:"
30+
savePanel.nameFieldStringValue = filename
31+
32+
let response = savePanel.runModal()
33+
return response == .OK ? savePanel.url : nil
34+
}
35+
36+
func savePNG(cgImage: CGImage, path: URL) {
37+
let image = NSImage(cgImage: cgImage, size: .zero)
38+
let imageRepresentation = NSBitmapImageRep(data: image.tiffRepresentation!)
39+
guard let pngData = imageRepresentation?.representation(using: .png, properties: [:]) else {
40+
print("Error generating PNG data")
41+
return
42+
}
43+
do {
44+
try pngData.write(to: path)
45+
} catch {
46+
print("Error saving: \(error)")
47+
}
48+
}
49+
50+
var body: some View {
51+
let imageView = Image(image, scale: 1, label: Text(name))
52+
HStack {
53+
ShareLink(item: imageView, preview: SharePreview(name, image: imageView))
54+
Button() {
55+
if let url = showSavePanel() {
56+
savePNG(cgImage: image, path: url)
57+
}
58+
} label: {
59+
Label("Save…", systemImage: "square.and.arrow.down")
60+
}
61+
}
62+
}
63+
}
64+
65+
struct ContentView: View {
66+
@StateObject var generation = GenerationContext()
67+
68+
func toolbar() -> any View {
69+
if case .complete(let prompt, let cgImage, _) = generation.state, let cgImage = cgImage {
70+
return ShareButtons(image: cgImage, name: prompt)
71+
} else {
72+
let prompt = DEFAULT_PROMPT
73+
let cgImage = NSImage(imageLiteralResourceName: "placeholder").cgImage(forProposedRect: nil, context: nil, hints: nil)!
74+
return ShareButtons(image: cgImage, name: prompt)
75+
}
76+
}
77+
78+
var body: some View {
79+
NavigationSplitView {
80+
ControlsView()
81+
.navigationSplitViewColumnWidth(min: 250, ideal: 300)
82+
} detail: {
83+
GeneratedImageView()
84+
.aspectRatio(contentMode: .fit)
85+
.frame(width: 512, height: 512)
86+
.cornerRadius(15)
87+
.toolbar {
88+
AnyView(toolbar())
89+
}
90+
91+
}
92+
.environmentObject(generation)
93+
}
94+
}
95+
96+
struct ContentView_Previews: PreviewProvider {
97+
static var previews: some View {
98+
ContentView()
99+
}
100+
}

Diffusion-macOS/ControlsView.swift

+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
//
2+
// PromptView.swift
3+
// Diffusion-macOS
4+
//
5+
// Created by Cyril Zakka on 1/12/23.
6+
// See LICENSE at https://github.com/huggingface/swift-coreml-diffusers/LICENSE
7+
//
8+
9+
import Combine
10+
import SwiftUI
11+
import CompactSlider
12+
13+
enum PipelineState {
14+
case downloading(Double)
15+
case uncompressing
16+
case loading
17+
case ready
18+
case failed(Error)
19+
}
20+
21+
struct ControlsView: View {
22+
@EnvironmentObject var generation: GenerationContext
23+
24+
static let models = ModelInfo.MODELS
25+
static let modelNames = models.map { $0.modelVersion }
26+
27+
@State private var model = Settings.shared.currentModel.modelVersion
28+
@State private var disclosedPrompt = true
29+
30+
// TODO: refactor download with similar code in Loading.swift (iOS)
31+
@State private var stateSubscriber: Cancellable?
32+
@State private var pipelineState: PipelineState = .downloading(0)
33+
34+
func modelDidChange(model: ModelInfo) {
35+
print("Loading model \(model)")
36+
Settings.shared.currentModel = model
37+
38+
pipelineState = .downloading(0)
39+
Task.init {
40+
let loader = PipelineLoader(model: model)
41+
stateSubscriber = loader.statePublisher.sink { state in
42+
DispatchQueue.main.async {
43+
switch state {
44+
case .downloading(let progress):
45+
pipelineState = .downloading(progress)
46+
case .uncompressing:
47+
pipelineState = .uncompressing
48+
case .readyOnDisk:
49+
pipelineState = .loading
50+
default:
51+
break
52+
}
53+
}
54+
}
55+
do {
56+
generation.pipeline = try await loader.prepare()
57+
pipelineState = .ready
58+
} catch {
59+
print("Could not load model, error: \(error)")
60+
pipelineState = .failed(error)
61+
}
62+
}
63+
}
64+
65+
var body: some View {
66+
VStack(alignment: .leading) {
67+
68+
Label("Adjustments", systemImage: "gearshape.2")
69+
.font(.headline)
70+
.fontWeight(.bold)
71+
Divider()
72+
73+
ScrollView {
74+
Group {
75+
DisclosureGroup {
76+
Picker("", selection: $model) {
77+
ForEach(Self.modelNames, id: \.self) {
78+
Text($0)
79+
}
80+
}
81+
.onChange(of: model) { theModel in
82+
guard let model = ModelInfo.from(modelVersion: theModel) else { return }
83+
modelDidChange(model: model)
84+
}
85+
} label: {
86+
Label("Model", systemImage: "cpu").foregroundColor(.secondary)
87+
}
88+
89+
Divider()
90+
91+
DisclosureGroup(isExpanded: $disclosedPrompt) {
92+
Group {
93+
TextField("Positive prompt", text: $generation.positivePrompt,
94+
axis: .vertical).lineLimit(5)
95+
.textFieldStyle(.squareBorder)
96+
.listRowInsets(EdgeInsets(top: 0, leading: -20, bottom: 0, trailing: 20))
97+
TextField("Negative prompt", text: $generation.negativePrompt,
98+
axis: .vertical).lineLimit(5)
99+
.textFieldStyle(.squareBorder)
100+
}.padding(.leading, 10)
101+
} label: {
102+
Label("Prompts", systemImage: "text.quote").foregroundColor(.secondary)
103+
}
104+
105+
Divider()
106+
107+
DisclosureGroup {
108+
CompactSlider(value: $generation.steps, in: 0...150, step: 5) {
109+
Text("Steps")
110+
Spacer()
111+
Text("\(Int(generation.steps))")
112+
}.padding(.leading, 10)
113+
} label: {
114+
Label("Step count", systemImage: "square.3.layers.3d.down.left").foregroundColor(.secondary)
115+
}
116+
Divider()
117+
118+
// DisclosureGroup() {
119+
// CompactSlider(value: $generation.numImages, in: 0...10, step: 1) {
120+
// Text("Number of Images")
121+
// Spacer()
122+
// Text("\(Int(generation.numImages))")
123+
// }.padding(.leading, 10)
124+
// } label: {
125+
// Label("Number of images", systemImage: "photo.stack").foregroundColor(.secondary)
126+
// }
127+
// Divider()
128+
129+
DisclosureGroup() {
130+
let sliderLabel = generation.seed < 0 ? "Random Seed" : "Seed"
131+
CompactSlider(value: $generation.seed, in: -1...1000, step: 1) {
132+
Text(sliderLabel)
133+
Spacer()
134+
Text("\(Int(generation.seed))")
135+
}.padding(.leading, 10)
136+
} label: {
137+
Label("Seed", systemImage: "leaf").foregroundColor(.secondary)
138+
}
139+
}
140+
}
141+
142+
StatusView(pipelineState: $pipelineState)
143+
}
144+
.padding()
145+
.onAppear {
146+
print(PipelineLoader.models)
147+
modelDidChange(model: ModelInfo.from(modelVersion: model) ?? ModelInfo.v2Base)
148+
}
149+
}
150+
}
151+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.security.app-sandbox</key>
6+
<true/>
7+
<key>com.apple.security.files.user-selected.read-write</key>
8+
<true/>
9+
<key>com.apple.security.network.client</key>
10+
<true/>
11+
</dict>
12+
</plist>
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//
2+
// Diffusion_macOSApp.swift
3+
// Diffusion-macOS
4+
//
5+
// Created by Cyril Zakka on 1/12/23.
6+
// See LICENSE at https://github.com/huggingface/swift-coreml-diffusers/LICENSE
7+
//
8+
9+
import SwiftUI
10+
11+
@main
12+
struct Diffusion_macOSApp: App {
13+
var body: some Scene {
14+
WindowGroup {
15+
ContentView()
16+
}
17+
}
18+
}
19+
20+
let runningOnMac = true
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//
2+
// GeneratedImageView.swift
3+
// Diffusion
4+
//
5+
// Created by Pedro Cuenca on 18/1/23.
6+
// See LICENSE at https://github.com/huggingface/swift-coreml-diffusers/LICENSE
7+
//
8+
9+
import SwiftUI
10+
11+
struct GeneratedImageView: View {
12+
@EnvironmentObject var generation: GenerationContext
13+
14+
var body: some View {
15+
switch generation.state {
16+
case .startup: return AnyView(Image("placeholder").resizable())
17+
case .running(let progress):
18+
guard let progress = progress, progress.stepCount > 0 else {
19+
// The first time it takes a little bit before generation starts
20+
return AnyView(ProgressView())
21+
}
22+
let step = Int(progress.step) + 1
23+
let fraction = Double(step) / Double(progress.stepCount)
24+
let label = "Step \(step) of \(progress.stepCount)"
25+
return AnyView(ProgressView(label, value: fraction, total: 1).padding())
26+
case .complete(_, let image, _):
27+
guard let theImage = image else {
28+
return AnyView(Image(systemName: "exclamationmark.triangle").resizable())
29+
}
30+
31+
return AnyView(Image(theImage, scale: 1, label: Text("generated"))
32+
.resizable()
33+
.clipShape(RoundedRectangle(cornerRadius: 20))
34+
)
35+
}
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"info" : {
3+
"author" : "xcode",
4+
"version" : 1
5+
}
6+
}

0 commit comments

Comments
 (0)