├── .github ├── release.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.LGPL ├── Package.swift ├── README.md ├── Sources └── SkipUI │ ├── Skip │ ├── DetectReorder.kt │ ├── DragCancelledAnimation.kt │ ├── DragGesture.kt │ ├── ItemPosition.kt │ ├── Redacted.kt │ ├── Reorderable.kt │ ├── ReorderableItem.kt │ ├── ReorderableLazyGridState.kt │ ├── ReorderableLazyListState.kt │ ├── ReorderableState.kt │ ├── Shadowed.kt │ └── skip.yml │ ├── SkipUI │ ├── Animation │ │ ├── Animation.swift │ │ ├── MoveKeyframe.swift │ │ ├── Spring.swift │ │ ├── Timeline.swift │ │ ├── Transaction.swift │ │ └── Transition.swift │ ├── App │ │ ├── App.swift │ │ ├── Document.swift │ │ ├── Preview.swift │ │ ├── Scene.swift │ │ ├── Widget.swift │ │ └── Window.swift │ ├── BridgeSupport │ │ ├── AppStorageSupport.swift │ │ ├── CompletionHandler.swift │ │ ├── EnvironmentSupport.swift │ │ ├── StateSupport.swift │ │ └── UserNotificationsDelegateSupport.swift │ ├── Color │ │ ├── Color.swift │ │ ├── ColorMatrix.swift │ │ ├── ColorRenderingMode.swift │ │ ├── ColorScheme.swift │ │ └── ColorSchemeContrast.swift │ ├── Commands │ │ ├── Actions.swift │ │ ├── Commands.swift │ │ ├── DismissBehavior.swift │ │ ├── EditActions.swift │ │ ├── EditMode.swift │ │ ├── Menu.swift │ │ ├── Search.swift │ │ ├── SubmitLabel.swift │ │ ├── SubmitTriggers.swift │ │ └── Toolbar.swift │ ├── Components │ │ ├── AsyncImage.swift │ │ ├── Divider.swift │ │ ├── EmptyView.swift │ │ ├── Gauge.swift │ │ ├── Image.swift │ │ ├── Link.swift │ │ ├── ProgressView.swift │ │ ├── ShareLink.swift │ │ └── Spacer.swift │ ├── Compose │ │ ├── ComposeBuilder.swift │ │ ├── ComposeContainer.swift │ │ ├── ComposeContext.swift │ │ ├── ComposeExtensions.swift │ │ ├── ComposeLambda.swift │ │ ├── ComposeLayouts.swift │ │ ├── ComposeModifierView.swift │ │ ├── ComposeModifiers.swift │ │ ├── ComposeStateSaver.swift │ │ └── ComposeView.swift │ ├── Containers │ │ ├── AnimatedContentArguments.swift │ │ ├── DisclosureGroup.swift │ │ ├── ForEach.swift │ │ ├── Form.swift │ │ ├── Grid.swift │ │ ├── Group.swift │ │ ├── GroupBox.swift │ │ ├── HStack.swift │ │ ├── LazyHGrid.swift │ │ ├── LazyHStack.swift │ │ ├── LazySupport.swift │ │ ├── LazyVGrid.swift │ │ ├── LazyVStack.swift │ │ ├── List.swift │ │ ├── Navigation.swift │ │ ├── OutlineGroup.swift │ │ ├── PresentationRoot.swift │ │ ├── ScrollView.swift │ │ ├── Section.swift │ │ ├── TabView.swift │ │ ├── Table.swift │ │ ├── VStack.swift │ │ ├── ViewThatFits.swift │ │ └── ZStack.swift │ ├── Controls │ │ ├── Button.swift │ │ ├── ColorPicker.swift │ │ ├── ControlGroup.swift │ │ ├── DatePicker.swift │ │ ├── Picker.swift │ │ ├── Slider.swift │ │ ├── Stepper.swift │ │ └── Toggle.swift │ ├── Environment │ │ ├── EnvironmentValues.swift │ │ ├── ModifiedContent.swift │ │ ├── PreferenceKey.swift │ │ └── ViewModifier.swift │ ├── Graphics │ │ ├── BackgroundProminence.swift │ │ ├── BlendMode.swift │ │ ├── Canvas.swift │ │ ├── Gradient.swift │ │ ├── GraphicsContext.swift │ │ ├── Material.swift │ │ ├── Path.swift │ │ ├── Shader.swift │ │ ├── ShadowStyle.swift │ │ ├── Shape.swift │ │ ├── ShapeStyle.swift │ │ ├── ShapeView.swift │ │ ├── StrokeStyle.swift │ │ ├── Symbol.swift │ │ ├── VectorArithmetic.swift │ │ └── VisualEffect.swift │ ├── Layout │ │ ├── Alignment.swift │ │ ├── AlignmentID.swift │ │ ├── Anchor.swift │ │ ├── Angle.swift │ │ ├── Axis.swift │ │ ├── CoordinateSpace.swift │ │ ├── Edge.swift │ │ ├── EdgeInsets.swift │ │ ├── GeometryEffect.swift │ │ ├── GeometryProxy.swift │ │ ├── GeometryReader.swift │ │ ├── HorizontalAlignment.swift │ │ ├── HorizontalEdge.swift │ │ ├── Layout.swift │ │ ├── MatchedGeometryProperties.swift │ │ ├── Namespace.swift │ │ ├── Presentation.swift │ │ ├── Unit.swift │ │ ├── VerticalAlignment.swift │ │ ├── VerticalEdge.swift │ │ ├── ViewDimensions.swift │ │ └── ViewSpacing.swift │ ├── Properties │ │ ├── AppStorage.swift │ │ ├── Bindable.swift │ │ ├── Binding.swift │ │ ├── Environment.swift │ │ ├── FocusState.swift │ │ ├── ObservedObject.swift │ │ ├── SceneStorage.swift │ │ ├── State.swift │ │ └── StateObject.swift │ ├── System │ │ ├── Accessibility.swift │ │ ├── Assets.swift │ │ ├── BackgroundTask.swift │ │ ├── BadgeProminence.swift │ │ ├── ContainerBackgroundPlacement.swift │ │ ├── ContentMarginPlacement.swift │ │ ├── ContentMode.swift │ │ ├── ContentShapeKinds.swift │ │ ├── ContentUnavailableView.swift │ │ ├── ControlSize.swift │ │ ├── DynamicProperty.swift │ │ ├── DynamicTypeSize.swift │ │ ├── DynamicViewContent.swift │ │ ├── EditableCollectionContent.swift │ │ ├── EmptyModifier.swift │ │ ├── EventModifiers.swift │ │ ├── FileDialogBrowserOptions.swift │ │ ├── Focus.swift │ │ ├── Gesture.swift │ │ ├── HoverEffect.swift │ │ ├── HoverPhase.swift │ │ ├── IndexViewStyle.swift │ │ ├── IndexedIdentifierCollection.swift │ │ ├── InterfaceOrientation.swift │ │ ├── KeyEquivalent.swift │ │ ├── KeyPress.swift │ │ ├── KeyboardShortcut.swift │ │ ├── LimitedAvailabilityConfiguration.swift │ │ ├── ManagedObject.swift │ │ ├── PageIndexViewStyle.swift │ │ ├── PaletteSelectionEffect.swift │ │ ├── PlaceholderContentView.swift │ │ ├── PopoverAttachmentAnchor.swift │ │ ├── ProjectionTransform.swift │ │ ├── Prominence.swift │ │ ├── ProposedViewSize.swift │ │ ├── SafeAreaRegions.swift │ │ ├── SensoryFeedback.swift │ │ ├── SidebarRowSize.swift │ │ ├── Spatial.swift │ │ ├── Transferable.swift │ │ ├── TypesettingLanguage.swift │ │ ├── UserActivity.swift │ │ ├── UserInterfaceSizeClass.swift │ │ └── Visibility.swift │ ├── Text │ │ ├── Font.swift │ │ ├── Label.swift │ │ ├── LocalizedStringKey.swift │ │ ├── SecureField.swift │ │ ├── Text.swift │ │ ├── TextEditor.swift │ │ ├── TextField.swift │ │ ├── TextInput.swift │ │ └── TextSelectability.swift │ ├── UIKit │ │ ├── UIApplication.swift │ │ ├── UIColor.swift │ │ ├── UIFeedbackGenerator.swift │ │ ├── UIImage.swift │ │ ├── UIKeyboardType.swift │ │ ├── UIKit.swift │ │ ├── UIPasteboard.swift │ │ ├── UITextContentType.swift │ │ └── UserNotifications.swift │ └── View │ │ ├── AnyView.swift │ │ ├── EquatableView.swift │ │ ├── Observable.swift │ │ ├── View.swift │ │ ├── ViewBuilder.swift │ │ └── ViewPlacement.swift │ └── Stub.swift └── Tests └── SkipUITests ├── CanvasTests.swift ├── ColorTests.swift ├── ImageTests.swift ├── LayoutTests.swift ├── ModifierTests.swift ├── Resources ├── Assets.xcassets │ ├── Contents.json │ └── dumbbell.fill.symbolset │ │ ├── Contents.json │ │ └── dumbbell.fill.svg └── Localizable.xcstrings ├── Skip ├── res │ ├── drawable │ │ └── battery_charging.xml │ └── values │ │ └── strings.xml └── skip.yml ├── SkipUITests.swift ├── TextTests.swift ├── XCSkipTests.swift └── XCSnapshotTestCase.swift /.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 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: skip-ui 2 | on: 3 | push: 4 | branches: '*' 5 | tags: "[0-9]+.[0-9]+.[0-9]+" 6 | schedule: 7 | - cron: '0 1,11 * * *' 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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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.5.16"), 12 | .package(url: "https://source.skip.tools/skip-model.git", from: "1.5.0"), 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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/Skip/ReorderableLazyGridState.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.Orientation 23 | import androidx.compose.foundation.gestures.scrollBy 24 | import androidx.compose.foundation.lazy.grid.LazyGridItemInfo 25 | import androidx.compose.foundation.lazy.grid.LazyGridState 26 | import androidx.compose.foundation.lazy.grid.rememberLazyGridState 27 | import androidx.compose.runtime.Composable 28 | import androidx.compose.runtime.LaunchedEffect 29 | import androidx.compose.runtime.remember 30 | import androidx.compose.runtime.rememberCoroutineScope 31 | import androidx.compose.ui.platform.LocalDensity 32 | import androidx.compose.ui.unit.Dp 33 | import androidx.compose.ui.unit.dp 34 | import kotlinx.coroutines.CoroutineScope 35 | 36 | @Composable 37 | fun rememberReorderableLazyGridState( 38 | onMove: (ItemPosition, ItemPosition) -> Unit, 39 | gridState: LazyGridState = rememberLazyGridState(), 40 | canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)? = null, 41 | onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))? = null, 42 | maxScrollPerFrame: Dp = 20.dp, 43 | dragCancelledAnimation: DragCancelledAnimation = SpringDragCancelledAnimation() 44 | ): ReorderableLazyGridState { 45 | val maxScroll = with(LocalDensity.current) { maxScrollPerFrame.toPx() } 46 | val scope = rememberCoroutineScope() 47 | val state = remember(gridState) { 48 | ReorderableLazyGridState(gridState, scope, maxScroll, onMove, canDragOver, onDragEnd, dragCancelledAnimation) 49 | } 50 | LaunchedEffect(state) { 51 | state.visibleItemsChanged() 52 | .collect { state.onDrag(0, 0) } 53 | } 54 | 55 | LaunchedEffect(state) { 56 | while (true) { 57 | val diff = state.scrollChannel.receive() 58 | gridState.scrollBy(diff) 59 | } 60 | } 61 | return state 62 | } 63 | 64 | class ReorderableLazyGridState( 65 | val gridState: LazyGridState, 66 | scope: CoroutineScope, 67 | maxScrollPerFrame: Float, 68 | onMove: (fromIndex: ItemPosition, toIndex: ItemPosition) -> (Unit), 69 | canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)? = null, 70 | onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))? = null, 71 | dragCancelledAnimation: DragCancelledAnimation = SpringDragCancelledAnimation() 72 | ) : ReorderableState(scope, maxScrollPerFrame, onMove, canDragOver, onDragEnd, dragCancelledAnimation) { 73 | override val isVerticalScroll: Boolean 74 | get() = gridState.layoutInfo.orientation == Orientation.Vertical 75 | override val LazyGridItemInfo.left: Int 76 | get() = offset.x 77 | override val LazyGridItemInfo.right: Int 78 | get() = offset.x + size.width 79 | override val LazyGridItemInfo.top: Int 80 | get() = offset.y 81 | override val LazyGridItemInfo.bottom: Int 82 | get() = offset.y + size.height 83 | override val LazyGridItemInfo.width: Int 84 | get() = size.width 85 | override val LazyGridItemInfo.height: Int 86 | get() = size.height 87 | override val LazyGridItemInfo.itemIndex: Int 88 | get() = index 89 | override val LazyGridItemInfo.itemKey: Any 90 | get() = key 91 | override val visibleItemsInfo: List 92 | get() = gridState.layoutInfo.visibleItemsInfo 93 | override val viewportStartOffset: Int 94 | get() = gridState.layoutInfo.viewportStartOffset 95 | override val viewportEndOffset: Int 96 | get() = gridState.layoutInfo.viewportEndOffset 97 | override val firstVisibleItemIndex: Int 98 | get() = gridState.firstVisibleItemIndex 99 | override val firstVisibleItemScrollOffset: Int 100 | get() = gridState.firstVisibleItemScrollOffset 101 | 102 | override suspend fun scrollToItem(index: Int, offset: Int) { 103 | gridState.scrollToItem(index, offset) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /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/Animation/MoveKeyframe.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if false 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 | #endif 19 | -------------------------------------------------------------------------------- /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/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 91 | case inactive 92 | case active 93 | 94 | public static func < (a: ScenePhase, b: ScenePhase) -> Bool { 95 | return a.rawValue < b.rawValue 96 | } 97 | } 98 | #endif 99 | -------------------------------------------------------------------------------- /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/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/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/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/Color/ColorMatrix.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if false 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 | #endif 63 | -------------------------------------------------------------------------------- /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/Color/ColorSchemeContrast.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if false 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 | #endif 53 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 EditActionsModifierView(view: self, 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 EditActionsModifierView(view: self, isMoveDisabled: isDisabled) 32 | #else 33 | return self 34 | #endif 35 | } 36 | } 37 | 38 | #if SKIP 39 | final class EditActionsModifierView: ComposeModifierView { 40 | var isDeleteDisabled: Bool? 41 | var isMoveDisabled: Bool? 42 | 43 | init(view: View, isDeleteDisabled: Bool? = nil, isMoveDisabled: Bool? = nil) { 44 | super.init(view: view) 45 | let wrappedEditActionsView = Self.unwrap(view: view) 46 | self.isDeleteDisabled = isDeleteDisabled ?? wrappedEditActionsView?.isDeleteDisabled 47 | self.isMoveDisabled = isMoveDisabled ?? wrappedEditActionsView?.isMoveDisabled 48 | } 49 | 50 | /// Return the edit actions modifier information for the given view. 51 | static func unwrap(view: View) -> EditActionsModifierView? { 52 | return view.strippingModifiers(until: { $0 is EditActionsModifierView }, perform: { $0 as? EditActionsModifierView }) 53 | } 54 | } 55 | 56 | extension Array { 57 | public mutating func remove(atOffsets offsets: IndexSet) { 58 | for offset in offsets.reversed() { 59 | remove(at: offset) 60 | } 61 | } 62 | 63 | public mutating func move(fromOffsets source: IndexSet, toOffset destination: Int) { 64 | // Calling with the same offset or the 65 | guard source.count > 1 || (destination != source[0] && destination != source[0] + 1) else { 66 | return 67 | } 68 | 69 | var moved: [Element] = [] 70 | var belowDestinationCount = 0 71 | for offset in source.reversed() { 72 | moved.append(remove(at: offset)) 73 | if offset < destination { 74 | belowDestinationCount += 1 75 | } 76 | } 77 | for m in moved { 78 | insert(m, at: destination - belowDestinationCount) 79 | } 80 | } 81 | } 82 | #endif 83 | #endif 84 | -------------------------------------------------------------------------------- /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/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/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/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 { 14 | // SKIP @bridge 15 | public init() { 16 | } 17 | 18 | #if SKIP 19 | @Composable public override func ComposeContent(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/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 { 10 | // SKIP @bridge 11 | public init() { 12 | } 13 | 14 | #if SKIP 15 | @Composable public override func ComposeContent(context: ComposeContext) { 16 | } 17 | #else 18 | public var body: some View { 19 | stubView() 20 | } 21 | #endif 22 | } 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /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 { 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(_ title: String, destination: URL) { 37 | self.init(destination: destination, label: { Text(verbatim: title) }) 38 | } 39 | 40 | #if SKIP 41 | @Composable override func ComposeContent(context: ComposeContext) { 42 | ComposeAction() 43 | content.Compose(context: context) 44 | } 45 | 46 | @Composable func ComposeAction() { 47 | openURL = EnvironmentValues.shared.openURL 48 | } 49 | #else 50 | public var body: some View { 51 | stubView() 52 | } 53 | #endif 54 | } 55 | 56 | #endif 57 | -------------------------------------------------------------------------------- /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.height 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 | #elseif canImport(CoreGraphics) 11 | import struct CoreGraphics.CGFloat 12 | #endif 13 | 14 | // SKIP @bridge 15 | public struct Spacer : View { 16 | private let minLength: CGFloat? 17 | 18 | // SKIP @bridge 19 | public init(minLength: CGFloat? = nil) { 20 | self.minLength = minLength 21 | } 22 | 23 | #if SKIP 24 | @Composable public override func ComposeContent(context: ComposeContext) { 25 | // We haven't found a way that works to get a minimum size and expanding behavior on a spacer, so use two spacers: the 26 | // first to enforce the minimum, and the second to expand. Note that this will cause some modifiers to behave incorrectly 27 | 28 | let axis = EnvironmentValues.shared._layoutAxis 29 | if let minLength, minLength > 0.0 { 30 | let minModifier: Modifier 31 | switch axis { 32 | case .horizontal: 33 | minModifier = Modifier.width(minLength.dp) 34 | case .vertical: 35 | minModifier = Modifier.height(minLength.dp) 36 | case nil: 37 | minModifier = Modifier 38 | } 39 | androidx.compose.foundation.layout.Spacer(modifier: minModifier.then(context.modifier)) 40 | } 41 | 42 | let fillModifier: Modifier 43 | switch axis { 44 | case .horizontal: 45 | fillModifier = EnvironmentValues.shared._fillWidth?() ?? Modifier 46 | case .vertical: 47 | fillModifier = EnvironmentValues.shared._fillHeight?() ?? Modifier 48 | case nil: 49 | fillModifier = Modifier 50 | } 51 | androidx.compose.foundation.layout.Spacer(modifier: fillModifier.then(context.modifier)) 52 | } 53 | #else 54 | public var body: some View { 55 | stubView() 56 | } 57 | #endif 58 | } 59 | 60 | #endif 61 | -------------------------------------------------------------------------------- /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 `ComposeContent` function. 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 public override func Compose(context: ComposeContext) -> ComposeResult { 56 | // If there is a composer that should recompose its caller, we execute it here so that its result escapes. 57 | // Otherwise we wait for ComposeContent where recomposes don't affect the caller 58 | if let composer = context.composer as? SideEffectComposer { 59 | return content(context) 60 | } else { 61 | ComposeContent(context) 62 | return ComposeResult.ok 63 | } 64 | } 65 | 66 | @Composable public override func ComposeContent(context: ComposeContext) { 67 | if let composer = context.composer as? RenderingComposer { 68 | composer.willCompose() 69 | let result = content(context) 70 | composer.didCompose(result: result) 71 | } else { 72 | content(context) 73 | } 74 | } 75 | 76 | /// Use a custom composer to collect the views composed within this view. 77 | @Composable public func collectViews(context: ComposeContext) -> [View] { 78 | var views: [View] = [] 79 | let viewCollectingContext = context.content(composer: SideEffectComposer { view, context in 80 | if let builder = view as? ComposeBuilder { 81 | views += builder.collectViews(context: context(false)) 82 | } else { 83 | views.append(view) 84 | } 85 | return ComposeResult.ok 86 | }) 87 | content(viewCollectingContext) 88 | return views 89 | } 90 | #else 91 | public var body: some View { 92 | stubView() 93 | } 94 | #endif 95 | } 96 | 97 | #endif 98 | -------------------------------------------------------------------------------- /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 | /// The result of composing content. 36 | /// 37 | /// Reserved for future use. Having a return value also expands recomposition scope. See `ComposeBuilder` for details. 38 | public struct ComposeResult { 39 | public static let ok = ComposeResult() 40 | } 41 | 42 | /// Mechanism for a parent view to change how a child view is composed. 43 | public protocol Composer { 44 | } 45 | 46 | /// Base type for composers that render content. 47 | public class RenderingComposer : Composer { 48 | private let compose: (@Composable (View, (Bool) -> ComposeContext) -> Void)? 49 | 50 | /// Optionally provide a compose block to execute instead of subclassing. 51 | /// 52 | /// - Note: This is a separate method from the default constructor rather than giving `compose` a default value to work around Kotlin runtime 53 | /// crashes related to using composable closures. 54 | init(compose: @Composable (View, (Bool) -> ComposeContext) -> Void) { 55 | self.compose = compose 56 | } 57 | 58 | init() { 59 | self.compose = nil 60 | } 61 | 62 | /// Called before a `ComposeBuilder` composes its content. 63 | public func willCompose() { 64 | } 65 | 66 | /// Called after a `ComposeBuilder` composes its content. 67 | public func didCompose(result: ComposeResult) { 68 | } 69 | 70 | /// Compose the given view's content. 71 | /// 72 | /// - Parameter context: The context to use to render the view, optionally retaining this composer. 73 | @Composable public func Compose(view: View, context: (Bool) -> ComposeContext) { 74 | if let compose { 75 | compose(view, context) 76 | } else { 77 | view.ComposeContent(context: context(false)) 78 | } 79 | } 80 | } 81 | 82 | /// Base type for composers that are used for side effects. 83 | /// 84 | /// Side effect composers are escaping, meaning that if the internal content needs to recompose, the calling context will also recompose. 85 | public class SideEffectComposer : Composer { 86 | private let compose: (@Composable (View, (Bool) -> ComposeContext) -> ComposeResult)? 87 | 88 | /// Optionally provide a compose block to execute instead of subclassing. 89 | /// 90 | /// - Note: This is a separate method from the default constructor rather than giving `compose` a default value to work around Kotlin runtime 91 | /// crashes related to using composable closures. 92 | init(compose: @Composable (View, (Bool) -> ComposeContext) -> ComposeResult) { 93 | self.compose = compose 94 | } 95 | 96 | init() { 97 | self.compose = nil 98 | } 99 | 100 | @Composable public func Compose(view: View, context: (Bool) -> ComposeContext) -> ComposeResult { 101 | if let compose { 102 | return compose(view, context) 103 | } else { 104 | return ComposeResult.ok 105 | } 106 | } 107 | } 108 | 109 | #endif 110 | -------------------------------------------------------------------------------- /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/Compose/ComposeModifierView.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 | /// Recognized modifier roles. 7 | public enum ComposeModifierRole { 8 | case accessibility 9 | case id 10 | case spacing 11 | case tag 12 | case unspecified 13 | } 14 | 15 | /// Used internally by modifiers to apply changes to the context supplied to modified views. 16 | public class ComposeModifierView: View { 17 | let view: View 18 | let role: ComposeModifierRole 19 | var action: (@Composable (inout ComposeContext) -> ComposeResult)? 20 | var composeContent: (@Composable (any View, ComposeContext) -> Void)? 21 | 22 | /// Constructor for subclasses. 23 | public init(view: any View, role: ComposeModifierRole = .unspecified) { 24 | // Don't copy view 25 | // SKIP REPLACE: this.view = view 26 | self.view = view 27 | self.role = role 28 | } 29 | 30 | /// A modfiier that performs an action, optionally modifying the `ComposeContext` passed to the modified view. 31 | public init(targetView: any View, role: ComposeModifierRole = .unspecified, action: @Composable (inout ComposeContext) -> ComposeResult) { 32 | self.init(view: targetView, role: role) 33 | self.action = action 34 | } 35 | 36 | /// A modifier that takes over the composition of the modified view. 37 | public init(contentView: any View, role: ComposeModifierRole = .unspecified, composeContent: @Composable (any View, ComposeContext) -> Void) { 38 | self.init(view: contentView, role: role) 39 | self.composeContent = composeContent 40 | } 41 | 42 | @Composable override func ComposeContent(context: ComposeContext) { 43 | if let composeContent { 44 | composeContent(view, context) 45 | } else if let action { 46 | var context = context 47 | let _ = action(&context) 48 | view.Compose(context: context) 49 | } else { 50 | view.Compose(context: context) 51 | } 52 | } 53 | 54 | func strippingModifiers(until: (ComposeModifierView) -> Bool = { _ in false }, perform: (any View?) -> R) -> R { 55 | if until(self) { 56 | return perform(self) 57 | } else { 58 | return view.strippingModifiers(until: until, perform: perform) 59 | } 60 | } 61 | } 62 | #endif 63 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 { 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 public override func ComposeContent(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 ComposeModifierView(targetView: self) { 60 | $0.modifier = modifier($0.modifier) 61 | return ComposeResult.ok 62 | } 63 | } 64 | #endif 65 | 66 | /// Apply the given `ContentModifier`. 67 | // SKIP @bridge 68 | public func applyContentModifier(bridgedContent: Any) -> any View { 69 | #if SKIP 70 | return (bridgedContent as? ContentModifier)?.modify(view: self) ?? self 71 | #else 72 | return self 73 | #endif 74 | } 75 | } 76 | #endif 77 | -------------------------------------------------------------------------------- /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 views: Array 11 | let idMap: (View) -> Any? 12 | let ids: Array 13 | let rememberedIds: MutableSet 14 | let newIds: Array 15 | let rememberedNewIds: MutableSet 16 | let composer: RenderingComposer? 17 | let isBridged: Bool 18 | 19 | static func ==(lhs: AnimatedContentArguments, rhs: AnimatedContentArguments) -> Bool { 20 | // In bridged mode there are cases where a content view (e.g. List/ForEach) will not recompose on its own 21 | // when the view's state changes, so shortcutting the AnimatedContent when the IDs compare equal results in 22 | // showing stale content. We have to shortcut in non-bridged mode, however, because otherwise we may see glitches 23 | // in animated content when the keyboard hides/shows. The reason for this is unknown, as is the reason we do 24 | // not see these glitches in bridged mode 25 | guard !isBridged else { 26 | return lhs === rhs 27 | } 28 | return lhs.ids == rhs.ids && lhs.rememberedIds == rhs.rememberedIds && lhs.newIds == rhs.newIds && lhs.rememberedNewIds == rhs.rememberedNewIds 29 | } 30 | } 31 | #endif 32 | -------------------------------------------------------------------------------- /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 public override func ComposeContent(context: ComposeContext) { 24 | let _ = list.Compose(context: context) 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 | #if false 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 | #endif 86 | -------------------------------------------------------------------------------- /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.animation.AnimatedContent 6 | import androidx.compose.animation.EnterTransition 7 | import androidx.compose.animation.ExitTransition 8 | import androidx.compose.animation.ExperimentalAnimationApi 9 | import androidx.compose.animation.togetherWith 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.ui.Modifier 13 | #endif 14 | 15 | // SKIP @bridge 16 | public struct Group : View { 17 | let content: ComposeBuilder 18 | let isBridged: Bool 19 | 20 | public init(@ViewBuilder content: () -> any View) { 21 | self.content = ComposeBuilder.from(content) 22 | self.isBridged = false 23 | } 24 | 25 | // SKIP @bridge 26 | public init(bridgedContent: any View) { 27 | self.content = ComposeBuilder.from { bridgedContent } 28 | self.isBridged = true 29 | } 30 | 31 | #if SKIP 32 | @Composable public override func Compose(context: ComposeContext) -> ComposeResult { 33 | ComposeContent(context: context) 34 | return ComposeResult.ok 35 | } 36 | 37 | @Composable public override func ComposeContent(context: ComposeContext) { 38 | let views = content.collectViews(context: context).filter { !($0 is EmptyView) } 39 | let idMap: (View) -> Any? = { TagModifierView.strip(from = it, role = ComposeModifierRole.id)?.value } 40 | let ids = views.compactMap(transform = idMap) 41 | let rememberedIds = remember { mutableSetOf() } 42 | let newIds = ids.filter { !rememberedIds.contains($0) } 43 | let rememberedNewIds = remember { mutableSetOf() } 44 | 45 | rememberedNewIds.addAll(newIds) 46 | rememberedIds.clear() 47 | rememberedIds.addAll(ids) 48 | 49 | if ids.count < views.count { 50 | views.forEach { $0.Compose(context: context) } 51 | } else { 52 | let arguments = AnimatedContentArguments(views: views, idMap: idMap, ids: ids, rememberedIds: rememberedIds, newIds: newIds, rememberedNewIds: rememberedNewIds, composer: nil, isBridged: isBridged) 53 | ComposeAnimatedContent(context: context, arguments: arguments) 54 | } 55 | } 56 | 57 | // Use separate method to avoid recompositions. Recomposing `AnimatedContent` during keyboard animations causes glitches. 58 | // SKIP INSERT: @OptIn(ExperimentalAnimationApi::class) 59 | @Composable private func ComposeAnimatedContent(context: ComposeContext, arguments: AnimatedContentArguments) { 60 | AnimatedContent(targetState: arguments.views, contentAlignment: androidx.compose.ui.Alignment.Center, transitionSpec: { 61 | // SKIP INSERT: EnterTransition.None togetherWith ExitTransition.None 62 | }, contentKey: { 63 | $0.map(arguments.idMap) 64 | }, content: { state in 65 | let animation = Animation.current(isAnimating: transition.isRunning) 66 | if animation == nil { 67 | arguments.rememberedNewIds.clear() 68 | } 69 | for view in state { 70 | let id = arguments.idMap(view) 71 | var modifier = context.modifier 72 | if let animation, arguments.newIds.contains(id) || arguments.rememberedNewIds.contains(id) || !arguments.ids.contains(id) { 73 | let transition = TransitionModifierView.transition(for: view) ?? OpacityTransition.shared 74 | let spec = animation.asAnimationSpec() 75 | let enter = transition.asEnterTransition(spec: spec) 76 | let exit = transition.asExitTransition(spec: spec) 77 | modifier = modifier.animateEnterExit(enter: enter, exit: exit) 78 | } 79 | view.Compose(context: context.content(modifier: modifier)) 80 | } 81 | }, label: "Group") 82 | } 83 | #else 84 | public var body: some View { 85 | stubView() 86 | } 87 | #endif 88 | } 89 | 90 | #endif 91 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Containers/Section.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 | // Erase generics to facilitate specialized constructor support. 9 | // SKIP @bridge 10 | public struct Section : View, LazyItemFactory { 11 | let header: ComposeBuilder? 12 | let footer: ComposeBuilder? 13 | let content: ComposeBuilder 14 | 15 | public init(@ViewBuilder content: () -> any View, @ViewBuilder header: () -> any View, @ViewBuilder footer: () -> any View) { 16 | self.header = ComposeBuilder.from(header) 17 | self.footer = ComposeBuilder.from(footer) 18 | self.content = ComposeBuilder.from(content) 19 | } 20 | 21 | public init(@ViewBuilder content: () -> any View, @ViewBuilder footer: () -> any View) { 22 | self.header = nil 23 | self.footer = ComposeBuilder.from(footer) 24 | self.content = ComposeBuilder.from(content) 25 | } 26 | 27 | public init(@ViewBuilder content: () -> any View, @ViewBuilder header: () -> any View) { 28 | self.header = ComposeBuilder.from(header) 29 | self.footer = nil 30 | self.content = ComposeBuilder.from(content) 31 | } 32 | 33 | public init(header: any View, @ViewBuilder content: () -> any View) { 34 | self.header = ComposeBuilder.from({ header }) 35 | self.footer = nil 36 | self.content = ComposeBuilder.from(content) 37 | } 38 | 39 | public init(@ViewBuilder content: () -> any View) { 40 | self.header = nil 41 | self.footer = nil 42 | self.content = ComposeBuilder.from(content) 43 | } 44 | 45 | public init(_ titleKey: LocalizedStringKey, @ViewBuilder content: () -> any View) { 46 | self.init(content: content, header: { Text(titleKey) }) 47 | } 48 | 49 | public init(_ title: String, @ViewBuilder content: () -> any View) { 50 | self.init(content: content, header: { Text(verbatim: title) }) 51 | } 52 | 53 | @available(*, unavailable) 54 | public init(_ titleKey: LocalizedStringKey, isExpanded: Binding, @ViewBuilder content: () -> any View) { 55 | self.init(titleKey, content: content) 56 | } 57 | 58 | @available(*, unavailable) 59 | public init(_ title: String, isExpanded: Binding, @ViewBuilder content: () -> any View) { 60 | self.init(title, content: content) 61 | } 62 | 63 | @available(*, unavailable) 64 | public init(isExpanded: Binding, @ViewBuilder content: () -> any View, @ViewBuilder header: () -> any View) { 65 | self.init(content: content, header: header) 66 | } 67 | 68 | // SKIP @bridge 69 | public init(bridgedContent: any View, bridgedHeader: (any View)?, bridgedFooter: (any View)?) { 70 | self.content = ComposeBuilder.from { bridgedContent } 71 | self.header = bridgedHeader == nil ? nil : ComposeBuilder.from { bridgedHeader! } 72 | self.footer = bridgedFooter == nil ? nil : ComposeBuilder.from { bridgedFooter! } 73 | } 74 | 75 | #if SKIP 76 | @Composable override func ComposeContent(context: ComposeContext) { 77 | if let header { 78 | header.Compose(context: context) 79 | } 80 | content.Compose(context: context) 81 | if let footer { 82 | footer.Compose(context: context) 83 | } 84 | } 85 | 86 | @Composable func appendLazyItemViews(to composer: LazyItemCollectingComposer, appendingContext: ComposeContext) -> ComposeResult { 87 | composer.append(LazySectionHeader(content: header ?? EmptyView())) 88 | content.Compose(context: appendingContext) 89 | composer.append(LazySectionFooter(content: footer ?? EmptyView())) 90 | return ComposeResult.ok 91 | } 92 | 93 | override func composeLazyItems(context: LazyItemFactoryContext, level: Int) { 94 | // Not called because the section does not append itself as a list item view 95 | } 96 | #else 97 | public var body: some View { 98 | stubView() 99 | } 100 | #endif 101 | } 102 | #endif 103 | -------------------------------------------------------------------------------- /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 | public struct ViewThatFits : View { 6 | @available(*, unavailable) 7 | public init(in axes: Axis.Set = [.horizontal, .vertical], @ViewBuilder content: () -> any View) { 8 | } 9 | 10 | #if !SKIP 11 | public var body: some View { 12 | stubView() 13 | } 14 | #endif 15 | } 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/Controls/ControlGroup.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 ControlGroup : View { 6 | @available(*, unavailable) 7 | public init(@ViewBuilder content: () -> any View) { 8 | } 9 | 10 | @available(*, unavailable) 11 | public init(@ViewBuilder content: () -> any View, @ViewBuilder label: () -> any View) /* where Content == LabeledControlGroupContent, C : View, L : View */ { 12 | } 13 | 14 | @available(*, unavailable) 15 | public init(_ titleKey: LocalizedStringKey, @ViewBuilder content: () -> any View) /* where Content == LabeledControlGroupContent, C : View */ { 16 | } 17 | 18 | @available(*, unavailable) 19 | public init(_ title: String, @ViewBuilder content: () -> any View) /* where Content == LabeledControlGroupContent */ { 20 | } 21 | 22 | #if !SKIP 23 | public var body: some View { 24 | stubView() 25 | } 26 | #endif 27 | } 28 | 29 | public struct ControlGroupStyle: RawRepresentable, Equatable { 30 | public let rawValue: Int 31 | 32 | public init(rawValue: Int) { 33 | self.rawValue = rawValue 34 | } 35 | 36 | public static let automatic = ControlGroupStyle(rawValue: 0) 37 | 38 | @available(*, unavailable) 39 | public static let navigation = ControlGroupStyle(rawValue: 1) 40 | 41 | @available(*, unavailable) 42 | public static let palette = ControlGroupStyle(rawValue: 2) 43 | 44 | @available(*, unavailable) 45 | public static let menu = ControlGroupStyle(rawValue: 3) 46 | 47 | @available(*, unavailable) 48 | public static let compactMenu = ControlGroupStyle(rawValue: 4) 49 | } 50 | 51 | extension View { 52 | public func controlGroupStyle(_ style: ControlGroupStyle) -> some View { 53 | return self 54 | } 55 | } 56 | 57 | #if false 58 | //@available(watchOS, unavailable) 59 | //extension ControlGroup where Content == ControlGroupStyleConfiguration.Content { 60 | // 61 | // /// Creates a control group based on a style configuration. 62 | // /// 63 | // /// Use this initializer within the 64 | // /// ``ControlGroupStyle/makeBody(configuration:)`` method of a 65 | // /// ``ControlGroupStyle`` instance to create an instance of the control group 66 | // /// being styled. This is useful for custom control group styles that modify 67 | // /// the current control group style. 68 | // /// 69 | // /// For example, the following code creates a new, custom style that places a 70 | // /// red border around the current control group: 71 | // /// 72 | // /// struct RedBorderControlGroupStyle: ControlGroupStyle { 73 | // /// func makeBody(configuration: Configuration) -> some View { 74 | // /// ControlGroup(configuration) 75 | // /// .border(Color.red) 76 | // /// } 77 | // /// } 78 | // /// 79 | // public init(_ configuration: ControlGroupStyleConfiguration) { fatalError() } 80 | //} 81 | 82 | /// The properties of a control group. 83 | @available(iOS 15.0, macOS 12.0, tvOS 17.0, *) 84 | @available(watchOS, unavailable) 85 | public struct ControlGroupStyleConfiguration { 86 | 87 | /// A type-erased content of a `ControlGroup`. 88 | public struct Content : View { 89 | 90 | /// The type of view representing the body of this view. 91 | /// 92 | /// When you create a custom view, Swift infers this type from your 93 | /// implementation of the required ``View/body-swift.property`` property. 94 | public typealias Body = NeverView 95 | public var body: Body { fatalError() } 96 | } 97 | 98 | /// A view that represents the content of the `ControlGroup`. 99 | public let content: ControlGroupStyleConfiguration.Content = { fatalError() }() 100 | 101 | /// A type-erased label of a ``ControlGroup``. 102 | @available(iOS 16.0, macOS 13.0, *) 103 | public struct Label : View { 104 | 105 | /// The type of view representing the body of this view. 106 | /// 107 | /// When you create a custom view, Swift infers this type from your 108 | /// implementation of the required ``View/body-swift.property`` property. 109 | public typealias Body = NeverView 110 | public var body: Body { fatalError() } 111 | } 112 | 113 | /// A view that provides the optional label of the ``ControlGroup``. 114 | @available(iOS 16.0, macOS 13.0, *) 115 | public let label: ControlGroupStyleConfiguration.Label = { fatalError() }() 116 | } 117 | 118 | #endif 119 | #endif 120 | -------------------------------------------------------------------------------- /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 { 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 public override func ComposeContent(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/Environment/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 | import SkipModel 5 | #if SKIP 6 | import androidx.compose.runtime.Composable 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 | /// Compose this modifier's content. 25 | @Composable public func Compose(content: Content, context: ComposeContext) -> Void { 26 | // Unfortunately we can't use try/finally around a @Composable invocation 27 | StateTracking.pushBody() 28 | body(content: content).Compose(context: context) 29 | StateTracking.popBody() 30 | } 31 | } 32 | #endif 33 | 34 | extension View { 35 | // SKIP @bridge 36 | public func modifier(_ viewModifier: any ViewModifier) -> any View { 37 | #if SKIP 38 | return ComposeModifierView(contentView: self) { view, context in 39 | viewModifier.Compose(content: view, context: context) 40 | } 41 | #else 42 | return self 43 | #endif 44 | } 45 | } 46 | 47 | #if false 48 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 49 | extension ViewModifier where Self.Body == Never { 50 | 51 | /// Gets the current body of the caller. 52 | /// 53 | /// `content` is a proxy for the view that will have the modifier 54 | /// represented by `Self` applied to it. 55 | public func body(content: Self.Content) -> Self.Body { fatalError() } 56 | } 57 | 58 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 59 | extension ViewModifier { 60 | 61 | /// Returns a new modifier that is the result of concatenating 62 | /// `self` with `modifier`. 63 | public func concat(_ modifier: T) -> ModifiedContent { fatalError() } 64 | } 65 | 66 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 67 | extension ViewModifier { 68 | 69 | /// Returns a new version of the modifier that will apply the 70 | /// transaction mutation function `transform` to all transactions 71 | /// within the modifier. 72 | // public func transaction(_ transform: @escaping (inout Transaction) -> Void) -> some ViewModifier { fatalError() } 73 | 74 | 75 | /// Returns a new version of the modifier that will apply 76 | /// `animation` to all animatable values within the modifier. 77 | // public func animation(_ animation: Animation?) -> some ViewModifier { fatalError() } 78 | 79 | } 80 | 81 | #endif 82 | #endif 83 | -------------------------------------------------------------------------------- /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 | #if false 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 | #endif 33 | -------------------------------------------------------------------------------- /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/Graphics/ShadowStyle.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if false 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 | #endif 45 | -------------------------------------------------------------------------------- /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/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/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/Layout/Anchor.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if false 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 | #endif 92 | -------------------------------------------------------------------------------- /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: 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/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/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/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/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/GeometryEffect.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if false 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 | #endif 30 | -------------------------------------------------------------------------------- /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/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 { 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 public override func ComposeContent(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/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/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/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/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 | -------------------------------------------------------------------------------- /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 : 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/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/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/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/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/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/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/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/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/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/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/System/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/System/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 | -------------------------------------------------------------------------------- /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/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 : CaseIterable, Hashable { 6 | case mini 7 | case small 8 | case regular 9 | case large 10 | case extraLarge 11 | } 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /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 | #if false 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 | #endif 31 | -------------------------------------------------------------------------------- /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/System/DynamicViewContent.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if false 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 | #endif 34 | -------------------------------------------------------------------------------- /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 | #if false 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 | #endif 20 | -------------------------------------------------------------------------------- /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 | 5 | // SKIP @bridge 6 | public struct EmptyModifier : ViewModifier { 7 | public static let identity: EmptyModifier = EmptyModifier() 8 | 9 | // SKIP @bridge 10 | public init() { 11 | } 12 | 13 | #if SKIP 14 | public func body(content: Content) -> some View { 15 | content 16 | } 17 | #endif 18 | } 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /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/FileDialogBrowserOptions.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if false 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 | #endif 84 | -------------------------------------------------------------------------------- /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/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/System/IndexViewStyle.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if false 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 | #endif 39 | -------------------------------------------------------------------------------- /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 | #if false 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 | #endif 47 | -------------------------------------------------------------------------------- /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 | #if false 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 | #endif 83 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/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/System/LimitedAvailabilityConfiguration.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if false 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 | #endif 21 | -------------------------------------------------------------------------------- /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 | #if false 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 | #endif 36 | -------------------------------------------------------------------------------- /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 | #if false 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 | #endif 88 | -------------------------------------------------------------------------------- /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 | #if false 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 | #endif 16 | -------------------------------------------------------------------------------- /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 | #if false 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 | #endif 18 | -------------------------------------------------------------------------------- /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 | #if false 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 | #endif 60 | -------------------------------------------------------------------------------- /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/System/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/System/SafeAreaRegions.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 | #endif 8 | 9 | public struct SafeAreaRegions : OptionSet { 10 | public let rawValue: Int 11 | 12 | public init(rawValue: Int) { 13 | self.rawValue = rawValue 14 | } 15 | 16 | public static let container = SafeAreaRegions(rawValue: 1) 17 | public static let keyboard = SafeAreaRegions(rawValue: 2) 18 | public static let all = SafeAreaRegions(rawValue: 3) 19 | } 20 | 21 | #if SKIP 22 | import androidx.compose.ui.geometry.Rect 23 | 24 | /// Track safe area. 25 | struct SafeArea: Equatable { 26 | /// Total bounds of presentation root. 27 | let presentationBoundsPx: Rect 28 | 29 | /// Safe bounds of presentation root. 30 | let safeBoundsPx: Rect 31 | 32 | /// The edges whose safe area is solely due to system bars. 33 | let absoluteSystemBarEdges: Edge.Set 34 | 35 | init(presentation: Rect, safe: Rect, absoluteSystemBars: Edge.Set = []) { 36 | self.presentationBoundsPx = presentation 37 | self.safeBoundsPx = safe 38 | self.absoluteSystemBarEdges = absoluteSystemBars 39 | } 40 | 41 | /// Update the safe area. 42 | @Composable func insetting(_ edge: Edge, to value: Float) -> SafeArea { 43 | guard value > Float(0.0) else { 44 | return self 45 | } 46 | var systemBarEdges = absoluteSystemBarEdges 47 | var (safeLeft, safeTop, safeRight, safeBottom) = safeBoundsPx 48 | switch edge { 49 | case .top: 50 | safeTop = value 51 | systemBarEdges.remove(.top) 52 | case .bottom: 53 | safeBottom = value 54 | systemBarEdges.remove(.bottom) 55 | case .leading: 56 | safeLeft = value 57 | systemBarEdges.remove(.leading) 58 | case .trailing: 59 | safeRight = value 60 | systemBarEdges.remove(.trailing) 61 | } 62 | return SafeArea(presentation: presentationBoundsPx, safe: Rect(top: safeTop, left: safeLeft, bottom: safeBottom, right: safeRight), absoluteSystemBars: systemBarEdges) 63 | } 64 | } 65 | #endif 66 | #endif 67 | -------------------------------------------------------------------------------- /Sources/SkipUI/SkipUI/System/SidebarRowSize.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2023–2025 Skip 2 | // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception 3 | #if false 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 | #endif 33 | -------------------------------------------------------------------------------- /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 | #if false 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 | #endif 83 | -------------------------------------------------------------------------------- /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: Hashable { 10 | case compact 11 | case regular 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 | -------------------------------------------------------------------------------- /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/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 | #if SKIP 5 | import androidx.compose.runtime.Composable 6 | #endif 7 | 8 | public struct SecureField : View { 9 | let textField: TextField 10 | 11 | public init(text: Binding, prompt: Text? = nil, @ViewBuilder label: () -> any View) { 12 | textField = TextField(text: text, prompt: prompt, isSecure: true, label: label) 13 | } 14 | 15 | public init(_ title: String, text: Binding, prompt: Text? = nil) { 16 | self.init(text: text, prompt: prompt, label: { Text(verbatim: title) }) 17 | } 18 | 19 | public init(_ titleKey: LocalizedStringKey, text: Binding, prompt: Text? = nil) { 20 | self.init(text: text, prompt: prompt, label: { Text(titleKey) }) 21 | } 22 | 23 | #if SKIP 24 | @Composable public override func ComposeContent(context: ComposeContext) { 25 | textField.Compose(context: context) 26 | } 27 | #else 28 | public var body: some View { 29 | stubView() 30 | } 31 | #endif 32 | } 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /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 | #if SKIP 5 | import androidx.compose.foundation.text.KeyboardActions 6 | import androidx.compose.foundation.text.KeyboardOptions 7 | import androidx.compose.material3.ExperimentalMaterial3Api 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 { 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 | #if SKIP 28 | // SKIP INSERT: @OptIn(ExperimentalMaterial3Api::class) 29 | @Composable public override func ComposeContent(context: ComposeContext) { 30 | let contentContext = context.content() 31 | let textEnvironment = EnvironmentValues.shared._textEnvironment 32 | let redaction = EnvironmentValues.shared.redactionReasons 33 | let styleInfo = Text.styleInfo(textEnvironment: textEnvironment, redaction: redaction, context: context) 34 | let animatable = styleInfo.style.asAnimatable(context: context) 35 | let keyboardOptions = EnvironmentValues.shared._keyboardOptions ?? KeyboardOptions.Default 36 | let keyboardActions = KeyboardActions(EnvironmentValues.shared._onSubmitState, LocalFocusManager.current) 37 | let colors = TextField.colors(styleInfo: styleInfo, outline: Color.clear) 38 | let visualTransformation = VisualTransformation.None 39 | OutlinedTextField(value: text.wrappedValue, onValueChange: { 40 | text.wrappedValue = $0 41 | }, modifier: context.modifier.fillSize(), textStyle: animatable.value, enabled: EnvironmentValues.shared.isEnabled, singleLine: false, keyboardOptions: keyboardOptions, keyboardActions: keyboardActions, colors: colors, visualTransformation: visualTransformation) 42 | } 43 | #else 44 | public var body: some View { 45 | stubView() 46 | } 47 | #endif 48 | } 49 | 50 | public struct TextEditorStyle: RawRepresentable, Equatable { 51 | public let rawValue: Int 52 | 53 | public init(rawValue: Int) { 54 | self.rawValue = rawValue 55 | } 56 | 57 | public static let automatic = TextEditorStyle(rawValue: 0) // For bridging 58 | public static let plain = TextEditorStyle(rawValue: 1) // For bridging 59 | } 60 | 61 | extension View { 62 | public func textEditorStyle(_ style: TextEditorStyle) -> any View { 63 | return self 64 | } 65 | 66 | // SKIP @bridge 67 | public func textEditorStyle(bridgedStyle: Int) -> any View { 68 | return textEditorStyle(TextEditorStyle(rawValue: bridgedStyle)) 69 | } 70 | 71 | @available(*, unavailable) 72 | public func findNavigator(isPresented: Binding) -> some View { 73 | return self 74 | } 75 | 76 | @available(*, unavailable) 77 | public func findDisabled(_ isDisabled: Bool = true) -> some View { 78 | return self 79 | } 80 | 81 | @available(*, unavailable) 82 | public func replaceDisabled(_ isDisabled: Bool = true) -> some View { 83 | return self 84 | } 85 | } 86 | 87 | #if false 88 | /// The properties of a text editor. 89 | @available(iOS 17.0, macOS 14.0, *) 90 | @available(tvOS, unavailable) 91 | @available(watchOS, unavailable) 92 | public struct TextEditorStyleConfiguration { 93 | } 94 | 95 | #endif 96 | #endif 97 | -------------------------------------------------------------------------------- /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 | #if false 31 | @available(iOS 17.0, *) 32 | @available(macOS, unavailable) 33 | @available(watchOS, unavailable) 34 | @available(tvOS, unavailable) 35 | public struct TextInputDictationActivation : Equatable, Sendable { 36 | 37 | /// A configuration that activates dictation when someone selects the 38 | /// microphone. 39 | @available(iOS 17.0, *) 40 | @available(macOS, unavailable) 41 | @available(watchOS, unavailable) 42 | @available(tvOS, unavailable) 43 | public static let onSelect: TextInputDictationActivation = { fatalError() }() 44 | 45 | /// A configuration that activates dictation when someone selects the 46 | /// microphone or looks at the entry field. 47 | @available(iOS 17.0, *) 48 | @available(macOS, unavailable) 49 | @available(watchOS, unavailable) 50 | @available(tvOS, unavailable) 51 | public static let onLook: TextInputDictationActivation = { fatalError() }() 52 | 53 | 54 | } 55 | 56 | @available(iOS 17.0, *) 57 | @available(macOS, unavailable) 58 | @available(watchOS, unavailable) 59 | @available(tvOS, unavailable) 60 | public struct TextInputDictationBehavior : Equatable, Sendable { 61 | 62 | /// A platform-appropriate default text input dictation behavior. 63 | /// 64 | /// The automatic behavior uses a ``TextInputDictationActivation`` value of 65 | /// ``TextInputDictationActivation/onLook`` for visionOS apps and 66 | /// ``TextInputDictationActivation/onSelect`` for iOS apps. 67 | @available(iOS 17.0, *) 68 | @available(macOS, unavailable) 69 | @available(watchOS, unavailable) 70 | @available(tvOS, unavailable) 71 | public static let automatic: TextInputDictationBehavior = { fatalError() }() 72 | 73 | /// Adds a dictation microphone in the search bar. 74 | @available(iOS 17.0, *) 75 | @available(macOS, unavailable) 76 | @available(watchOS, unavailable) 77 | @available(tvOS, unavailable) 78 | public static func inline(activation: TextInputDictationActivation) -> TextInputDictationBehavior { fatalError() } 79 | 80 | 81 | } 82 | 83 | #endif 84 | #endif 85 | -------------------------------------------------------------------------------- /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/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/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/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 public override func ComposeContent(context: ComposeContext) { 21 | let _ = view.Compose(context: context) 22 | } 23 | #else 24 | public var body: some View { 25 | stubView() 26 | } 27 | #endif 28 | } 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /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 public override func ComposeContent(context: ComposeContext) { 19 | content.Compose(context: context) 20 | } 21 | #else 22 | public var body: some View { 23 | stubView() 24 | } 25 | #endif 26 | } 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /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 ComposeModifierView(targetView: self) { _ 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/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/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 tagged = ViewPlacement(rawValue: 8) 13 | static let toolbar = ViewPlacement(rawValue: 16) 14 | } 15 | 16 | #endif 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Tests/SkipUITests/ModifierTests.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 | 6 | final class ModifierTests: SkipUITestCase { 7 | func testModifierViewDoesNotCopy() { 8 | #if SKIP 9 | let text = Text("test") 10 | let modified = text.font(.title).padding(.all, 10.0) 11 | modified.strippingModifiers { XCTAssertEqual($0, text) } 12 | #endif 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Tests/SkipUITests/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /Tests/SkipUITests/Skip/res/drawable/battery_charging.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | 17 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/SkipUITests/Skip/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | My Application 3 | Hello, World 4 | 5 | -------------------------------------------------------------------------------- /Tests/SkipUITests/Skip/skip.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skiptools/skip-ui/e094affd8703ddad61debffc5c0274ea5c4dbcb6/Tests/SkipUITests/Skip/skip.yml -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------