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
108 changes: 60 additions & 48 deletions Examples/Sources/ControlsExample/ControlsApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,71 +16,83 @@ 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)
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 @@ -485,6 +485,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 @@ -1168,11 +1177,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 setSize(
ofProgressSpinner widget: Widget,
to 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
17 changes: 17 additions & 0 deletions Sources/SwiftCrossUI/Backend/AppBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,16 @@ public protocol AppBackend: Sendable {
/// Creates an indeterminate progress spinner.
func createProgressSpinner() -> Widget

/// Sets the size of a progress spinner.
///
/// This method exists because AppKitBackend requires special handling to resize progress spinners.
///
/// The default implementation forwards to ``AppBackend/setSize(of:to:)``.
func setSize(
ofProgressSpinner widget: Widget,
to 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 @@ -1123,6 +1133,13 @@ extension AppBackend {
todo()
}

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

public func createProgressBar() -> Widget {
todo()
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftCrossUI/Builders/MenuItemsBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ public struct MenuItemsBuilder {
public static func buildBlock() -> [MenuItem] {
[]
}

public static func buildPartialBlock(first: Button) -> [MenuItem] {
[.button(first)]
}
Expand Down
3 changes: 2 additions & 1 deletion Sources/SwiftCrossUI/Layout/LayoutSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ public enum LayoutSystem {
var tag: String?

public init(
computeLayout: @escaping @MainActor (ProposedViewSize, EnvironmentValues) ->
computeLayout:
@escaping @MainActor (ProposedViewSize, EnvironmentValues) ->
ViewLayoutResult,
commit: @escaping @MainActor () -> ViewLayoutResult,
tag: String? = nil
Expand Down
4 changes: 2 additions & 2 deletions Sources/SwiftCrossUI/Views/EitherView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ extension EitherView: TypeSafeView {
switch storage {
case .a(let a):
switch children.node {
case let .a(nodeA):
case .a(let nodeA):
result = nodeA.computeLayout(
with: a,
proposedSize: proposedSize,
Expand All @@ -82,7 +82,7 @@ extension EitherView: TypeSafeView {
}
case .b(let b):
switch children.node {
case let .b(nodeB):
case .b(let nodeB):
result = nodeB.computeLayout(
with: b,
proposedSize: proposedSize,
Expand Down
47 changes: 42 additions & 5 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 @@ -113,16 +126,40 @@ struct ProgressSpinnerView: ElementaryView {
environment: EnvironmentValues,
backend: Backend
) -> ViewLayoutResult {
let size = ViewSize(backend.naturalSize(of: widget))
return ViewLayoutResult.leafView(size: size)
let naturalSize = backend.naturalSize(of: widget)

guard isResizable else {
// Required to reset its size when resizability
// gets changed at runtime
return ViewLayoutResult.leafView(size: ViewSize(naturalSize))
}

let minimumDimension: Double

if let proposedWidth = proposedSize.width,
let proposedHeight = proposedSize.height
{
minimumDimension = max(min(proposedWidth, proposedHeight), 0)
} else {
minimumDimension = Double(min(naturalSize.x, naturalSize.y))
}

return ViewLayoutResult.leafView(
size: .init(minimumDimension, minimumDimension)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use ViewSize instead of .init as a stylistic thing

)
}

func commit<Backend: AppBackend>(
_ widget: Backend.Widget,
layout: ViewLayoutResult,
environment: EnvironmentValues,
backend: Backend
) {}
) {
// 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(ofProgressSpinner: widget, to: layout.size.vector)
}
}

struct ProgressBarView: ElementaryView {
Expand Down
6 changes: 4 additions & 2 deletions Sources/SwiftCrossUI/Views/ScrollView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,16 @@ public struct ScrollView<Content: View>: TypeSafeView, View {
// Compute the outer size.
var outerSize = finalChildResult.size
if axes.contains(.horizontal) {
outerSize.width = proposedSize.width
outerSize.width =
proposedSize.width
?? (finalChildResult.size.width + verticalScrollBarWidth)
} else {
outerSize.width += verticalScrollBarWidth
}

if axes.contains(.vertical) {
outerSize.height = proposedSize.height
outerSize.height =
proposedSize.height
?? (finalChildResult.size.height + horizontalScrollBarHeight)
} else {
outerSize.height += horizontalScrollBarHeight
Expand Down
Loading