From 2797197c1f82d6c73372b61ae555db186ed68a07 Mon Sep 17 00:00:00 2001 From: stackotter Date: Tue, 24 Jun 2025 15:05:09 +1000 Subject: [PATCH] Introduce 'inspect' modifiers for accessing underlying native widgets These changes make platform-specific native customizations significantly easier to perform. Hopefully this will make SwiftCrossUI significantly more viable for actual production apps that often just need to get things working even if there's not a nice neat first party API for it yet. Inspiration was taken from swiftui-introspect. --- .github/workflows/build-test-and-docs.yml | 14 +- Examples/Bundler.toml | 5 + Examples/Package.swift | 7 +- .../AdvancedCustomizationApp.swift | 198 ++++++++++++++++++ .../AdvancedCustomizationExample/Banner.png | Bin 0 -> 14960 bytes .../AppKitBackend/InspectionModifiers.swift | 107 ++++++++++ Sources/Gtk3/Pixbuf.swift | 6 +- Sources/Gtk3Backend/InspectionModifiers.swift | 132 ++++++++++++ Sources/GtkBackend/InspectionModifiers.swift | 141 +++++++++++++ .../SwiftCrossUI.docc/Examples.md | 1 + Sources/SwiftCrossUI/Views/Image.swift | 20 +- .../Views/Modifiers/InspectModifier.swift | 101 +++++++++ .../SwiftCrossUI/Views/NavigationLink.swift | 2 +- .../UIKitBackend/InspectionModifiers.swift | 155 ++++++++++++++ .../UIKitBackend/UIKitBackend+Container.swift | 2 +- .../WinUIBackend/InspectionModifiers.swift | 140 +++++++++++++ 16 files changed, 1014 insertions(+), 17 deletions(-) create mode 100644 Examples/Sources/AdvancedCustomizationExample/AdvancedCustomizationApp.swift create mode 100644 Examples/Sources/AdvancedCustomizationExample/Banner.png create mode 100644 Sources/AppKitBackend/InspectionModifiers.swift create mode 100644 Sources/Gtk3Backend/InspectionModifiers.swift create mode 100644 Sources/GtkBackend/InspectionModifiers.swift create mode 100644 Sources/SwiftCrossUI/Views/Modifiers/InspectModifier.swift create mode 100644 Sources/UIKitBackend/InspectionModifiers.swift create mode 100644 Sources/WinUIBackend/InspectionModifiers.swift diff --git a/.github/workflows/build-test-and-docs.yml b/.github/workflows/build-test-and-docs.yml index bc0d77b321..a61ad64dc1 100644 --- a/.github/workflows/build-test-and-docs.yml +++ b/.github/workflows/build-test-and-docs.yml @@ -41,6 +41,7 @@ jobs: cd Examples && \ swift build --target GtkBackend && \ swift build --target Gtk3Backend && \ + swift build --target GtkExample && \ swift build --target CounterExample && \ swift build --target ControlsExample && \ swift build --target RandomNumberGeneratorExample && \ @@ -51,8 +52,9 @@ jobs: swift build --target StressTestExample && \ swift build --target SpreadsheetExample && \ swift build --target NotesExample && \ - swift build --target GtkExample && \ - swift build --target PathsExample + swift build --target PathsExample && \ + swift build --target WebViewExample && \ + swift build --target AdvancedCustomizationExample - name: Test run: swift test --test-product swift-cross-uiPackageTests @@ -101,6 +103,8 @@ jobs: buildtarget StressTestExample buildtarget NotesExample buildtarget PathsExample + buildtarget WebViewExample + buildtarget AdvancedCustomizationExample if [ $device_type != TV ]; then # Slider is not implemented for tvOS @@ -161,6 +165,8 @@ jobs: buildtarget PathsExample buildtarget ControlsExample buildtarget RandomNumberGeneratorExample + buildtarget WebViewExample + buildtarget AdvancedCustomizationExample # TODO test whether this works on Catalyst # buildtarget SplitExample @@ -281,6 +287,7 @@ jobs: - name: Build examples working-directory: ./Examples run: | + swift build --target GtkExample && \ swift build --target CounterExample && \ swift build --target ControlsExample && \ swift build --target RandomNumberGeneratorExample && \ @@ -291,7 +298,8 @@ jobs: swift build --target StressTestExample && \ swift build --target SpreadsheetExample && \ swift build --target NotesExample && \ - swift build --target GtkExample + swift build --target PathsExample && \ + swift build --target AdvancedCustomizationExample - name: Test run: swift test --test-product swift-cross-uiPackageTests diff --git a/Examples/Bundler.toml b/Examples/Bundler.toml index 7d0f8864a9..4a226830e1 100644 --- a/Examples/Bundler.toml +++ b/Examples/Bundler.toml @@ -59,3 +59,8 @@ version = '0.1.0' identifier = 'dev.swiftcrossui.WebViewExample' product = 'WebViewExample' version = '0.1.0' + +[apps.AdvancedCustomizationExample] +identifier = 'dev.swiftcrossui.AdvancedCustomizationExample' +product = 'AdvancedCustomizationExample' +version = '0.1.0' diff --git a/Examples/Package.swift b/Examples/Package.swift index 11f210b821..fbfb9c28af 100644 --- a/Examples/Package.swift +++ b/Examples/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 5.10 import Foundation import PackageDescription @@ -72,6 +72,11 @@ let package = Package( .executableTarget( name: "WebViewExample", dependencies: exampleDependencies + ), + .executableTarget( + name: "AdvancedCustomizationExample", + dependencies: exampleDependencies, + resources: [.copy("Banner.png")] ) ] ) diff --git a/Examples/Sources/AdvancedCustomizationExample/AdvancedCustomizationApp.swift b/Examples/Sources/AdvancedCustomizationExample/AdvancedCustomizationApp.swift new file mode 100644 index 0000000000..ff8054e317 --- /dev/null +++ b/Examples/Sources/AdvancedCustomizationExample/AdvancedCustomizationApp.swift @@ -0,0 +1,198 @@ +import DefaultBackend +import Foundation +import SwiftCrossUI + +#if canImport(WinUIBackend) + import WinUI +#endif + +#if canImport(SwiftBundlerRuntime) + import SwiftBundlerRuntime +#endif + +@main +@HotReloadable +struct CounterApp: App { + @State var count = 0 + @State var value = 0.0 + @State var color: String? = nil + @State var name = "" + + var body: some Scene { + WindowGroup("CounterExample: \(count)") { + #hotReloadable { + ScrollView { + HStack(spacing: 20) { + Button("-") { + count -= 1 + } + + Text("Count: \(count)") + .inspect { text in + #if canImport(AppKitBackend) + text.isSelectable = true + #elseif canImport(UIKitBackend) + #if !targetEnvironment(macCatalyst) + text.isHighlighted = true + text.highlightTextColor = .yellow + #endif + #elseif canImport(WinUIBackend) + text.isTextSelectionEnabled = true + #elseif canImport(GtkBackend) + text.selectable = true + #elseif canImport(Gtk3Backend) + text.selectable = true + #endif + } + + Button("+") { + count += 1 + }.inspect(.afterUpdate) { button in + #if canImport(AppKitBackend) + // Button is an NSButton on macOS + button.bezelColor = .red + #elseif canImport(UIKitBackend) + if #available(iOS 15.0, *) { + button.configuration = .bordered() + } + #elseif canImport(WinUIBackend) + button.cornerRadius.topLeft = 10 + let brush = WinUI.SolidColorBrush() + brush.color = .init(a: 255, r: 255, g: 0, b: 0) + button.background = brush + #elseif canImport(GtkBackend) + button.css.set(property: .backgroundColor(.init(1, 0, 0))) + #elseif canImport(Gtk3Backend) + button.css.set(property: .backgroundColor(.init(1, 0, 0))) + #endif + } + } + + Slider($value, minimum: 0, maximum: 10) + .inspect { slider in + #if canImport(AppKitBackend) + slider.numberOfTickMarks = 10 + #elseif canImport(UIKitBackend) + slider.thumbTintColor = .blue + #elseif canImport(WinUIBackend) + slider.isThumbToolTipEnabled = true + #elseif canImport(GtkBackend) + slider.drawValue = true + #elseif canImport(Gtk3Backend) + slider.drawValue = true + #endif + } + + #if !canImport(Gtk3Backend) + Picker(of: ["Red", "Green", "Blue"], selection: $color) + .inspect(.afterUpdate) { picker in + #if canImport(AppKitBackend) + picker.preferredEdge = .maxX + #elseif canImport(UIKitBackend) && os(iOS) + // Can't think of something to do to the + // UIPickerView, but the point is that you + // could do something if you needed to! + // This would be a UITableView on tvOS. + // And could be either a UITableView or a + // UIPickerView on Mac Catalyst depending + // on Mac Catalyst version and interface + // idiom. + #elseif canImport(WinUIBackend) + let brush = WinUI.SolidColorBrush() + brush.color = .init(a: 255, r: 255, g: 0, b: 0) + picker.background = brush + #elseif canImport(GtkBackend) + picker.enableSearch = true + #endif + } + #endif + + TextField("Name", text: $name) + .inspect(.afterUpdate) { textField in + #if canImport(AppKitBackend) + textField.backgroundColor = .blue + #elseif canImport(UIKitBackend) + textField.borderStyle = .bezel + #elseif canImport(WinUIBackend) + 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 + #elseif canImport(GtkBackend) + textField.xalign = 1 + textField.css.set(property: .backgroundColor(.init(0, 0, 1))) + #elseif canImport(Gtk3Backend) + textField.hasFrame = false + textField.css.set(property: .backgroundColor(.init(0, 0, 1))) + #endif + } + + ScrollView { + ForEach(Array(1...50)) { number in + Text("Line \(number)") + }.padding() + }.inspect(.afterUpdate) { scrollView in + #if canImport(AppKitBackend) + scrollView.borderType = .grooveBorder + #elseif canImport(UIKitBackend) + scrollView.alwaysBounceHorizontal = true + #elseif canImport(WinUIBackend) + let brush = WinUI.SolidColorBrush() + brush.color = .init(a: 255, r: 0, g: 255, b: 0) + scrollView.borderBrush = brush + scrollView.borderThickness = .init( + left: 1, top: 1, right: 1, bottom: 1 + ) + #elseif canImport(GtkBackend) + scrollView.css.set(property: .border(color: .init(1, 0, 0), width: 2)) + #elseif canImport(Gtk3Backend) + scrollView.css.set(property: .border(color: .init(1, 0, 0), width: 2)) + #endif + }.frame(height: 200) + + List(["Red", "Green", "Blue"], id: \.self, selection: $color) { color in + Text(color) + }.inspect(.afterUpdate) { table in + #if canImport(AppKitBackend) + table.usesAlternatingRowBackgroundColors = true + #elseif canImport(UIKitBackend) + table.isEditing = true + #elseif canImport(WinUIBackend) + let brush = WinUI.SolidColorBrush() + brush.color = .init(a: 255, r: 255, g: 0, b: 255) + table.borderBrush = brush + table.borderThickness = .init( + left: 1, top: 1, right: 1, bottom: 1 + ) + #elseif canImport(GtkBackend) + table.showSeparators = true + #elseif canImport(Gtk3Backend) + table.selectionMode = .multiple + #endif + } + + Image(Bundle.module.bundleURL.appendingPathComponent("Banner.png")) + .resizable() + .inspect(.afterUpdate) { image in + #if canImport(AppKitBackend) + image.isEditable = true + #elseif canImport(UIKitBackend) + image.layer.borderWidth = 1 + image.layer.borderColor = .init(red: 0, green: 1, blue: 0, alpha: 1) + #elseif canImport(WinUIBackend) + // Couldn't find anything visually interesting + // to do to the WinUI.Image, but the point is + // that you could do something if you wanted to. + #elseif canImport(GtkBackend) + image.css.set(property: .border(color: .init(0, 1, 0), width: 2)) + #elseif canImport(Gtk3Backend) + image.css.set(property: .border(color: .init(0, 1, 0), width: 2)) + #endif + } + .aspectRatio(contentMode: .fit) + }.padding() + } + } + .defaultSize(width: 400, height: 200) + } +} diff --git a/Examples/Sources/AdvancedCustomizationExample/Banner.png b/Examples/Sources/AdvancedCustomizationExample/Banner.png new file mode 100644 index 0000000000000000000000000000000000000000..c958f40aef6ae2b34c2e9172224b43c929cc0cdf GIT binary patch literal 14960 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 H2>8DMYJ?7_ literal 0 HcmV?d00001 diff --git a/Sources/AppKitBackend/InspectionModifiers.swift b/Sources/AppKitBackend/InspectionModifiers.swift new file mode 100644 index 0000000000..24e17205f2 --- /dev/null +++ b/Sources/AppKitBackend/InspectionModifiers.swift @@ -0,0 +1,107 @@ +import AppKit +import SwiftCrossUI + +extension View { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (NSView) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension Button { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (NSButton) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension Text { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (NSTextField) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension Slider { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (NSSlider) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension Picker { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (NSPopUpButton) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension TextField { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (NSTextField) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension ScrollView { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (NSScrollView) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension List { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (NSTableView) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints) { (view: NSScrollView) in + action(view.documentView as! NSTableView) + } + } +} + +extension NavigationSplitView { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (NSSplitView) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints) { (view: NSView) in + action(view.subviews[0] as! NSSplitView) + } + } +} + +extension Image { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (NSImageView) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints) { (_: NSView, children: ImageChildren) in + action(children.imageWidget.into()) + } + } +} + +extension Table { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (NSScrollView) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} diff --git a/Sources/Gtk3/Pixbuf.swift b/Sources/Gtk3/Pixbuf.swift index 06b8a657d4..6a169c3b4b 100644 --- a/Sources/Gtk3/Pixbuf.swift +++ b/Sources/Gtk3/Pixbuf.swift @@ -34,10 +34,12 @@ public struct Pixbuf { } public func scaled(toWidth width: Int, andHeight height: Int) -> Pixbuf { + // This operation fails if the destination width or destination height + // is 0, so just make sure neither dimension hits zero. let newPointer = gdk_pixbuf_scale_simple( pointer, - gint(width), - gint(height), + gint(max(width, 1)), + gint(max(height, 1)), GDK_INTERP_BILINEAR ) return Pixbuf(pointer: newPointer!) diff --git a/Sources/Gtk3Backend/InspectionModifiers.swift b/Sources/Gtk3Backend/InspectionModifiers.swift new file mode 100644 index 0000000000..35edd818b1 --- /dev/null +++ b/Sources/Gtk3Backend/InspectionModifiers.swift @@ -0,0 +1,132 @@ +import Gtk3 +import SwiftCrossUI + +extension View { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk3.Widget) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension SwiftCrossUI.Button { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk3.Button) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension Text { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk3.Label) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension Slider { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk3.Scale) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension TextField { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk3.Entry) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension ScrollView { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk3.ScrolledWindow) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension List { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk3.ListBox) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension NavigationSplitView { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk3.Paned) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints) { (view: Gtk3.Fixed) in + action(view.children[0] as! Gtk3.Paned) + } + } +} + +extension SwiftCrossUI.Image { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk3.Image) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints) { (_: Gtk3.Widget, children: ImageChildren) in + action(children.imageWidget.into()) + } + } +} + +extension HStack { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk3.Fixed) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension VStack { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk3.Fixed) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension ZStack { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk3.Fixed) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension Group { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk3.Fixed) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension Shape { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk3.DrawingArea) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} diff --git a/Sources/GtkBackend/InspectionModifiers.swift b/Sources/GtkBackend/InspectionModifiers.swift new file mode 100644 index 0000000000..c791d7e73e --- /dev/null +++ b/Sources/GtkBackend/InspectionModifiers.swift @@ -0,0 +1,141 @@ +import Gtk +import SwiftCrossUI + +extension View { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk.Widget) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension SwiftCrossUI.Button { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk.Button) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension Text { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk.Label) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension Slider { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk.Scale) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension Picker { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk.DropDown) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension TextField { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk.Entry) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension ScrollView { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk.ScrolledWindow) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension List { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk.ListBox) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension NavigationSplitView { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk.Paned) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints) { (view: Gtk.Fixed) in + action(view.children[0] as! Gtk.Paned) + } + } +} + +extension SwiftCrossUI.Image { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk.Picture) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints) { (_: Gtk.Widget, children: ImageChildren) in + action(children.imageWidget.into()) + } + } +} + +extension HStack { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk.Fixed) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension VStack { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk.Fixed) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension ZStack { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk.Fixed) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension Group { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk.Fixed) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension Shape { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (Gtk.DrawingArea) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} diff --git a/Sources/SwiftCrossUI/SwiftCrossUI.docc/Examples.md b/Sources/SwiftCrossUI/SwiftCrossUI.docc/Examples.md index ab54ea10e2..1888f248fa 100644 --- a/Sources/SwiftCrossUI/SwiftCrossUI.docc/Examples.md +++ b/Sources/SwiftCrossUI/SwiftCrossUI.docc/Examples.md @@ -20,6 +20,7 @@ A few examples are included with SwiftCrossUI to demonstrate some of its basic f - `NotesExample`, an app showcasing multi-line text editing and a more realistic usage of SwiftCrossUI. - `PathsExample`, an app showcasing the use of ``Path`` to draw various shapes. - `WebViewExample`, an app showcasing the use of ``WebView`` to display websites. Only works on Apple platforms so far. +- `AdvancedCustomizationExample`, an app showcasing SwiftCrossUI's more advanced APIs for customizing the underlying native views of your app. ## Running examples diff --git a/Sources/SwiftCrossUI/Views/Image.swift b/Sources/SwiftCrossUI/Views/Image.swift index 2f3be3e6f8..26b0e59815 100644 --- a/Sources/SwiftCrossUI/Views/Image.swift +++ b/Sources/SwiftCrossUI/Views/Image.swift @@ -46,7 +46,7 @@ extension Image: View { extension Image: TypeSafeView { func layoutableChildren( backend: Backend, - children: _ImageChildren + children: ImageChildren ) -> [LayoutSystem.LayoutableChild] { [] } @@ -55,12 +55,12 @@ extension Image: TypeSafeView { backend: Backend, snapshots: [ViewGraphSnapshotter.NodeSnapshot]?, environment: EnvironmentValues - ) -> _ImageChildren { - _ImageChildren(backend: backend) + ) -> ImageChildren { + ImageChildren(backend: backend) } func asWidget( - _ children: _ImageChildren, + _ children: ImageChildren, backend: Backend ) -> Backend.Widget { children.container.into() @@ -68,7 +68,7 @@ extension Image: TypeSafeView { func update( _ widget: Backend.Widget, - children: _ImageChildren, + children: ImageChildren, proposedSize: SIMD2, environment: EnvironmentValues, backend: Backend, @@ -159,12 +159,14 @@ extension Image: TypeSafeView { } } -class _ImageChildren: ViewGraphNodeChildren { +/// Image's persistent storage. Only exposed with the `package` access level +/// in order for backends to implement the `Image.inspect(_:_:)` modifier. +package class ImageChildren: ViewGraphNodeChildren { var cachedImageSource: Image.Source? = nil var cachedImage: ImageFormats.Image? = nil var cachedImageDisplaySize: SIMD2 = .zero var container: AnyWidget - var imageWidget: AnyWidget + package var imageWidget: AnyWidget var imageChanged = false var isContainerEmpty = true var lastScaleFactor: Double = 1 @@ -174,6 +176,6 @@ class _ImageChildren: ViewGraphNodeChildren { imageWidget = AnyWidget(backend.createImageView()) } - var widgets: [AnyWidget] = [] - var erasedNodes: [ErasedViewGraphNode] = [] + package var widgets: [AnyWidget] = [] + package var erasedNodes: [ErasedViewGraphNode] = [] } diff --git a/Sources/SwiftCrossUI/Views/Modifiers/InspectModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/InspectModifier.swift new file mode 100644 index 0000000000..e8f5d3fd7c --- /dev/null +++ b/Sources/SwiftCrossUI/Views/Modifiers/InspectModifier.swift @@ -0,0 +1,101 @@ +/// A point at which a view's underlying widget can be inspected. +public struct InspectionPoints: OptionSet, RawRepresentable, Hashable, Sendable { + public var rawValue: UInt32 + + public init(rawValue: UInt32) { + self.rawValue = rawValue + } + + public static let onCreate = Self(rawValue: 1 << 0) + public static let beforeUpdate = Self(rawValue: 1 << 1) + public static let afterUpdate = Self(rawValue: 1 << 2) +} + +/// The `View.inspect(_:_:)` family of modifiers is implemented within each +/// backend. Make sure to import your chosen backend in any files where you +/// need to inspect a widget. This type simply supports the implementation of +/// those backend-specific modifiers. +package struct InspectView { + var child: Child + var inspectionPoints: InspectionPoints + var action: @MainActor (_ widget: AnyWidget, _ children: any ViewGraphNodeChildren) -> Void + + package init( + child: Child, + inspectionPoints: InspectionPoints, + action: @escaping @MainActor @Sendable (WidgetType) -> Void + ) { + self.child = child + self.inspectionPoints = inspectionPoints + self.action = { widget, _ in + action(widget.into()) + } + } + + package init( + child: Child, + inspectionPoints: InspectionPoints, + action: @escaping @MainActor @Sendable (WidgetType, Children) -> Void + ) { + self.child = child + self.inspectionPoints = inspectionPoints + self.action = { widget, children in + action(widget.into(), children as! Children) + } + } +} + +extension InspectView: View { + package var body: some View { EmptyView() } + + package func asWidget( + _ children: any ViewGraphNodeChildren, + backend: Backend + ) -> Backend.Widget { + let widget = child.asWidget(children, backend: backend) + if inspectionPoints.contains(.onCreate) { + action(AnyWidget(widget), children) + } + return widget + } + + package func children( + backend: Backend, + snapshots: [ViewGraphSnapshotter.NodeSnapshot]?, + environment: EnvironmentValues + ) -> any ViewGraphNodeChildren { + child.children(backend: backend, snapshots: snapshots, environment: environment) + } + + package func layoutableChildren( + backend: Backend, + children: any ViewGraphNodeChildren + ) -> [LayoutSystem.LayoutableChild] { + child.layoutableChildren(backend: backend, children: children) + } + + package func update( + _ widget: Backend.Widget, + children: any ViewGraphNodeChildren, + proposedSize: SIMD2, + environment: EnvironmentValues, + backend: Backend, + dryRun: Bool + ) -> ViewUpdateResult { + if inspectionPoints.contains(.beforeUpdate) { + action(AnyWidget(widget), children) + } + let result = child.update( + widget, + children: children, + proposedSize: proposedSize, + environment: environment, + backend: backend, + dryRun: dryRun + ) + if inspectionPoints.contains(.afterUpdate) { + action(AnyWidget(widget), children) + } + return result + } +} diff --git a/Sources/SwiftCrossUI/Views/NavigationLink.swift b/Sources/SwiftCrossUI/Views/NavigationLink.swift index 388e704c4e..76f7c8d3dd 100644 --- a/Sources/SwiftCrossUI/Views/NavigationLink.swift +++ b/Sources/SwiftCrossUI/Views/NavigationLink.swift @@ -2,7 +2,7 @@ // some practical examples). /// A navigation primitive that appends a value to the current navigation path on click. /// -/// Unlike Apples SwiftUI API a `NavigationLink` can be outside of a `NavigationStack` +/// Unlike Apple's SwiftUI API, a `NavigationLink` can be outside of a `NavigationStack` /// as long as they share the same `NavigationPath`. public struct NavigationLink: View { public var body: some View { diff --git a/Sources/UIKitBackend/InspectionModifiers.swift b/Sources/UIKitBackend/InspectionModifiers.swift new file mode 100644 index 0000000000..511d7b1daa --- /dev/null +++ b/Sources/UIKitBackend/InspectionModifiers.swift @@ -0,0 +1,155 @@ +import UIKit +import SwiftCrossUI + +extension View { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (UIView) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints) { (view: any WidgetProtocol) in + action(view.view) + } + } + + nonisolated func inspectAsWrapperWidget( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (WrapperWidget) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension Button { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (UIButton) -> Void + ) -> some View { + inspectAsWrapperWidget(inspectionPoints) { wrapper in + action(wrapper.child) + } + } +} + +extension Text { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (UILabel) -> Void + ) -> some View { + inspectAsWrapperWidget(inspectionPoints) { wrapper in + action(wrapper.child) + } + } +} + +extension Slider { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (UISlider) -> Void + ) -> some View { + inspectAsWrapperWidget(inspectionPoints) { wrapper in + action(wrapper.child) + } + } +} + +extension SwiftCrossUI.Picker { + /// Inspects the picker's underlying `UIView` on Mac Catalyst. Will be a + /// `UIPickerView` if running on Mac Catalyst 14.0+ with the Mac user + /// interface idiom, and a `UIPickerView` otherwise. + @available(macCatalyst 13.0, *) + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (UIView) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints) { (view: any WidgetProtocol) in + if let view = view as? UITableViewPicker { + action(view.child) + } else if let view = view as? UIPickerViewPicker { + action(view.child) + } else { + action(view.view) + } + } + } + + /// Inspects the picker's underlying `UITableView` on tvOS. + @available(tvOS 13.0, *) + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (UITableView) -> Void + ) -> some View { + inspectAsWrapperWidget(inspectionPoints) { wrapper in + action(wrapper.child) + } + } + + /// Inspects the picker's underlying `UIPickerView` on iOS. + @available(iOS 13.0, *) + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (UIPickerView) -> Void + ) -> some View { + inspectAsWrapperWidget(inspectionPoints) { wrapper in + action(wrapper.child) + } + } +} + +extension TextField { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (UITextField) -> Void + ) -> some View { + inspectAsWrapperWidget(inspectionPoints) { wrapper in + action(wrapper.child) + } + } +} + +extension ScrollView { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (UIScrollView) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints) { (view: ScrollWidget) in + action(view.scrollView) + } + } +} + +extension List { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (UITableView) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints) { + (view: WrapperWidget) in + action(view.child) + } + } +} + +extension NavigationSplitView { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (UISplitViewController) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints) { + (view: WrapperControllerWidget) in + action(view.child) + } + } +} + +extension Image { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (UIImageView) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints) { + (_: UIView, children: ImageChildren) in + let wrapper: WrapperWidget = children.imageWidget.into() + action(wrapper.child) + } + } +} diff --git a/Sources/UIKitBackend/UIKitBackend+Container.swift b/Sources/UIKitBackend/UIKitBackend+Container.swift index 5375a24aa4..e19d0b5546 100644 --- a/Sources/UIKitBackend/UIKitBackend+Container.swift +++ b/Sources/UIKitBackend/UIKitBackend+Container.swift @@ -2,7 +2,7 @@ import SwiftCrossUI import UIKit final class ScrollWidget: ContainerWidget { - private var scrollView = UIScrollView() + var scrollView = UIScrollView() private var childWidthConstraint: NSLayoutConstraint? private var childHeightConstraint: NSLayoutConstraint? diff --git a/Sources/WinUIBackend/InspectionModifiers.swift b/Sources/WinUIBackend/InspectionModifiers.swift new file mode 100644 index 0000000000..3706e67076 --- /dev/null +++ b/Sources/WinUIBackend/InspectionModifiers.swift @@ -0,0 +1,140 @@ +import WinUI +import SwiftCrossUI + +extension View { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (WinUI.FrameworkElement) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension SwiftCrossUI.Button { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (WinUI.Button) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension Text { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (WinUI.TextBlock) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension SwiftCrossUI.Slider { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (WinUI.Slider) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension Picker { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (WinUI.ComboBox) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension TextField { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (WinUI.TextBox) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension SwiftCrossUI.ScrollView { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (WinUI.ScrollViewer) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension List { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (WinUI.ListView) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension NavigationSplitView { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (WinUI.SplitView) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension SwiftCrossUI.Image { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (WinUI.Image) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints) { + (_: WinUI.FrameworkElement, children: ImageChildren) in + action(children.imageWidget.into()) + } + } +} + +extension HStack { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (WinUI.Canvas) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension VStack { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (WinUI.Canvas) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension ZStack { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (WinUI.Canvas) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension Group { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (WinUI.Canvas) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +} + +extension SwiftCrossUI.Shape { + public func inspect( + _ inspectionPoints: InspectionPoints = .onCreate, + _ action: @escaping @MainActor @Sendable (WinUI.Path) -> Void + ) -> some View { + InspectView(child: self, inspectionPoints: inspectionPoints, action: action) + } +}