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
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ struct CounterApp: App {
#elseif canImport(UIKitBackend)
textField.borderStyle = .bezel
#elseif canImport(WinUIBackend)
textField.selectionHighlightColor.color = .init(a: 255, r: 0, g: 255, b: 0)
textField.selectionHighlightColor.color = .init(
a: 255, r: 0, g: 255, b: 0)
let brush = WinUI.SolidColorBrush()
brush.color = .init(a: 255, r: 0, g: 0, b: 255)
textField.background = brush
Expand Down
14 changes: 13 additions & 1 deletion Examples/Sources/ControlsExample/ControlsApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ struct ControlsApp: App {
@State var flavor: String? = nil
@State var enabled = true
@State var menuToggleState = false
@State var progressViewSize: Int = 10
@State var isProgressViewResizable = true

var body: some Scene {
WindowGroup("ControlsApp") {
Expand Down Expand Up @@ -83,11 +85,21 @@ struct ControlsApp: App {
Text("Value: \(text)")
}

VStack {
Toggle(
"Enable ProgressView resizability", isOn: $isProgressViewResizable)
Slider(value: $progressViewSize, 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)
Picker(
of: ["Vanilla", "Chocolate", "Strawberry"], selection: $flavor)
}
Text("You chose: \(flavor ?? "Nothing yet!")")
}
Expand Down
32 changes: 31 additions & 1 deletion Sources/AppKitBackend/AppKitBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,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 @@ -1199,11 +1208,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
3 changes: 2 additions & 1 deletion Sources/AppKitBackend/InspectionModifiers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ extension Image {
_ inspectionPoints: InspectionPoints = .onCreate,
_ action: @escaping @MainActor @Sendable (NSImageView) -> Void
) -> some View {
InspectView(child: self, inspectionPoints: inspectionPoints) { (_: NSView, children: ImageChildren) in
InspectView(child: self, inspectionPoints: inspectionPoints) {
(_: NSView, children: ImageChildren) in
action(children.imageWidget.into())
}
}
Expand Down
6 changes: 4 additions & 2 deletions Sources/AppKitBackend/NSViewRepresentable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,11 @@ extension NSViewRepresentable {
let growsHorizontally = nsView.contentHuggingPriority(for: .horizontal) < .defaultHigh
let growsVertically = nsView.contentHuggingPriority(for: .vertical) < .defaultHigh

let idealWidth = intrinsicSize.width == NSView.noIntrinsicMetric
let idealWidth =
intrinsicSize.width == NSView.noIntrinsicMetric
? 10 : intrinsicSize.width
let idealHeight = intrinsicSize.height == NSView.noIntrinsicMetric
let idealHeight =
intrinsicSize.height == NSView.noIntrinsicMetric
? 10 : intrinsicSize.height

// When the view doesn't grow along a dimension, we use its fittingSize
Expand Down
10 changes: 5 additions & 5 deletions Sources/Gtk3/Utility/GSimpleAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ public class GSimpleAction: GAction, GObjectRepresentable {
private func connectActionSignal(
_ value: some AnyObject,
handler:
@convention(c) (
UnsafeMutableRawPointer,
OpaquePointer,
UnsafeMutableRawPointer
) -> Void
@convention(c) (
UnsafeMutableRawPointer,
OpaquePointer,
UnsafeMutableRawPointer
) -> Void
) {
g_signal_connect_data(
UnsafeMutableRawPointer(actionPointer),
Expand Down
3 changes: 2 additions & 1 deletion Sources/Gtk3Backend/InspectionModifiers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ extension SwiftCrossUI.Image {
_ inspectionPoints: InspectionPoints = .onCreate,
_ action: @escaping @MainActor @Sendable (Gtk3.Image) -> Void
) -> some View {
InspectView(child: self, inspectionPoints: inspectionPoints) { (_: Gtk3.Widget, children: ImageChildren) in
InspectView(child: self, inspectionPoints: inspectionPoints) {
(_: Gtk3.Widget, children: ImageChildren) in
action(children.imageWidget.into())
}
}
Expand Down
3 changes: 2 additions & 1 deletion Sources/GtkBackend/InspectionModifiers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ extension SwiftCrossUI.Image {
_ inspectionPoints: InspectionPoints = .onCreate,
_ action: @escaping @MainActor @Sendable (Gtk.Picture) -> Void
) -> some View {
InspectView(child: self, inspectionPoints: inspectionPoints) { (_: Gtk.Widget, children: ImageChildren) in
InspectView(child: self, inspectionPoints: inspectionPoints) {
(_: Gtk.Widget, children: ImageChildren) in
action(children.imageWidget.into())
}
}
Expand Down
8 changes: 6 additions & 2 deletions Sources/SwiftCrossUI/Backend/AnyWidget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ public class AnyWidget {
for backend: Backend.Type
) -> Backend.Widget {
guard let widget = widget as? Backend.Widget else {
fatalError("AnyWidget used with incompatible backend \(backend); widget type is \(type(of: widget))")
fatalError(
"AnyWidget used with incompatible backend \(backend); widget type is \(type(of: widget))"
)
}
return widget
}
Expand All @@ -33,7 +35,9 @@ public class AnyWidget {
/// more concise than using ``AnyWidget/concreteWidget(for:)``.
public func into<T>() -> T {
guard let widget = widget as? T else {
fatalError("AnyWidget used with incompatible widget type \(T.self); actual widget type is \(type(of: widget))")
fatalError(
"AnyWidget used with incompatible widget type \(T.self); actual widget type is \(type(of: widget))"
)
}
return 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/Scenes/AlertScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public final class AlertSceneNode: SceneGraphNode {
}

self.alert = alert
} else if !scene.isPresented, let alert {
} else if !scene.isPresented, let alert {
backend.dismissAlert(alert as! Backend.Alert, window: nil)
self.alert = nil
}
Expand Down
5 changes: 3 additions & 2 deletions Sources/SwiftCrossUI/Views/Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,9 @@ extension Menu: TypeSafeView {
action: {}
)
case .menuButton:
let menu = children.menu.flatMap { $0 as? Backend.Menu }
?? backend.createPopoverMenu()
let menu =
children.menu.flatMap { $0 as? Backend.Menu }
?? backend.createPopoverMenu()
children.menu = menu
backend.updateButton(
widget,
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 {
return ViewLayoutResult.leafView(size: ViewSize(naturalSize))
}

let dimension: Double

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

return ViewLayoutResult.leafView(
size: ViewSize(dimension, dimension)
)
}

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
2 changes: 1 addition & 1 deletion Sources/UIKitBackend/InspectionModifiers.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import UIKit
import SwiftCrossUI
import UIKit

extension View {
public func inspect(
Expand Down
11 changes: 6 additions & 5 deletions Sources/UIKitBackend/UIKitBackend+Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,13 @@
setButtonTitle(buttonWidget, label, environment: environment)
buttonWidget.child.menu = menu.uiMenu
buttonWidget.child.showsMenuAsPrimaryAction = true
if #available(iOS 16, tvOS 17, macCatalyst 16, *) {

Check warning on line 61 in Sources/UIKitBackend/UIKitBackend+Menu.swift

View workflow job for this annotation

GitHub Actions / uikit (TV)

unnecessary check for 'tvOS'; enclosing scope ensures guard will always be true
buttonWidget.child.preferredMenuElementOrder = switch environment.menuOrder {
case .automatic: .automatic
case .priority: .priority
case .fixed: .fixed
}
buttonWidget.child.preferredMenuElementOrder =
switch environment.menuOrder {
case .automatic: .automatic
case .priority: .priority
case .fixed: .fixed
}
}
} else {
preconditionFailure("Current OS is too old to support menu buttons.")
Expand Down
6 changes: 4 additions & 2 deletions Sources/UIKitBackend/UIViewRepresentable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,11 @@ func defaultViewSize(proposal: ProposedViewSize, view: UIView) -> ViewSize {
let growsHorizontally = view.contentHuggingPriority(for: .horizontal) < .defaultHigh
let growsVertically = view.contentHuggingPriority(for: .vertical) < .defaultHigh

let idealWidth = intrinsicSize.width == UIView.noIntrinsicMetric
let idealWidth =
intrinsicSize.width == UIView.noIntrinsicMetric
? 10 : intrinsicSize.width
let idealHeight = intrinsicSize.height == UIView.noIntrinsicMetric
let idealHeight =
intrinsicSize.height == UIView.noIntrinsicMetric
? 10 : intrinsicSize.height

// When the view doesn't grow along a dimension, we use its fittingSize
Expand Down
2 changes: 1 addition & 1 deletion Sources/WinUIBackend/InspectionModifiers.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import WinUI
import SwiftCrossUI
import WinUI

extension View {
public func inspect(
Expand Down
2 changes: 1 addition & 1 deletion Sources/WinUIBackend/WinUIElementRepresentable.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import SwiftCrossUI
import WinUI
import WindowsFoundation
import SwiftCrossUI

// Many force tries are required for the WinUI backend but we don't really want them
// anywhere else so just disable the lint rule at a file level.
Expand Down
Loading