Skip to content
Open
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
111 changes: 63 additions & 48 deletions Examples/Sources/ControlsExample/ControlsApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,71 +16,86 @@ struct ControlsApp: App {
@State var text = ""
@State var flavor: String? = nil
@State var enabled = true
@State var progressViewSize: Int = 10
@State var isProgressViewResizable = true

var body: some Scene {
WindowGroup("ControlsApp") {
#hotReloadable {
VStack(spacing: 30) {
VStack {
Text("Button")
Button("Click me!") {
count += 1
ScrollView {
VStack(spacing: 30) {
VStack {
Text("Button")
Button("Click me!") {
count += 1
}
Text("Count: \(count)")
}
Text("Count: \(count)")
}
.padding(.bottom, 20)
.padding(.bottom, 20)

#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("Toggle button")
Toggle("Toggle me!", active: $exampleButtonState)
.toggleStyle(.button)
Text("Currently enabled: \(exampleButtonState)")
Text("Toggle switch")
Toggle("Toggle me:", active: $exampleSwitchState)
.toggleStyle(.switch)
Text("Currently enabled: \(exampleSwitchState)")
}
.padding(.bottom, 20)
#endif

VStack {
Text("Toggle switch")
Toggle("Toggle me:", active: $exampleSwitchState)
.toggleStyle(.switch)
Text("Currently enabled: \(exampleSwitchState)")
}
#if !canImport(UIKitBackend)
VStack {
Text("Checkbox")
Toggle("Toggle me:", active: $exampleCheckboxState)
.toggleStyle(.checkbox)
Text("Currently enabled: \(exampleCheckboxState)")
}
#endif

#if !canImport(UIKitBackend)
VStack {
Text("Checkbox")
Toggle("Toggle me:", active: $exampleCheckboxState)
.toggleStyle(.checkbox)
Text("Currently enabled: \(exampleCheckboxState)")
Text("Slider")
Slider($sliderValue, minimum: 0, maximum: 10)
.frame(maxWidth: 200)
Text("Value: \(String(format: "%.02f", sliderValue))")
}
#endif

VStack {
Text("Slider")
Slider($sliderValue, minimum: 0, maximum: 10)
.frame(maxWidth: 200)
Text("Value: \(String(format: "%.02f", sliderValue))")
}
VStack {
Text("Text field")
TextField("Text field", text: $text)
Text("Value: \(text)")
}

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()
}
.frame(minHeight: 600)
}
}.defaultSize(width: 400, height: 600)
}
Expand Down
32 changes: 31 additions & 1 deletion Sources/AppKitBackend/AppKitBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,15 @@ public final class AppKitBackend: AppBackend {
}

public func naturalSize(of widget: Widget) -> SIMD2<Int> {
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),
Expand Down Expand Up @@ -1181,11 +1190,32 @@ 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<Int>
) {
guard Int(widget.frame.size.height) != size.y else { return }
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 {
Expand Down
16 changes: 16 additions & 0 deletions Sources/SwiftCrossUI/Backend/AppBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Int>)``
/// on other Backends.
func setProgressSpinnerSize(
_ widget: Widget,
_ size: SIMD2<Int>
)

/// Creates a progress bar.
func createProgressBar() -> Widget
/// Updates a progress bar to reflect the given progress (between 0 and 1), and the
Expand Down Expand Up @@ -1028,6 +1037,13 @@ extension AppBackend {
todo()
}

public func setProgressSpinnerSize(
_ widget: Widget,
_ size: SIMD2<Int>
) {
setSize(of: widget, to: size)
}

public func createProgressBar() -> Widget {
todo()
}
Expand Down
46 changes: 42 additions & 4 deletions Sources/SwiftCrossUI/Views/ProgressView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public struct ProgressView<Label: View>: View {
private var label: Label
private var progress: Double?
private var kind: Kind
private var isSpinnerResizable: Bool = false

private enum Kind {
case spinner
Expand All @@ -23,7 +24,7 @@ public struct ProgressView<Label: View>: View {
private var progressIndicator: some View {
switch kind {
case .spinner:
ProgressSpinnerView()
ProgressSpinnerView(isResizable: isSpinnerResizable)
case .bar:
ProgressBarView(value: progress)
}
Expand All @@ -50,6 +51,14 @@ public struct ProgressView<Label: View>: View {
self.kind = .bar
self.progress = value.map(Double.init)
}

/// Makes the ProgressView resize to fit the available space.
/// Only affects ``Kind/spinner``.
public func resizable(_ isResizable: Bool = true) -> Self {
var progressView = self
progressView.isSpinnerResizable = isResizable
return progressView
}
}

extension ProgressView where Label == EmptyView {
Expand Down Expand Up @@ -101,7 +110,11 @@ extension ProgressView where Label == Text {
}

struct ProgressSpinnerView: ElementaryView {
init() {}
let isResizable: Bool

init(isResizable: Bool = false) {
self.isResizable = isResizable
}

func asWidget<Backend: AppBackend>(backend: Backend) -> Backend.Widget {
backend.createProgressSpinner()
Expand All @@ -114,8 +127,33 @@ struct ProgressSpinnerView: ElementaryView {
backend: Backend,
dryRun: Bool
) -> ViewUpdateResult {
ViewUpdateResult.leafView(
size: ViewSize(fixedSize: backend.naturalSize(of: widget))
let naturalSize = backend.naturalSize(of: widget)
guard isResizable else {
// Required to reset its size when resizability
// gets changed at runtime
backend.setProgressSpinnerSize(widget, naturalSize)
return ViewUpdateResult.leafView(size: ViewSize(fixedSize: naturalSize))
}
let minimumDimension = max(min(proposedSize.x, proposedSize.y), 0)
let size = SIMD2(
minimumDimension,
minimumDimension
)
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.setProgressSpinnerSize(widget, size)
}
return ViewUpdateResult.leafView(
size: ViewSize(
size: size,
idealSize: naturalSize,
minimumWidth: 0,
minimumHeight: 0,
maximumWidth: nil,
maximumHeight: nil
)
)
}
}
Expand Down
Loading