├── Tests └── SkipUITests │ ├── Skip │ ├── skip.yml │ └── res │ │ ├── values │ │ └── strings.xml │ │ └── drawable │ │ └── battery_charging.xml │ ├── Resources │ ├── Assets.xcassets │ │ ├── Contents.json │ │ └── dumbbell.fill.symbolset │ │ │ └── Contents.json │ └── Localizable.xcstrings │ ├── XCSkipTests.swift │ └── CanvasTests.swift ├── Sources └── SkipUI │ ├── Skip │ ├── ItemPosition.kt │ ├── Redacted.kt │ ├── DragCancelledAnimation.kt │ ├── DetectReorder.kt │ ├── Reorderable.kt │ ├── Shadowed.kt │ └── ReorderableItem.kt │ ├── SkipUI │ ├── System │ │ ├── ContainerBackgroundPlacement.swift │ │ ├── Prominence.swift │ │ ├── HoverEffect.swift │ │ ├── BadgeProminence.swift │ │ ├── Visibility.swift │ │ ├── HoverPhase.swift │ │ ├── PlaceholderContentView.swift │ │ ├── EmptyModifier.swift │ │ ├── PopoverAttachmentAnchor.swift │ │ ├── ContentShapeKinds.swift │ │ ├── EditableCollectionContent.swift │ │ ├── BackgroundTask.swift │ │ ├── DynamicTypeSize.swift │ │ ├── EventModifiers.swift │ │ ├── LimitedAvailabilityConfiguration.swift │ │ ├── UserInterfaceSizeClass.swift │ │ ├── DynamicProperty.swift │ │ ├── DynamicViewContent.swift │ │ ├── Keyboard.swift │ │ ├── IndexViewStyle.swift │ │ ├── PageIndexViewStyle.swift │ │ ├── ProjectionTransform.swift │ │ ├── InterfaceOrientation.swift │ │ ├── KeyEquivalent.swift │ │ ├── TypesettingLanguage.swift │ │ ├── FileDialogBrowserOptions.swift │ │ └── PaletteSelectionEffect.swift │ ├── Text │ │ ├── TextSelectability.swift │ │ ├── SecureField.swift │ │ ├── TextInput.swift │ │ └── TextEditor.swift │ ├── Commands │ │ ├── DismissBehavior.swift │ │ ├── EditMode.swift │ │ ├── SubmitTriggers.swift │ │ ├── KeyboardShortcut.swift │ │ ├── SubmitLabel.swift │ │ └── EditActions.swift │ ├── Properties │ │ ├── StateObject.swift │ │ ├── ObservedObject.swift │ │ ├── Bindable.swift │ │ ├── Environment.swift │ │ ├── FocusState.swift │ │ └── State.swift │ ├── Color │ │ ├── ColorRenderingMode.swift │ │ ├── ColorMatrix.swift │ │ └── ColorSchemeContrast.swift │ ├── Layout │ │ ├── ContentMarginPlacement.swift │ │ ├── ContentMode.swift │ │ ├── Namespace.swift │ │ ├── MatchedGeometryProperties.swift │ │ ├── Axis.swift │ │ ├── HorizontalEdge.swift │ │ ├── VerticalEdge.swift │ │ ├── EdgeInsets.swift │ │ ├── SidebarRowSize.swift │ │ ├── Edge.swift │ │ ├── ProposedViewSize.swift │ │ ├── GeometryEffect.swift │ │ ├── GeometryReader.swift │ │ ├── VerticalAlignment.swift │ │ ├── Angle.swift │ │ ├── HorizontalAlignment.swift │ │ ├── GeometryProxy.swift │ │ ├── CoordinateSpace.swift │ │ ├── Alignment.swift │ │ ├── Unit.swift │ │ ├── Anchor.swift │ │ └── SafeArea.swift │ ├── Controls │ │ ├── ControlSize.swift │ │ └── Slider.swift │ ├── Compose │ │ ├── ComposeResult.swift │ │ ├── ComposeLambda.swift │ │ ├── Composer.swift │ │ ├── ComposeContext.swift │ │ ├── Renderable.swift │ │ ├── ComposeView.swift │ │ ├── ComposeModifiers.swift │ │ ├── ComposeBuilder.swift │ │ └── ComposeStateSaver.swift │ ├── App │ │ ├── App.swift │ │ └── Scene.swift │ ├── View │ │ ├── ViewPlacement.swift │ │ ├── ViewBuilder.swift │ │ ├── EquatableView.swift │ │ ├── AnyView.swift │ │ ├── Observable.swift │ │ ├── ViewModifier.swift │ │ ├── EvaluateOptions.swift │ │ └── View.swift │ ├── Components │ │ ├── EmptyView.swift │ │ ├── Divider.swift │ │ ├── Link.swift │ │ └── Spacer.swift │ ├── UIKit │ │ ├── UIColor.swift │ │ └── UIKeyboardType.swift │ ├── Graphics │ │ ├── BlendMode.swift │ │ ├── BackgroundProminence.swift │ │ ├── ShadowStyle.swift │ │ ├── Glass.swift │ │ ├── StrokeStyle.swift │ │ └── VectorArithmetic.swift │ ├── Animation │ │ └── MoveKeyframe.swift │ ├── BridgeSupport │ │ ├── CompletionHandler.swift │ │ ├── EnvironmentSupport.swift │ │ ├── StateSupport.swift │ │ └── UserNotificationsDelegateSupport.swift │ └── Containers │ │ ├── Group.swift │ │ ├── AnimatedContentArguments.swift │ │ ├── Form.swift │ │ └── ViewThatFits.swift │ └── Stub.swift ├── .github ├── workflows │ └── ci.yml └── release.yml ├── Package.swift └── .gitignore /Tests/SkipUITests/Skip/skip.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Tests/SkipUITests/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tests/SkipUITests/Skip/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | My Application 3 | Hello, World 4 | 5 | -------------------------------------------------------------------------------- /Sources/SkipUI/Skip/ItemPosition.kt: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 2 | package org.burnoutcrew.reorderable 3 | 4 | data class ItemPosition(val index: Int, val key: Any?) 5 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/ContainerBackgroundPlacement.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public struct ContainerBackgroundPlacement : Hashable { 6 | } 7 | 8 | #endif 9 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Text/TextSelectability.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public enum TextSelectability { 6 | case enabled 7 | case disabled 8 | } 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/Prominence.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public enum Prominence : Hashable { 6 | case standard 7 | case increased 8 | } 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Commands/DismissBehavior.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public enum DismissBehavior { 6 | case interactive 7 | case destructive 8 | } 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /Tests/SkipUITests/Resources/Assets.xcassets/dumbbell.fill.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "dumbbell.fill.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/HoverEffect.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public enum HoverEffect { 6 | case automatic 7 | case highlight 8 | case lift 9 | } 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Properties/StateObject.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | // For our purposes, State and StateObject act exactly the same 6 | public typealias StateObject = State 7 | 8 | #endif 9 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Properties/ObservedObject.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | // For our purposes, Bindable and ObservedObject act exactly the same 6 | public typealias ObservedObject = Bindable 7 | 8 | #endif 9 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/BadgeProminence.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public enum BadgeProminence : Hashable { 6 | case decreased 7 | case standard 8 | case increased 9 | } 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Color/ColorRenderingMode.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public enum ColorRenderingMode : Hashable { 6 | case nonLinear 7 | case linear 8 | case extendedLinear 9 | } 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Layout/ContentMarginPlacement.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public enum ContentMarginPlacement { 6 | case automatic 7 | case scrollContent 8 | case scrollIndicators 9 | } 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Layout/ContentMode.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public enum ContentMode : Int, Hashable, CaseIterable { 6 | case fit = 0 // For bridging 7 | case fill = 1 // For bridging 8 | } 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: skip-ui 2 | on: 3 | push: 4 | branches: [ main ] 5 | tags: "[0-9]+.[0-9]+.[0-9]+" 6 | schedule: 7 | - cron: '0 10 * * *' 8 | workflow_dispatch: 9 | pull_request: 10 | 11 | permissions: 12 | contents: write 13 | 14 | jobs: 15 | call-workflow: 16 | uses: skiptools/actions/.github/workflows/skip-framework.yml@v1 17 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/Visibility.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public enum Visibility : Int, Hashable, CaseIterable { 6 | case automatic = 0 // For bridging 7 | case visible = 1 // For bridging 8 | case hidden = 2 // For bridging 9 | } 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Controls/ControlSize.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public enum ControlSize : Int, CaseIterable, Hashable /*, Comparable */ { 6 | case mini 7 | case small 8 | case regular 9 | case large 10 | case extraLarge 11 | } 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Commands/EditMode.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public enum EditMode : Hashable { 6 | case inactive 7 | case transient 8 | case active 9 | 10 | public var isEditing: Bool { 11 | return self != .inactive 12 | } 13 | } 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/HoverPhase.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if !SKIP 5 | #if canImport(CoreGraphics) 6 | import struct CoreGraphics.CGPoint 7 | #endif 8 | #endif 9 | 10 | public enum HoverPhase : Equatable { 11 | case active(CGPoint) 12 | case ended 13 | } 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Compose/ComposeResult.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | 4 | /// The result of composing content. 5 | /// 6 | /// Reserved for future use. Having a return value also expands recomposition scope. See `ComposeBuilder` for details. 7 | public struct ComposeResult { 8 | public static let ok = ComposeResult() 9 | } 10 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Layout/Namespace.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public struct Namespace /* : DynamicProperty */ { 6 | public init() { 7 | } 8 | 9 | public var wrappedValue: Namespace.ID { 10 | fatalError() 11 | } 12 | 13 | public struct ID : Hashable { 14 | } 15 | } 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | authors: 6 | - skipbuilder 7 | categories: 8 | - title: Breaking Change 9 | labels: 10 | - Semver-Major 11 | - breaking-change 12 | - title: Enhancement 13 | labels: 14 | - Semver-Minor 15 | - enhancement 16 | - title: Other Changes 17 | labels: 18 | - "*" 19 | 20 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/App/App.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | @available(*, unavailable) 6 | public protocol App { 7 | // associatedtype Body : Scene 8 | // @SceneBuilder @MainActor var body: Self.Body { get } 9 | // @MainActor init() 10 | // @MainActor public static func main() { fatalError() } 11 | } 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Compose/ComposeLambda.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if SKIP 4 | import androidx.compose.runtime.Composable 5 | 6 | /// Used by the transpiler in place of SkipLib's standard `linvoke` when dealing with Composable code. 7 | @Composable public func linvokeComposable(l: @Composable () -> R) -> R { 8 | return l() 9 | } 10 | #endif 11 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Commands/SubmitTriggers.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public struct SubmitTriggers : OptionSet { 6 | public let rawValue: Int 7 | 8 | public init(rawValue: Int) { 9 | self.rawValue = rawValue 10 | } 11 | 12 | public static let text = SubmitTriggers(rawValue: 1 << 0) // For bridging 13 | public static let search = SubmitTriggers(rawValue: 1 << 1) // For bridging 14 | } 15 | 16 | #endif 17 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/View/ViewPlacement.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if SKIP 4 | 5 | /// Allow views to specialize based on their placement. 6 | struct ViewPlacement: RawRepresentable, OptionSet { 7 | let rawValue: Int 8 | 9 | static let listItem = ViewPlacement(rawValue: 1) 10 | static let systemTextColor = ViewPlacement(rawValue: 2) 11 | static let onPrimaryColor = ViewPlacement(rawValue: 4) 12 | static let toolbar = ViewPlacement(rawValue: 8) 13 | } 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/PlaceholderContentView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | /* 4 | /// A placeholder used to construct an inline modifier, transition, or other 5 | /// helper type. 6 | /// 7 | /// You don't use this type directly. Instead SkipUI creates this type on 8 | /// your behalf. 9 | @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) 10 | public struct PlaceholderContentView : View { 11 | 12 | public typealias Body = NeverView 13 | public var body: Body { fatalError() } 14 | } 15 | */ 16 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Components/EmptyView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if SKIP 5 | import androidx.compose.runtime.Composable 6 | #endif 7 | 8 | // SKIP @bridge 9 | public struct EmptyView: View, Renderable { 10 | // SKIP @bridge 11 | public init() { 12 | } 13 | 14 | #if SKIP 15 | @Composable override func Render(context: ComposeContext) { 16 | } 17 | #else 18 | public var body: some View { 19 | stubView() 20 | } 21 | #endif 22 | } 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Layout/MatchedGeometryProperties.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public struct MatchedGeometryProperties : OptionSet { 6 | public let rawValue: Int 7 | 8 | public init(rawValue: Int) { 9 | self.rawValue = rawValue 10 | } 11 | 12 | public static let position = MatchedGeometryProperties(rawValue: 1 << 0) 13 | public static let size = MatchedGeometryProperties(rawValue: 1 << 1) 14 | public static let frame = MatchedGeometryProperties(rawValue: 1 << 2) 15 | } 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/EmptyModifier.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if SKIP 5 | import androidx.compose.runtime.Composable 6 | import skip.model.StateTracking 7 | #endif 8 | 9 | // SKIP @bridge 10 | public struct EmptyModifier : ViewModifier { 11 | public static let identity: EmptyModifier = EmptyModifier() 12 | 13 | // SKIP @bridge 14 | public init() { 15 | } 16 | 17 | #if SKIP 18 | public func body(content: Content) -> some View { 19 | content 20 | } 21 | #endif 22 | } 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/View/ViewBuilder.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | // Note: @ViewBuilder support is built into the Skip transpiler. 4 | // This file does not need SKIP support. This stub is maintained 5 | // to allow this package to compile in Swift. 6 | 7 | #if !SKIP_BRIDGE 8 | #if !SKIP 9 | 10 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 11 | @resultBuilder public struct ViewBuilder { 12 | public static func buildBlock(_ content: Content) -> Content{ 13 | fatalError() 14 | } 15 | } 16 | 17 | #endif 18 | #endif 19 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/UIKit/UIColor.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if !SKIP 5 | import struct CoreGraphics.CGFloat 6 | #endif 7 | 8 | // SKIP @bridge 9 | public final class UIColor { 10 | let red: CGFloat 11 | let green: CGFloat 12 | let blue: CGFloat 13 | let alpha: CGFloat 14 | 15 | // SKIP @bridge 16 | public init(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) { 17 | self.red = red 18 | self.green = green 19 | self.blue = blue 20 | self.alpha = alpha 21 | } 22 | } 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/PopoverAttachmentAnchor.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | /* 4 | import struct CoreGraphics.CGRect 5 | 6 | /// An attachment anchor for a popover. 7 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 8 | public enum PopoverAttachmentAnchor { 9 | 10 | /// The anchor point for the popover relative to the source's frame. 11 | case rect(Anchor.Source) 12 | 13 | /// The anchor point for the popover expressed as a unit point that 14 | /// describes possible alignments relative to a SkipUI view. 15 | case point(UnitPoint) 16 | } 17 | */ 18 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Properties/Bindable.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | // Model Bindable as a class rather than struct to avoid copy overhead on mutation 6 | public final class Bindable { 7 | public init(_ wrappedValue: Value) { 8 | self.wrappedValue = wrappedValue 9 | } 10 | 11 | public init(wrappedValue: Value) { 12 | self.wrappedValue = wrappedValue 13 | } 14 | 15 | public var wrappedValue: Value 16 | 17 | public var projectedValue: Bindable { 18 | return Bindable(wrappedValue: wrappedValue) 19 | } 20 | } 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Properties/Environment.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | // Model Environment as a class rather than struct to mutate by reference and avoid copy overhead 6 | public final class Environment where Value: Any { 7 | public init() { 8 | } 9 | 10 | public init(wrappedValue: Value) { 11 | self.wrappedValue = wrappedValue 12 | } 13 | 14 | public var wrappedValue: Value! 15 | 16 | public var projectedValue: Binding { 17 | return Binding(get: { self.wrappedValue }, set: { self.wrappedValue = $0 }) 18 | } 19 | } 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Graphics/BlendMode.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public enum BlendMode : Hashable { 6 | case normal 7 | case multiply 8 | case screen 9 | case overlay 10 | case darken 11 | case lighten 12 | case colorDodge 13 | case colorBurn 14 | case softLight 15 | case hardLight 16 | case difference 17 | case exclusion 18 | case hue 19 | case saturation 20 | case color 21 | case luminosity 22 | case sourceAtop 23 | case destinationOver 24 | case destinationOut 25 | case plusDarker 26 | case plusLighter 27 | } 28 | 29 | #endif 30 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Animation/MoveKeyframe.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | /* 4 | /// A keyframe that immediately moves to the given value without interpolating. 5 | @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) 6 | public struct MoveKeyframe : KeyframeTrackContent where Value : Animatable { 7 | 8 | /// Creates a new keyframe using the given value. 9 | /// 10 | /// - Parameters: 11 | /// - to: The value of the keyframe. 12 | public init(_ to: Value) { fatalError() } 13 | 14 | public typealias Value = Value 15 | public typealias Body = MoveKeyframe 16 | public var body: Body { fatalError() } 17 | } 18 | */ 19 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/ContentShapeKinds.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public struct ContentShapeKinds : OptionSet { 6 | public let rawValue: Int 7 | 8 | public init(rawValue: Int) { 9 | self.rawValue = rawValue 10 | } 11 | 12 | public static let interaction = ContentShapeKinds(rawValue: 1 << 0) 13 | public static let dragPreview = ContentShapeKinds(rawValue: 1 << 1) 14 | public static let contextMenuPreview = ContentShapeKinds(rawValue: 1 << 2) 15 | public static let hoverEffect = ContentShapeKinds(rawValue: 1 << 3) 16 | public static let accessibility = ContentShapeKinds(rawValue: 1 << 4) 17 | } 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/EditableCollectionContent.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | /* 4 | /// An opaque wrapper view that adds editing capabilities to a row in a list. 5 | /// 6 | /// You don't use this type directly. Instead SkipUI creates this type on 7 | /// your behalf. 8 | @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) 9 | public struct EditableCollectionContent { 10 | } 11 | 12 | @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) 13 | extension EditableCollectionContent : View where Content : View { 14 | 15 | @MainActor public var body: some View { get { return stubView() } } 16 | 17 | // public typealias Body = some View 18 | } 19 | */ 20 | -------------------------------------------------------------------------------- /Sources/SkipUI/Stub.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if !SKIP 5 | 6 | /// No-op 7 | func stub() -> T { 8 | fatalError("stub") 9 | } 10 | 11 | // SkipUI.kt:13:14 'Nothing' return type can't be specified with type alias 12 | public typealias Nothing = Never 13 | 14 | /// No-op 15 | func stubView() -> some View { 16 | return EmptyView() 17 | } 18 | 19 | /// No-op 20 | @usableFromInline func never() -> Nothing { 21 | stub() 22 | } 23 | 24 | public typealias NeverView = Never 25 | 26 | /// A stub view 27 | public struct StubView : View { 28 | public typealias Body = Never 29 | public var body: Body { 30 | fatalError() 31 | } 32 | } 33 | 34 | #endif 35 | #endif 36 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Layout/Axis.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public enum Axis : Int, Hashable, CaseIterable { 6 | case horizontal = 1 // For bridging 7 | case vertical = 2 // For bridging 8 | 9 | public struct Set : OptionSet, Hashable { 10 | public let rawValue: Int 11 | 12 | public init(rawValue: Int) { 13 | self.rawValue = rawValue 14 | } 15 | 16 | public static let horizontal: Axis.Set = Axis.Set(Axis.horizontal) 17 | public static let vertical: Axis.Set = Axis.Set(Axis.vertical) 18 | 19 | public init(_ axis: Axis) { 20 | self.rawValue = axis.rawValue 21 | } 22 | } 23 | } 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/View/EquatableView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if SKIP 5 | import androidx.compose.runtime.Composable 6 | #endif 7 | 8 | // SKIP @bridge 9 | public struct EquatableView : View { 10 | public let content: any View 11 | 12 | // SKIP @bridge 13 | public init(content: any View) { 14 | self.content = content 15 | } 16 | 17 | #if SKIP 18 | @Composable override func Evaluate(context: ComposeContext, options: Int) -> kotlin.collections.List { 19 | return content.Evaluate(context: context, options: options) 20 | } 21 | #else 22 | public var body: some View { 23 | stubView() 24 | } 25 | #endif 26 | } 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/BackgroundTask.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public struct BackgroundTask { 6 | @available(*, unavailable) 7 | public static var urlSession: BackgroundTask { get { fatalError() } } 8 | 9 | @available(*, unavailable) 10 | public static func urlSession(_ identifier: String) -> BackgroundTask { fatalError() } 11 | 12 | @available(*, unavailable) 13 | public static func urlSession(matching: @escaping (String) -> Bool) -> BackgroundTask { fatalError() } 14 | 15 | @available(*, unavailable) 16 | public static func appRefresh(_ identifier: String) -> BackgroundTask { fatalError() } 17 | } 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/DynamicTypeSize.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public enum DynamicTypeSize : Int, Hashable, Comparable, CaseIterable { 6 | case xSmall 7 | case small 8 | case medium 9 | case large 10 | case xLarge 11 | case xxLarge 12 | case xxxLarge 13 | case accessibility1 14 | case accessibility2 15 | case accessibility3 16 | case accessibility4 17 | case accessibility5 18 | 19 | public var isAccessibilitySize: Bool { 20 | return rawValue >= DynamicTypeSize.accessibility1.rawValue 21 | } 22 | 23 | public static func < (a: DynamicTypeSize, b: DynamicTypeSize) -> Bool { 24 | return a.rawValue < b.rawValue 25 | } 26 | } 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/View/AnyView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if SKIP 5 | import androidx.compose.runtime.Composable 6 | #endif 7 | 8 | public struct AnyView : View { 9 | private let view: any View 10 | 11 | public init(_ view: any View) { 12 | self.view = view 13 | } 14 | 15 | public init(erasing view: any View) { 16 | self.view = view 17 | } 18 | 19 | #if SKIP 20 | @Composable override func Evaluate(context: ComposeContext, options: Int) -> kotlin.collections.List { 21 | return view.Evaluate(context: context, options: options) 22 | } 23 | #else 24 | public var body: some View { 25 | stubView() 26 | } 27 | #endif 28 | } 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/EventModifiers.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | @frozen public struct EventModifiers : OptionSet, Hashable { 6 | public let rawValue: Int 7 | 8 | public init(rawValue: Int) { 9 | self.rawValue = rawValue 10 | } 11 | 12 | public static let capsLock = EventModifiers(rawValue: 1) 13 | public static let shift = EventModifiers(rawValue: 2) 14 | public static let control = EventModifiers(rawValue: 4) 15 | public static let option = EventModifiers(rawValue: 8) 16 | public static let command = EventModifiers(rawValue: 16) 17 | public static let numericPad = EventModifiers(rawValue: 32) 18 | public static let all = EventModifiers(rawValue: 63) 19 | } 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/LimitedAvailabilityConfiguration.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | /* 4 | /// A type-erased widget configuration. 5 | /// 6 | /// You don't use this type directly. Instead SkipUI creates this type on 7 | /// your behalf. 8 | @available(iOS 16.1, macOS 13.0, watchOS 9.1, *) 9 | @available(tvOS, unavailable) 10 | @frozen public struct LimitedAvailabilityConfiguration : WidgetConfiguration { 11 | 12 | /// The type of widget configuration representing the body of 13 | /// this configuration. 14 | /// 15 | /// When you create a custom widget, Swift infers this type from your 16 | /// implementation of the required `body` property. 17 | public typealias Body = NeverView 18 | public var body: Body { fatalError() } 19 | } 20 | */ 21 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Layout/HorizontalEdge.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public enum HorizontalEdge : Int, CaseIterable, Codable, Hashable { 6 | case leading = 1 7 | case trailing = 2 8 | 9 | public struct Set : OptionSet { 10 | public let rawValue: Int 11 | 12 | public init(rawValue: Int) { 13 | self.rawValue = rawValue 14 | } 15 | 16 | public static let leading: HorizontalEdge.Set = HorizontalEdge.Set(rawValue: 1) 17 | public static let trailing: HorizontalEdge.Set = HorizontalEdge.Set(rawValue: 2) 18 | public static let all: HorizontalEdge.Set = HorizontalEdge.Set(rawValue: 3) 19 | 20 | public init(_ edge: HorizontalEdge) { 21 | self.rawValue = edge.rawValue 22 | } 23 | } 24 | } 25 | 26 | #endif 27 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Layout/VerticalEdge.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public enum VerticalEdge : Int, Hashable, CaseIterable, Codable { 6 | case top = 1 // For bridging 7 | case bottom = 2 // For bridging 8 | 9 | public struct Set : OptionSet { 10 | public let rawValue: Int 11 | 12 | public init(rawValue: Int) { 13 | self.rawValue = rawValue 14 | } 15 | 16 | public static let top: VerticalEdge.Set = VerticalEdge.Set(VerticalEdge.top) 17 | public static let bottom: VerticalEdge.Set = VerticalEdge.Set(VerticalEdge.bottom) 18 | public static let all: VerticalEdge.Set = VerticalEdge.Set(rawValue: 3) 19 | 20 | public init(_ e: VerticalEdge) { 21 | self.init(rawValue: e.rawValue) 22 | } 23 | } 24 | } 25 | 26 | #endif 27 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/UserInterfaceSizeClass.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if SKIP 5 | import androidx.window.core.layout.WindowHeightSizeClass 6 | import androidx.window.core.layout.WindowWidthSizeClass 7 | #endif 8 | 9 | public enum UserInterfaceSizeClass: Int, Hashable { 10 | case compact = 1 // For bridging 11 | case regular = 2 // For bridging 12 | 13 | #if SKIP 14 | public static func fromWindowHeightSizeClass(_ sizeClass: WindowHeightSizeClass) -> UserInterfaceSizeClass { 15 | return sizeClass == WindowHeightSizeClass.COMPACT ? .compact : .regular 16 | } 17 | 18 | public static func fromWindowWidthSizeClass(_ sizeClass: WindowWidthSizeClass) -> UserInterfaceSizeClass { 19 | return sizeClass == WindowWidthSizeClass.COMPACT ? .compact : .regular 20 | } 21 | #endif 22 | } 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /Tests/SkipUITests/Skip/res/drawable/battery_charging.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | 17 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Commands/KeyboardShortcut.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public struct KeyboardShortcut : Hashable { 6 | public enum Localization : Hashable { 7 | case automatic 8 | case withoutMirroring 9 | case custom 10 | } 11 | 12 | public static let defaultAction = KeyboardShortcut(.return, modifiers: []) 13 | public static let cancelAction = KeyboardShortcut(.escape, modifiers: []) 14 | 15 | public let key: KeyEquivalent 16 | public let modifiers: EventModifiers 17 | public let localization: KeyboardShortcut.Localization 18 | 19 | public init(_ key: KeyEquivalent, modifiers: EventModifiers = .command, localization: KeyboardShortcut.Localization = .automatic) { 20 | self.key = key 21 | self.modifiers = modifiers 22 | self.localization = localization 23 | } 24 | } 25 | 26 | #endif 27 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Layout/EdgeInsets.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if SKIP 5 | import androidx.compose.foundation.layout.PaddingValues 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.unit.dp 8 | #elseif canImport(CoreGraphics) 9 | import struct CoreGraphics.CGFloat 10 | #endif 11 | 12 | public struct EdgeInsets : Equatable { 13 | public var top: CGFloat 14 | public var leading: CGFloat 15 | public var bottom: CGFloat 16 | public var trailing: CGFloat 17 | 18 | public init(top: CGFloat = 0.0, leading: CGFloat = 0.0, bottom: CGFloat = 0.0, trailing: CGFloat = 0.0) { 19 | self.top = top 20 | self.leading = leading 21 | self.bottom = bottom 22 | self.trailing = trailing 23 | } 24 | 25 | #if SKIP 26 | @Composable func asPaddingValues() -> PaddingValues { 27 | return PaddingValues(start: leading.dp, top: top.dp, end: trailing.dp, bottom: bottom.dp) 28 | } 29 | #endif 30 | } 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Layout/SidebarRowSize.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | /* 4 | /// The standard sizes of sidebar rows. 5 | /// 6 | /// On macOS, sidebar rows have three different sizes: small, medium, and large. 7 | /// The size is primarily controlled by the current users' "Sidebar Icon Size" 8 | /// in Appearance settings, and applies to all applications. 9 | /// 10 | /// On all other platforms, the only supported sidebar size is `.medium`. 11 | /// 12 | /// This size can be read or written in the environment using 13 | /// `EnvironmentValues.sidebarRowSize`. 14 | @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) 15 | public enum SidebarRowSize : Sendable { 16 | 17 | /// The standard "small" row size 18 | case small 19 | 20 | /// The standard "medium" row size 21 | case medium 22 | 23 | /// The standard "large" row size 24 | case large 25 | 26 | 27 | } 28 | 29 | @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) 30 | extension SidebarRowSize : Hashable { 31 | } 32 | */ 33 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/DynamicProperty.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | /* 4 | /// An interface for a stored variable that updates an external property of a 5 | /// view. 6 | /// 7 | /// The view gives values to these properties prior to recomputing the view's 8 | /// ``View/body-swift.property``. 9 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 10 | public protocol DynamicProperty { 11 | 12 | /// Updates the underlying value of the stored value. 13 | /// 14 | /// SkipUI calls this function before rendering a view's 15 | /// ``View/body-swift.property`` to ensure the view has the most recent 16 | /// value. 17 | mutating func update() 18 | } 19 | 20 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 21 | extension DynamicProperty { 22 | 23 | /// Updates the underlying value of the stored value. 24 | /// 25 | /// SkipUI calls this function before rendering a view's 26 | /// ``View/body-swift.property`` to ensure the view has the most recent 27 | /// value. 28 | public mutating func update() { fatalError() } 29 | } 30 | */ 31 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/DynamicViewContent.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | /* 4 | /// No-op 5 | func stubDynamicViewContent() -> some DynamicViewContent { 6 | //return never() // raises warning: “A call to a never-returning function” 7 | struct NeverDynamicViewContent : DynamicViewContent { 8 | typealias Body = Never 9 | var body: Body { fatalError() } 10 | typealias Data = [Never] 11 | var data: Data { fatalError() } 12 | } 13 | return NeverDynamicViewContent() 14 | } 15 | 16 | /// A type of view that generates views from an underlying collection of data. 17 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 18 | public protocol DynamicViewContent : View { 19 | 20 | /// The type of the underlying collection of data. 21 | associatedtype Data : Collection 22 | 23 | /// The collection of underlying data. 24 | var data: Self.Data { get } 25 | } 26 | 27 | extension Never : DynamicViewContent { 28 | public typealias Data = [Never] 29 | 30 | /// The collection of underlying data. 31 | public var data: Self.Data { fatalError() } 32 | } 33 | */ 34 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Layout/Edge.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public enum Edge : Int, Hashable, CaseIterable { 6 | case top = 1 // For bridging 7 | case leading = 2 // For bridging 8 | case bottom = 4 // For bridging 9 | case trailing = 8 // For bridging 10 | 11 | public struct Set : OptionSet, Equatable { 12 | public let rawValue: Int 13 | 14 | public init(rawValue: Int) { 15 | self.rawValue = rawValue 16 | } 17 | 18 | public static let top: Edge.Set = Edge.Set(Edge.top) 19 | public static let leading: Edge.Set = Edge.Set(Edge.leading) 20 | public static let bottom: Edge.Set = Edge.Set(Edge.bottom) 21 | public static let trailing: Edge.Set = Edge.Set(Edge.trailing) 22 | 23 | public static let all: Edge.Set = Edge.Set(rawValue: 15) 24 | public static let horizontal: Edge.Set = Edge.Set(rawValue: 10) 25 | public static let vertical: Edge.Set = Edge.Set(rawValue: 5) 26 | 27 | public init(_ e: Edge) { 28 | self.rawValue = e.rawValue 29 | } 30 | } 31 | } 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Layout/ProposedViewSize.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if !SKIP 5 | #if canImport(CoreGraphics) 6 | import struct CoreGraphics.CGFloat 7 | import struct CoreGraphics.CGSize 8 | #endif 9 | #endif 10 | 11 | public struct ProposedViewSize : Equatable { 12 | public var width: CGFloat? 13 | public var height: CGFloat? 14 | 15 | public static let zero: ProposedViewSize = ProposedViewSize(width: 0.0, height: 0.0) 16 | public static let unspecified: ProposedViewSize = ProposedViewSize(width: nil, height: nil) 17 | public static let infinity: ProposedViewSize = ProposedViewSize(width: .infinity, height: .infinity) 18 | 19 | public init(width: CGFloat?, height: CGFloat?) { 20 | self.width = width 21 | self.height = height 22 | } 23 | 24 | public init(_ size: CGSize) { 25 | self.init(width: size.width, height: size.height) 26 | } 27 | 28 | public func replacingUnspecifiedDimensions(by size: CGSize = CGSize(width: 10.0, height: 10.0)) -> CGSize { 29 | return CGSize(width: width == nil ? size.width : width!, height: height == nil ? size.height : height!) 30 | } 31 | } 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Components/Divider.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if SKIP 5 | import androidx.compose.foundation.layout.fillMaxHeight 6 | import androidx.compose.foundation.layout.width 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.unit.dp 10 | #endif 11 | 12 | // SKIP @bridge 13 | public struct Divider : View, Renderable { 14 | // SKIP @bridge 15 | public init() { 16 | } 17 | 18 | #if SKIP 19 | @Composable override func Render(context: ComposeContext) { 20 | let dividerColor = Color.separator.colorImpl() 21 | let modifier: Modifier 22 | switch EnvironmentValues.shared._layoutAxis { 23 | case .horizontal: 24 | // If in a horizontal container, create a vertical divider 25 | modifier = Modifier.width(1.dp).then(context.modifier.fillHeight()) 26 | case .vertical, nil: 27 | modifier = context.modifier 28 | } 29 | androidx.compose.material3.Divider(modifier: modifier, color: dividerColor) 30 | } 31 | #else 32 | public var body: some View { 33 | stubView() 34 | } 35 | #endif 36 | } 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Compose/Composer.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if SKIP 4 | import androidx.compose.runtime.Composable 5 | 6 | /// Mechanism for a parent to change how a child view is composed. 7 | /// 8 | /// Composers are escaping, meaning that if the internal content needs to recompose, the calling context will also recompose. 9 | public class Composer { 10 | private let compose: (@Composable (View, (Bool) -> ComposeContext) -> ComposeResult)? 11 | 12 | /// Optionally provide a compose block to execute instead of subclassing. 13 | /// 14 | /// - Note: This is a separate method from the default constructor rather than giving `compose` a default value to work around Kotlin runtime 15 | /// crashes related to using composable closures. 16 | init(compose: @Composable (View, (Bool) -> ComposeContext) -> ComposeResult) { 17 | self.compose = compose 18 | } 19 | 20 | init() { 21 | self.compose = nil 22 | } 23 | 24 | @Composable public func Compose(view: View, context: (Bool) -> ComposeContext) -> ComposeResult { 25 | if let compose { 26 | return compose(view, context) 27 | } else { 28 | return ComposeResult.ok 29 | } 30 | } 31 | } 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Layout/GeometryEffect.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | /* 4 | import struct CoreGraphics.CGSize 5 | 6 | /// An effect that changes the visual appearance of a view, largely without 7 | /// changing its ancestors or descendants. 8 | /// 9 | /// The only change the effect makes to the view's ancestors and descendants is 10 | /// to change the coordinate transform to and from them. 11 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 12 | public protocol GeometryEffect : Animatable, ViewModifier where Self.Body == Never { 13 | 14 | /// Returns the current value of the effect. 15 | func effectValue(size: CGSize) -> ProjectionTransform 16 | } 17 | 18 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 19 | extension GeometryEffect { 20 | 21 | /// Returns an effect that produces the same geometry transform as this 22 | /// effect, but only applies the transform while rendering its view. 23 | /// 24 | /// Use this method to disable layout changes during transitions. The view 25 | /// ignores the transform returned by this method while the view is 26 | /// performing its layout calculations. 27 | // public func ignoredByLayout() -> _IgnoredByLayoutEffect { fatalError() } 28 | } 29 | */ 30 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Commands/SubmitLabel.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if SKIP 5 | import androidx.compose.ui.text.input.ImeAction 6 | #endif 7 | 8 | public enum SubmitLabel: Int { 9 | case done = 0 // For bridging 10 | case go = 1 // For bridging 11 | case send = 2 // For bridging 12 | case join = 3 // For bridging 13 | case route = 4 // For bridging 14 | case search = 5 // For bridging 15 | case `return` = 6 // For bridging 16 | case next = 7 // For bridging 17 | case `continue` = 8 // For bridging 18 | 19 | #if SKIP 20 | func asImeAction() -> ImeAction { 21 | switch self { 22 | case .done: 23 | return ImeAction.Done 24 | case .go: 25 | return ImeAction.Go 26 | case .send: 27 | return ImeAction.Send 28 | case .join: 29 | return ImeAction.Go 30 | case .route: 31 | return ImeAction.Go 32 | case .search: 33 | return ImeAction.Search 34 | case .return: 35 | return ImeAction.Default 36 | case .next: 37 | return ImeAction.Next 38 | case .continue: 39 | return ImeAction.Next 40 | } 41 | } 42 | #endif 43 | } 44 | 45 | #endif 46 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/BridgeSupport/CompletionHandler.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | /// Generic completion handler to take the place of passing a completion closure to a bridged closure, as we 6 | /// do not yet supporting bridging closure arguments to closures. 7 | // SKIP @bridge 8 | public final class CompletionHandler : @unchecked Sendable { 9 | private let handler: () -> Void 10 | 11 | public init(_ handler: @escaping () -> Void) { 12 | self.handler = handler 13 | } 14 | 15 | // SKIP @bridge 16 | public func run() { 17 | handler() 18 | } 19 | 20 | // SKIP @bridge 21 | public var onCancel: (() -> Void)? 22 | } 23 | 24 | /// Generic completion handler to take the place of passing a completion closure to a bridged closure, as we 25 | /// do not yet supporting bridging closure arguments to closures. 26 | // SKIP @bridge 27 | public final class ValueCompletionHandler : @unchecked Sendable { 28 | private let handler: (Any?) -> Void 29 | 30 | public init(_ handler: @escaping (Any?) -> Void) { 31 | self.handler = handler 32 | } 33 | 34 | // SKIP @bridge 35 | public func run(_ value: Any?) { 36 | handler(value) 37 | } 38 | 39 | // SKIP @bridge 40 | public var onCancel: (() -> Void)? 41 | } 42 | 43 | #endif 44 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Text/SecureField.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | import Foundation 5 | #if SKIP 6 | import androidx.compose.runtime.Composable 7 | #endif 8 | 9 | public struct SecureField : View, Renderable { 10 | let textField: TextField 11 | 12 | public init(text: Binding, prompt: Text? = nil, @ViewBuilder label: () -> any View) { 13 | textField = TextField(text: text, prompt: prompt, isSecure: true, label: label) 14 | } 15 | 16 | public init(_ title: String, text: Binding, prompt: Text? = nil) { 17 | self.init(text: text, prompt: prompt, label: { Text(verbatim: title) }) 18 | } 19 | 20 | public init(_ titleKey: LocalizedStringKey, text: Binding, prompt: Text? = nil) { 21 | self.init(text: text, prompt: prompt, label: { Text(titleKey) }) 22 | } 23 | 24 | public init(_ titleResource: LocalizedStringResource, text: Binding, prompt: Text? = nil) { 25 | self.init(text: text, prompt: prompt, label: { Text(titleResource) }) 26 | } 27 | 28 | #if SKIP 29 | @Composable override func Render(context: ComposeContext) { 30 | textField.Compose(context: context) 31 | } 32 | #else 33 | public var body: some View { 34 | stubView() 35 | } 36 | #endif 37 | } 38 | 39 | #endif 40 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Containers/Group.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if SKIP 5 | import androidx.compose.runtime.Composable 6 | #endif 7 | 8 | // SKIP @bridge 9 | public struct Group : View { 10 | let content: ComposeBuilder 11 | 12 | public init(@ViewBuilder content: () -> any View) { 13 | self.content = ComposeBuilder.from(content) 14 | } 15 | 16 | @available(*, unavailable) 17 | public init(subviews view: any View, @ViewBuilder transform: @escaping (Any /* SubviewsCollection */) -> any View) { 18 | self.content = ComposeBuilder(view: EmptyView()) 19 | } 20 | 21 | @available(*, unavailable) 22 | public init(sections view: any View, @ViewBuilder transform: @escaping (Any /* SectionCollection */) -> any View) { 23 | self.content = ComposeBuilder(view: EmptyView()) 24 | } 25 | 26 | // SKIP @bridge 27 | public init(bridgedContent: any View) { 28 | self.content = ComposeBuilder.from { bridgedContent } 29 | } 30 | 31 | #if SKIP 32 | @Composable override func Evaluate(context: ComposeContext, options: Int) -> kotlin.collections.List { 33 | return content.Evaluate(context: context, options: options) 34 | } 35 | #else 36 | public var body: some View { 37 | stubView() 38 | } 39 | #endif 40 | } 41 | 42 | #endif 43 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/Keyboard.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if SKIP 4 | import androidx.compose.foundation.layout.WindowInsets 5 | import androidx.compose.runtime.State 6 | import androidx.compose.ui.focus.FocusManager 7 | import androidx.compose.ui.geometry.Offset 8 | import androidx.compose.ui.input.nestedscroll.NestedScrollConnection 9 | import androidx.compose.ui.input.nestedscroll.NestedScrollSource 10 | import androidx.compose.ui.platform.SoftwareKeyboardController 11 | import androidx.compose.ui.unit.Density 12 | 13 | /// Use to dismiss the keyboard on scroll. 14 | /// 15 | /// - Seealso: `Modifier.scrollDismissesKeyboardMode(_:)` 16 | struct KeyboardDismissingNestedScrollConnection: NestedScrollConnection { 17 | let keyboardController: State 18 | let focusManager: State 19 | let imeInsets: WindowInsets 20 | let density: Density 21 | 22 | override func onPreScroll(available: Offset, source: NestedScrollSource) -> Offset { 23 | if source == NestedScrollSource.Drag { 24 | let keyboardIsVisible = imeInsets.getBottom(density) > 0 25 | if keyboardIsVisible { 26 | keyboardController.value?.hide() 27 | focusManager.value?.clearFocus() 28 | } 29 | } 30 | return Offset.Zero 31 | } 32 | } 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/BridgeSupport/EnvironmentSupport.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | /// Support for bridged`SwiftUI.@Environment`. 6 | /// 7 | /// The Compose side manages object lifecycles, so we hold references to the native value. 8 | /// Environment values are not necessarily bridged or bridgable, so we use an opaque pointer. 9 | /// This support object is placed in the Compose environment. 10 | // SKIP @bridge 11 | public final class EnvironmentSupport { 12 | /// Supply a Swift pointer to an object that holds the environment value and a block to release the object on finalize. 13 | // SKIP @bridge 14 | public init(valueHolder: Int64) { 15 | self.valueHolder = valueHolder 16 | self.builtinValue = nil 17 | } 18 | 19 | // SKIP @bridge 20 | public init(builtinValue: Any?) { 21 | self.builtinValue = builtinValue 22 | self.valueHolder = Int64(0) 23 | } 24 | 25 | #if SKIP 26 | deinit { 27 | if valueHolder != Int64(0) { 28 | valueHolder = Swift_release(valueHolder) 29 | } 30 | } 31 | 32 | /// - Seealso `SkipSwiftUI.Environment` 33 | // SKIP EXTERN 34 | private func Swift_release(Swift_valueHolder: Int64) -> Int64 35 | #endif 36 | 37 | 38 | // SKIP @bridge 39 | public private(set) var valueHolder: Int64 40 | 41 | // SKIP @bridge 42 | public let builtinValue: Any? 43 | } 44 | 45 | #endif 46 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Compose/ComposeContext.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if SKIP 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.Stable 6 | import androidx.compose.runtime.saveable.Saver 7 | import androidx.compose.ui.Modifier 8 | 9 | /// Context to provide modifiers, etc to composables. 10 | /// 11 | /// This type is often used as an argument to internal `@Composable` functions and is not mutated by reference, so mark `@Stable` 12 | /// to avoid excessive recomposition. 13 | @Stable public struct ComposeContext: Equatable{ 14 | /// Modifiers to apply. 15 | public var modifier: Modifier = Modifier 16 | 17 | /// Mechanism for a parent view to change how a child view is composed. 18 | public var composer: Composer? 19 | 20 | /// Use in conjunction with `rememberSaveable` to store view state. 21 | public var stateSaver: Saver = ComposeStateSaver() 22 | 23 | /// The context to pass to child content of a container view. 24 | /// 25 | /// By default, modifiers and the `composer` are reset for child content. 26 | public func content(modifier: Modifier = Modifier, composer: Composer? = nil, stateSaver: Saver? = nil) -> ComposeContext { 27 | var context = self 28 | context.modifier = modifier 29 | context.composer = composer 30 | context.stateSaver = stateSaver ?? self.stateSaver 31 | return context 32 | } 33 | } 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "skip-ui", 6 | platforms: [.iOS(.v16), .macOS(.v13), .tvOS(.v16), .watchOS(.v9), .macCatalyst(.v16)], 7 | products: [ 8 | .library(name: "SkipUI", targets: ["SkipUI"]), 9 | ], 10 | dependencies: [ 11 | .package(url: "https://source.skip.tools/skip.git", from: "1.6.21"), 12 | .package(url: "https://source.skip.tools/skip-model.git", from: "1.6.2"), 13 | ], 14 | targets: [ 15 | .target(name: "SkipUI", dependencies: [.product(name: "SkipModel", package: "skip-model")], plugins: [.plugin(name: "skipstone", package: "skip")]), 16 | .testTarget(name: "SkipUITests", dependencies: ["SkipUI", .product(name: "SkipTest", package: "skip")], resources: [.process("Resources")], plugins: [.plugin(name: "skipstone", package: "skip")]), 17 | ] 18 | ) 19 | 20 | if Context.environment["SKIP_BRIDGE"] ?? "0" != "0" { 21 | package.dependencies += [.package(url: "https://source.skip.tools/skip-bridge.git", "0.0.0"..<"2.0.0")] 22 | package.targets.forEach({ target in 23 | target.dependencies += [.product(name: "SkipBridge", package: "skip-bridge")] 24 | }) 25 | // all library types must be dynamic to support bridging 26 | package.products = package.products.map({ product in 27 | guard let libraryProduct = product as? Product.Library else { return product } 28 | return .library(name: libraryProduct.name, type: .dynamic, targets: libraryProduct.targets) 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/IndexViewStyle.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | /* 4 | /// Defines the implementation of all `IndexView` instances within a view 5 | /// hierarchy. 6 | /// 7 | /// To configure the current `IndexViewStyle` for a view hierarchy, use the 8 | /// `.indexViewStyle()` modifier. 9 | @available(iOS 14.0, tvOS 14.0, watchOS 8.0, *) 10 | @available(macOS, unavailable) 11 | public protocol IndexViewStyle { 12 | } 13 | 14 | @available(iOS 14.0, tvOS 14.0, watchOS 8.0, *) 15 | @available(macOS, unavailable) 16 | extension IndexViewStyle where Self == PageIndexViewStyle { 17 | 18 | /// An index view style that places a page index view over its content. 19 | public static var page: PageIndexViewStyle { get { fatalError() } } 20 | 21 | /// An index view style that places a page index view over its content. 22 | /// 23 | /// - Parameter backgroundDisplayMode: The display mode of the background of 24 | /// any page index views receiving this style 25 | public static func page(backgroundDisplayMode: PageIndexViewStyle.BackgroundDisplayMode) -> PageIndexViewStyle { fatalError() } 26 | } 27 | 28 | @available(iOS 14.0, tvOS 14.0, watchOS 8.0, *) 29 | @available(macOS, unavailable) 30 | extension View { 31 | 32 | /// Sets the style for the index view within the current environment. 33 | /// 34 | /// - Parameter style: The style to apply to this view. 35 | public func indexViewStyle(_ style: S) -> some View where S : IndexViewStyle { return stubView() } 36 | 37 | } 38 | */ 39 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Containers/AnimatedContentArguments.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if SKIP 4 | import androidx.compose.runtime.Stable 5 | import androidx.compose.ui.Modifier 6 | 7 | /// Used in our containers to prevent recomposing animated content unnecessarily. 8 | @Stable 9 | struct AnimatedContentArguments: Equatable { 10 | let renderables: kotlin.collections.List 11 | let idMap: (Renderable) -> Any? 12 | let ids: kotlin.collections.List 13 | let rememberedIds: MutableSet 14 | let newIds: kotlin.collections.List 15 | let rememberedNewIds: MutableSet 16 | let isBridged: Bool 17 | 18 | static func ==(lhs: AnimatedContentArguments, rhs: AnimatedContentArguments) -> Bool { 19 | // In bridged mode there are cases where a content renderable (e.g. List/ForEach) will not recompose on its own 20 | // when the renderable's state changes, so shortcutting the AnimatedContent when the IDs compare equal results in 21 | // showing stale content. We have to shortcut in non-bridged mode, however, because otherwise we may see glitches 22 | // in animated content when the keyboard hides/shows. The reason for this is unknown, as is the reason we do 23 | // not see these glitches in bridged mode 24 | guard !isBridged else { 25 | return lhs === rhs 26 | } 27 | return lhs.ids == rhs.ids && lhs.rememberedIds == rhs.rememberedIds && lhs.newIds == rhs.newIds && lhs.rememberedNewIds == rhs.rememberedNewIds 28 | } 29 | } 30 | #endif 31 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Properties/FocusState.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if SKIP 4 | import SkipModel 5 | 6 | import androidx.compose.runtime.MutableState 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.SideEffect 9 | import androidx.compose.runtime.remember 10 | 11 | // Model State as a class rather than struct to mutate by reference and avoid copy overhead. 12 | public final class FocusState: StateTracker { 13 | public init() { 14 | _wrappedValue = false as! Value 15 | StateTracking.register(self) 16 | } 17 | 18 | /// Used by the transpiler to handle both `Bool` and `Hashable` types. 19 | public init(initialValue: Value) { 20 | _wrappedValue = initialValue 21 | StateTracking.register(self) 22 | } 23 | 24 | public var wrappedValue: Value { 25 | get { 26 | if let _wrappedValueState { 27 | return _wrappedValueState.value 28 | } 29 | return _wrappedValue 30 | } 31 | set { 32 | _wrappedValue = newValue 33 | _wrappedValueState?.value = _wrappedValue 34 | } 35 | } 36 | private var _wrappedValue: Value 37 | private var _wrappedValueState: MutableState? 38 | 39 | public var projectedValue: Binding { 40 | return Binding(get: { self.wrappedValue }, set: { self.wrappedValue = $0 }) 41 | } 42 | 43 | public func trackState() { 44 | _wrappedValueState = mutableStateOf(_wrappedValue) 45 | } 46 | } 47 | #endif 48 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Layout/GeometryReader.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if SKIP 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.ui.geometry.Rect 10 | import androidx.compose.ui.layout.LayoutCoordinates 11 | import androidx.compose.ui.layout.boundsInParent 12 | import androidx.compose.ui.layout.boundsInRoot 13 | import androidx.compose.ui.platform.LocalDensity 14 | #endif 15 | 16 | // SKIP @bridge 17 | public struct GeometryReader : View, Renderable { 18 | public let content: (GeometryProxy) -> any View 19 | 20 | // SKIP @bridge 21 | public init(@ViewBuilder content: @escaping (GeometryProxy) -> any View) { 22 | self.content = content 23 | } 24 | 25 | #if SKIP 26 | @Composable override func Render(context: ComposeContext) { 27 | let rememberedGlobalFramePx = remember { mutableStateOf(nil) } 28 | Box(modifier: context.modifier.fillSize().onGloballyPositionedInRoot { 29 | rememberedGlobalFramePx.value = $0 30 | }) { 31 | if let globalFramePx = rememberedGlobalFramePx.value { 32 | let proxy = GeometryProxy(globalFramePx: globalFramePx, density: LocalDensity.current) 33 | content(proxy).Compose(context.content()) 34 | } 35 | } 36 | } 37 | #else 38 | public var body: some View { 39 | stubView() 40 | } 41 | #endif 42 | } 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Graphics/BackgroundProminence.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | /* 4 | /// The prominence of backgrounds underneath other views. 5 | /// 6 | /// Background prominence should influence foreground styling to maintain 7 | /// sufficient contrast against the background. For example, selected rows in 8 | /// a `List` and `Table` can have increased prominence backgrounds with 9 | /// accent color fills when focused; the foreground content above the background 10 | /// should be adjusted to reflect that level of prominence. 11 | /// 12 | /// This can be read and written for views with the 13 | /// `EnvironmentValues.backgroundProminence` property. 14 | @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) 15 | public struct BackgroundProminence : Hashable, Sendable { 16 | 17 | /// The standard prominence of a background 18 | /// 19 | /// This is the default level of prominence and doesn't require any 20 | /// adjustment to achieve satisfactory contrast with the background. 21 | public static let standard: BackgroundProminence = { fatalError() }() 22 | 23 | /// A more prominent background that likely requires some changes to the 24 | /// views above it. 25 | /// 26 | /// This is the level of prominence for more highly saturated and full 27 | /// color backgrounds, such as focused/emphasized selected list rows. 28 | /// Typically foreground content should take on monochrome styling to 29 | /// have greater contrast against the background. 30 | public static let increased: BackgroundProminence = { fatalError() }() 31 | } 32 | */ 33 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Layout/VerticalAlignment.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if !SKIP 5 | #if canImport(CoreGraphics) 6 | import struct CoreGraphics.CGFloat 7 | #endif 8 | #endif 9 | 10 | // NOTE: Keep in sync with SkipSwiftUI.VerticalAlignment 11 | public struct VerticalAlignment : Equatable { 12 | let key: String 13 | 14 | init(key: String) { 15 | self.key = key 16 | } 17 | 18 | @available(*, unavailable) 19 | public init(_ id: Any /* AlignmentID.Type */) { 20 | key = "" 21 | } 22 | 23 | @available(*, unavailable) 24 | public func combineExplicit(_ values: any Sequence) -> CGFloat? { 25 | fatalError() 26 | } 27 | 28 | public static let center = VerticalAlignment(key: "center") 29 | public static let top = VerticalAlignment(key: "top") 30 | public static let bottom = VerticalAlignment(key: "bottom") 31 | public static let firstTextBaseline = VerticalAlignment(key: "firstTextBaseline") 32 | public static let lastTextBaseline = VerticalAlignment(key: "lastTextBaseline") 33 | 34 | #if SKIP 35 | /// Return the equivalent Compose alignment. 36 | public func asComposeAlignment() -> androidx.compose.ui.Alignment.Vertical { 37 | switch self { 38 | case .bottom: 39 | return androidx.compose.ui.Alignment.Bottom 40 | case .top: 41 | return androidx.compose.ui.Alignment.Top 42 | default: 43 | return androidx.compose.ui.Alignment.CenterVertically 44 | } 45 | } 46 | #endif 47 | } 48 | 49 | #endif 50 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/PageIndexViewStyle.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | /* 4 | /// An index view style that places a page index view over its content. 5 | /// 6 | /// You can also use ``IndexViewStyle/page`` to construct this style. 7 | @available(iOS 14.0, tvOS 14.0, watchOS 8.0, *) 8 | @available(macOS, unavailable) 9 | public struct PageIndexViewStyle : IndexViewStyle { 10 | 11 | /// The background style for the page index view. 12 | public struct BackgroundDisplayMode : Sendable { 13 | 14 | /// Background will use the default for the platform. 15 | public static let automatic: PageIndexViewStyle.BackgroundDisplayMode = { fatalError() }() 16 | 17 | /// Background is only shown while the index view is interacted with. 18 | @available(watchOS, unavailable) 19 | public static let interactive: PageIndexViewStyle.BackgroundDisplayMode = { fatalError() }() 20 | 21 | /// Background is always displayed behind the page index view. 22 | @available(watchOS, unavailable) 23 | public static let always: PageIndexViewStyle.BackgroundDisplayMode = { fatalError() }() 24 | 25 | /// Background is never displayed behind the page index view. 26 | public static let never: PageIndexViewStyle.BackgroundDisplayMode = { fatalError() }() 27 | } 28 | 29 | /// Creates a page index view style. 30 | /// 31 | /// - Parameter backgroundDisplayMode: The display mode of the background of any 32 | /// page index views receiving this style 33 | public init(backgroundDisplayMode: PageIndexViewStyle.BackgroundDisplayMode = .automatic) { fatalError() } 34 | } 35 | */ 36 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Layout/Angle.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public struct Angle: Codable, Hashable { 6 | public static var zero = Angle() 7 | 8 | public var radians: Double 9 | public var degrees: Double { 10 | get { 11 | return Self.radiansToDegrees(radians) 12 | } 13 | set { 14 | radians = Self.degreesToRadians(newValue) 15 | } 16 | } 17 | 18 | public init() { 19 | self.radians = 0.0 20 | } 21 | 22 | public init(radians: Double) { 23 | self.radians = radians 24 | } 25 | 26 | public init(degrees: Double) { 27 | self.radians = Self.degreesToRadians(degrees) 28 | } 29 | 30 | public static func radians(_ radians: Double) -> Angle { 31 | return Angle(radians: radians) 32 | } 33 | 34 | public static func degrees(_ degrees: Double) -> Angle { 35 | return Angle(degrees: degrees) 36 | } 37 | 38 | private static func radiansToDegrees(_ radians: Double) -> Double { 39 | return radians * 180 / Double.pi 40 | } 41 | 42 | private static func degreesToRadians(_ degrees: Double) -> Double { 43 | return degrees * Double.pi / 180 44 | } 45 | } 46 | 47 | extension Angle: Comparable { 48 | public static func < (lhs: Angle, rhs: Angle) -> Bool { 49 | return lhs.radians < rhs.radians 50 | } 51 | } 52 | 53 | #if !SKIP 54 | 55 | // Stubs needed to compile this package: 56 | 57 | extension Angle : Animatable { 58 | public var animatableData: AnimatableData { get { fatalError() } set { } } 59 | public typealias AnimatableData = Double 60 | } 61 | 62 | #endif 63 | #endif 64 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Layout/HorizontalAlignment.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if !SKIP 5 | #if canImport(CoreGraphics) 6 | import struct CoreGraphics.CGFloat 7 | #endif 8 | #endif 9 | 10 | // NOTE: Keep in sync with SkipSwiftUI.HorizontalAlignment 11 | public struct HorizontalAlignment : Equatable { 12 | let key: String 13 | 14 | init(key: String) { 15 | self.key = key 16 | } 17 | 18 | @available(*, unavailable) 19 | public init(_ id: Any /* AlignmentID.Type */) { 20 | key = "" 21 | } 22 | 23 | @available(*, unavailable) 24 | public func combineExplicit(_ values: any Sequence) -> CGFloat? { 25 | fatalError() 26 | } 27 | 28 | public static let center: HorizontalAlignment = HorizontalAlignment(key: "center") 29 | public static let leading: HorizontalAlignment = HorizontalAlignment(key: "leading") 30 | public static let trailing: HorizontalAlignment = HorizontalAlignment(key: "trailing") 31 | public static let listRowSeparatorLeading = HorizontalAlignment(key: "listRowSeparatorLeading") 32 | public static let listRowSeparatorTrailing = HorizontalAlignment(key: "listRowSeparatorTrailing") 33 | 34 | #if SKIP 35 | /// Return the equivalent Compose alignment. 36 | public func asComposeAlignment() -> androidx.compose.ui.Alignment.Horizontal { 37 | switch self { 38 | case .leading: 39 | return androidx.compose.ui.Alignment.Start 40 | case .trailing: 41 | return androidx.compose.ui.Alignment.End 42 | default: 43 | return androidx.compose.ui.Alignment.CenterHorizontally 44 | } 45 | } 46 | #endif 47 | } 48 | 49 | #endif 50 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Compose/Renderable.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if SKIP 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | #endif 8 | 9 | /// Renders content via Compose. 10 | public protocol Renderable { 11 | #if SKIP 12 | @Composable func Render(context: ComposeContext) 13 | #endif 14 | } 15 | 16 | #if SKIP 17 | extension Renderable { 18 | /// Whether this renderable specializes for list items. 19 | /// 20 | /// - Returns: A tuple containing whether this item specializes rendering for list items and any list item action it applies. 21 | /// The given action will become a tap action on the entire list item cell. 22 | @Composable public func shouldRenderListItem(context: ComposeContext) -> (Bool, (() -> Void)?) { 23 | return (false, nil) 24 | } 25 | 26 | /// Render as a list item. 27 | @Composable public func RenderListItem(context: ComposeContext, modifiers: kotlin.collections.List) { 28 | } 29 | 30 | /// Whether this is an empty view. 31 | public final var isSwiftUIEmptyView: Bool { 32 | return strip() is EmptyView 33 | } 34 | 35 | /// Strip enclosing modifiers, etc. 36 | public func strip() -> Renderable { 37 | return self 38 | } 39 | 40 | /// Perform an action for every modifier. 41 | /// 42 | /// The first non-nil value will be returned. 43 | public func forEachModifier(perform action: (ModifierProtocol) -> R?) -> R? { 44 | return nil 45 | } 46 | 47 | /// Represent this `Renderable` as a `View`. 48 | public func asView() -> View { 49 | return self as? View ?? ComposeView(content: { self.Render($0) }) 50 | } 51 | } 52 | 53 | #endif 54 | #endif 55 | 56 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Properties/State.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | import SkipModel 5 | #if SKIP 6 | import androidx.compose.runtime.MutableState 7 | import androidx.compose.runtime.mutableStateOf 8 | #endif 9 | 10 | // Model State as a class rather than struct to mutate by reference and avoid copy overhead. 11 | public final class State: StateTracker { 12 | public init(initialValue: Value) { 13 | _wrappedValue = initialValue 14 | StateTracking.register(self) 15 | } 16 | 17 | public convenience init(wrappedValue: Value) { 18 | self.init(initialValue: wrappedValue) 19 | } 20 | 21 | public var wrappedValue: Value { 22 | get { 23 | #if SKIP 24 | if let _wrappedValueState { 25 | return _wrappedValueState.value 26 | } 27 | #endif 28 | return _wrappedValue 29 | } 30 | set { 31 | _wrappedValue = newValue 32 | #if SKIP 33 | _wrappedValueState?.value = _wrappedValue 34 | #endif 35 | } 36 | } 37 | private var _wrappedValue: Value 38 | #if SKIP 39 | private var _wrappedValueState: MutableState? 40 | #endif 41 | 42 | public var projectedValue: Binding { 43 | return Binding(get: { self.wrappedValue }, set: { self.wrappedValue = $0 }) 44 | } 45 | 46 | public func trackState() { 47 | #if SKIP 48 | _wrappedValueState = mutableStateOf(_wrappedValue) 49 | #endif 50 | } 51 | } 52 | 53 | #if SKIP 54 | // extension State where Value : ExpressibleByNilLiteral { 55 | // public init() { 56 | @available(*, unavailable) 57 | public func State() { 58 | fatalError() 59 | } 60 | // } 61 | #endif 62 | #endif 63 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Graphics/ShadowStyle.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | /* 4 | import struct CoreGraphics.CGFloat 5 | 6 | /// A style to use when rendering shadows. 7 | @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) 8 | public struct ShadowStyle : Equatable, Sendable { 9 | 10 | /// Creates a custom drop shadow style. 11 | /// 12 | /// Drop shadows draw behind the source content by blurring, 13 | /// tinting and offsetting its per-pixel alpha values. 14 | /// 15 | /// - Parameters: 16 | /// - color: The shadow's color. 17 | /// - radius: The shadow's size. 18 | /// - x: A horizontal offset you use to position the shadow 19 | /// relative to this view. 20 | /// - y: A vertical offset you use to position the shadow 21 | /// relative to this view. 22 | /// 23 | /// - Returns: A new shadow style. 24 | public static func drop(color: Color = .init(/* .sRGBLinear, */ white: 0, opacity: 0.33), radius: CGFloat, x: CGFloat = 0, y: CGFloat = 0) -> ShadowStyle { fatalError() } 25 | 26 | /// Creates a custom inner shadow style. 27 | /// 28 | /// Inner shadows draw on top of the source content by blurring, 29 | /// tinting, inverting and offsetting its per-pixel alpha values. 30 | /// 31 | /// - Parameters: 32 | /// - color: The shadow's color. 33 | /// - radius: The shadow's size. 34 | /// - x: A horizontal offset you use to position the shadow 35 | /// relative to this view. 36 | /// - y: A vertical offset you use to position the shadow 37 | /// relative to this view. 38 | /// 39 | /// - Returns: A new shadow style. 40 | public static func inner(color: Color = .init(/* .sRGBLinear, */ white: 0, opacity: 0.55), radius: CGFloat, x: CGFloat = 0, y: CGFloat = 0) -> ShadowStyle { fatalError() } 41 | 42 | 43 | } 44 | */ 45 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/BridgeSupport/StateSupport.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | import SkipModel 5 | #if SKIP 6 | import androidx.compose.runtime.MutableState 7 | import androidx.compose.runtime.mutableStateOf 8 | #endif 9 | 10 | /// Support for bridged`SwiftUI.@State`. 11 | /// 12 | /// The Compose side manages object lifecycles, so we hold references to the native value and a `MutableState` recomposition trigger. 13 | /// State values are not necessarily bridged or bridgable, so we use an opaque pointer. This support object is `remembered` and synced to 14 | /// and from the native view. 15 | // SKIP @bridge 16 | public final class StateSupport: StateTracker { 17 | #if SKIP 18 | private var state: MutableState? = nil 19 | #endif 20 | 21 | /// Supply a Swift pointer to an object that holds the `@State` value and a block to release the object on finalize. 22 | // SKIP @bridge 23 | public init(valueHolder: Int64) { 24 | self.valueHolder = valueHolder 25 | StateTracking.register(self) 26 | } 27 | 28 | #if SKIP 29 | deinit { 30 | valueHolder = Swift_release(valueHolder) 31 | } 32 | 33 | /// - Seealso `SkipSwiftUI.BridgedStateBox` 34 | // SKIP EXTERN 35 | private func Swift_release(Swift_valueHolder: Int64) -> Int64 36 | #endif 37 | 38 | // SKIP @bridge 39 | public private(set) var valueHolder: Int64 40 | 41 | // SKIP @bridge 42 | public func access() { 43 | #if SKIP 44 | let _ = state?.value 45 | #endif 46 | } 47 | 48 | // SKIP @bridge 49 | public func update() { 50 | #if SKIP 51 | state?.value += 1 52 | #endif 53 | } 54 | 55 | // SKIP @bridge 56 | public func trackState() { 57 | #if SKIP 58 | state = mutableStateOf(0) 59 | #endif 60 | } 61 | } 62 | 63 | #endif 64 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/ProjectionTransform.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | /* 4 | import struct CoreGraphics.CGAffineTransform 5 | import struct CoreGraphics.CGFloat 6 | import struct QuartzCore.CATransform3D 7 | 8 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 9 | @frozen public struct ProjectionTransform { 10 | 11 | public var m11: CGFloat { get { fatalError() } } 12 | 13 | public var m12: CGFloat { get { fatalError() } } 14 | 15 | public var m13: CGFloat { get { fatalError() } } 16 | 17 | public var m21: CGFloat { get { fatalError() } } 18 | 19 | public var m22: CGFloat { get { fatalError() } } 20 | 21 | public var m23: CGFloat { get { fatalError() } } 22 | 23 | public var m31: CGFloat { get { fatalError() } } 24 | 25 | public var m32: CGFloat { get { fatalError() } } 26 | 27 | public var m33: CGFloat { get { fatalError() } } 28 | 29 | @inlinable public init() { fatalError() } 30 | 31 | @inlinable public init(_ m: CGAffineTransform) { fatalError() } 32 | 33 | @inlinable public init(_ m: CATransform3D) { fatalError() } 34 | 35 | @inlinable public var isIdentity: Bool { get { fatalError() } } 36 | 37 | @inlinable public var isAffine: Bool { get { fatalError() } } 38 | 39 | public mutating func invert() -> Bool { fatalError() } 40 | 41 | public func inverted() -> ProjectionTransform { fatalError() } 42 | } 43 | 44 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 45 | extension ProjectionTransform : Equatable { 46 | 47 | 48 | } 49 | 50 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 51 | extension ProjectionTransform { 52 | 53 | public func concatenating(_ rhs: ProjectionTransform) -> ProjectionTransform { fatalError() } 54 | } 55 | 56 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 57 | extension ProjectionTransform : Sendable { 58 | } 59 | */ 60 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/UIKit/UIKeyboardType.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if SKIP 5 | import androidx.compose.ui.text.input.KeyboardType 6 | #endif 7 | 8 | public enum UIKeyboardType: Int { 9 | case `default` = 0 // For bridging 10 | case asciiCapable = 1 // For bridging 11 | case numbersAndPunctuation = 2 // For bridging 12 | case URL = 3 // For bridging 13 | case numberPad = 4 // For bridging 14 | case phonePad = 5 // For bridging 15 | case namePhonePad = 6 // For bridging 16 | case emailAddress = 7 // For bridging 17 | case decimalPad = 8 // For bridging 18 | case twitter = 9 // For bridging 19 | case webSearch = 10 // For bridging 20 | case asciiCapableNumberPad = 11 // For bridging 21 | case alphabet = 12 // For bridging 22 | 23 | #if SKIP 24 | func asComposeKeyboardType() -> KeyboardType { 25 | switch self { 26 | case .default: 27 | return KeyboardType.Text 28 | case .asciiCapable: 29 | return KeyboardType.Ascii 30 | case .numbersAndPunctuation: 31 | return KeyboardType.Text 32 | case .URL: 33 | return KeyboardType.Uri 34 | case .numberPad: 35 | return KeyboardType.Number 36 | case .phonePad: 37 | return KeyboardType.Phone 38 | case .namePhonePad: 39 | return KeyboardType.Text 40 | case .emailAddress: 41 | return KeyboardType.Email 42 | case .decimalPad: 43 | return KeyboardType.Decimal 44 | case .twitter: 45 | return KeyboardType.Text 46 | case .webSearch: 47 | return KeyboardType.Text 48 | case .asciiCapableNumberPad: 49 | return KeyboardType.Text 50 | case .alphabet: 51 | return KeyboardType.Text 52 | } 53 | } 54 | #endif 55 | } 56 | 57 | #endif 58 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Components/Link.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | import Foundation 5 | #if SKIP 6 | import androidx.compose.runtime.Composable 7 | #endif 8 | 9 | // Use a class to be able to update our openURL action on compose by reference. 10 | // SKIP @bridge 11 | public final class Link : View, Renderable { 12 | let content: Button 13 | var openURL = OpenURLAction.default 14 | 15 | public init(destination: URL, @ViewBuilder label: () -> any View) { 16 | #if SKIP 17 | content = Button(action: { self.openURL(destination) }, label: label) 18 | #else 19 | content = Button("", action: {}) 20 | #endif 21 | } 22 | 23 | // SKIP @bridge 24 | public init(destination: URL, bridgedLabel: any View) { 25 | #if SKIP 26 | content = Button(bridgedRole: nil, action: { self.openURL(destination) }, bridgedLabel: bridgedLabel) 27 | #else 28 | content = Button("", action: {}) 29 | #endif 30 | } 31 | 32 | public convenience init(_ titleKey: LocalizedStringKey, destination: URL) { 33 | self.init(destination: destination, label: { Text(titleKey) }) 34 | } 35 | 36 | public convenience init(_ titleResource: LocalizedStringResource, destination: URL) { 37 | self.init(destination: destination, label: { Text(titleResource) }) 38 | } 39 | 40 | public convenience init(_ title: String, destination: URL) { 41 | self.init(destination: destination, label: { Text(verbatim: title) }) 42 | } 43 | 44 | #if SKIP 45 | @Composable override func Render(context: ComposeContext) { 46 | ComposeAction() 47 | content.Compose(context: context) 48 | } 49 | 50 | @Composable func ComposeAction() { 51 | openURL = EnvironmentValues.shared.openURL 52 | } 53 | #else 54 | public var body: some View { 55 | stubView() 56 | } 57 | #endif 58 | } 59 | 60 | #endif 61 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/View/Observable.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | import Combine 5 | #if SKIP 6 | import androidx.compose.runtime.DisposableEffect 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.runtime.rememberUpdatedState 9 | #endif 10 | 11 | extension View { 12 | // SKIP DECLARE: fun onReceive(publisher: P, perform: (Output) -> Unit): View where P: Publisher 13 | public func onReceive

