From ba361de2a4045e23019e52638083a28ab4ebbdf9 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Sat, 25 Oct 2025 18:51:08 +0200 Subject: [PATCH 1/9] Fixed ProgressViewResizing on AppKitBackend Gtk & WinUI testing pending --- .../Sources/ControlsExample/ControlsApp.swift | 5 ++++ Sources/SwiftCrossUI/Views/ProgressView.swift | 23 +++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/Examples/Sources/ControlsExample/ControlsApp.swift b/Examples/Sources/ControlsExample/ControlsApp.swift index fe94e13ee8..54e84609aa 100644 --- a/Examples/Sources/ControlsExample/ControlsApp.swift +++ b/Examples/Sources/ControlsExample/ControlsApp.swift @@ -16,6 +16,7 @@ struct ControlsApp: App { @State var text = "" @State var flavor: String? = nil @State var enabled = true + @State var progressViewSize: Int = 10 var body: some Scene { WindowGroup("ControlsApp") { @@ -69,6 +70,10 @@ struct ControlsApp: App { Text("Value: \(text)") } + Slider($progressViewSize, minimum: 0, maximum: 100) + ProgressView() + .frame(width: progressViewSize) + VStack { Text("Drop down") HStack { diff --git a/Sources/SwiftCrossUI/Views/ProgressView.swift b/Sources/SwiftCrossUI/Views/ProgressView.swift index 1653410ba6..6f33b3749c 100644 --- a/Sources/SwiftCrossUI/Views/ProgressView.swift +++ b/Sources/SwiftCrossUI/Views/ProgressView.swift @@ -114,8 +114,27 @@ struct ProgressSpinnerView: ElementaryView { backend: Backend, dryRun: Bool ) -> ViewUpdateResult { - ViewUpdateResult.leafView( - size: ViewSize(fixedSize: backend.naturalSize(of: widget)) + let naturalSize = backend.naturalSize(of: widget) + let min = min(proposedSize.x, proposedSize.y) + let size = SIMD2( + min, + min + ) + if !dryRun { + // Doesn't change the rendered size of ProgressSpinner + // on UIKitBackend, but still sets container size to + // (width: n, height: n) n = min(proposedSize.x, proposedSize.y) + backend.setSize(of: widget, to: size) + } + return ViewUpdateResult.leafView( + size: ViewSize( + size: size, + idealSize: naturalSize, + minimumWidth: 0, + minimumHeight: 0, + maximumWidth: nil, + maximumHeight: nil + ) ) } } From 660f67887f332a228132be5d7bdac4e03d84d4ef Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Sat, 25 Oct 2025 18:58:00 +0200 Subject: [PATCH 2/9] added min height&width to prevent pushing it too small --- Sources/SwiftCrossUI/Views/ProgressView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftCrossUI/Views/ProgressView.swift b/Sources/SwiftCrossUI/Views/ProgressView.swift index 6f33b3749c..38d81553b1 100644 --- a/Sources/SwiftCrossUI/Views/ProgressView.swift +++ b/Sources/SwiftCrossUI/Views/ProgressView.swift @@ -130,8 +130,8 @@ struct ProgressSpinnerView: ElementaryView { size: ViewSize( size: size, idealSize: naturalSize, - minimumWidth: 0, - minimumHeight: 0, + minimumWidth: 10, + minimumHeight: 10, maximumWidth: nil, maximumHeight: nil ) From 6879d40ea436106e52adad4c65fab0f5a40d5bbe Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Sat, 25 Oct 2025 19:02:40 +0200 Subject: [PATCH 3/9] now the minWidth actually takes effect (max(min(x, y), 10) --- Examples/Sources/ControlsExample/ControlsApp.swift | 2 +- Sources/SwiftCrossUI/Views/ProgressView.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/Sources/ControlsExample/ControlsApp.swift b/Examples/Sources/ControlsExample/ControlsApp.swift index 54e84609aa..360b4ec87c 100644 --- a/Examples/Sources/ControlsExample/ControlsApp.swift +++ b/Examples/Sources/ControlsExample/ControlsApp.swift @@ -72,7 +72,7 @@ struct ControlsApp: App { Slider($progressViewSize, minimum: 0, maximum: 100) ProgressView() - .frame(width: progressViewSize) + .frame(width: progressViewSize, height: progressViewSize) VStack { Text("Drop down") diff --git a/Sources/SwiftCrossUI/Views/ProgressView.swift b/Sources/SwiftCrossUI/Views/ProgressView.swift index 38d81553b1..8876fc651c 100644 --- a/Sources/SwiftCrossUI/Views/ProgressView.swift +++ b/Sources/SwiftCrossUI/Views/ProgressView.swift @@ -115,7 +115,7 @@ struct ProgressSpinnerView: ElementaryView { dryRun: Bool ) -> ViewUpdateResult { let naturalSize = backend.naturalSize(of: widget) - let min = min(proposedSize.x, proposedSize.y) + let min = max(min(proposedSize.x, proposedSize.y), 10) let size = SIMD2( min, min From c3bc98196de27a13d99f9c15f57794c668d935cc Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Tue, 28 Oct 2025 05:40:14 +0100 Subject: [PATCH 4/9] Made ProgressView resizability optional through .resizable() Not resizable by default, can be changed at runtime. Does not affect ProgressBarView --- .../Sources/ControlsExample/ControlsApp.swift | 6 +++- Sources/SwiftCrossUI/Views/ProgressView.swift | 33 +++++++++++++++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/Examples/Sources/ControlsExample/ControlsApp.swift b/Examples/Sources/ControlsExample/ControlsApp.swift index 360b4ec87c..61824427c4 100644 --- a/Examples/Sources/ControlsExample/ControlsApp.swift +++ b/Examples/Sources/ControlsExample/ControlsApp.swift @@ -17,6 +17,7 @@ struct ControlsApp: App { @State var flavor: String? = nil @State var enabled = true @State var progressViewSize: Int = 10 + @State var isProgressViewResizable = true var body: some Scene { WindowGroup("ControlsApp") { @@ -70,8 +71,10 @@ struct ControlsApp: App { Text("Value: \(text)") } - Slider($progressViewSize, minimum: 0, maximum: 100) + Toggle("Enable ProgressView resizability", active: $isProgressViewResizable) + Slider($progressViewSize, minimum: 10, maximum: 100) ProgressView() + .resizable(isProgressViewResizable) .frame(width: progressViewSize, height: progressViewSize) VStack { @@ -87,6 +90,7 @@ struct ControlsApp: App { Toggle(enabled ? "Disable all" : "Enable all", active: $enabled) .padding() } + }.defaultSize(width: 400, height: 600) } } diff --git a/Sources/SwiftCrossUI/Views/ProgressView.swift b/Sources/SwiftCrossUI/Views/ProgressView.swift index 8876fc651c..9a4a4001a3 100644 --- a/Sources/SwiftCrossUI/Views/ProgressView.swift +++ b/Sources/SwiftCrossUI/Views/ProgressView.swift @@ -4,6 +4,7 @@ public struct ProgressView: View { private var label: Label private var progress: Double? private var kind: Kind + private var isSpinnerResizable: Bool = false private enum Kind { case spinner @@ -23,7 +24,7 @@ public struct ProgressView: View { private var progressIndicator: some View { switch kind { case .spinner: - ProgressSpinnerView() + ProgressSpinnerView(isResizable: isSpinnerResizable) case .bar: ProgressBarView(value: progress) } @@ -50,6 +51,25 @@ public struct ProgressView: View { self.kind = .bar self.progress = value.map(Double.init) } + + /// Used to make a copy with applied changes. + private init( + label: Label, + _ progress: Double?, + kind: Kind, + isSpinnerResizable: Bool + ) { + self.label = label + self.progress = progress + self.kind = kind + self.isSpinnerResizable = isSpinnerResizable + } + + /// Makes the ProgressView resizable. + /// Only affects `Kind.spinner`. + public func resizable(_ isResizable: Bool = true) -> Self { + Self(label: label, progress, kind: kind, isSpinnerResizable: isResizable) + } } extension ProgressView where Label == EmptyView { @@ -101,7 +121,10 @@ extension ProgressView where Label == Text { } struct ProgressSpinnerView: ElementaryView { - init() {} + let isResizable: Bool + init(isResizable: Bool = false) { + self.isResizable = isResizable + } func asWidget(backend: Backend) -> Backend.Widget { backend.createProgressSpinner() @@ -115,6 +138,12 @@ struct ProgressSpinnerView: ElementaryView { dryRun: Bool ) -> ViewUpdateResult { let naturalSize = backend.naturalSize(of: widget) + guard isResizable else { + // Required to reset its size when resizability + // gets changed at runtime + backend.setSize(of: widget, to: naturalSize) + return ViewUpdateResult.leafView(size: ViewSize(fixedSize: naturalSize)) + } let min = max(min(proposedSize.x, proposedSize.y), 10) let size = SIMD2( min, From 4645c19f207fd1aeb71b4552c4ef40354c7e83bb Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Tue, 28 Oct 2025 05:48:16 +0100 Subject: [PATCH 5/9] changed .resizable() to modify a copy instead of Initializing with all values --- Sources/SwiftCrossUI/Views/ProgressView.swift | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/Sources/SwiftCrossUI/Views/ProgressView.swift b/Sources/SwiftCrossUI/Views/ProgressView.swift index 9a4a4001a3..60ce3b3cda 100644 --- a/Sources/SwiftCrossUI/Views/ProgressView.swift +++ b/Sources/SwiftCrossUI/Views/ProgressView.swift @@ -52,23 +52,12 @@ public struct ProgressView: View { self.progress = value.map(Double.init) } - /// Used to make a copy with applied changes. - private init( - label: Label, - _ progress: Double?, - kind: Kind, - isSpinnerResizable: Bool - ) { - self.label = label - self.progress = progress - self.kind = kind - self.isSpinnerResizable = isSpinnerResizable - } - - /// Makes the ProgressView resizable. + /// Makes the ProgressView resize to fit the available space. /// Only affects `Kind.spinner`. public func resizable(_ isResizable: Bool = true) -> Self { - Self(label: label, progress, kind: kind, isSpinnerResizable: isResizable) + var progressView = self + progressView.isSpinnerResizable = isResizable + return progressView } } From b0cc9771961d72445a02a2dfab0cb59db4e78222 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Mon, 3 Nov 2025 15:01:01 +0100 Subject: [PATCH 6/9] formatting and naming --- Examples/Sources/ControlsExample/ControlsApp.swift | 1 - Sources/SwiftCrossUI/Views/ProgressView.swift | 13 +++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Examples/Sources/ControlsExample/ControlsApp.swift b/Examples/Sources/ControlsExample/ControlsApp.swift index 61824427c4..225e045306 100644 --- a/Examples/Sources/ControlsExample/ControlsApp.swift +++ b/Examples/Sources/ControlsExample/ControlsApp.swift @@ -90,7 +90,6 @@ struct ControlsApp: App { Toggle(enabled ? "Disable all" : "Enable all", active: $enabled) .padding() } - }.defaultSize(width: 400, height: 600) } } diff --git a/Sources/SwiftCrossUI/Views/ProgressView.swift b/Sources/SwiftCrossUI/Views/ProgressView.swift index 60ce3b3cda..3dec76baac 100644 --- a/Sources/SwiftCrossUI/Views/ProgressView.swift +++ b/Sources/SwiftCrossUI/Views/ProgressView.swift @@ -53,7 +53,7 @@ public struct ProgressView: View { } /// Makes the ProgressView resize to fit the available space. - /// Only affects `Kind.spinner`. + /// Only affects ``Kind/spinner``. public func resizable(_ isResizable: Bool = true) -> Self { var progressView = self progressView.isSpinnerResizable = isResizable @@ -111,6 +111,7 @@ extension ProgressView where Label == Text { struct ProgressSpinnerView: ElementaryView { let isResizable: Bool + init(isResizable: Bool = false) { self.isResizable = isResizable } @@ -133,10 +134,10 @@ struct ProgressSpinnerView: ElementaryView { backend.setSize(of: widget, to: naturalSize) return ViewUpdateResult.leafView(size: ViewSize(fixedSize: naturalSize)) } - let min = max(min(proposedSize.x, proposedSize.y), 10) + let minimumDimension = max(min(proposedSize.x, proposedSize.y), 10) let size = SIMD2( - min, - min + minimumDimension, + minimumDimension ) if !dryRun { // Doesn't change the rendered size of ProgressSpinner @@ -148,8 +149,8 @@ struct ProgressSpinnerView: ElementaryView { size: ViewSize( size: size, idealSize: naturalSize, - minimumWidth: 10, - minimumHeight: 10, + minimumWidth: 0, + minimumHeight: 0, maximumWidth: nil, maximumHeight: nil ) From 89271875b3b44f215496b9ed28be4242bce7834b Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Mon, 3 Nov 2025 19:21:45 +0100 Subject: [PATCH 7/9] changing AppKits ProgressSpinner size through replacing it with a spinner sized pre view graph insertion Doesn't seem to be a performance issue, but its not a nice way. Sadly nothing else I tried works. --- .../Sources/ControlsExample/ControlsApp.swift | 114 +++++++++--------- Sources/AppKitBackend/AppKitBackend.swift | 31 ++++- Sources/SwiftCrossUI/Backend/AppBackend.swift | 16 +++ Sources/SwiftCrossUI/Views/ProgressView.swift | 6 +- 4 files changed, 109 insertions(+), 58 deletions(-) diff --git a/Examples/Sources/ControlsExample/ControlsApp.swift b/Examples/Sources/ControlsExample/ControlsApp.swift index 225e045306..f3f64eb460 100644 --- a/Examples/Sources/ControlsExample/ControlsApp.swift +++ b/Examples/Sources/ControlsExample/ControlsApp.swift @@ -22,73 +22,79 @@ struct ControlsApp: App { var body: some Scene { WindowGroup("ControlsApp") { #hotReloadable { - VStack(spacing: 30) { - VStack { - Text("Button") - Button("Click me!") { - count += 1 - } - Text("Count: \(count)") - } - .padding(.bottom, 20) - - #if !canImport(UIKitBackend) + ScrollView { + VStack(spacing: 30) { VStack { - Text("Toggle button") - Toggle("Toggle me!", active: $exampleButtonState) - .toggleStyle(.button) - Text("Currently enabled: \(exampleButtonState)") + Text("Button") + Button("Click me!") { + count += 1 + } + Text("Count: \(count)") } .padding(.bottom, 20) - #endif - VStack { - Text("Toggle switch") - Toggle("Toggle me:", active: $exampleSwitchState) - .toggleStyle(.switch) - Text("Currently enabled: \(exampleSwitchState)") - } + #if !canImport(UIKitBackend) + VStack { + Text("Toggle button") + Toggle("Toggle me!", active: $exampleButtonState) + .toggleStyle(.button) + Text("Currently enabled: \(exampleButtonState)") + } + .padding(.bottom, 20) + #endif - #if !canImport(UIKitBackend) VStack { - Text("Checkbox") - Toggle("Toggle me:", active: $exampleCheckboxState) - .toggleStyle(.checkbox) - Text("Currently enabled: \(exampleCheckboxState)") + Text("Toggle switch") + Toggle("Toggle me:", active: $exampleSwitchState) + .toggleStyle(.switch) + Text("Currently enabled: \(exampleSwitchState)") } - #endif - VStack { - Text("Slider") - Slider($sliderValue, minimum: 0, maximum: 10) - .frame(maxWidth: 200) - Text("Value: \(String(format: "%.02f", sliderValue))") - } + #if !canImport(UIKitBackend) + VStack { + Text("Checkbox") + Toggle("Toggle me:", active: $exampleCheckboxState) + .toggleStyle(.checkbox) + Text("Currently enabled: \(exampleCheckboxState)") + } + #endif - VStack { - Text("Text field") - TextField("Text field", text: $text) - Text("Value: \(text)") - } + VStack { + Text("Slider") + Slider($sliderValue, minimum: 0, maximum: 10) + .frame(maxWidth: 200) + Text("Value: \(String(format: "%.02f", sliderValue))") + } - Toggle("Enable ProgressView resizability", active: $isProgressViewResizable) - Slider($progressViewSize, minimum: 10, maximum: 100) - ProgressView() - .resizable(isProgressViewResizable) - .frame(width: progressViewSize, height: progressViewSize) + VStack { + Text("Text field") + TextField("Text field", text: $text) + Text("Value: \(text)") + } + + Toggle("Enable ProgressView resizability", active: $isProgressViewResizable) + Slider($progressViewSize, minimum: 10, maximum: 100) + Button("Randomize progress view size") { + progressViewSize = Int.random(in: 10...100) + } + ProgressView() + .resizable(isProgressViewResizable) + .frame(width: progressViewSize, height: progressViewSize) - VStack { - Text("Drop down") - HStack { - Text("Flavor: ") - Picker(of: ["Vanilla", "Chocolate", "Strawberry"], selection: $flavor) + VStack { + Text("Drop down") + HStack { + Text("Flavor: ") + Picker( + of: ["Vanilla", "Chocolate", "Strawberry"], selection: $flavor) + } + Text("You chose: \(flavor ?? "Nothing yet!")") } - Text("You chose: \(flavor ?? "Nothing yet!")") - } - }.padding().disabled(!enabled) + }.padding().disabled(!enabled) - Toggle(enabled ? "Disable all" : "Enable all", active: $enabled) - .padding() + Toggle(enabled ? "Disable all" : "Enable all", active: $enabled) + .padding() + } } }.defaultSize(width: 400, height: 600) } diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index f0f3fa4817..f2ebde605e 100644 --- a/Sources/AppKitBackend/AppKitBackend.swift +++ b/Sources/AppKitBackend/AppKitBackend.swift @@ -484,6 +484,15 @@ public final class AppKitBackend: AppBackend { } public func naturalSize(of widget: Widget) -> SIMD2 { + if let spinner = widget.subviews.first as? NSProgressIndicator, + spinner.style == .spinning + { + let size = spinner.intrinsicContentSize + return SIMD2( + Int(size.width), + Int(size.height) + ) + } let size = widget.intrinsicContentSize return SIMD2( Int(size.width), @@ -1181,11 +1190,31 @@ public final class AppKitBackend: AppBackend { } public func createProgressSpinner() -> Widget { + let container = NSView() + let spinner = NSProgressIndicator() + spinner.translatesAutoresizingMaskIntoConstraints = false + spinner.isIndeterminate = true + spinner.style = .spinning + spinner.startAnimation(nil) + container.addSubview(spinner) + return container + } + + public func setProgressSpinnerSize( + _ widget: Widget, + _ size: SIMD2 + ) { + setSize(of: widget, to: size) let spinner = NSProgressIndicator() + spinner.translatesAutoresizingMaskIntoConstraints = false spinner.isIndeterminate = true spinner.style = .spinning spinner.startAnimation(nil) - return spinner + spinner.widthAnchor.constraint(equalToConstant: CGFloat(size.x)).isActive = true + spinner.heightAnchor.constraint(equalToConstant: CGFloat(size.y)).isActive = true + + widget.subviews = [] + widget.addSubview(spinner) } public func createProgressBar() -> Widget { diff --git a/Sources/SwiftCrossUI/Backend/AppBackend.swift b/Sources/SwiftCrossUI/Backend/AppBackend.swift index 31a412cf2f..296e857837 100644 --- a/Sources/SwiftCrossUI/Backend/AppBackend.swift +++ b/Sources/SwiftCrossUI/Backend/AppBackend.swift @@ -541,6 +541,15 @@ public protocol AppBackend: Sendable { /// Creates an indeterminate progress spinner. func createProgressSpinner() -> Widget + /// Changes the Spinner's Size. + /// Required due to AppKitBackend needing special treatment. + /// Forward to ``AppBackend/setSize(of widget: Widget, to size: SIMD2)`` + /// on other Backends. + func setProgressSpinnerSize( + _ widget: Widget, + _ size: SIMD2 + ) + /// Creates a progress bar. func createProgressBar() -> Widget /// Updates a progress bar to reflect the given progress (between 0 and 1), and the @@ -1028,6 +1037,13 @@ extension AppBackend { todo() } + public func setProgressSpinnerSize( + _ widget: Widget, + _ size: SIMD2 + ) { + setSize(of: widget, to: size) + } + public func createProgressBar() -> Widget { todo() } diff --git a/Sources/SwiftCrossUI/Views/ProgressView.swift b/Sources/SwiftCrossUI/Views/ProgressView.swift index 3dec76baac..cc27097ecc 100644 --- a/Sources/SwiftCrossUI/Views/ProgressView.swift +++ b/Sources/SwiftCrossUI/Views/ProgressView.swift @@ -131,10 +131,10 @@ struct ProgressSpinnerView: ElementaryView { guard isResizable else { // Required to reset its size when resizability // gets changed at runtime - backend.setSize(of: widget, to: naturalSize) + backend.setProgressSpinnerSize(widget, naturalSize) return ViewUpdateResult.leafView(size: ViewSize(fixedSize: naturalSize)) } - let minimumDimension = max(min(proposedSize.x, proposedSize.y), 10) + let minimumDimension = max(min(proposedSize.x, proposedSize.y), 0) let size = SIMD2( minimumDimension, minimumDimension @@ -143,7 +143,7 @@ struct ProgressSpinnerView: ElementaryView { // Doesn't change the rendered size of ProgressSpinner // on UIKitBackend, but still sets container size to // (width: n, height: n) n = min(proposedSize.x, proposedSize.y) - backend.setSize(of: widget, to: size) + backend.setProgressSpinnerSize(widget, size) } return ViewUpdateResult.leafView( size: ViewSize( From 6d7049a5e8f4ef114786a05c877cd8952551d8d5 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Mon, 3 Nov 2025 19:25:49 +0100 Subject: [PATCH 8/9] Added minHeight to ControlsExample to make it not spawn with 0 height on GtkBackend --- Examples/Sources/ControlsExample/ControlsApp.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Examples/Sources/ControlsExample/ControlsApp.swift b/Examples/Sources/ControlsExample/ControlsApp.swift index f3f64eb460..5f782cdb23 100644 --- a/Examples/Sources/ControlsExample/ControlsApp.swift +++ b/Examples/Sources/ControlsExample/ControlsApp.swift @@ -95,6 +95,7 @@ struct ControlsApp: App { Toggle(enabled ? "Disable all" : "Enable all", active: $enabled) .padding() } + .frame(minHeight: 600) } }.defaultSize(width: 400, height: 600) } From ea80229be388b8bc67f6f7338aca7bf3ba8ebcb9 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Mon, 3 Nov 2025 19:37:41 +0100 Subject: [PATCH 9/9] added a guard to not recreate ProgressSpinners on AppKit when the size didn't change --- Sources/AppKitBackend/AppKitBackend.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index f2ebde605e..efce0e934a 100644 --- a/Sources/AppKitBackend/AppKitBackend.swift +++ b/Sources/AppKitBackend/AppKitBackend.swift @@ -1204,6 +1204,7 @@ public final class AppKitBackend: AppBackend { _ widget: Widget, _ size: SIMD2 ) { + guard Int(widget.frame.size.height) != size.y else { return } setSize(of: widget, to: size) let spinner = NSProgressIndicator() spinner.translatesAutoresizingMaskIntoConstraints = false