(_ publisher: P, perform action: @escaping (P.Output) -> Void) -> some View where P : Publisher { 14 | #if SKIP 15 | return ModifiedContent(content: self, modifier: SideEffectModifier { _ in 16 | let latestAction = rememberUpdatedState(action) 17 | let subscription = remember { 18 | publisher.sink { output in 19 | latestAction.value(output) 20 | } 21 | } 22 | DisposableEffect(subscription) { 23 | onDispose { 24 | subscription.cancel() 25 | } 26 | } 27 | return ComposeResult.ok 28 | }) 29 | #else 30 | return self 31 | #endif 32 | } 33 | } 34 | 35 | public struct SubscriptionView : View where /* PublisherType : Publisher, */ Content : View /*, PublisherType.Failure == Never */ { 36 | public let content: Content 37 | public let publisher: PublisherType 38 | public let action: (Any /* PublisherType.Output */) -> Void 39 | 40 | @available(*, unavailable) 41 | public init(content: Content, publisher: PublisherType, action: @escaping (Any /* PublisherType.Output */) -> Void) { 42 | self.content = content 43 | self.publisher = publisher 44 | self.action = action 45 | } 46 | 47 | #if !SKIP 48 | public var body: some View { 49 | stubView() 50 | } 51 | #endif 52 | } 53 | 54 | #endif 55 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Color/ColorMatrix.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | /* 4 | /// A matrix to use in an RGBA color transformation. 5 | /// 6 | /// The matrix has five columns, each with a red, green, blue, and alpha 7 | /// component. You can use the matrix for tasks like creating a color 8 | /// transformation ``GraphicsContext/Filter`` for a ``GraphicsContext`` using 9 | /// the ``GraphicsContext/Filter/colorMatrix(_:)`` method. 10 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 11 | @frozen public struct ColorMatrix : Equatable { 12 | 13 | public var r1: Float { get { fatalError() } } 14 | 15 | public var r2: Float { get { fatalError() } } 16 | 17 | public var r3: Float { get { fatalError() } } 18 | 19 | public var r4: Float { get { fatalError() } } 20 | 21 | public var r5: Float { get { fatalError() } } 22 | 23 | public var g1: Float { get { fatalError() } } 24 | 25 | public var g2: Float { get { fatalError() } } 26 | 27 | public var g3: Float { get { fatalError() } } 28 | 29 | public var g4: Float { get { fatalError() } } 30 | 31 | public var g5: Float { get { fatalError() } } 32 | 33 | public var b1: Float { get { fatalError() } } 34 | 35 | public var b2: Float { get { fatalError() } } 36 | 37 | public var b3: Float { get { fatalError() } } 38 | 39 | public var b4: Float { get { fatalError() } } 40 | 41 | public var b5: Float { get { fatalError() } } 42 | 43 | public var a1: Float { get { fatalError() } } 44 | 45 | public var a2: Float { get { fatalError() } } 46 | 47 | public var a3: Float { get { fatalError() } } 48 | 49 | public var a4: Float { get { fatalError() } } 50 | 51 | public var a5: Float { get { fatalError() } } 52 | 53 | /// Creates the identity matrix. 54 | @inlinable public init() { fatalError() } 55 | 56 | 57 | } 58 | 59 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 60 | extension ColorMatrix : Sendable { 61 | } 62 | */ 63 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/InterfaceOrientation.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | /* 4 | /// The orientation of the interface from the user's perspective. 5 | /// 6 | /// By default, device previews appear right side up, using orientation 7 | /// ``InterfaceOrientation/portrait``. You can change the orientation 8 | /// with a call to the ``View/previewInterfaceOrientation(_:)`` modifier: 9 | /// 10 | /// struct CircleImage_Previews: PreviewProvider { 11 | /// static var previews: some View { 12 | /// CircleImage() 13 | /// .previewInterfaceOrientation(.landscapeRight) 14 | /// } 15 | /// } 16 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 17 | public struct InterfaceOrientation : CaseIterable, Identifiable, Equatable, Sendable { 18 | 19 | /// A collection of all values of this type. 20 | public static var allCases: [InterfaceOrientation] { get { fatalError() } } 21 | 22 | /// The stable identity of the entity associated with this instance. 23 | public var id: String { get { fatalError() } } 24 | 25 | /// The device is in portrait mode, with the top of the device on top. 26 | public static let portrait: InterfaceOrientation = { fatalError() }() 27 | 28 | /// The device is in portrait mode, but is upside down. 29 | public static let portraitUpsideDown: InterfaceOrientation = { fatalError() }() 30 | 31 | /// The device is in landscape mode, with the top of the device on the left. 32 | public static let landscapeLeft: InterfaceOrientation = { fatalError() }() 33 | 34 | /// The device is in landscape mode, with the top of the device on the right. 35 | public static let landscapeRight: InterfaceOrientation = { fatalError() }() 36 | 37 | 38 | 39 | /// A type that can represent a collection of all values of this type. 40 | public typealias AllCases = [InterfaceOrientation] 41 | 42 | /// A type representing the stable identity of the entity associated with 43 | /// an instance. 44 | public typealias ID = String 45 | } 46 | */ 47 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Color/ColorSchemeContrast.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | /* 4 | /// The contrast between the app's foreground and background colors. 5 | /// 6 | /// You receive a contrast value when you read the 7 | /// ``EnvironmentValues/colorSchemeContrast`` environment value. The value 8 | /// tells you if a standard or increased contrast currently applies to the view. 9 | /// SkipUI updates the value whenever the contrast changes, and redraws 10 | /// views that depend on the value. For example, the following ``Text`` view 11 | /// automatically updates when the user enables increased contrast: 12 | /// 13 | /// @Environment(\.colorSchemeContrast) private var colorSchemeContrast 14 | /// 15 | /// var body: some View { 16 | /// Text(colorSchemeContrast == .standard ? "Standard" : "Increased") 17 | /// } 18 | /// 19 | /// The user sets the contrast by selecting the Increase Contrast option in 20 | /// Accessibility > Display in System Preferences on macOS, or 21 | /// Accessibility > Display & Text Size in the Settings app on iOS. 22 | /// Your app can't override the user's choice. 23 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 24 | public enum ColorSchemeContrast : CaseIterable, Sendable { 25 | 26 | /// SkipUI displays views with standard contrast between the app's 27 | /// foreground and background colors. 28 | case standard 29 | 30 | /// SkipUI displays views with increased contrast between the app's 31 | /// foreground and background colors. 32 | case increased 33 | 34 | 35 | 36 | 37 | /// A type that can represent a collection of all values of this type. 38 | public typealias AllCases = [ColorSchemeContrast] 39 | 40 | /// A collection of all values of this type. 41 | public static var allCases: [ColorSchemeContrast] { get { fatalError() } } 42 | 43 | } 44 | 45 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 46 | extension ColorSchemeContrast : Equatable { 47 | } 48 | 49 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 50 | extension ColorSchemeContrast : Hashable { 51 | } 52 | */ 53 | -------------------------------------------------------------------------------- /Sources/SkipUI/Skip/Redacted.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2024–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | package skip.ui 4 | 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.draw.drawWithContent 8 | import androidx.compose.ui.geometry.Rect 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.graphics.ColorFilter 11 | import androidx.compose.ui.graphics.ColorMatrix 12 | import androidx.compose.ui.graphics.Paint 13 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas 14 | import androidx.compose.ui.layout.Layout 15 | import androidx.compose.ui.platform.LocalDensity 16 | import androidx.compose.ui.unit.Constraints 17 | import androidx.compose.ui.unit.Dp 18 | import kotlin.math.ceil 19 | 20 | /// Compose the given content with opaque redaction treatment. 21 | @Composable fun Redacted(context: ComposeContext, color: Color, content: @Composable (ComposeContext) -> Unit) { 22 | val contentContext = context.content() 23 | val redactedContext = context.content(modifier = Modifier 24 | .drawWithContent { 25 | val matrix = redactedColorMatrix(color) 26 | val filter = ColorFilter.colorMatrix(matrix) 27 | val paint = Paint().apply { 28 | colorFilter = filter 29 | } 30 | drawIntoCanvas { canvas -> 31 | canvas.saveLayer(Rect(0f, 0f, size.width, size.height), paint) 32 | drawContent() 33 | canvas.restore() 34 | } 35 | } 36 | ) 37 | content(redactedContext) 38 | } 39 | 40 | private fun redactedColorMatrix(color: Color): ColorMatrix { 41 | return ColorMatrix().apply { 42 | set(0, 0, 0f) // Do not preserve original R 43 | set(1, 1, 0f) // Do not preserve original G 44 | set(2, 2, 0f) // Do not preserve original B 45 | 46 | set(0, 4, color.red * 255) // Use given color's R 47 | set(1, 4, color.green * 255) // Use given color's G 48 | set(2, 4, color.blue * 255) // Use given color's B 49 | set(3, 3, color.alpha) // Multiply original alpha by shadow color alpha 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/BridgeSupport/UserNotificationsDelegateSupport.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | // SKIP @bridge 6 | public final class UserNotificationCenterDelegateSupport : UNUserNotificationCenterDelegate { 7 | let didReceive: (UNNotificationResponse, CompletionHandler) -> Void 8 | let willPresent: (UNNotification, ValueCompletionHandler) -> Void 9 | let openSettings: (UNNotification?) -> Void 10 | 11 | // SKIP @bridge 12 | public init(didReceive: @escaping (UNNotificationResponse, CompletionHandler) -> Void, willPresent: @escaping (UNNotification, ValueCompletionHandler) -> Void, openSettings: @escaping (UNNotification?) -> Void) { 13 | self.didReceive = didReceive 14 | self.willPresent = willPresent 15 | self.openSettings = openSettings 16 | } 17 | 18 | public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive notification: UNNotificationResponse) async { 19 | #if SKIP 20 | kotlin.coroutines.suspendCoroutine { continuation in 21 | let completionHandler = CompletionHandler { continuation.resumeWith(kotlin.Result.success(Unit)) } 22 | self.didReceive(notification, completionHandler) 23 | } 24 | #endif 25 | } 26 | 27 | public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions { 28 | #if SKIP 29 | kotlin.coroutines.suspendCoroutine { continuation in 30 | let completionHandler = ValueCompletionHandler { 31 | let options = UNNotificationPresentationOptions(rawValue: $0 as! Int) 32 | continuation.resumeWith(kotlin.Result.success(options)) 33 | } 34 | self.willPresent(notification, completionHandler) 35 | } 36 | #else 37 | return [] 38 | #endif 39 | } 40 | 41 | public func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) { 42 | openSettings(notification) 43 | } 44 | } 45 | 46 | 47 | #endif 48 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Compose/ComposeView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if SKIP 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | #endif 8 | 9 | /// Used to directly wrap user Compose content. 10 | /// 11 | /// - Seealso: `ComposeBuilder` 12 | // SKIP @bridge 13 | public struct ComposeView: View, Renderable { 14 | #if SKIP 15 | private let content: @Composable (ComposeContext) -> Void 16 | 17 | /// Constructor. 18 | /// 19 | /// The supplied `content` is the content to compose. 20 | public init(content: @Composable (ComposeContext) -> Void) { 21 | self.content = content 22 | } 23 | #endif 24 | 25 | // SKIP @bridge 26 | public init(bridgedContent: Any) { 27 | #if SKIP 28 | self.content = { (bridgedContent as? ContentComposer)?.Compose(context: $0) } 29 | #endif 30 | } 31 | 32 | #if SKIP 33 | @Composable override func Render(context: ComposeContext) { 34 | content(context) 35 | } 36 | #else 37 | public var body: some View { 38 | stubView() 39 | } 40 | #endif 41 | } 42 | 43 | #if SKIP 44 | /// Encapsulation of Composable content. 45 | public protocol ContentComposer { 46 | @Composable func Compose(context: ComposeContext) 47 | } 48 | 49 | /// Encapsulation of a Compose modifier. 50 | public protocol ContentModifier { 51 | func modify(view: any View) -> any View 52 | } 53 | #endif 54 | 55 | extension View { 56 | #if SKIP 57 | /// Add the given modifier to the underlying Compose view. 58 | public func composeModifier(_ modifier: (Modifier) -> Modifier) -> View { 59 | return ModifiedContent(content: self, modifier: RenderModifier { 60 | return modifier($0.modifier) 61 | }) 62 | } 63 | #endif 64 | 65 | /// Apply the given `ContentModifier`. 66 | // SKIP @bridge 67 | public func applyContentModifier(bridgedContent: Any) -> any View { 68 | #if SKIP 69 | return (bridgedContent as? ContentModifier)?.modify(view: self) ?? self 70 | #else 71 | return self 72 | #endif 73 | } 74 | } 75 | #endif 76 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Compose/ComposeModifiers.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if SKIP 4 | import androidx.compose.ui.draw.DrawModifier 5 | import androidx.compose.ui.geometry.Rect 6 | import androidx.compose.ui.graphics.ColorFilter 7 | import androidx.compose.ui.graphics.ColorMatrix 8 | import androidx.compose.ui.graphics.Paint 9 | import androidx.compose.ui.graphics.drawscope.ContentDrawScope 10 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas 11 | 12 | class ColorInvertModifier : DrawModifier { 13 | // SKIP DECLARE: override fun ContentDrawScope.draw() 14 | override func draw() { 15 | let invertMatrix = ColorMatrix().apply { setToInvert() } 16 | let invertFilter = ColorFilter.colorMatrix(invertMatrix) 17 | let paint = Paint().apply { 18 | colorFilter = invertFilter 19 | } 20 | drawIntoCanvas { 21 | $0.saveLayer(Rect(Float(0.0), Float(0.0), size.width, size.height), paint) 22 | drawContent() 23 | $0.restore() 24 | } 25 | } 26 | } 27 | 28 | extension ColorMatrix { 29 | func setToInvert() { 30 | set(ColorMatrix(floatArrayOf( 31 | Float(-1), Float(0), Float(0), Float(0), Float(255), 32 | Float(0), Float(-1), Float(0), Float(0), Float(255), 33 | Float(0), Float(0), Float(-1), Float(0), Float(255), 34 | Float(0), Float(0), Float(0), Float(1), Float(0) 35 | ))) 36 | } 37 | } 38 | 39 | class GrayscaleModifier : DrawModifier { 40 | let amount: Double 41 | 42 | init(amount: Double) { 43 | self.amount = amount 44 | } 45 | 46 | // SKIP DECLARE: override fun ContentDrawScope.draw() 47 | override func draw() { 48 | let saturationMatrix = ColorMatrix().apply { setToSaturation(Float(max(0.0, 1.0 - amount))) } 49 | let saturationFilter = ColorFilter.colorMatrix(saturationMatrix) 50 | let paint = Paint().apply { 51 | colorFilter = saturationFilter 52 | } 53 | drawIntoCanvas { 54 | $0.saveLayer(Rect(Float(0.0), Float(0.0), size.width, size.height), paint) 55 | drawContent() 56 | $0.restore() 57 | } 58 | } 59 | } 60 | #endif 61 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Graphics/Glass.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if SKIP 5 | import androidx.compose.runtime.Composable 6 | import skip.model.StateTracking 7 | #elseif canImport(CoreGraphics) 8 | import struct CoreGraphics.CGFloat 9 | #endif 10 | 11 | public struct Glass : Equatable, Sendable { 12 | public static var regular: Glass { 13 | return Glass() 14 | } 15 | 16 | public func tint(_ color: Color?) -> Glass { 17 | return self 18 | } 19 | 20 | public func interactive(_ isEnabled: Bool = true) -> Glass { 21 | return self 22 | } 23 | } 24 | 25 | public struct GlassEffectContainer : View, Sendable where Content : View { 26 | @available(*, unavailable) 27 | public init(spacing: CGFloat? = nil, @ViewBuilder content: () -> Content) { 28 | } 29 | 30 | public var body: some View { 31 | EmptyView() 32 | } 33 | } 34 | 35 | public struct GlassEffectTransition : Sendable { 36 | @available(*, unavailable) 37 | public static var matchedGeometry: GlassEffectTransition { 38 | return GlassEffectTransition() 39 | } 40 | 41 | @available(*, unavailable) 42 | public static func matchedGeometry(properties: MatchedGeometryProperties = .frame, anchor: UnitPoint = .center) -> GlassEffectTransition { 43 | return GlassEffectTransition() 44 | } 45 | 46 | public static var identity: GlassEffectTransition { 47 | return GlassEffectTransition() 48 | } 49 | } 50 | 51 | extension View { 52 | @available(*, unavailable) 53 | public func glassEffect(_ glass: Glass = .regular, in shape: some Shape = .capsule, isEnabled: Bool = true) -> some View { 54 | return self 55 | } 56 | 57 | public func glassEffectTransition(_ transition: GlassEffectTransition, isEnabled: Bool = true) -> some View { 58 | return self 59 | } 60 | 61 | @available(*, unavailable) 62 | public func glassEffectUnion(id: (any Hashable)?, namespace: Namespace.ID) -> some View { 63 | return self 64 | } 65 | 66 | @available(*, unavailable) 67 | public func glassEffectID(_ id: (any Hashable)?, in namespace: Namespace.ID) -> some View { 68 | return self 69 | } 70 | } 71 | 72 | #endif 73 | -------------------------------------------------------------------------------- /Tests/SkipUITests/Resources/Localizable.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "Done" : { 5 | "localizations" : { 6 | "ar" : { 7 | "stringUnit" : { 8 | "state" : "translated", 9 | "value" : "تم" 10 | } 11 | }, 12 | "fr" : { 13 | "stringUnit" : { 14 | "state" : "translated", 15 | "value" : "Terminé" 16 | } 17 | }, 18 | "he" : { 19 | "stringUnit" : { 20 | "state" : "translated", 21 | "value" : "סיום" 22 | } 23 | }, 24 | "ja" : { 25 | "stringUnit" : { 26 | "state" : "translated", 27 | "value" : "完了" 28 | } 29 | }, 30 | "pt-BR" : { 31 | "stringUnit" : { 32 | "state" : "translated", 33 | "value" : "OK" 34 | } 35 | }, 36 | "ru" : { 37 | "stringUnit" : { 38 | "state" : "translated", 39 | "value" : "Готово" 40 | } 41 | }, 42 | "sv" : { 43 | "stringUnit" : { 44 | "state" : "translated", 45 | "value" : "Klar" 46 | } 47 | }, 48 | "uk" : { 49 | "stringUnit" : { 50 | "state" : "translated", 51 | "value" : "Готово" 52 | } 53 | }, 54 | "zh-Hans" : { 55 | "stringUnit" : { 56 | "state" : "translated", 57 | "value" : "完成" 58 | } 59 | } 60 | } 61 | }, 62 | "Done: %@" : { 63 | "extractionState" : "manual", 64 | "localizations" : { 65 | "fr" : { 66 | "stringUnit" : { 67 | "state" : "translated", 68 | "value" : "Terminé: %@" 69 | } 70 | } 71 | } 72 | }, 73 | "Welcome" : { 74 | "extractionState" : "manual", 75 | "localizations" : { 76 | "zh-Hans" : { 77 | "stringUnit" : { 78 | "state" : "translated", 79 | "value" : "欢迎" 80 | } 81 | }, 82 | "zh-Hant" : { 83 | "stringUnit" : { 84 | "state" : "translated", 85 | "value" : "歡迎" 86 | } 87 | } 88 | } 89 | } 90 | }, 91 | "version" : "1.0" 92 | } -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/View/ViewModifier.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if SKIP 5 | import androidx.compose.runtime.Composable 6 | import skip.model.StateTracking 7 | #endif 8 | 9 | // SKIP @bridge 10 | public protocol ViewModifier { 11 | #if SKIP 12 | // SKIP DECLARE: fun body(content: View): View = content 13 | @ViewBuilder @MainActor func body(content: View) -> any View 14 | typealias Content = View 15 | #else 16 | // associatedtype Body : View 17 | // @ViewBuilder @MainActor func body(content: Self.Content) -> Self.Body 18 | // associatedtype Content 19 | #endif 20 | } 21 | 22 | #if SKIP 23 | extension ViewModifier { 24 | /// Evaluate renderable content. 25 | /// 26 | /// - Warning: Do not give `options` a default value in this function signature. We have seen it cause bugs in which 27 | /// the default version of the function is always invoked, ignoring implementor overrides. 28 | /// - Seealso: `View.Evaluate(context:options:)` 29 | @Composable public func Evaluate(content: Content, context: ComposeContext, options: Int) -> kotlin.collections.List { 30 | StateTracking.pushBody() 31 | let renderables = body(content: content).Evaluate(context: context, options: options) 32 | StateTracking.popBody() 33 | return renderables 34 | } 35 | } 36 | 37 | final class ViewModifierView: View { 38 | let view: View 39 | let modifier: ViewModifier 40 | 41 | init(view: View, modifier: ViewModifier) { 42 | // Don't copy 43 | // SKIP REPLACE: this.view = view 44 | self.view = view 45 | // SKIP REPLACE: this.modifier = modifier 46 | self.modifier = modifier 47 | } 48 | 49 | @Composable override func Evaluate(context: ComposeContext, options: Int) -> kotlin.collections.List { 50 | return modifier.Evaluate(content: view, context: context, options: options) 51 | } 52 | } 53 | #endif 54 | 55 | extension View { 56 | // SKIP @bridge 57 | public func modifier(_ viewModifier: any ViewModifier) -> any View { 58 | #if SKIP 59 | return ViewModifierView(view: self, modifier: viewModifier) 60 | #else 61 | return self 62 | #endif 63 | } 64 | } 65 | 66 | #endif 67 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Layout/GeometryProxy.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if SKIP 5 | import androidx.compose.ui.geometry.Rect 6 | import androidx.compose.ui.unit.Density 7 | #elseif canImport(CoreGraphics) 8 | import struct CoreGraphics.CGFloat 9 | import struct CoreGraphics.CGRect 10 | import struct CoreGraphics.CGSize 11 | #endif 12 | 13 | // SKIP @bridge 14 | public struct GeometryProxy { 15 | #if SKIP 16 | let globalFramePx: Rect 17 | let density: Density 18 | #endif 19 | 20 | public var size: CGSize { 21 | #if SKIP 22 | return with(density) { 23 | CGSize(width: Double(globalFramePx.width.toDp().value), height: Double(globalFramePx.height.toDp().value)) 24 | } 25 | #else 26 | return .zero 27 | #endif 28 | } 29 | 30 | // SKIP @bridge 31 | public var bridgedSize: (CGFloat, CGFloat) { 32 | let size = self.size 33 | return (size.width, size.height) 34 | } 35 | 36 | @available(*, unavailable) 37 | public subscript(anchor: Any /* Anchor */) -> T { 38 | fatalError() 39 | } 40 | 41 | @available(*, unavailable) 42 | public var safeAreaInsets: EdgeInsets { 43 | fatalError() 44 | } 45 | 46 | @available(*, unavailable) 47 | public func bounds(of coordinateSpace: NamedCoordinateSpace) -> CGRect? { 48 | fatalError() 49 | } 50 | 51 | public func frame(in coordinateSpace: some CoordinateSpaceProtocol) -> CGRect { 52 | #if SKIP 53 | if coordinateSpace.coordinateSpace.isGlobal { 54 | return with(density) { 55 | CGRect(x: Double(globalFramePx.left.toDp().value), y: Double(globalFramePx.top.toDp().value), width: Double(globalFramePx.width.toDp().value), height: Double(globalFramePx.height.toDp().value)) 56 | } 57 | } else { 58 | return CGRect(origin: .zero, size: size) 59 | } 60 | #else 61 | return .zero 62 | #endif 63 | } 64 | 65 | // SKIP @bridge 66 | public func frame(bridgedCoordinateSpace: Int, name: Any?) -> (CGFloat, CGFloat, CGFloat, CGFloat) { 67 | let coordinateSpace = CoordinateSpaceProtocolFrom(bridged: bridgedCoordinateSpace, name: name as? AnyHashable) 68 | let frame = self.frame(in: coordinateSpace) 69 | return (frame.origin.x, frame.origin.y, frame.width, frame.height) 70 | } 71 | } 72 | 73 | #endif 74 | -------------------------------------------------------------------------------- /Sources/SkipUI/Skip/DragCancelledAnimation.kt: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 2 | // 3 | // This file uses code publised with the following header: 4 | // 5 | /* 6 | * Copyright 2022 André Claßen 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * https://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | package org.burnoutcrew.reorderable 21 | 22 | import androidx.compose.animation.core.Animatable 23 | import androidx.compose.animation.core.Spring 24 | import androidx.compose.animation.core.VectorConverter 25 | import androidx.compose.animation.core.VisibilityThreshold 26 | import androidx.compose.animation.core.spring 27 | import androidx.compose.runtime.getValue 28 | import androidx.compose.runtime.mutableStateOf 29 | import androidx.compose.runtime.setValue 30 | import androidx.compose.ui.geometry.Offset 31 | 32 | interface DragCancelledAnimation { 33 | suspend fun dragCancelled(position: ItemPosition, offset: Offset) 34 | val position: ItemPosition? 35 | val offset: Offset 36 | } 37 | 38 | class NoDragCancelledAnimation : DragCancelledAnimation { 39 | override suspend fun dragCancelled(position: ItemPosition, offset: Offset) {} 40 | override val position: ItemPosition? = null 41 | override val offset: Offset = Offset.Zero 42 | } 43 | 44 | class SpringDragCancelledAnimation(private val stiffness: Float = Spring.StiffnessMediumLow) : DragCancelledAnimation { 45 | private val animatable = Animatable(Offset.Zero, Offset.VectorConverter) 46 | override val offset: Offset 47 | get() = animatable.value 48 | 49 | override var position by mutableStateOf(null) 50 | private set 51 | 52 | override suspend fun dragCancelled(position: ItemPosition, offset: Offset) { 53 | this.position = position 54 | animatable.snapTo(offset) 55 | animatable.animateTo( 56 | Offset.Zero, 57 | spring(stiffness = stiffness, visibilityThreshold = Offset.VisibilityThreshold) 58 | ) 59 | this.position = null 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/SkipUI/Skip/DetectReorder.kt: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 2 | // 3 | // This file uses code publised with the following header: 4 | // 5 | /* 6 | * Copyright 2022 André Claßen 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * https://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | package org.burnoutcrew.reorderable 21 | 22 | import androidx.compose.foundation.gestures.awaitFirstDown 23 | import androidx.compose.foundation.gestures.forEachGesture 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.geometry.Offset 26 | import androidx.compose.ui.input.pointer.PointerInputChange 27 | import androidx.compose.ui.input.pointer.pointerInput 28 | 29 | fun Modifier.detectReorder(state: ReorderableState<*>) = 30 | this.then( 31 | Modifier.pointerInput(Unit) { 32 | forEachGesture { 33 | awaitPointerEventScope { 34 | val down = awaitFirstDown(requireUnconsumed = false) 35 | var drag: PointerInputChange? 36 | var overSlop = Offset.Zero 37 | do { 38 | drag = awaitPointerSlopOrCancellation(down.id, down.type) { change, over -> 39 | change.consume() 40 | overSlop = over 41 | } 42 | } while (drag != null && !drag.isConsumed) 43 | if (drag != null) { 44 | state.interactions.trySend(StartDrag(down.id, overSlop)) 45 | } 46 | } 47 | } 48 | } 49 | ) 50 | 51 | 52 | fun Modifier.detectReorderAfterLongPress(state: ReorderableState<*>) = 53 | this.then( 54 | Modifier.pointerInput(Unit) { 55 | forEachGesture { 56 | val down = awaitPointerEventScope { 57 | awaitFirstDown(requireUnconsumed = false) 58 | } 59 | awaitLongPressOrCancellation(down)?.also { 60 | state.interactions.trySend(StartDrag(down.id)) 61 | } 62 | } 63 | } 64 | ) 65 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Layout/CoordinateSpace.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public enum CoordinateSpace: Hashable { 6 | case global 7 | case local 8 | case named(AnyHashable) 9 | 10 | public var isGlobal: Bool { 11 | return self == .global 12 | } 13 | 14 | public var isLocal: Bool { 15 | return self == .local 16 | } 17 | } 18 | 19 | public protocol CoordinateSpaceProtocol { 20 | var coordinateSpace: CoordinateSpace { get } 21 | } 22 | 23 | func CoordinateSpaceProtocolFrom(bridged: Int, name: AnyHashable?) -> CoordinateSpaceProtocol { 24 | switch bridged { 25 | case 0: 26 | return GlobalCoordinateSpace.global 27 | case 1: 28 | return LocalCoordinateSpace.local 29 | default: 30 | return NamedCoordinateSpace.named(name ?? "" as AnyHashable) 31 | } 32 | } 33 | 34 | extension CoordinateSpaceProtocol { 35 | public static func scrollView(axis: Axis) -> NamedCoordinateSpace { 36 | return named("_scrollView_axis_\(axis.rawValue)_") 37 | } 38 | 39 | public static var scrollView: NamedCoordinateSpace { 40 | return named("_scrollView_") 41 | } 42 | 43 | public static func named(_ name: some Hashable) -> NamedCoordinateSpace { 44 | return NamedCoordinateSpace(coordinateSpace: .named(name)) 45 | } 46 | } 47 | 48 | extension CoordinateSpaceProtocol where Self == LocalCoordinateSpace { 49 | public static var local: LocalCoordinateSpace { 50 | return LocalCoordinateSpace() 51 | } 52 | } 53 | 54 | extension CoordinateSpaceProtocol where Self == GlobalCoordinateSpace { 55 | public static var global: GlobalCoordinateSpace { 56 | return GlobalCoordinateSpace() 57 | } 58 | } 59 | 60 | public class NamedCoordinateSpace : CoordinateSpaceProtocol, Equatable { 61 | private let _coordinateSpace: CoordinateSpace 62 | 63 | init(coordinateSpace: CoordinateSpace) { 64 | _coordinateSpace = coordinateSpace 65 | } 66 | 67 | public var coordinateSpace: CoordinateSpace { 68 | return _coordinateSpace 69 | } 70 | 71 | public static func ==(lhs: NamedCoordinateSpace, rhs: NamedCoordinateSpace) -> Bool { 72 | return lhs.coordinateSpace == rhs.coordinateSpace 73 | } 74 | } 75 | 76 | public class LocalCoordinateSpace : CoordinateSpaceProtocol { 77 | public var coordinateSpace: CoordinateSpace { 78 | return .local 79 | } 80 | } 81 | 82 | public class GlobalCoordinateSpace : CoordinateSpaceProtocol { 83 | public var coordinateSpace: CoordinateSpace { 84 | return .global 85 | } 86 | } 87 | 88 | #endif 89 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Containers/Form.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if SKIP 5 | import androidx.compose.runtime.Composable 6 | #endif 7 | 8 | // SKIP @bridge 9 | public struct Form : View { 10 | // It appears that on iOS, List and Form render the same 11 | let list: List 12 | 13 | public init(@ViewBuilder content: () -> any View) { 14 | self.list = List(content: content) 15 | } 16 | 17 | // SKIP @bridge 18 | public init(bridgedContent: any View) { 19 | self.list = List(bridgedContent: bridgedContent) 20 | } 21 | 22 | #if SKIP 23 | @Composable override func Evaluate(context: ComposeContext, options: Int) -> kotlin.collections.List { 24 | return list.Evaluate(context: context, options: options) 25 | } 26 | #else 27 | public var body: some View { 28 | stubView() 29 | } 30 | #endif 31 | } 32 | 33 | public struct FormStyle: RawRepresentable, Equatable { 34 | public let rawValue: Int 35 | 36 | public init(rawValue: Int) { 37 | self.rawValue = rawValue 38 | } 39 | 40 | public static let automatic = FormStyle(rawValue: 0) 41 | 42 | @available(*, unavailable) 43 | public static let columns = FormStyle(rawValue: 1) 44 | 45 | @available(*, unavailable) 46 | public static let grouped = FormStyle(rawValue: 2) 47 | } 48 | 49 | extension View { 50 | public func formStyle(_ style: FormStyle) -> some View { 51 | return self 52 | } 53 | } 54 | 55 | /* 56 | @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) 57 | extension Form where Content == FormStyleConfiguration.Content { 58 | 59 | /// Creates a form based on a form style configuration. 60 | /// 61 | /// - Parameter configuration: The properties of the form. 62 | public init(_ configuration: FormStyleConfiguration) { fatalError() } 63 | } 64 | 65 | /// The properties of a form instance. 66 | @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) 67 | public struct FormStyleConfiguration { 68 | 69 | /// A type-erased content of a form. 70 | public struct Content : View { 71 | 72 | /// The type of view representing the body of this view. 73 | /// 74 | /// When you create a custom view, Swift infers this type from your 75 | /// implementation of the required ``View/body-swift.property`` property. 76 | public typealias Body = NeverView 77 | public var body: Body { fatalError() } 78 | } 79 | 80 | /// A view that is the content of the form. 81 | public let content: FormStyleConfiguration.Content = { fatalError() }() 82 | } 83 | */ 84 | #endif 85 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/KeyEquivalent.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | public struct KeyEquivalent : Hashable { 6 | public let character: Character 7 | 8 | public init(_ character: Character) { 9 | self.character = character 10 | } 11 | 12 | /// Up Arrow (U+F700) 13 | public static let upArrow = KeyEquivalent("\u{F700}") 14 | 15 | /// Down Arrow (U+F701) 16 | public static let downArrow = KeyEquivalent("\u{F701}") 17 | 18 | /// Left Arrow (U+F702) 19 | public static let leftArrow = KeyEquivalent("\u{F702}") 20 | 21 | /// Right Arrow (U+F703) 22 | public static let rightArrow = KeyEquivalent("\u{F703}") 23 | 24 | /// Escape (U+001B) 25 | public static let escape = KeyEquivalent("\u{001B}") 26 | 27 | /// Delete (U+0008) 28 | public static let delete = KeyEquivalent("\u{0008}") 29 | 30 | /// Delete Forward (U+F728) 31 | public static let deleteForward = KeyEquivalent("\u{F728}") 32 | 33 | /// Home (U+F729) 34 | public static let home = KeyEquivalent("\u{F729}") 35 | 36 | /// End (U+F72B) 37 | public static let end = KeyEquivalent("\u{F72B}") 38 | 39 | /// Page Up (U+F72C) 40 | public static let pageUp = KeyEquivalent("\u{F72C}") 41 | 42 | /// Page Down (U+F72D) 43 | public static let pageDown = KeyEquivalent("\u{F72D}") 44 | 45 | /// Clear (U+F739) 46 | public static let clear = KeyEquivalent("\u{F739}") 47 | 48 | /// Tab (U+0009) 49 | public static let tab = KeyEquivalent("\u{0009}") 50 | 51 | /// Space (U+0020) 52 | public static let space = KeyEquivalent("\u{0020}") 53 | 54 | /// Return (U+000D) 55 | public static let `return` = KeyEquivalent("\u{000D}") 56 | } 57 | 58 | /* 59 | @available(iOS 14.0, macOS 11.0, tvOS 17.0, *) 60 | @available(watchOS, unavailable) 61 | extension KeyEquivalent : ExpressibleByExtendedGraphemeClusterLiteral { 62 | 63 | /// Creates an instance initialized to the given value. 64 | /// 65 | /// - Parameter value: The value of the new instance. 66 | public init(extendedGraphemeClusterLiteral: Character) { fatalError() } 67 | 68 | /// A type that represents an extended grapheme cluster literal. 69 | /// 70 | /// Valid types for `ExtendedGraphemeClusterLiteralType` are `Character`, 71 | /// `String`, and `StaticString`. 72 | public typealias ExtendedGraphemeClusterLiteralType = Character 73 | 74 | /// A type that represents a Unicode scalar literal. 75 | /// 76 | /// Valid types for `UnicodeScalarLiteralType` are `Unicode.Scalar`, 77 | /// `Character`, `String`, and `StaticString`. 78 | public typealias UnicodeScalarLiteralType = Character 79 | } 80 | */ 81 | #endif 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | xcodebuild*.log 9 | 10 | java_pid*.hprof 11 | 12 | .*.swp 13 | .DS_Store 14 | 15 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 16 | *.xcscmblueprint 17 | *.xccheckout 18 | 19 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 20 | build/ 21 | DerivedData/ 22 | .android/ 23 | .kotlin/ 24 | *.moved-aside 25 | *.pbxuser 26 | !default.pbxuser 27 | *.mode1v3 28 | !default.mode1v3 29 | *.mode2v3 30 | !default.mode2v3 31 | *.perspectivev3 32 | !default.perspectivev3 33 | 34 | ## Obj-C/Swift specific 35 | *.hmap 36 | 37 | ## App packaging 38 | *.ipa 39 | *.dSYM.zip 40 | *.dSYM 41 | 42 | ## Playgrounds 43 | timeline.xctimeline 44 | playground.xcworkspace 45 | 46 | # Swift Package Manager 47 | # 48 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 49 | Packages/ 50 | Package.pins 51 | Package.resolved 52 | *.xcodeproj 53 | 54 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 55 | # hence it is not needed unless you have added a package configuration file to your project 56 | .swiftpm 57 | 58 | .build/ 59 | 60 | # CocoaPods 61 | # 62 | # We recommend against adding the Pods directory to your .gitignore. However 63 | # you should judge for yourself, the pros and cons are mentioned at: 64 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 65 | # 66 | # Pods/ 67 | # 68 | # Add this line if you want to avoid checking in source code from the Xcode workspace 69 | # *.xcworkspace 70 | 71 | # Carthage 72 | # 73 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 74 | # Carthage/Checkouts 75 | 76 | Carthage/Build/ 77 | 78 | # Accio dependency management 79 | Dependencies/ 80 | .accio/ 81 | 82 | # fastlane 83 | # 84 | # It is recommended to not store the screenshots in the git repo. 85 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 86 | # For more information about the recommended setup visit: 87 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 88 | 89 | fastlane/report.xml 90 | fastlane/Preview.html 91 | fastlane/screenshots/**/*.png 92 | fastlane/test_output 93 | 94 | # Code Injection 95 | # 96 | # After new code Injection tools there's a generated folder /iOSInjectionProject 97 | # https://github.com/johnno1962/injectionforxcode 98 | 99 | iOSInjectionProject/ 100 | 101 | 102 | 103 | # Ignore Gradle project-specific cache directory 104 | .gradle 105 | 106 | # Ignore Gradle build output directory 107 | build 108 | 109 | # gradle properties 110 | local.properties 111 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/View/EvaluateOptions.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if SKIP 4 | 5 | /// Manage options used in `View.Evaluate(context:options:)`. 6 | struct EvaluateOptions { 7 | private static let keepForEach = 1 << 0 8 | private static let keepNonModified = 1 << 1 9 | // We use values < 1000 for bitwise options and add 1000 per lazy item level (+ 1) 10 | private static let lazyItemLevels = 1000 11 | 12 | init(_ value: Int) { 13 | self.value = value 14 | } 15 | 16 | init(isKeepForEach: Bool = false, isKeepNonModified: Bool = false, lazyItemLevel: Int? = nil) { 17 | var options = EvaluateOptions(0) 18 | options.isKeepForEach = isKeepForEach 19 | options.isKeepNonModified = isKeepNonModified 20 | options.lazyItemLevel = lazyItemLevel 21 | self = options 22 | } 23 | 24 | private(set) var value: Int 25 | 26 | /// Option to keep `ForEach` instances rather than evaluating them. 27 | var isKeepForEach: Bool { 28 | get { 29 | return (value % Self.lazyItemLevels) & Self.keepForEach == Self.keepForEach 30 | } 31 | set { 32 | if newValue { 33 | value = value | Self.keepForEach 34 | } else { 35 | value = value & ~Self.keepForEach 36 | } 37 | } 38 | } 39 | 40 | /// Option to keep any view that is not a `ModifiedContent` rather than evaluating it. 41 | /// 42 | /// If the view is not a `Renderable`, it is wrapped in one. 43 | /// 44 | /// - Warning: Only works reliably when evaluating a `ComposeBuilder`. 45 | var isKeepNonModified: Bool { 46 | get { 47 | return (value % Self.lazyItemLevels) & Self.keepNonModified == Self.keepNonModified 48 | } 49 | set { 50 | if newValue { 51 | value = value | Self.keepNonModified 52 | } else { 53 | value = value & ~Self.keepNonModified 54 | } 55 | } 56 | } 57 | 58 | /// Manage lazy item evaluation. 59 | /// 60 | /// - Seealso: `LazyItemFactory` 61 | var lazyItemLevel: Int? { 62 | get { 63 | guard value >= Self.lazyItemLevels else { 64 | return nil 65 | } 66 | var level = 0 67 | while true { 68 | value -= Self.lazyItemLevels 69 | guard value >= Self.lazyItemLevels else { 70 | break 71 | } 72 | level += 1 73 | } 74 | return level 75 | } 76 | set { 77 | var value = self.value % Self.lazyItemLevels 78 | if let newValue { 79 | value += Self.lazyItemLevels * (newValue + 1) 80 | } 81 | self.value = value 82 | } 83 | } 84 | } 85 | 86 | #endif 87 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Layout/Alignment.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | // NOTE: Keep in sync with SkipSwiftUI.Alignment 6 | public struct Alignment : Equatable { 7 | public var horizontal: HorizontalAlignment 8 | public var vertical: VerticalAlignment 9 | 10 | public static let center = Alignment(horizontal: .center, vertical: .center) 11 | public static let leading = Alignment(horizontal: .leading, vertical: .center) 12 | public static let trailing = Alignment(horizontal: .trailing, vertical: .center) 13 | public static let top = Alignment(horizontal: .center, vertical: .top) 14 | public static let bottom = Alignment(horizontal: .center, vertical: .bottom) 15 | public static let topLeading = Alignment(horizontal: .leading, vertical: .top) 16 | public static let topTrailing = Alignment(horizontal: .trailing, vertical: .top) 17 | public static let bottomLeading = Alignment(horizontal: .leading, vertical: .bottom) 18 | public static let bottomTrailing = Alignment(horizontal: .trailing, vertical: .bottom) 19 | 20 | public static var centerFirstTextBaseline = Alignment(horizontal: .center, vertical: .firstTextBaseline) 21 | public static var centerLastTextBaseline = Alignment(horizontal: .center, vertical: .lastTextBaseline) 22 | public static var leadingFirstTextBaseline = Alignment(horizontal: .leading, vertical: .firstTextBaseline) 23 | public static var leadingLastTextBaseline = Alignment(horizontal: .leading, vertical: .lastTextBaseline) 24 | public static var trailingFirstTextBaseline = Alignment(horizontal: .trailing, vertical: .firstTextBaseline) 25 | public static var trailingLastTextBaseline = Alignment(horizontal: .trailing, vertical: .lastTextBaseline) 26 | 27 | #if SKIP 28 | /// Return the Compose alignment equivalent. 29 | func asComposeAlignment() -> androidx.compose.ui.Alignment { 30 | switch self { 31 | case .leading, .leadingFirstTextBaseline, .leadingLastTextBaseline: 32 | return androidx.compose.ui.Alignment.CenterStart 33 | case .trailing, .trailingFirstTextBaseline, .trailingLastTextBaseline: 34 | return androidx.compose.ui.Alignment.CenterEnd 35 | case .top: 36 | return androidx.compose.ui.Alignment.TopCenter 37 | case .bottom: 38 | return androidx.compose.ui.Alignment.BottomCenter 39 | case .topLeading: 40 | return androidx.compose.ui.Alignment.TopStart 41 | case .topTrailing: 42 | return androidx.compose.ui.Alignment.TopEnd 43 | case .bottomLeading: 44 | return androidx.compose.ui.Alignment.BottomStart 45 | case .bottomTrailing: 46 | return androidx.compose.ui.Alignment.BottomEnd 47 | default: 48 | return androidx.compose.ui.Alignment.Center 49 | } 50 | } 51 | #endif 52 | } 53 | 54 | #endif 55 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Graphics/StrokeStyle.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if SKIP 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.graphics.PathEffect 7 | import androidx.compose.ui.graphics.StrokeCap 8 | import androidx.compose.ui.graphics.StrokeJoin 9 | import androidx.compose.ui.graphics.drawscope.DrawStyle 10 | import androidx.compose.ui.graphics.drawscope.Stroke 11 | import androidx.compose.ui.platform.LocalDensity 12 | import androidx.compose.ui.unit.dp 13 | 14 | public enum CGLineCap : Int { 15 | case butt = 0 // For bridging 16 | case round = 1 // For bridging 17 | case square = 2 // For bridging 18 | 19 | } 20 | public enum CGLineJoin : Int { 21 | case miter = 0 // For bridging 22 | case round = 1 // For bridging 23 | case bevel = 2 // For bridging 24 | } 25 | #elseif canImport(CoreGraphics) 26 | import struct CoreGraphics.CGFloat 27 | import enum CoreGraphics.CGLineCap 28 | import enum CoreGraphics.CGLineJoin 29 | #endif 30 | 31 | public struct StrokeStyle : Equatable { 32 | public var lineWidth: CGFloat 33 | public var lineCap: CGLineCap 34 | public var lineJoin: CGLineJoin 35 | public var miterLimit: CGFloat 36 | public var dash: [CGFloat] 37 | public var dashPhase: CGFloat 38 | 39 | public init(lineWidth: CGFloat = 1.0, lineCap: CGLineCap = .butt, lineJoin: CGLineJoin = .miter, miterLimit: CGFloat = 10.0, dash: [CGFloat] = [], dashPhase: CGFloat = 0.0) { 40 | self.lineWidth = lineWidth 41 | self.lineCap = lineCap 42 | self.lineJoin = lineJoin 43 | self.miterLimit = miterLimit 44 | self.dash = dash 45 | self.dashPhase = dashPhase 46 | } 47 | 48 | #if SKIP 49 | @Composable func asDrawStyle() -> DrawStyle { 50 | let density = LocalDensity.current 51 | let widthPx = with(density) { lineWidth.dp.toPx() } 52 | 53 | let cap: StrokeCap 54 | switch lineCap { 55 | case CGLineCap.butt: 56 | cap = StrokeCap.Butt 57 | case CGLineCap.round: 58 | cap = StrokeCap.Round 59 | case CGLineCap.square: 60 | cap = StrokeCap.Square 61 | } 62 | 63 | let join: StrokeJoin 64 | switch lineJoin { 65 | case CGLineJoin.bevel: 66 | join = StrokeJoin.Bevel 67 | case CGLineJoin.round: 68 | join = StrokeJoin.Round 69 | case CGLineJoin.miter: 70 | join = StrokeJoin.Miter 71 | } 72 | 73 | var pathEffect: PathEffect? = nil 74 | if !dash.isEmpty { 75 | let intervals = FloatArray(max(2, dash.count)) { element in 76 | with(density) { dash[min(element, dash.count - 1)].dp.toPx() } 77 | } 78 | let phase = with(density) { dashPhase.dp.toPx() } 79 | pathEffect = PathEffect.dashPathEffect(intervals, phase) 80 | } 81 | return Stroke(width = widthPx, miter = Float(miterLimit), cap, join, pathEffect) 82 | } 83 | #endif 84 | } 85 | 86 | #endif 87 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Layout/Unit.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if SKIP 5 | import androidx.compose.animation.core.CubicBezierEasing 6 | import androidx.compose.animation.core.Easing 7 | #endif 8 | 9 | public struct UnitPoint : Codable, Hashable { 10 | public var x = 0.0 11 | public var y = 0.0 12 | 13 | public static let zero = UnitPoint(x: 0.0, y: 0.0) 14 | public static let center = UnitPoint(x: 0.5, y: 0.5) 15 | public static let leading = UnitPoint(x: 0.0, y: 0.5) 16 | public static let trailing = UnitPoint(x: 1.0, y: 0.5) 17 | public static let top = UnitPoint(x: 0.5, y: 0.0) 18 | public static let bottom = UnitPoint(x: 0.5, y: 1.0) 19 | public static let topLeading = UnitPoint(x: 0.0, y: 0.0) 20 | public static let topTrailing = UnitPoint(x: 1.0, y: 0.0) 21 | public static let bottomLeading = UnitPoint(x: 0.0, y: 1.0) 22 | public static let bottomTrailing = UnitPoint(x: 1.0, y: 1.0) 23 | } 24 | 25 | public struct UnitCurve: Hashable { 26 | private let startControlPoint: UnitPoint 27 | private let endControlPoint: UnitPoint 28 | 29 | public init(startControlPoint: UnitPoint, endControlPoint: UnitPoint) { 30 | self.startControlPoint = startControlPoint 31 | self.endControlPoint = endControlPoint 32 | } 33 | 34 | public static func bezier(startControlPoint: UnitPoint, endControlPoint: UnitPoint) -> UnitCurve { 35 | return UnitCurve(startControlPoint: startControlPoint, endControlPoint: endControlPoint) 36 | } 37 | 38 | @available(*, unavailable) 39 | public func value(at progress: Double) -> Double { 40 | fatalError() 41 | } 42 | 43 | @available(*, unavailable) 44 | public func velocity(at progress: Double) -> Double { 45 | fatalError() 46 | } 47 | 48 | @available(*, unavailable) 49 | public var inverse: UnitCurve { 50 | fatalError() 51 | } 52 | 53 | public static let easeInOut = UnitCurve(startControlPoint: UnitPoint(x: 0.42, y: 0.0), endControlPoint: UnitPoint(x: 0.58, y: 1.0)) 54 | 55 | public static let easeIn = UnitCurve(startControlPoint: UnitPoint(x: 0.42, y: 0.0), endControlPoint: UnitPoint(x: 1.0, y: 1.0)) 56 | 57 | public static let easeOut = UnitCurve(startControlPoint: UnitPoint(x: 0.0, y: 0.0), endControlPoint: UnitPoint(x: 0.58, y: 1.0)) 58 | 59 | @available(*, unavailable) 60 | public static var circularEaseIn: UnitCurve { 61 | fatalError() 62 | } 63 | 64 | @available(*, unavailable) 65 | public static var circularEaseOut: UnitCurve { 66 | fatalError() 67 | } 68 | 69 | @available(*, unavailable) 70 | public static var circularEaseInOut: UnitCurve { 71 | fatalError() 72 | } 73 | 74 | public static let linear = UnitCurve(startControlPoint: UnitPoint(x: 0.0, y: 0.0), endControlPoint: UnitPoint(x: 1.0, y: 1.0)) 75 | 76 | #if SKIP 77 | public func asEasing() -> Easing { 78 | return CubicBezierEasing(Float(startControlPoint.x), Float(startControlPoint.y), Float(endControlPoint.x), Float(endControlPoint.y)) 79 | } 80 | #endif 81 | } 82 | 83 | #endif 84 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Commands/EditActions.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | import Foundation 5 | 6 | public struct EditActions /* */ : OptionSet { 7 | public let rawValue: Int 8 | 9 | public init(rawValue: Int) { 10 | self.rawValue = rawValue 11 | } 12 | 13 | public static let move = EditActions(rawValue: 1) // For bridging 14 | public static let delete = EditActions(rawValue: 2) // For bridging 15 | public static let all = EditActions(rawValue: 3) // For bridging 16 | } 17 | 18 | extension View { 19 | // SKIP @bridge 20 | public func deleteDisabled(_ isDisabled: Bool) -> any View { 21 | #if SKIP 22 | return ModifiedContent(content: self, modifier: EditActionsModifier(isDeleteDisabled: isDisabled)) 23 | #else 24 | return self 25 | #endif 26 | } 27 | 28 | // SKIP @bridge 29 | public func moveDisabled(_ isDisabled: Bool) -> any View { 30 | #if SKIP 31 | return ModifiedContent(content: self, modifier: EditActionsModifier(isMoveDisabled: isDisabled)) 32 | #else 33 | return self 34 | #endif 35 | } 36 | } 37 | 38 | #if SKIP 39 | final class EditActionsModifier: RenderModifier { 40 | let isDeleteDisabled: Bool? 41 | let isMoveDisabled: Bool? 42 | 43 | init(isDeleteDisabled: Bool? = nil, isMoveDisabled: Bool? = nil) { 44 | self.isDeleteDisabled = isDeleteDisabled 45 | self.isMoveDisabled = isMoveDisabled 46 | super.init() 47 | } 48 | 49 | /// Return the edit actions modifier information for the given view. 50 | static func combined(for renderable: Renderable) -> EditActionsModifier { 51 | var isDeleteDisabled: Bool? = nil 52 | var isMoveDisabled: Bool? = nil 53 | renderable.forEachModifier { 54 | if let editActionsModifier = $0 as? EditActionsModifier { 55 | isDeleteDisabled = isDeleteDisabled ?? editActionsModifier.isDeleteDisabled 56 | isMoveDisabled = isMoveDisabled ?? editActionsModifier.isMoveDisabled 57 | } 58 | return nil 59 | } 60 | return EditActionsModifier(isDeleteDisabled: isDeleteDisabled, isMoveDisabled: isMoveDisabled) 61 | } 62 | } 63 | 64 | extension Array { 65 | public mutating func remove(atOffsets offsets: IndexSet) { 66 | for offset in offsets.reversed() { 67 | remove(at: offset) 68 | } 69 | } 70 | 71 | public mutating func move(fromOffsets source: IndexSet, toOffset destination: Int) { 72 | // Calling with the same offset or the 73 | guard source.count > 1 || (destination != source[0] && destination != source[0] + 1) else { 74 | return 75 | } 76 | 77 | var moved: [Element] = [] 78 | var belowDestinationCount = 0 79 | for offset in source.reversed() { 80 | moved.append(remove(at: offset)) 81 | if offset < destination { 82 | belowDestinationCount += 1 83 | } 84 | } 85 | for m in moved { 86 | insert(m, at: destination - belowDestinationCount) 87 | } 88 | } 89 | } 90 | #endif 91 | #endif 92 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/App/Scene.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | import Foundation 5 | 6 | public protocol Scene { 7 | // associatedtype Body : Scene 8 | // @SceneBuilder @MainActor var body: Self.Body { get } 9 | } 10 | 11 | extension Scene { 12 | @available(*, unavailable) 13 | public func backgroundTask(_ task: BackgroundTask, action: @escaping (D) async -> R) -> some Scene /* where D : Sendable, R : Sendable */ { 14 | return self 15 | } 16 | 17 | @available(*, unavailable) 18 | public func commands/* */(/* @CommandsBuilder */ content: () -> Any /* Content */) -> some Scene /* where Content : Commands */ { 19 | return self 20 | } 21 | 22 | @available(*, unavailable) 23 | public func commandsRemoved() -> some Scene { 24 | return self 25 | } 26 | 27 | @available(*, unavailable) 28 | public func commandsReplaced/* */(/* @CommandsBuilder */ content: () -> Any /* Content */) -> some Scene /* where Content : Commands */ { 29 | return self 30 | } 31 | 32 | @available(*, unavailable) 33 | public func defaultAppStorage(_ store: UserDefaults) -> some Scene { 34 | return self 35 | } 36 | 37 | @available(*, unavailable) 38 | public func defaultSize(_ size: CGSize) -> some Scene { 39 | return self 40 | } 41 | 42 | @available(*, unavailable) 43 | public func defaultSize(width: CGFloat, height: CGFloat) -> some Scene { 44 | return self 45 | } 46 | 47 | @available(*, unavailable) 48 | public func handlesExternalEvents(matching conditions: Set) -> some Scene { 49 | return self 50 | } 51 | 52 | @available(*, unavailable) 53 | public func onChange(of value: V, perform action: @escaping (_ newValue: V) -> Void) -> some Scene where V : Equatable { 54 | return self 55 | } 56 | 57 | @available(*, unavailable) 58 | public func onChange(of value: V, initial: Bool = false, _ action: @escaping (_ oldValue: V, _ newValue: V) -> Void) -> some Scene where V : Equatable { 59 | return self 60 | } 61 | 62 | @available(*, unavailable) 63 | public func onChange(of value: V, initial: Bool = false, _ action: @escaping () -> Void) -> some Scene where V : Equatable { 64 | return self 65 | } 66 | 67 | @available(*, unavailable) 68 | public func windowResizability(_ resizability: WindowResizability) -> some Scene { 69 | return self 70 | } 71 | } 72 | 73 | public struct ScenePadding : Equatable { 74 | public static let minimum = ScenePadding() 75 | } 76 | 77 | extension View { 78 | @available(*, unavailable) 79 | public func scenePadding(_ edges: Edge.Set = .all) -> some View { 80 | return self 81 | } 82 | 83 | @available(*, unavailable) 84 | public func scenePadding(_ padding: ScenePadding, edges: Edge.Set = .all) -> some View { 85 | return self 86 | } 87 | } 88 | 89 | public enum ScenePhase : Int, Comparable, Hashable { 90 | case background = 1 // For bridging 91 | case inactive = 2 // For bridging 92 | case active = 3 // For bridging 93 | 94 | public static func < (a: ScenePhase, b: ScenePhase) -> Bool { 95 | return a.rawValue < b.rawValue 96 | } 97 | } 98 | #endif 99 | -------------------------------------------------------------------------------- /Sources/SkipUI/Skip/Reorderable.kt: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 2 | // 3 | // This file uses code publised with the following header: 4 | // 5 | /* 6 | * Copyright 2022 André Claßen 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * https://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | package org.burnoutcrew.reorderable 21 | 22 | import androidx.compose.foundation.gestures.drag 23 | import androidx.compose.foundation.gestures.forEachGesture 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.geometry.Offset 26 | import androidx.compose.ui.input.pointer.PointerId 27 | import androidx.compose.ui.input.pointer.PointerInputChange 28 | import androidx.compose.ui.input.pointer.PointerInputScope 29 | import androidx.compose.ui.input.pointer.changedToUp 30 | import androidx.compose.ui.input.pointer.pointerInput 31 | import androidx.compose.ui.input.pointer.positionChange 32 | 33 | fun Modifier.reorderable( 34 | state: ReorderableState<*> 35 | ) = then( 36 | Modifier.pointerInput(Unit) { 37 | forEachGesture { 38 | val dragStart = state.interactions.receive() 39 | val down = awaitPointerEventScope { 40 | currentEvent.changes.firstOrNull { it.id == dragStart.id } 41 | } 42 | if (down != null && state.onDragStart(down.position.x.toInt(), down.position.y.toInt())) { 43 | dragStart.offset?.apply { 44 | state.onDrag(x.toInt(), y.toInt()) 45 | } 46 | detectDrag( 47 | down.id, 48 | onDragEnd = { 49 | state.onDragCanceled() 50 | }, 51 | onDragCancel = { 52 | state.onDragCanceled() 53 | }, 54 | onDrag = { change, dragAmount -> 55 | change.consume() 56 | state.onDrag(dragAmount.x.toInt(), dragAmount.y.toInt()) 57 | }) 58 | } 59 | } 60 | }) 61 | 62 | internal suspend fun PointerInputScope.detectDrag( 63 | down: PointerId, 64 | onDragEnd: () -> Unit = { }, 65 | onDragCancel: () -> Unit = { }, 66 | onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit, 67 | ) { 68 | awaitPointerEventScope { 69 | if ( 70 | drag(down) { 71 | onDrag(it, it.positionChange()) 72 | it.consume() 73 | } 74 | ) { 75 | // consume up if we quit drag gracefully with the up 76 | currentEvent.changes.forEach { 77 | if (it.changedToUp()) it.consume() 78 | } 79 | onDragEnd() 80 | } else { 81 | onDragCancel() 82 | } 83 | } 84 | } 85 | 86 | internal data class StartDrag(val id: PointerId, val offset: Offset? = null) 87 | -------------------------------------------------------------------------------- /Tests/SkipUITests/XCSkipTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | import Foundation 4 | import XCTest 5 | 6 | #if os(macOS) 7 | import SkipTest 8 | 9 | /// This test case will run the transpiled tests for the Skip module. 10 | @available(macOS 13, macCatalyst 16, *) 11 | final class XCSkipTests: XCTestCase, XCGradleHarness { 12 | public func testSkipModule() async throws { 13 | // set device ID to run in Android emulator vs. robolectric 14 | try await runGradleTests() 15 | } 16 | } 17 | #endif 18 | 19 | 20 | open class SkipUITestCase : XCTestCase { 21 | 22 | #if SKIP 23 | // must be public and be prefixed wit "@get:" or else org.junit.runners.model.InvalidTestClassError: "The @Rule '_testName' must be public" 24 | // SKIP INSERT: @get:org.junit.Rule 25 | public let _testName: org.junit.rules.TestName = org.junit.rules.TestName() 26 | #endif 27 | 28 | public var testName: String? { 29 | #if SKIP 30 | let tname = _testName.methodName // "testLocalizedText$SkipUI_debugUnitTest" 31 | return tname.split(separator: Char("$")).first 32 | #else 33 | let tname = testRun?.test.name // "-[SkipUITests testLocalizedText]" 34 | return tname? 35 | .trimmingCharacters(in: CharacterSet(charactersIn: "-[]")) 36 | .split(separator: " ") 37 | .last? 38 | .description 39 | #endif 40 | } 41 | 42 | open override func setUp() { 43 | super.setUp() 44 | 45 | // this is where we could try to identify whether we are just running a single test case, and if so, we can run the `gradle test` command with flags to filter the test cases to just run the single transpiled equivalent test case – see https://github.com/skiptools/skip-unit/issues/1 46 | let filteredTestCase: String? = nil 47 | 48 | if let filteredTestCase = filteredTestCase { 49 | if self.testName != filteredTestCase { 50 | #if SKIP 51 | throw XCTSkip("skipping filtered test \(self.testName)") 52 | #endif 53 | } else { 54 | // we are running a restricted 55 | #if !SKIP 56 | // TODO: launch gradle with the correct arguments to run the tests with the filtered test case 57 | #endif 58 | } 59 | } 60 | } 61 | 62 | open override func tearDown() { 63 | super.tearDown() 64 | } 65 | } 66 | 67 | public class TestIntrospectionTests : SkipUITestCase { 68 | func testTestIntrospection() { 69 | XCTAssertEqual("testTestIntrospection", self.testName) 70 | } 71 | } 72 | 73 | /// True when running in a transpiled Java runtime environment 74 | let isJava = ProcessInfo.processInfo.environment["java.io.tmpdir"] != nil 75 | /// True when running within an Android environment (either an emulator or device) 76 | let isAndroid = isJava && ProcessInfo.processInfo.environment["ANDROID_ROOT"] != nil 77 | /// True is the transpiled code is currently running in the local Robolectric test environment 78 | let isRobolectric = isJava && !isAndroid 79 | #if os(macOS) 80 | let isMacOS = true 81 | #else 82 | let isMacOS = false 83 | #endif 84 | #if os(iOS) 85 | let isIOS = true 86 | #else 87 | let isIOS = false 88 | #endif 89 | #if os(Linux) 90 | let isLinux = true 91 | #else 92 | let isLinux = false 93 | #endif 94 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Layout/Anchor.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | /* 4 | import struct CoreGraphics.CGPoint 5 | import struct CoreGraphics.CGRect 6 | 7 | /// An opaque value derived from an anchor source and a particular view. 8 | /// 9 | /// You can convert the anchor to a `Value` in the coordinate space of a target 10 | /// view by using a ``GeometryProxy`` to specify the target view. 11 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 12 | @frozen public struct Anchor { 13 | 14 | /// A type-erased geometry value that produces an anchored value of a given 15 | /// type. 16 | /// 17 | /// SkipUI passes anchored geometry values around the view tree via 18 | /// preference keys. It then converts them back into the local coordinate 19 | /// space using a ``GeometryProxy`` value. 20 | @frozen public struct Source { 21 | } 22 | } 23 | 24 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 25 | extension Anchor : Sendable where Value : Sendable { 26 | } 27 | 28 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 29 | extension Anchor : Equatable where Value : Equatable { 30 | 31 | } 32 | 33 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 34 | extension Anchor : Hashable where Value : Hashable { 35 | 36 | 37 | } 38 | 39 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 40 | extension Anchor.Source where Value == CGRect { 41 | 42 | /// Returns an anchor source rect defined by `r` in the current view. 43 | public static func rect(_ r: CGRect) -> Anchor.Source { fatalError() } 44 | 45 | /// An anchor source rect defined as the entire bounding rect of the current 46 | /// view. 47 | public static var bounds: Anchor.Source { get { fatalError() } } 48 | } 49 | 50 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 51 | extension Anchor.Source where Value == CGPoint { 52 | 53 | public static func point(_ p: CGPoint) -> Anchor.Source { fatalError() } 54 | 55 | public static func unitPoint(_ p: UnitPoint) -> Anchor.Source { fatalError() } 56 | 57 | public static var topLeading: Anchor.Source { get { fatalError() } } 58 | 59 | public static var top: Anchor.Source { get { fatalError() } } 60 | 61 | public static var topTrailing: Anchor.Source { get { fatalError() } } 62 | 63 | public static var leading: Anchor.Source { get { fatalError() } } 64 | 65 | public static var center: Anchor.Source { get { fatalError() } } 66 | 67 | public static var trailing: Anchor.Source { get { fatalError() } } 68 | 69 | public static var bottomLeading: Anchor.Source { get { fatalError() } } 70 | 71 | public static var bottom: Anchor.Source { get { fatalError() } } 72 | 73 | public static var bottomTrailing: Anchor.Source { get { fatalError() } } 74 | } 75 | 76 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 77 | extension Anchor.Source : Sendable where Value : Sendable { 78 | } 79 | 80 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 81 | extension Anchor.Source { 82 | 83 | public init(_ array: [Anchor.Source]) where Value == [T] { fatalError() } 84 | } 85 | 86 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 87 | extension Anchor.Source { 88 | 89 | public init(_ anchor: Anchor.Source?) where Value == T? { fatalError() } 90 | } 91 | */ 92 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Graphics/VectorArithmetic.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | // TODO: Process for use in SkipUI 5 | 6 | #if !SKIP 7 | 8 | import struct CoreGraphics.CGFloat 9 | 10 | /// A type that can serve as the animatable data of an animatable type. 11 | /// 12 | /// `VectorArithmetic` extends the `AdditiveArithmetic` protocol with scalar 13 | /// multiplication and a way to query the vector magnitude of the value. Use 14 | /// this type as the `animatableData` associated type of a type that conforms to 15 | /// the ``Animatable`` protocol. 16 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 17 | public protocol VectorArithmetic : AdditiveArithmetic { 18 | 19 | /// Multiplies each component of this value by the given value. 20 | mutating func scale(by rhs: Double) 21 | 22 | /// Returns the dot-product of this vector arithmetic instance with itself. 23 | var magnitudeSquared: Double { get } 24 | } 25 | 26 | #endif 27 | 28 | #if !SKIP 29 | 30 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 31 | extension VectorArithmetic { 32 | 33 | /// Returns a value with each component of this value multiplied by the 34 | /// given value. 35 | public func scaled(by rhs: Double) -> Self { fatalError() } 36 | 37 | /// Interpolates this value with `other` by the specified `amount`. 38 | /// 39 | /// This is equivalent to `self = self + (other - self) * amount`. 40 | public mutating func interpolate(towards other: Self, amount: Double) { fatalError() } 41 | 42 | /// Returns this value interpolated with `other` by the specified `amount`. 43 | /// 44 | /// This result is equivalent to `self + (other - self) * amount`. 45 | public func interpolated(towards other: Self, amount: Double) -> Self { fatalError() } 46 | } 47 | 48 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 49 | extension Float : VectorArithmetic { 50 | 51 | /// Multiplies each component of this value by the given value. 52 | public mutating func scale(by rhs: Double) { fatalError() } 53 | 54 | /// Returns the dot-product of this vector arithmetic instance with itself. 55 | public var magnitudeSquared: Double { get { fatalError() } } 56 | } 57 | 58 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 59 | extension Double : VectorArithmetic { 60 | 61 | /// Multiplies each component of this value by the given value. 62 | public mutating func scale(by rhs: Double) { fatalError() } 63 | 64 | /// Returns the dot-product of this vector arithmetic instance with itself. 65 | public var magnitudeSquared: Double { get { fatalError() } } 66 | } 67 | 68 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 69 | extension CGFloat : VectorArithmetic { 70 | 71 | /// Multiplies each component of this value by the given value. 72 | public mutating func scale(by rhs: Double) { fatalError() } 73 | 74 | /// Returns the dot-product of this vector arithmetic instance with itself. 75 | public var magnitudeSquared: Double { get { fatalError() } } 76 | } 77 | 78 | //extension Never : VectorArithmetic { 79 | // public static func - (lhs: Never, rhs: Never) -> Never { } 80 | // public static func + (lhs: Never, rhs: Never) -> Never { } 81 | // public mutating func scale(by rhs: Double) { fatalError() } 82 | // public var magnitudeSquared: Double { fatalError() } 83 | // public static var zero: Never { fatalError() } 84 | //} 85 | 86 | #endif 87 | #endif 88 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Text/TextInput.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if SKIP 5 | import androidx.compose.ui.text.input.KeyboardCapitalization 6 | #endif 7 | 8 | public enum TextInputAutocapitalization: Int { 9 | case never = 0 // For bridging 10 | case words = 1 // For bridging 11 | case sentences = 2 // For bridging 12 | case characters = 3 // For bridging 13 | 14 | #if SKIP 15 | func asKeyboardCapitalization() -> KeyboardCapitalization { 16 | switch self { 17 | case .never: 18 | return KeyboardCapitalization.None 19 | case .words: 20 | return KeyboardCapitalization.Words 21 | case .sentences: 22 | return KeyboardCapitalization.Sentences 23 | case .characters: 24 | return KeyboardCapitalization.Characters 25 | } 26 | } 27 | #endif 28 | } 29 | 30 | public struct TextInputFormattingControlPlacement { 31 | public struct Set : OptionSet { 32 | public let rawValue: Int 33 | 34 | public init(rawValue: Int) { 35 | self.rawValue = rawValue 36 | } 37 | 38 | public static let contextMenu = TextInputFormattingControlPlacement.Set(rawValue: 1 << 0) 39 | public static let inputAssistant = TextInputFormattingControlPlacement.Set(rawValue: 1 << 1) 40 | public static let all = TextInputFormattingControlPlacement.Set(rawValue: 1 << 2) 41 | public static let `default` = TextInputFormattingControlPlacement.Set(rawValue: 1 << 3) 42 | } 43 | } 44 | 45 | /* 46 | @available(iOS 17.0, *) 47 | @available(macOS, unavailable) 48 | @available(watchOS, unavailable) 49 | @available(tvOS, unavailable) 50 | public struct TextInputDictationActivation : Equatable, Sendable { 51 | 52 | /// A configuration that activates dictation when someone selects the 53 | /// microphone. 54 | @available(iOS 17.0, *) 55 | @available(macOS, unavailable) 56 | @available(watchOS, unavailable) 57 | @available(tvOS, unavailable) 58 | public static let onSelect: TextInputDictationActivation = { fatalError() }() 59 | 60 | /// A configuration that activates dictation when someone selects the 61 | /// microphone or looks at the entry field. 62 | @available(iOS 17.0, *) 63 | @available(macOS, unavailable) 64 | @available(watchOS, unavailable) 65 | @available(tvOS, unavailable) 66 | public static let onLook: TextInputDictationActivation = { fatalError() }() 67 | 68 | 69 | } 70 | 71 | @available(iOS 17.0, *) 72 | @available(macOS, unavailable) 73 | @available(watchOS, unavailable) 74 | @available(tvOS, unavailable) 75 | public struct TextInputDictationBehavior : Equatable, Sendable { 76 | 77 | /// A platform-appropriate default text input dictation behavior. 78 | /// 79 | /// The automatic behavior uses a ``TextInputDictationActivation`` value of 80 | /// ``TextInputDictationActivation/onLook`` for visionOS apps and 81 | /// ``TextInputDictationActivation/onSelect`` for iOS apps. 82 | @available(iOS 17.0, *) 83 | @available(macOS, unavailable) 84 | @available(watchOS, unavailable) 85 | @available(tvOS, unavailable) 86 | public static let automatic: TextInputDictationBehavior = { fatalError() }() 87 | 88 | /// Adds a dictation microphone in the search bar. 89 | @available(iOS 17.0, *) 90 | @available(macOS, unavailable) 91 | @available(watchOS, unavailable) 92 | @available(tvOS, unavailable) 93 | public static func inline(activation: TextInputDictationActivation) -> TextInputDictationBehavior { fatalError() } 94 | 95 | 96 | } 97 | */ 98 | #endif 99 | -------------------------------------------------------------------------------- /Sources/SkipUI/Skip/Shadowed.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | package skip.ui 4 | 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.draw.BlurredEdgeTreatment 9 | import androidx.compose.ui.draw.blur 10 | import androidx.compose.ui.draw.drawWithContent 11 | import androidx.compose.ui.geometry.Rect 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.graphics.ColorFilter 14 | import androidx.compose.ui.graphics.ColorMatrix 15 | import androidx.compose.ui.graphics.Paint 16 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas 17 | import androidx.compose.ui.layout.Layout 18 | import androidx.compose.ui.platform.LocalDensity 19 | import androidx.compose.ui.unit.Constraints 20 | import androidx.compose.ui.unit.Dp 21 | import kotlin.math.ceil 22 | 23 | /// Compose the given content with a drop shadow on all non-transparent pixels. 24 | @Composable fun Shadowed(context: ComposeContext, color: Color, offsetX: Dp, offsetY: Dp, blurRadius: Dp, content: @Composable (ComposeContext) -> Unit) { 25 | val density = LocalDensity.current 26 | val offsetXPx = with(density) { offsetX.toPx() }.toInt() 27 | val offsetYPx = with(density) { offsetY.toPx() }.toInt() 28 | val blurRadiusPx = ceil(with(density) { blurRadius.toPx() }).toInt() 29 | 30 | val contentContext = context.content() 31 | val shadowContext = context.content(modifier = Modifier 32 | .drawWithContent { 33 | val matrix = shadowColorMatrix(color) 34 | val filter = ColorFilter.colorMatrix(matrix) 35 | val paint = Paint().apply { 36 | colorFilter = filter 37 | } 38 | drawIntoCanvas { canvas -> 39 | // Paint content again with shadow color 40 | canvas.saveLayer(Rect(0f, 0f, size.width, size.height), paint) 41 | drawContent() 42 | canvas.restore() 43 | } 44 | } 45 | .blur(radius = blurRadius, BlurredEdgeTreatment.Unbounded) 46 | .padding(all = blurRadius) // Pad to prevent clipping blur 47 | ) 48 | 49 | Layout(modifier = context.modifier, content = { 50 | content(contentContext) // Render normally 51 | content(shadowContext) // Render as shadow 52 | }) { measurables, constraints -> 53 | // Allow shadow to extend beyond bounds without affecting layout 54 | val contentPlaceable = measurables[0].measure(constraints) 55 | val shadowPlaceable = measurables[1].measure(Constraints(maxWidth = contentPlaceable.width + blurRadiusPx * 2, maxHeight = contentPlaceable.height + blurRadiusPx * 2)) 56 | layout(width = contentPlaceable.width, height = contentPlaceable.height) { 57 | shadowPlaceable.placeRelative(x = offsetXPx - blurRadiusPx, y = offsetYPx - blurRadiusPx) 58 | contentPlaceable.placeRelative(x = 0, y = 0) 59 | } 60 | } 61 | } 62 | 63 | /// Return a color matrix with which to paint our content as a shadow of the given color. 64 | private fun shadowColorMatrix(color: Color): ColorMatrix { 65 | return ColorMatrix().apply { 66 | set(0, 0, 0f) // Do not preserve original R 67 | set(1, 1, 0f) // Do not preserve original G 68 | set(2, 2, 0f) // Do not preserve original B 69 | 70 | set(0, 4, color.red * 255) // Use given color's R 71 | set(1, 4, color.green * 255) // Use given color's G 72 | set(2, 4, color.blue * 255) // Use given color's B 73 | set(3, 3, color.alpha) // Multiply original alpha by shadow color alpha 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/TypesettingLanguage.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | /* 4 | import struct Foundation.Locale 5 | 6 | /// Defines how typesetting language is determined for text. 7 | /// 8 | /// Use ``View/typesettingLanguage(_:isEnabled:)`` or 9 | /// ``Text/typesettingLanguage(_:isEnabled:)`` to specify 10 | /// the typesetting language . 11 | @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) 12 | public struct TypesettingLanguage : Sendable, Equatable { 13 | 14 | /// Automatic language behavior. 15 | /// 16 | /// When determining the language to use for typesetting the current UI 17 | /// language and preferred languages will be considiered. For example, if 18 | /// the current UI locale is for English and Thai is included in the 19 | /// preferred languages then line heights will be taller to accommodate the 20 | /// taller glyphs used by Thai. 21 | public static let automatic: TypesettingLanguage = { fatalError() }() 22 | 23 | /// Use explicit language. 24 | /// 25 | /// An explicit language will be used for typesetting. For example, if used 26 | /// with Thai language the line heights will be as tall as needed to 27 | /// accommodate Thai. 28 | /// 29 | /// - Parameters: 30 | /// - language: The language to use for typesetting. 31 | /// - Returns: A `TypesettingLanguage`. 32 | public static func explicit(_ language: Locale.Language) -> TypesettingLanguage { fatalError() } 33 | 34 | 35 | } 36 | 37 | extension View { 38 | 39 | /// Specifies the language for typesetting. 40 | /// 41 | /// In some cases `Text` may contain text of a particular language which 42 | /// doesn't match the device UI language. In that case it's useful to 43 | /// specify a language so line height, line breaking and spacing will 44 | /// respect the script used for that language. For example: 45 | /// 46 | /// Text(verbatim: "แอปเปิล") 47 | /// .typesettingLanguage(.init(languageCode: .thai)) 48 | /// 49 | /// Note: this language does not affect text localization. 50 | /// 51 | /// - Parameters: 52 | /// - language: The explicit language to use for typesetting. 53 | /// - isEnabled: A Boolean value that indicates whether text langauge is 54 | /// added 55 | /// - Returns: A view with the typesetting language set to the value you 56 | /// supply. 57 | @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) 58 | public func typesettingLanguage(_ language: Locale.Language, isEnabled: Bool = true) -> some View { return stubView() } 59 | 60 | 61 | /// Specifies the language for typesetting. 62 | /// 63 | /// In some cases `Text` may contain text of a particular language which 64 | /// doesn't match the device UI language. In that case it's useful to 65 | /// specify a language so line height, line breaking and spacing will 66 | /// respect the script used for that language. For example: 67 | /// 68 | /// Text(verbatim: "แอปเปิล").typesettingLanguage( 69 | /// .explicit(.init(languageCode: .thai))) 70 | /// 71 | /// Note: this language does not affect text localized localization. 72 | /// 73 | /// - Parameters: 74 | /// - language: The language to use for typesetting. 75 | /// - isEnabled: A Boolean value that indicates whether text language is 76 | /// added 77 | /// - Returns: A view with the typesetting language set to the value you 78 | /// supply. 79 | @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) 80 | public func typesettingLanguage(_ language: TypesettingLanguage, isEnabled: Bool = true) -> some View { return stubView() } 81 | } 82 | */ 83 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/FileDialogBrowserOptions.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | /* 4 | /// The way that file dialogs present the file system. 5 | /// 6 | /// Apply the options using the ``View/fileDialogBrowserOptions(_:)`` modifier. 7 | @available(iOS 17.0, macOS 14.0, *) 8 | @available(tvOS, unavailable) 9 | @available(watchOS, unavailable) 10 | public struct FileDialogBrowserOptions : OptionSet { 11 | 12 | /// The corresponding value of the raw type. 13 | /// 14 | /// A new instance initialized with `rawValue` will be equivalent to this 15 | /// instance. For example: 16 | /// 17 | /// enum PaperSize: String { 18 | /// case A4, A5, Letter, Legal 19 | /// } 20 | /// 21 | /// let selectedSize = PaperSize.Letter 22 | /// print(selectedSize.rawValue) 23 | /// // Prints "Letter" 24 | /// 25 | /// print(selectedSize == PaperSize(rawValue: selectedSize.rawValue)!) 26 | /// // Prints "true" 27 | public let rawValue: Int = { fatalError() }() 28 | 29 | /// Creates a new option set from the given raw value. 30 | /// 31 | /// This initializer always succeeds, even if the value passed as `rawValue` 32 | /// exceeds the static properties declared as part of the option set. This 33 | /// example creates an instance of `ShippingOptions` with a raw value beyond 34 | /// the highest element, with a bit mask that effectively contains all the 35 | /// declared static members. 36 | /// 37 | /// let extraOptions = ShippingOptions(rawValue: 255) 38 | /// print(extraOptions.isStrictSuperset(of: .all)) 39 | /// // Prints "true" 40 | /// 41 | /// - Parameter rawValue: The raw value of the option set to create. Each bit 42 | /// of `rawValue` potentially represents an element of the option set, 43 | /// though raw values may include bits that are not defined as distinct 44 | /// values of the `OptionSet` type. 45 | public init(rawValue: Int) { fatalError() } 46 | 47 | /// Allows enumerating packages contents in contrast to the default behavior 48 | /// when packages are represented flatly, similar to files. 49 | public static let enumeratePackages: FileDialogBrowserOptions = { fatalError() }() 50 | 51 | /// Displays the files that are hidden by default. 52 | public static let includeHiddenFiles: FileDialogBrowserOptions = { fatalError() }() 53 | 54 | /// On iOS, configures the `fileExporter`, `fileImporter`, 55 | /// or `fileMover` to show or hide file extensions. 56 | /// Default behavior is to hide them. 57 | /// On macOS, this option has no effect. 58 | public static let displayFileExtensions: FileDialogBrowserOptions = { fatalError() }() 59 | 60 | /// The type of the elements of an array literal. 61 | public typealias ArrayLiteralElement = FileDialogBrowserOptions 62 | 63 | /// The element type of the option set. 64 | /// 65 | /// To inherit all the default implementations from the `OptionSet` protocol, 66 | /// the `Element` type must be `Self`, the default. 67 | public typealias Element = FileDialogBrowserOptions 68 | 69 | /// The raw type that can be used to represent all values of the conforming 70 | /// type. 71 | /// 72 | /// Every distinct value of the conforming type has a corresponding unique 73 | /// value of the `RawValue` type, but there may be values of the `RawValue` 74 | /// type that don't have a corresponding value of the conforming type. 75 | public typealias RawValue = Int 76 | } 77 | 78 | @available(iOS 17.0, macOS 14.0, *) 79 | @available(tvOS, unavailable) 80 | @available(watchOS, unavailable) 81 | extension FileDialogBrowserOptions : Sendable { 82 | } 83 | */ 84 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Components/Spacer.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if SKIP 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.width 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.dp 11 | #elseif canImport(CoreGraphics) 12 | import struct CoreGraphics.CGFloat 13 | #endif 14 | 15 | // We use a class rather than struct to be able to mutate the `positionalMinLength` for layout. 16 | // SKIP @bridge 17 | public final class Spacer : View, Renderable { 18 | let minLength: CGFloat? 19 | 20 | // SKIP @bridge 21 | public init(minLength: CGFloat? = nil) { 22 | self.minLength = minLength 23 | } 24 | 25 | #if SKIP 26 | /// When we layout an `HStack` or `VStack` we apply a positional min length to spacers between elements. 27 | var positionalMinLength: CGFloat? 28 | 29 | @Composable override func Render(context: ComposeContext) { 30 | let layoutImplementationVersion = EnvironmentValues.shared._layoutImplementationVersion 31 | let axis = EnvironmentValues.shared._layoutAxis 32 | let effectiveMinLength = minLength ?? positionalMinLength 33 | let minLengthFloat: Float? = effectiveMinLength != nil && effectiveMinLength! > 0.0 ? Float(effectiveMinLength!) : nil 34 | if layoutImplementationVersion == 0 { 35 | // Maintain previous layout behavior for users who opt in 36 | if let minLengthFloat { 37 | let minModifier: Modifier 38 | switch axis { 39 | case .horizontal: 40 | minModifier = Modifier.width(minLengthFloat.dp) 41 | case .vertical: 42 | minModifier = Modifier.height(minLengthFloat.dp) 43 | case nil: 44 | minModifier = Modifier 45 | } 46 | androidx.compose.foundation.layout.Spacer(modifier: minModifier.then(context.modifier)) 47 | } 48 | 49 | let fillModifier: Modifier 50 | switch axis { 51 | case .horizontal: 52 | fillModifier = EnvironmentValues.shared._flexibleWidth?(nil, nil, Float.flexibleSpace) ?? Modifier 53 | case .vertical: 54 | fillModifier = EnvironmentValues.shared._flexibleHeight?(nil, nil, Float.flexibleSpace) ?? Modifier 55 | case nil: 56 | fillModifier = Modifier 57 | } 58 | androidx.compose.foundation.layout.Spacer(modifier: fillModifier.then(context.modifier)) 59 | } else { 60 | let modifier: Modifier 61 | switch axis { 62 | case .horizontal: 63 | if let flexibleWidth = EnvironmentValues.shared._flexibleWidth { 64 | modifier = flexibleWidth(nil, minLengthFloat, Float.flexibleSpace) 65 | } else { 66 | modifier = Modifier 67 | } 68 | case .vertical: 69 | if let flexibleHeight = EnvironmentValues.shared._flexibleHeight { 70 | modifier = flexibleHeight(nil, minLengthFloat, Float.flexibleSpace) 71 | } else { 72 | modifier = Modifier 73 | } 74 | case nil: 75 | modifier = Modifier.fillMaxSize() 76 | } 77 | androidx.compose.foundation.layout.Spacer(modifier: modifier.then(context.modifier)) 78 | } 79 | } 80 | #else 81 | public var body: some View { 82 | stubView() 83 | } 84 | #endif 85 | } 86 | 87 | public enum SpacerSizing: Int { 88 | case flexible = 1 // For bridging 89 | case fixed = 2 // For bridging 90 | } 91 | 92 | #endif 93 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Compose/ComposeBuilder.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if SKIP 5 | import androidx.compose.runtime.Composable 6 | #endif 7 | 8 | /// Used to wrap the content of SwiftUI `@ViewBuilders` for rendering by Compose. 9 | // SKIP @bridge 10 | public struct ComposeBuilder: View { 11 | #if SKIP 12 | private let content: @Composable (ComposeContext) -> ComposeResult 13 | #endif 14 | 15 | /// If the result of the given block is a `ComposeBuilder` return it, else create a `ComposeBuilder` whose content is the 16 | /// resulting view. 17 | public static func from(_ content: () -> any View) -> ComposeBuilder { 18 | let view = content() 19 | return view as? ComposeBuilder ?? ComposeBuilder(view: view) 20 | } 21 | 22 | /// Construct with static content. 23 | /// 24 | /// Used primarily when manually constructing views for internal use. 25 | public init(view: any View) { 26 | #if SKIP 27 | self.content = { context in 28 | return view.Compose(context: context) 29 | } 30 | #endif 31 | } 32 | 33 | // SKIP @bridge 34 | public init(bridgedViews: [any View]) { 35 | #if SKIP 36 | self.content = { context in 37 | bridgedViews.forEach { $0.Compose(context: context) } 38 | return ComposeResult.ok 39 | } 40 | #endif 41 | } 42 | 43 | #if SKIP 44 | /// Constructor. 45 | /// 46 | /// The supplied `content` is the content to compose. When transpiling SwiftUI code, this is the logic embedded in the user's `body` and within each container view in 47 | /// that `body`, as well as within other `@ViewBuilders`. 48 | /// 49 | /// - Note: Returning a result from `content` is important. This prevents Compose from recomposing `content` on its own. Instead, a change that would recompose 50 | /// `content` elevates to our void `Renderable.Render`. This allows us to prepare for recompositions, e.g. making the proper callbacks to the context's `composer`. 51 | public init(content: @Composable (ComposeContext) -> ComposeResult) { 52 | self.content = content 53 | } 54 | 55 | @Composable override func Compose(context: ComposeContext) -> ComposeResult { 56 | // If there is a composer, allow its result to escape. Otherwise compose in a non-escaping context 57 | // to avoid unneeded recomposes 58 | if context.composer != nil { 59 | return content(context) 60 | } else { 61 | _ComposeContent(context) 62 | return ComposeResult.ok 63 | } 64 | } 65 | 66 | /// Create a non-escaping context to avoid unnecessary recomposition. 67 | @Composable private func _ComposeContent(context: ComposeContext) { 68 | content(context) 69 | } 70 | 71 | @Composable override func Evaluate(context: ComposeContext, options: Int) -> kotlin.collections.List { 72 | let renderables: kotlin.collections.MutableList = mutableListOf() 73 | let isKeepNonModified = EvaluateOptions(options).isKeepNonModified 74 | let evalContext = context.content(composer: Composer { view, context in 75 | // Note: this logic is also in `ModifiedContent`, but we need to check here as well in case no modifiers are used 76 | if isKeepNonModified && !(view is ModifiedContent) && !(view is ForEach) && !(view is Group) { 77 | renderables.add(view.asRenderable()) 78 | } else { 79 | renderables.addAll(view.Evaluate(context: context(false), options: options)) 80 | } 81 | return ComposeResult.ok 82 | }) 83 | content(evalContext) 84 | return renderables 85 | } 86 | #else 87 | public var body: some View { 88 | stubView() 89 | } 90 | #endif 91 | } 92 | #endif 93 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Containers/ViewThatFits.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | 5 | #if SKIP 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.layout.Layout 8 | import androidx.compose.ui.unit.Constraints 9 | #elseif canImport(CoreGraphics) 10 | import struct CoreGraphics.CGFloat 11 | #endif 12 | 13 | #if SKIP 14 | // SKIP @bridge 15 | public struct ViewThatFits : View, Renderable { 16 | let axes: Axis.Set 17 | let content: ComposeBuilder 18 | 19 | public init(in axes: Axis.Set = [.horizontal, .vertical], @ViewBuilder content: () -> any View) { 20 | self.axes = axes 21 | self.content = ComposeBuilder.from(content) 22 | } 23 | 24 | // SKIP @bridge 25 | public init(bridgedAxes: Int, bridgedContent: any View) { 26 | self.axes = Axis.Set(rawValue: bridgedAxes) 27 | self.content = ComposeBuilder.from { bridgedContent } 28 | } 29 | 30 | @Composable override func Render(context: ComposeContext) { 31 | let candidates = content.Evaluate(context: context, options: 0).filter { !$0.isSwiftUIEmptyView } 32 | guard !candidates.isEmpty() else { 33 | return 34 | } 35 | 36 | let contentContext = context.content() 37 | ComposeContainer(modifier: context.modifier) { modifier in 38 | Layout(modifier: modifier, content: { 39 | for candidate in candidates { 40 | candidate.Render(context: contentContext) 41 | } 42 | }) { measurables, constraints in 43 | guard !measurables.isEmpty() else { 44 | return layout(width: 0, height: 0) { } 45 | } 46 | 47 | let maxWidth = constraints.maxWidth 48 | let maxHeight = constraints.maxHeight 49 | 50 | func fits(measuredWidth: Int, measuredHeight: Int) -> Bool { 51 | if axes.contains(.horizontal) && maxWidth != Constraints.Infinity && measuredWidth > maxWidth { 52 | return false 53 | } 54 | if axes.contains(.vertical) && maxHeight != Constraints.Infinity && measuredHeight > maxHeight { 55 | return false 56 | } 57 | return true 58 | } 59 | 60 | // Determine each candidate's ideal size using intrinsics with an unconstrained cross-axis, 61 | // then pick the first that fits within the parent constraints in the specified axes. 62 | var chosenIndex = measurables.size - 1 63 | for i in 0.. any View) { 85 | } 86 | 87 | public init(bridgedAxes: Int, bridgedContent: any View) { 88 | } 89 | 90 | public var body: some View { 91 | stubView() 92 | } 93 | } 94 | #endif 95 | 96 | #endif 97 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Layout/SafeArea.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if SKIP 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.geometry.Rect 7 | #elseif canImport(CoreGraphics) 8 | import struct CoreGraphics.CGFloat 9 | #endif 10 | 11 | public struct SafeAreaRegions : OptionSet { 12 | public let rawValue: Int 13 | 14 | public init(rawValue: Int) { 15 | self.rawValue = rawValue 16 | } 17 | 18 | public static let container = SafeAreaRegions(rawValue: 1) 19 | public static let keyboard = SafeAreaRegions(rawValue: 2) 20 | public static let all = SafeAreaRegions(rawValue: 3) 21 | } 22 | 23 | #if SKIP 24 | import androidx.compose.ui.geometry.Rect 25 | 26 | /// Track safe area. 27 | struct SafeArea: Equatable { 28 | /// Total bounds of presentation root. 29 | let presentationBoundsPx: Rect 30 | 31 | /// Safe bounds of presentation root. 32 | let safeBoundsPx: Rect 33 | 34 | /// The edges whose safe area is solely due to system bars. 35 | let absoluteSystemBarEdges: Edge.Set 36 | 37 | init(presentation: Rect, safe: Rect, absoluteSystemBars: Edge.Set = []) { 38 | self.presentationBoundsPx = presentation 39 | self.safeBoundsPx = safe 40 | self.absoluteSystemBarEdges = absoluteSystemBars 41 | } 42 | 43 | /// Update the safe area. 44 | @Composable func insetting(_ edge: Edge, to value: Float) -> SafeArea { 45 | guard value > Float(0.0) else { 46 | return self 47 | } 48 | var systemBarEdges = absoluteSystemBarEdges 49 | var (safeLeft, safeTop, safeRight, safeBottom) = safeBoundsPx 50 | switch edge { 51 | case .top: 52 | safeTop = value 53 | systemBarEdges.remove(.top) 54 | case .bottom: 55 | safeBottom = value 56 | systemBarEdges.remove(.bottom) 57 | case .leading: 58 | safeLeft = value 59 | systemBarEdges.remove(.leading) 60 | case .trailing: 61 | safeRight = value 62 | systemBarEdges.remove(.trailing) 63 | } 64 | return SafeArea(presentation: presentationBoundsPx, safe: Rect(top: safeTop, left: safeLeft, bottom: safeBottom, right: safeRight), absoluteSystemBars: systemBarEdges) 65 | } 66 | } 67 | #endif 68 | 69 | extension View { 70 | @available(*, unavailable) 71 | public func safeAreaInset(edge: VerticalEdge, alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> any View) -> some View { 72 | return self 73 | } 74 | 75 | @available(*, unavailable) 76 | public func safeAreaInset(edge: HorizontalEdge, alignment: VerticalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> any View) -> some View { 77 | return self 78 | } 79 | 80 | @available(*, unavailable) 81 | public func safeAreaPadding(_ insets: EdgeInsets) -> some View { 82 | return self 83 | } 84 | 85 | @available(*, unavailable) 86 | public func safeAreaPadding(_ edges: Edge.Set = .all, _ length: CGFloat? = nil) -> some View { 87 | return self 88 | } 89 | 90 | @available(*, unavailable) 91 | public func safeAreaPadding(_ length: CGFloat) -> some View { 92 | return self 93 | } 94 | 95 | @available(*, unavailable) 96 | public func safeAreaBar(edge: VerticalEdge, alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> some View) -> some View { 97 | return self 98 | } 99 | 100 | @available(*, unavailable) 101 | public func safeAreaBar(edge: HorizontalEdge, alignment: VerticalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> some View) -> some View { 102 | return self 103 | } 104 | } 105 | 106 | #endif 107 | -------------------------------------------------------------------------------- /Sources/SkipUI/Skip/ReorderableItem.kt: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 2 | // 3 | // This file uses code publised with the following header: 4 | // 5 | /* 6 | * Copyright 2022 André Claßen 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * https://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | package org.burnoutcrew.reorderable 21 | 22 | import androidx.compose.foundation.ExperimentalFoundationApi 23 | import androidx.compose.foundation.layout.Box 24 | import androidx.compose.foundation.layout.BoxScope 25 | import androidx.compose.foundation.lazy.LazyItemScope 26 | import androidx.compose.foundation.lazy.grid.LazyGridItemScope 27 | import androidx.compose.runtime.Composable 28 | import androidx.compose.ui.Modifier 29 | import androidx.compose.ui.graphics.graphicsLayer 30 | import androidx.compose.ui.zIndex 31 | 32 | @OptIn(ExperimentalFoundationApi::class) 33 | @Composable 34 | fun LazyItemScope.ReorderableItem( 35 | reorderableState: ReorderableState<*>, 36 | key: Any?, 37 | modifier: Modifier = Modifier, 38 | index: Int? = null, 39 | orientationLocked: Boolean = true, 40 | content: @Composable BoxScope.(isDragging: Boolean) -> Unit 41 | ) = ReorderableItem(reorderableState, key, modifier, Modifier.animateItem(), orientationLocked, index, content) 42 | 43 | @OptIn(ExperimentalFoundationApi::class) 44 | @Composable 45 | fun LazyGridItemScope.ReorderableItem( 46 | reorderableState: ReorderableState<*>, 47 | key: Any?, 48 | modifier: Modifier = Modifier, 49 | index: Int? = null, 50 | content: @Composable BoxScope.(isDragging: Boolean) -> Unit 51 | ) = ReorderableItem(reorderableState, key, modifier, Modifier.animateItem(), false, index, content) 52 | 53 | @Composable 54 | fun ReorderableItem( 55 | state: ReorderableState<*>, 56 | key: Any?, 57 | modifier: Modifier = Modifier, 58 | defaultDraggingModifier: Modifier = Modifier, 59 | orientationLocked: Boolean = true, 60 | index: Int? = null, 61 | content: @Composable BoxScope.(isDragging: Boolean) -> Unit 62 | ) { 63 | val isDragging = if (index != null) { 64 | index == state.draggingItemIndex 65 | } else { 66 | key == state.draggingItemKey 67 | } 68 | val draggingModifier = 69 | if (isDragging) { 70 | Modifier 71 | .zIndex(1f) 72 | .graphicsLayer { 73 | translationX = if (!orientationLocked || !state.isVerticalScroll) state.draggingItemLeft else 0f 74 | translationY = if (!orientationLocked || state.isVerticalScroll) state.draggingItemTop else 0f 75 | } 76 | } else { 77 | val cancel = if (index != null) { 78 | index == state.dragCancelledAnimation.position?.index 79 | } else { 80 | key == state.dragCancelledAnimation.position?.key 81 | } 82 | if (cancel) { 83 | Modifier.zIndex(1f) 84 | .graphicsLayer { 85 | translationX = if (!orientationLocked || !state.isVerticalScroll) state.dragCancelledAnimation.offset.x else 0f 86 | translationY = if (!orientationLocked || state.isVerticalScroll) state.dragCancelledAnimation.offset.y else 0f 87 | } 88 | } else { 89 | defaultDraggingModifier 90 | } 91 | } 92 | Box(modifier = modifier.then(draggingModifier)) { 93 | content(isDragging) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Controls/Slider.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if SKIP 5 | import androidx.compose.material.ContentAlpha 6 | import androidx.compose.material3.SliderColors 7 | import androidx.compose.material3.SliderDefaults 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | #endif 11 | 12 | // SKIP @bridge 13 | public struct Slider : View, Renderable { 14 | let value: Binding 15 | let bounds: ClosedRange 16 | let step: Double? 17 | 18 | public init(value: Binding, in bounds: Any? = nil, step: Double? = nil) { 19 | self.value = value 20 | self.bounds = Self.bounds(for: bounds) 21 | self.step = step 22 | } 23 | 24 | @available(*, unavailable) 25 | public init(value: Binding, in bounds: Any? = nil, step: Double? = nil, onEditingChanged: @escaping (Bool) -> Void) { 26 | self.value = value 27 | self.bounds = Self.bounds(for: bounds) 28 | self.step = step 29 | } 30 | 31 | public init(value: Binding, in bounds: Any? = nil, step: Double? = nil, @ViewBuilder label: () -> any View) { 32 | self.init(value: value, in: bounds, step: step) 33 | } 34 | 35 | // SKIP @bridge 36 | public init(getValue: @escaping () -> Double, setValue: @escaping (Double) -> Void, min: Double, max: Double, step: Double?, bridgedLabel: (any View)?) { 37 | self.value = Binding(get: getValue, set: setValue) 38 | self.bounds = min...max 39 | self.step = step 40 | // Note: label is ignored 41 | } 42 | 43 | @available(*, unavailable) 44 | public init(value: Binding, in bounds: Any? = nil, step: Double? = nil, @ViewBuilder label: () -> any View, onEditingChanged: @escaping (Bool) -> Void) { 45 | self.value = value 46 | self.bounds = Self.bounds(for: bounds) 47 | self.step = step 48 | } 49 | 50 | @available(*, unavailable) 51 | public init(value: Binding, in bounds: Any? = nil, step: Double? = nil, @ViewBuilder label: () -> any View, @ViewBuilder minimumValueLabel: () -> any View, @ViewBuilder maximumValueLabel: () -> any View, onEditingChanged: @escaping (Bool) -> Void = { _ in }) { 52 | self.value = value 53 | self.bounds = Self.bounds(for: bounds) 54 | self.step = step 55 | } 56 | 57 | private static func bounds(for bounds: Any?) -> ClosedRange { 58 | #if SKIP 59 | guard let range = bounds as? ClosedRange else { 60 | return 0.0...1.0 61 | } 62 | return Double(range.start as! kotlin.Number)...Double(range.endInclusive as! kotlin.Number) 63 | #else 64 | return 0.0...1.0 65 | #endif 66 | } 67 | 68 | #if SKIP 69 | @Composable override func Render(context: ComposeContext) { 70 | var steps = 0 71 | if let step, step > 0.0 { 72 | steps = max(0, Int(ceil(bounds.endInclusive - bounds.start) / step) - 1) 73 | } 74 | let colors: SliderColors 75 | if let tint = EnvironmentValues.shared._tint { 76 | let activeColor = tint.colorImpl() 77 | let disabledColor = activeColor.copy(alpha: ContentAlpha.disabled) 78 | colors = SliderDefaults.colors(thumbColor: activeColor, activeTrackColor: activeColor, disabledThumbColor: disabledColor, disabledActiveTrackColor: disabledColor) 79 | } else { 80 | colors = SliderDefaults.colors() 81 | } 82 | let modifier = Modifier.fillWidth().then(context.modifier) 83 | androidx.compose.material3.Slider(modifier: modifier, value: Float(value.get()), onValueChange: { value.set(Double($0)) }, enabled: EnvironmentValues.shared.isEnabled, valueRange: Float(bounds.start)...Float(bounds.endInclusive), steps: steps, colors: colors) 84 | } 85 | #else 86 | public var body: some View { 87 | stubView() 88 | } 89 | #endif 90 | } 91 | 92 | #endif 93 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/PaletteSelectionEffect.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | /* 4 | /// The selection effect to apply to a palette item. 5 | /// 6 | /// You can configure the selection effect of a palette item by using the 7 | /// ``View/paletteSelectionEffect(_:)`` view modifier. 8 | @available(iOS 17.0, macOS 14.0, *) 9 | @available(tvOS, unavailable) 10 | @available(watchOS, unavailable) 11 | public struct PaletteSelectionEffect : Sendable, Equatable { 12 | 13 | /// Applies the system's default effect when selected. 14 | /// 15 | /// When using un-tinted SF Symbols or template images, the current tint 16 | /// color is applied to the selected items' image. 17 | /// If the provided SF Symbols have custom tints, a stroke is drawn around selected items. 18 | public static var automatic: PaletteSelectionEffect { get { fatalError() } } 19 | 20 | /// Applies the specified symbol variant when selected. 21 | /// 22 | /// - Note: This effect only applies to SF Symbols. 23 | //public static func symbolVariant(_ variant: SymbolVariants) -> PaletteSelectionEffect { fatalError() } 24 | 25 | /// Does not apply any system effect when selected. 26 | /// 27 | /// - Note: Make sure to manually implement a way to indicate selection when 28 | /// using this case. For example, you could dynamically resolve the item's 29 | /// image based on the selection state. 30 | public static var custom: PaletteSelectionEffect { get { fatalError() } } 31 | } 32 | 33 | @available(iOS 17.0, macOS 14.0, *) 34 | @available(tvOS, unavailable) 35 | @available(watchOS, unavailable) 36 | extension View { 37 | 38 | /// Specifies the selection effect to apply to a palette item. 39 | /// 40 | /// ``PaletteSelectionEffect/automatic`` applies the system's default 41 | /// appearance when selected. When using un-tinted SF Symbols or template 42 | /// images, the current tint color is applied to the selected items' image. 43 | /// If the provided SF Symbols have custom tints, a stroke is drawn around selected items. 44 | /// 45 | /// If you wish to provide a specific image (or SF Symbol) to indicate 46 | /// selection, use ``PaletteSelectionEffect/custom`` to forgo the system's 47 | /// default selection appearance allowing the provided image to solely 48 | /// indicate selection instead. 49 | /// 50 | /// The following example creates a palette picker that disables the 51 | /// system selection behavior: 52 | /// 53 | /// Menu { 54 | /// Picker("Palettes", selection: $selection) { 55 | /// ForEach(palettes) { palette in 56 | /// Label(palette.title, image: selection == palette ? 57 | /// "selected-palette" : "palette") 58 | /// .tint(palette.tint) 59 | /// .tag(palette) 60 | /// } 61 | /// } 62 | /// .pickerStyle(.palette) 63 | /// .paletteSelectionEffect(.custom) 64 | /// } label: { 65 | /// ... 66 | /// } 67 | /// 68 | /// If a specific SF Symbol variant is preferable instead, use 69 | /// ``PaletteSelectionEffect/symbolVariant(_:)``. 70 | /// 71 | /// Menu { 72 | /// ControlGroup { 73 | /// ForEach(ColorTags.allCases) { colorTag in 74 | /// Toggle(isOn: $selectedColorTags[colorTag]) { 75 | /// Label(colorTag.name, systemImage: "circle") 76 | /// } 77 | /// .tint(colorTag.color) 78 | /// } 79 | /// } 80 | /// .controlGroupStyle(.palette) 81 | /// .paletteSelectionEffect(.symbolVariant(.fill)) 82 | /// } 83 | /// 84 | /// - Parameter effect: The type of effect to apply when a palette item is selected. 85 | public func paletteSelectionEffect(_ effect: PaletteSelectionEffect) -> some View { return stubView() } 86 | } 87 | */ 88 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Text/TextEditor.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | import Foundation 5 | #if SKIP 6 | import androidx.compose.foundation.text.KeyboardActions 7 | import androidx.compose.foundation.text.KeyboardOptions 8 | import androidx.compose.material3.OutlinedTextField 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.platform.LocalFocusManager 11 | import androidx.compose.ui.text.input.VisualTransformation 12 | #endif 13 | 14 | // SKIP @bridge 15 | public struct TextEditor : View, Renderable { 16 | let text: Binding 17 | 18 | public init(text: Binding) { 19 | self.text = text 20 | } 21 | 22 | // SKIP @bridge 23 | public init(getText: @escaping () -> String, setText: @escaping (String) -> Void) { 24 | self.text = Binding(get: getText, set: setText) 25 | } 26 | 27 | public init(_ titleResource: LocalizedStringResource, text: Binding) { 28 | self.text = text 29 | } 30 | 31 | @available(*, unavailable) 32 | public init(text: Binding, selection: Any? /* Binding */) { 33 | self.text = Binding(get: { "" }, set: { _ in }) 34 | } 35 | 36 | // @available(*, unavailable) 37 | // public init(text: Binding, selection: Any? /* Binding? */ = nil) { 38 | // self.text = Binding(get: { "" }, set: { _ in }) 39 | // } 40 | 41 | #if SKIP 42 | @Composable override func Render(context: ComposeContext) { 43 | let contentContext = context.content() 44 | let textEnvironment = EnvironmentValues.shared._textEnvironment 45 | let redaction = EnvironmentValues.shared.redactionReasons 46 | let styleInfo = Text.styleInfo(textEnvironment: textEnvironment, redaction: redaction, context: context) 47 | let animatable = styleInfo.style.asAnimatable(context: context) 48 | let keyboardOptions = EnvironmentValues.shared._keyboardOptions ?? KeyboardOptions.Default 49 | let keyboardActions = KeyboardActions(EnvironmentValues.shared._onSubmitState, LocalFocusManager.current) 50 | let colors = TextField.colors(styleInfo: styleInfo, outline: Color.clear) 51 | let visualTransformation = VisualTransformation.None 52 | OutlinedTextField(value: text.wrappedValue, onValueChange: { 53 | text.wrappedValue = $0 54 | }, modifier: context.modifier.fillSize(), textStyle: animatable.value, enabled: EnvironmentValues.shared.isEnabled, singleLine: false, keyboardOptions: keyboardOptions, keyboardActions: keyboardActions, colors: colors, visualTransformation: visualTransformation) 55 | } 56 | #else 57 | public var body: some View { 58 | stubView() 59 | } 60 | #endif 61 | } 62 | 63 | public struct TextEditorStyle: RawRepresentable, Equatable { 64 | public let rawValue: Int 65 | 66 | public init(rawValue: Int) { 67 | self.rawValue = rawValue 68 | } 69 | 70 | public static let automatic = TextEditorStyle(rawValue: 0) // For bridging 71 | public static let plain = TextEditorStyle(rawValue: 1) // For bridging 72 | } 73 | 74 | extension View { 75 | public func textEditorStyle(_ style: TextEditorStyle) -> any View { 76 | return self 77 | } 78 | 79 | // SKIP @bridge 80 | public func textEditorStyle(bridgedStyle: Int) -> any View { 81 | return textEditorStyle(TextEditorStyle(rawValue: bridgedStyle)) 82 | } 83 | 84 | @available(*, unavailable) 85 | public func findNavigator(isPresented: Binding) -> some View { 86 | return self 87 | } 88 | 89 | @available(*, unavailable) 90 | public func findDisabled(_ isDisabled: Bool = true) -> some View { 91 | return self 92 | } 93 | 94 | @available(*, unavailable) 95 | public func replaceDisabled(_ isDisabled: Bool = true) -> some View { 96 | return self 97 | } 98 | } 99 | 100 | /* 101 | /// The properties of a text editor. 102 | @available(iOS 17.0, macOS 14.0, *) 103 | @available(tvOS, unavailable) 104 | @available(watchOS, unavailable) 105 | public struct TextEditorStyleConfiguration { 106 | } 107 | */ 108 | #endif 109 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/View/View.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if !SKIP_BRIDGE 4 | #if SKIP 5 | import androidx.compose.runtime.Composable 6 | import skip.model.StateTracking 7 | #endif 8 | 9 | // SKIP @bridge 10 | public protocol View { 11 | #if SKIP 12 | // Note: We default the body to invoke the deprecated `ComposeContent` function for backwards compatibility 13 | // with custom pre-Renderable views that overrode `ComposeContent` 14 | // SKIP DECLARE: fun body(): View = ComposeView({ ComposeContent(it) }) 15 | @ViewBuilder @MainActor var body: any View { get } 16 | #else 17 | associatedtype Body : View 18 | @ViewBuilder @MainActor var body: Body { get } 19 | #endif 20 | } 21 | 22 | #if SKIP 23 | extension View { 24 | /// Compose this view without an existing context - typically called when integrating a SwiftUI view tree into pure Compose. 25 | /// 26 | /// - Seealso: `Compose(context:)` 27 | @Composable public func Compose() -> ComposeResult { 28 | return Compose(context: ComposeContext()) 29 | } 30 | 31 | /// Compose this view's content. 32 | /// 33 | /// Calls to `Compose` are added by the transpiler. 34 | @Composable public func Compose(context: ComposeContext) -> ComposeResult { 35 | if let composer = context.composer { 36 | let composerContext: (Bool) -> ComposeContext = { retain in 37 | guard !retain else { 38 | return context 39 | } 40 | var context = context 41 | context.composer = nil 42 | return context 43 | } 44 | return composer.Compose(self, composerContext) 45 | } else { 46 | _ComposeContent(context: context) 47 | return ComposeResult.ok 48 | } 49 | } 50 | 51 | /// DEPRECATED 52 | @Composable public func ComposeContent(context: ComposeContext) { 53 | } 54 | 55 | /// This function provides a non-escaping compose context to avoid excessive recompositions when the calling code 56 | /// does not need to access the underlying `Renderables`. 57 | @Composable public func _ComposeContent(context: ComposeContext) { 58 | for renderable in Evaluate(context: context, options: 0) { 59 | renderable.Render(context: context) 60 | } 61 | } 62 | 63 | /// Evaluate renderable content. 64 | /// 65 | /// - Warning: Do not give `options` a default value in this function signature. We have seen it cause bugs in which 66 | /// the default version of the function is always invoked, ignoring implementor overrides. 67 | @Composable public func Evaluate(context: ComposeContext, options: Int) -> kotlin.collections.List { 68 | if let renderable = self as? Renderable { 69 | return listOf(self) 70 | } else { 71 | StateTracking.pushBody() 72 | let renderables = body.Evaluate(context: context, options: options) 73 | StateTracking.popBody() 74 | return renderables 75 | } 76 | } 77 | 78 | /// Helper for the rare cases that we want to treat a `View` as a `Renderable` without evaluating it. 79 | /// 80 | /// - Warning: For special cases only. 81 | public func asRenderable() -> Renderable { 82 | return self as? Renderable ?? ViewRenderable(view: self) 83 | } 84 | } 85 | 86 | /// Helper for the rare cases that we want to treat a `View` as a `Renderable` without evaluating it. 87 | /// 88 | /// - Warning: For special cases only. 89 | final class ViewRenderable: Renderable { 90 | let view: View 91 | 92 | init(view: View) { 93 | // Don't copy 94 | // SKIP REPLACE: this.view = view 95 | self.view = view 96 | } 97 | 98 | @Composable override func Render(context: ComposeContext) { 99 | view.Compose(context: context) 100 | } 101 | } 102 | 103 | #else 104 | 105 | // Stubs needed to compile this package: 106 | 107 | extension Optional : View where Wrapped : View { 108 | public var body: some View { 109 | stubView() 110 | } 111 | } 112 | 113 | extension Never : View { 114 | } 115 | 116 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 117 | extension Never { 118 | public typealias Body = NeverView 119 | public var body: Never { get { fatalError() } } 120 | } 121 | 122 | #endif 123 | #endif 124 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Compose/ComposeStateSaver.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if SKIP 4 | import android.os.Parcel 5 | import android.os.Parcelable 6 | import androidx.compose.runtime.saveable.Saver 7 | import androidx.compose.runtime.saveable.SaverScope 8 | 9 | /// Use to make a Bundle-saveable string from a SwiftUI value. 10 | /// 11 | /// We typically use a `ComposeStateSaver` to save state, but when working with Compose internal state like `LazyList` state, use this function 12 | /// to turn user-supplied values into strings that Compose can save natively. 13 | /// 14 | /// - Seealso: `SkipSwiftUI.Java_composeBundleString(for:)` 15 | func composeBundleString(for value: Any?) -> String { 16 | if let identifiable = value as? Identifiable { 17 | return String(describing: identifiable.id) 18 | } else if let rawRepresentable = value as? RawRepresentable { 19 | return String(describing: rawRepresentable.rawValue) 20 | } else { 21 | return String(describing: value) 22 | } 23 | } 24 | 25 | /// Used in conjunction with `rememberSaveable` to save and restore state with SwiftUI-like behavior. 26 | struct ComposeStateSaver: Saver { 27 | private static let nilMarker = "__SkipUI.ComposeStateSaver.nilMarker" 28 | private let state: MutableMap = mutableMapOf() 29 | 30 | override func restore(value: Any) -> Any? { 31 | if value == Self.nilMarker { 32 | return nil 33 | } else if let key = value as? Key { 34 | return state[key] 35 | } else { 36 | return value 37 | } 38 | } 39 | 40 | // SKIP DECLARE: override fun SaverScope.save(value: Any?): Any 41 | override func save(value: Any?) -> Any { 42 | if value == nil { 43 | return Self.nilMarker 44 | } else if value is Boolean || value is Number || value is String || value is Char { 45 | return value 46 | } else { 47 | let key = Key.next() 48 | state[key] = value 49 | return key 50 | } 51 | } 52 | 53 | /// Key under which to save values that cannot be stored directly in the Bundle. 54 | struct Key: Parcelable { 55 | private let value: Int 56 | 57 | init(value: Int) { 58 | self.value = value 59 | } 60 | 61 | init(parcel: Parcel) { 62 | self.init(parcel.readInt()) 63 | } 64 | 65 | override func writeToParcel(parcel: Parcel, flags: Int) { 66 | parcel.writeInt(value) 67 | } 68 | 69 | override func describeContents() -> Int { 70 | return 0 71 | } 72 | 73 | // We must use a companion CREATOR to meet the Java Parcelable contract. Note that if this code breaks, it may have no 74 | // immediate noticable effect. However, we've had dev reports that it can cause crashes on a high percentage of user 75 | // devices, even though we don't know how to exercise it. We can manually test for contract compatibility with: 76 | /* 77 | val key = ComposeStateSaver.Key(99999) 78 | val bundle = android.os.Bundle() 79 | bundle.putParcelable(key::class.java.name, key) 80 | val parcel = Parcel.obtain() 81 | parcel.writeBundle(bundle) 82 | val bytes = parcel.marshall() 83 | val rparcel = Parcel.obtain() 84 | rparcel.unmarshall(bytes, 0, bytes.size) 85 | rparcel.setDataPosition(0) 86 | val rbundle = rparcel.readBundle() 87 | rbundle?.classLoader = key::class.java.classLoader 88 | val rkey: ComposeStateSaver.Key? = rbundle?.getParcelable(key::class.java.name) 89 | android.util.Log.e("", "Roundtripped: $rkey") 90 | */ 91 | 92 | // SKIP DECLARE: companion object CREATOR: Parcelable.Creator 93 | private final class CREATOR: Parcelable.Creator { 94 | private var keyValue = 0 95 | 96 | func next() -> Key { 97 | keyValue += 1 98 | return Key(value: keyValue) 99 | } 100 | 101 | override func createFromParcel(parcel: Parcel) -> Key { 102 | return Key(parcel) 103 | } 104 | 105 | override func newArray(size: Int) -> kotlin.Array { 106 | return arrayOfNulls(size) 107 | } 108 | } 109 | } 110 | } 111 | #endif 112 | -------------------------------------------------------------------------------- /Tests/SkipUITests/CanvasTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | import SwiftUI 4 | import XCTest 5 | import OSLog 6 | import Foundation 7 | 8 | #if SKIP 9 | import androidx.compose.foundation.background 10 | import androidx.compose.foundation.border 11 | import androidx.compose.foundation.layout.size 12 | import androidx.compose.ui.unit.dp 13 | #endif 14 | 15 | final class CanvasTests: XCSnapshotTestCase { 16 | func testZStackOpacityOverlay() throws { 17 | XCTAssertEqual(try render(compact: 1, view: ZStack { 18 | Color.black.frame(width: 12.0, height: 12.0) 19 | Color.white.opacity(0.6).frame(width: 6.0, height: 6.0) 20 | }).pixmap, 21 | plaf(""" 22 | 0 0 0 0 0 0 0 0 0 0 0 0 23 | 0 0 0 0 0 0 0 0 0 0 0 0 24 | 0 0 0 0 0 0 0 0 0 0 0 0 25 | 0 0 0 9 9 9 9 9 9 0 0 0 26 | 0 0 0 9 9 9 9 9 9 0 0 0 27 | 0 0 0 9 9 9 9 9 9 0 0 0 28 | 0 0 0 9 9 9 9 9 9 0 0 0 29 | 0 0 0 9 9 9 9 9 9 0 0 0 30 | 0 0 0 9 9 9 9 9 9 0 0 0 31 | 0 0 0 0 0 0 0 0 0 0 0 0 32 | 0 0 0 0 0 0 0 0 0 0 0 0 33 | 0 0 0 0 0 0 0 0 0 0 0 0 34 | """)) 35 | } 36 | 37 | func testZStackMultiOpacityOverlay() throws { 38 | if isAndroid { 39 | throw XCTSkip("opacity overlay not passing on Android emulator") 40 | } 41 | 42 | XCTAssertEqual(try render(compact: 1, view: ZStack { 43 | Color.black.frame(width: 12.0, height: 12.0) 44 | Color.white.opacity(0.8).frame(width: 8.0, height: 8.0) 45 | Color.black.opacity(0.5).frame(width: 4.0, height: 4.0) 46 | Color.white.opacity(0.22).frame(width: 2.0, height: 2.0) 47 | }).pixmap, 48 | plaf(""" 49 | 0 0 0 0 0 0 0 0 0 0 0 0 50 | 0 0 0 0 0 0 0 0 0 0 0 0 51 | 0 0 C C C C C C C C 0 0 52 | 0 0 C C C C C C C C 0 0 53 | 0 0 C C 6 6 6 6 C C 0 0 54 | 0 0 C C 6 8 8 6 C C 0 0 55 | 0 0 C C 6 8 8 6 C C 0 0 56 | 0 0 C C 6 6 6 6 C C 0 0 57 | 0 0 C C C C C C C C 0 0 58 | 0 0 C C C C C C C C 0 0 59 | 0 0 0 0 0 0 0 0 0 0 0 0 60 | 0 0 0 0 0 0 0 0 0 0 0 0 61 | """)) 62 | } 63 | 64 | 65 | func testRenderCustomShape() throws { 66 | #if !SKIP 67 | throw XCTSkip("Android-only function") 68 | #else 69 | XCTAssertEqual(try render(compact: 1, view: ComposeBuilder(content: { ctx in 70 | androidx.compose.foundation.layout.Box(modifier: androidx.compose.ui.Modifier.background(androidx.compose.ui.graphics.Color.White).size(12.dp), contentAlignment: androidx.compose.ui.Alignment.Center) { 71 | androidx.compose.foundation.layout.Box(modifier: androidx.compose.ui.Modifier.background(androidx.compose.ui.graphics.Color.Black).size(6.dp, 6.dp)) 72 | } 73 | return .ok 74 | })).pixmap, 75 | plaf(""" 76 | F F F F F F F F F F F F 77 | F F F F F F F F F F F F 78 | F F F F F F F F F F F F 79 | F F F 0 0 0 0 0 0 F F F 80 | F F F 0 0 0 0 0 0 F F F 81 | F F F 0 0 0 0 0 0 F F F 82 | F F F 0 0 0 0 0 0 F F F 83 | F F F 0 0 0 0 0 0 F F F 84 | F F F 0 0 0 0 0 0 F F F 85 | F F F F F F F F F F F F 86 | F F F F F F F F F F F F 87 | F F F F F F F F F F F F 88 | """)) 89 | #endif 90 | } 91 | 92 | func testRenderCustomCanvas() throws { 93 | #if !SKIP 94 | throw XCTSkip("Android-only function") 95 | #else 96 | XCTAssertEqual(try render(compact: 1, view: ComposeBuilder(content: { ctx in 97 | androidx.compose.foundation.layout.Box(modifier: androidx.compose.ui.Modifier.size(12.dp).background(androidx.compose.ui.graphics.Color.White), contentAlignment: androidx.compose.ui.Alignment.Center) { 98 | androidx.compose.foundation.layout.Box(modifier: androidx.compose.ui.Modifier.size(6.dp, 6.dp).background(androidx.compose.ui.graphics.Color.Black)) 99 | } 100 | return .ok 101 | })).pixmap, 102 | plaf(""" 103 | F F F F F F F F F F F F 104 | F F F F F F F F F F F F 105 | F F F F F F F F F F F F 106 | F F F 0 0 0 0 0 0 F F F 107 | F F F 0 0 0 0 0 0 F F F 108 | F F F 0 0 0 0 0 0 F F F 109 | F F F 0 0 0 0 0 0 F F F 110 | F F F 0 0 0 0 0 0 F F F 111 | F F F 0 0 0 0 0 0 F F F 112 | F F F F F F F F F F F F 113 | F F F F F F F F F F F F 114 | F F F F F F F F F F F F 115 | """)) 116 | #endif 117 | } 118 | } 119 | --------------------------------------------------------------------------------