craft-native 0.0.395 → 0.0.396
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/android/templates/CraftNativeFlatList.kt.template +7 -0
- package/dist/android/templates/CraftNativeMapGoogle.kt.template +3 -0
- package/dist/android/templates/CraftNativeMapLibre.kt.template +3 -0
- package/dist/android/templates/CraftNativeRefreshableFlatList.kt.template +9 -0
- package/dist/android/templates/MainActivityNative.kt.template +317 -20
- package/dist/cli.js +1 -1
- package/dist/components/index.d.ts +16 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/templates/CraftNativeFlatList.swift +4 -0
- package/dist/ios/templates/CraftNativeMutation.swift +27 -2
- package/dist/ios/templates/CraftNativeScreen.swift +292 -44
- package/dist/macos/templates/CraftNativeMutation.swift +27 -2
- package/package.json +1 -1
|
@@ -1108,6 +1108,10 @@ private func craftNativeAlignContentCode(_ value: String) -> UInt32 {
|
|
|
1108
1108
|
|
|
1109
1109
|
/// Keep the indicator's intrinsic size separate from authored style transforms.
|
|
1110
1110
|
private final class CraftNativeActivityIndicatorView: UIActivityIndicatorView {
|
|
1111
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
1112
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1111
1115
|
var indicatorScale: CGFloat = 1
|
|
1112
1116
|
}
|
|
1113
1117
|
|
|
@@ -1122,6 +1126,10 @@ private final class CraftNativeActivityIndicatorView: UIActivityIndicatorView {
|
|
|
1122
1126
|
/// `enableBackgroundAudio` writes), and comes back into its place when the
|
|
1123
1127
|
/// app does.
|
|
1124
1128
|
private final class CraftNativeVideoView: UIView, AVPlayerViewControllerDelegate {
|
|
1129
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
1130
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1125
1133
|
private let playerController = AVPlayerViewController()
|
|
1126
1134
|
private var player: AVPlayer?
|
|
1127
1135
|
private var sourceURI = ""
|
|
@@ -1454,6 +1462,10 @@ private final class CraftMarkerPhotoRedirects: NSObject, URLSessionTaskDelegate
|
|
|
1454
1462
|
}
|
|
1455
1463
|
|
|
1456
1464
|
private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
1465
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
1466
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1457
1469
|
private var annotationsByID: [String: MKPointAnnotation] = [:]
|
|
1458
1470
|
private var overlaysByID: [String: MKPolyline] = [:]
|
|
1459
1471
|
private var overlayCoordinatesByID: [String: [CLLocationCoordinate2D]] = [:]
|
|
@@ -2394,6 +2406,46 @@ private final class CraftNativeLayoutStyleBox {
|
|
|
2394
2406
|
init(_ style: CraftNativeLayoutStyle) { self.style = style }
|
|
2395
2407
|
}
|
|
2396
2408
|
|
|
2409
|
+
/// Pointer policy applies to authored nodes, not UIKit's private control views.
|
|
2410
|
+
enum CraftNativePointerEvents: String {
|
|
2411
|
+
case auto, none
|
|
2412
|
+
case boxNone = "box-none"
|
|
2413
|
+
case boxOnly = "box-only"
|
|
2414
|
+
|
|
2415
|
+
static func resolve(props: [String: Any], style: [String: Any]) -> Self {
|
|
2416
|
+
let prop = props["pointerEvents"]
|
|
2417
|
+
let value = prop == nil || prop is NSNull ? style["pointerEvents"] : prop
|
|
2418
|
+
return (value as? String).flatMap(Self.init(rawValue:)) ?? .auto
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
private var craftNativePointerEventsKey: UInt8 = 0
|
|
2423
|
+
|
|
2424
|
+
extension UIView {
|
|
2425
|
+
// nil identifies an internal view; even authored `auto` nodes are marked.
|
|
2426
|
+
var craftPointerEvents: CraftNativePointerEvents? {
|
|
2427
|
+
get { (objc_getAssociatedObject(self, &craftNativePointerEventsKey) as? String).flatMap(CraftNativePointerEvents.init(rawValue:)) }
|
|
2428
|
+
set { objc_setAssociatedObject(self, &craftNativePointerEventsKey, newValue?.rawValue, .OBJC_ASSOCIATION_COPY_NONATOMIC) }
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
func craftPointerHitTest(_ point: CGPoint, with event: UIEvent?, native: () -> UIView?) -> UIView? {
|
|
2432
|
+
guard let policy = craftPointerEvents, policy != .auto else { return native() }
|
|
2433
|
+
guard policy != .none, !isHidden, alpha > 0.01, isUserInteractionEnabled else { return nil }
|
|
2434
|
+
if policy == .boxOnly && !self.point(inside: point, with: event) { return nil }
|
|
2435
|
+
let hit = native()
|
|
2436
|
+
var candidate = hit
|
|
2437
|
+
var authoredChild = false
|
|
2438
|
+
while let view = candidate, view !== self {
|
|
2439
|
+
if view.craftPointerEvents != nil { authoredChild = true; break }
|
|
2440
|
+
candidate = view.superview
|
|
2441
|
+
}
|
|
2442
|
+
if policy == .boxNone { return authoredChild ? hit : nil }
|
|
2443
|
+
// Preserve private controls (picker wheels, text selection, map gestures),
|
|
2444
|
+
// while excluding authored descendants of a box-only container.
|
|
2445
|
+
return authoredChild ? self : hit ?? self
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2397
2449
|
private var craftNativeLayoutStyleKey: UInt8 = 0
|
|
2398
2450
|
private var craftNativeCornerRadiusKey: UInt8 = 0
|
|
2399
2451
|
private var craftNativeCornerRadiiKey: UInt8 = 0
|
|
@@ -3021,12 +3073,14 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
3021
3073
|
/// is inside the view, so a floating button absolutely positioned out of a
|
|
3022
3074
|
/// zero-height parent (a component's wrapper) drew but could not be tapped.
|
|
3023
3075
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3076
|
+
return craftPointerHitTest(point, with: event) {
|
|
3077
|
+
if let hit = super.hitTest(point, with: event) { return hit }
|
|
3078
|
+
guard !clipsToBounds, !isHidden, alpha > 0.01, isUserInteractionEnabled else { return nil }
|
|
3079
|
+
for child in subviews.reversed() where !child.isHidden && child.alpha > 0.01 && child.isUserInteractionEnabled {
|
|
3080
|
+
if let hit = child.hitTest(child.convert(point, from: self), with: event) { return hit }
|
|
3081
|
+
}
|
|
3082
|
+
return nil
|
|
3028
3083
|
}
|
|
3029
|
-
return nil
|
|
3030
3084
|
}
|
|
3031
3085
|
|
|
3032
3086
|
// MARK: Children
|
|
@@ -4100,10 +4154,107 @@ private final class CraftNativeKeyboardAvoidingView: CraftNativeFlowView {
|
|
|
4100
4154
|
}
|
|
4101
4155
|
}
|
|
4102
4156
|
|
|
4157
|
+
// Preserve source dimensions when the loader downsamples decoded pixels. CSS
|
|
4158
|
+
// scale-down/none use intrinsic dimensions, never the thumbnail's display size.
|
|
4159
|
+
private var craftImageIntrinsicKey: UInt8 = 0
|
|
4160
|
+
private extension UIImage {
|
|
4161
|
+
var craftIntrinsicSize: CGSize {
|
|
4162
|
+
get { (objc_getAssociatedObject(self, &craftImageIntrinsicKey) as? NSValue)?.cgSizeValue ?? size }
|
|
4163
|
+
set { objc_setAssociatedObject(self, &craftImageIntrinsicKey, NSValue(cgSize: newValue), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
|
|
4167
|
+
/// Explicit CSS-facing image geometry; the loader and semantic view stay retained.
|
|
4168
|
+
class CraftNativePositionedImageView: UIImageView {
|
|
4169
|
+
private var storedImage: UIImage?
|
|
4170
|
+
private var fittedImage: UIImageView?
|
|
4171
|
+
private var fit: String?
|
|
4172
|
+
private var position = CGPoint(x: 0.5, y: 0.5)
|
|
4173
|
+
|
|
4174
|
+
override var image: UIImage? {
|
|
4175
|
+
get { storedImage }
|
|
4176
|
+
set {
|
|
4177
|
+
storedImage = newValue
|
|
4178
|
+
super.image = fit == nil ? newValue : nil
|
|
4179
|
+
fittedImage?.image = newValue
|
|
4180
|
+
invalidateIntrinsicContentSize()
|
|
4181
|
+
setNeedsLayout()
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4185
|
+
override var intrinsicContentSize: CGSize {
|
|
4186
|
+
fit == nil ? super.intrinsicContentSize : storedImage?.craftIntrinsicSize ?? CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
|
|
4187
|
+
}
|
|
4188
|
+
|
|
4189
|
+
func configureObjectGeometry(props: [String: Any], style: [String: Any]) {
|
|
4190
|
+
func value(_ name: String) -> Any? {
|
|
4191
|
+
if let raw = props[name], !(raw is NSNull) { return raw }
|
|
4192
|
+
if let raw = style[name], !(raw is NSNull) { return raw }
|
|
4193
|
+
return nil
|
|
4194
|
+
}
|
|
4195
|
+
let rawFit = value("objectFit"), rawPosition = value("objectPosition")
|
|
4196
|
+
if rawFit != nil || rawPosition != nil {
|
|
4197
|
+
let requested = rawFit as? String ?? "fill"
|
|
4198
|
+
fit = ["fill", "contain", "cover", "none", "scale-down"].contains(requested) ? requested : "fill"
|
|
4199
|
+
position = Self.objectPosition(rawPosition)
|
|
4200
|
+
if fittedImage == nil {
|
|
4201
|
+
let child = UIImageView()
|
|
4202
|
+
child.isUserInteractionEnabled = false
|
|
4203
|
+
child.isAccessibilityElement = false
|
|
4204
|
+
child.contentMode = .scaleToFill
|
|
4205
|
+
insertSubview(child, at: 0)
|
|
4206
|
+
fittedImage = child
|
|
4207
|
+
}
|
|
4208
|
+
} else { fit = nil }
|
|
4209
|
+
fittedImage?.isHidden = fit == nil
|
|
4210
|
+
super.image = fit == nil ? storedImage : nil
|
|
4211
|
+
fittedImage?.image = storedImage
|
|
4212
|
+
invalidateIntrinsicContentSize()
|
|
4213
|
+
setNeedsLayout()
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4216
|
+
var usesObjectGeometry: Bool { fit != nil }
|
|
4217
|
+
var objectCovers: Bool { fit == "cover" }
|
|
4218
|
+
|
|
4219
|
+
private static func objectPosition(_ raw: Any?) -> CGPoint {
|
|
4220
|
+
guard let text = raw as? String else { return CGPoint(x: 0.5, y: 0.5) }
|
|
4221
|
+
var parts = text.lowercased().split(whereSeparator: { $0.isWhitespace }).map(String.init)
|
|
4222
|
+
guard (1...2).contains(parts.count) else { return CGPoint(x: 0.5, y: 0.5) }
|
|
4223
|
+
if parts.count == 1 {
|
|
4224
|
+
parts = ["top", "bottom"].contains(parts[0]) ? ["center", parts[0]] : [parts[0], "center"]
|
|
4225
|
+
} else if ["top", "bottom"].contains(parts[0]) || ["left", "right"].contains(parts[1]) { parts.swapAt(0, 1) }
|
|
4226
|
+
func fraction(_ part: String, low: String, high: String) -> CGFloat? {
|
|
4227
|
+
if part == low { return 0 }; if part == high { return 1 }; if part == "center" { return 0.5 }
|
|
4228
|
+
guard part.hasSuffix("%"), let number = Double(part.dropLast()), number.isFinite else { return nil }
|
|
4229
|
+
return CGFloat(number / 100)
|
|
4230
|
+
}
|
|
4231
|
+
guard let x = fraction(parts[0], low: "left", high: "right"), let y = fraction(parts[1], low: "top", high: "bottom") else { return CGPoint(x: 0.5, y: 0.5) }
|
|
4232
|
+
return CGPoint(x: x, y: y)
|
|
4233
|
+
}
|
|
4234
|
+
|
|
4235
|
+
override func layoutSubviews() {
|
|
4236
|
+
super.layoutSubviews()
|
|
4237
|
+
guard let fit, let child = fittedImage else { return }
|
|
4238
|
+
let source = storedImage?.craftIntrinsicSize ?? .zero
|
|
4239
|
+
guard source.width > 0, source.height > 0 else { child.frame = .zero; return }
|
|
4240
|
+
let contain = min(bounds.width / source.width, bounds.height / source.height)
|
|
4241
|
+
let scale: CGFloat = fit == "cover" ? max(bounds.width / source.width, bounds.height / source.height)
|
|
4242
|
+
: fit == "none" ? 1 : fit == "scale-down" ? min(1, contain) : contain
|
|
4243
|
+
let size = fit == "fill" ? bounds.size : CGSize(width: source.width * scale, height: source.height * scale)
|
|
4244
|
+
child.frame = CGRect(x: bounds.minX + (bounds.width - size.width) * position.x,
|
|
4245
|
+
y: bounds.minY + (bounds.height - size.height) * position.y, width: size.width, height: size.height)
|
|
4246
|
+
child.tintColor = tintColor
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
|
|
4103
4250
|
/// An image-backed flex container. The image stays behind ordinary native
|
|
4104
4251
|
/// children so `ImageBackground` keeps the same layout and interaction
|
|
4105
4252
|
/// semantics as a `View` while using the shared image loader.
|
|
4106
|
-
private final class CraftNativeImageBackgroundView:
|
|
4253
|
+
private final class CraftNativeImageBackgroundView: CraftNativePositionedImageView {
|
|
4254
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
4255
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
4256
|
+
}
|
|
4257
|
+
|
|
4107
4258
|
let contentStack = CraftNativeFlowView()
|
|
4108
4259
|
|
|
4109
4260
|
override init(frame: CGRect) {
|
|
@@ -4142,6 +4293,10 @@ private final class CraftNativeSegmentElement: UIAccessibilityElement {
|
|
|
4142
4293
|
}
|
|
4143
4294
|
|
|
4144
4295
|
private final class CraftNativeSegmentedControl: UISegmentedControl {
|
|
4296
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
4297
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
4298
|
+
}
|
|
4299
|
+
|
|
4145
4300
|
var values: [String] = []
|
|
4146
4301
|
private var segments: [CraftNativeSegmentElement] = []
|
|
4147
4302
|
override var accessibilityElements: [Any]? {
|
|
@@ -4391,28 +4546,30 @@ private final class CraftNativeModalView: UIView, UIGestureRecognizerDelegate, U
|
|
|
4391
4546
|
}
|
|
4392
4547
|
|
|
4393
4548
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4549
|
+
return craftPointerHitTest(point, with: event) {
|
|
4550
|
+
guard !isHidden, alpha > 0.01, isUserInteractionEnabled else { return nil }
|
|
4551
|
+
let contentPoint = contentStack.convert(point, from: self)
|
|
4552
|
+
if contentStack.bounds.contains(contentPoint), let hit = contentStack.hitTest(contentPoint, with: event), hit !== contentStack {
|
|
4553
|
+
// Flow/layout views occupy the modal's entire content rect, but a
|
|
4554
|
+
// blank part of one must still be a scrim tap. Preserve touches
|
|
4555
|
+
// only when the hit view (or one of its ancestors) actually owns
|
|
4556
|
+
// a control or gesture recognizer.
|
|
4557
|
+
var candidate: UIView? = hit
|
|
4558
|
+
while let view = candidate, view !== contentStack {
|
|
4559
|
+
if view is UIControl || !(view.gestureRecognizers?.isEmpty ?? true) { return hit }
|
|
4560
|
+
candidate = view.superview
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
if !blocksBackground {
|
|
4564
|
+
// A dismissible popup consumes its outside hit so the close event
|
|
4565
|
+
// cannot also activate the screen below. Otherwise it owns the
|
|
4566
|
+
// hit only when a blocking modal is underneath it; with no modal
|
|
4567
|
+
// below, the screen remains pass-through.
|
|
4568
|
+
if dismissOnOutsideTap { return self }
|
|
4569
|
+
return craftScreenController?.hasBlockingModalBelow(self) == true ? self : nil
|
|
4570
|
+
}
|
|
4571
|
+
return blocker.hitTest(blocker.convert(point, from: self), with: event)
|
|
4572
|
+
}
|
|
4416
4573
|
}
|
|
4417
4574
|
|
|
4418
4575
|
override func layoutSubviews() {
|
|
@@ -4513,6 +4670,10 @@ func craftHandleKeyboardTap(in container: UIView, hit: UIView?, persistTaps: Str
|
|
|
4513
4670
|
}
|
|
4514
4671
|
|
|
4515
4672
|
private final class CraftNativeScrollView: UIScrollView, UIGestureRecognizerDelegate {
|
|
4673
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
4674
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
4675
|
+
}
|
|
4676
|
+
|
|
4516
4677
|
let contentStack = CraftNativeFlowView()
|
|
4517
4678
|
private let keyboardTapGesture = UITapGestureRecognizer()
|
|
4518
4679
|
private var keyboardShouldPersistTaps = "never"
|
|
@@ -4877,6 +5038,10 @@ private final class CraftNativeScrollView: UIScrollView, UIGestureRecognizerDele
|
|
|
4877
5038
|
/// inherited down the tree as SVG does; `dark:` classes arrive already
|
|
4878
5039
|
/// resolved for the appearance, so a redraw is all a theme change needs.
|
|
4879
5040
|
final class CraftNativeSvgView: UIView {
|
|
5041
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
5042
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
5043
|
+
}
|
|
5044
|
+
|
|
4880
5045
|
var drawing: [String: Any] = [:] {
|
|
4881
5046
|
didSet {
|
|
4882
5047
|
animates = Self.hasAnimation(drawing["children"] as? [Any] ?? [])
|
|
@@ -5282,6 +5447,10 @@ private final class CraftNativeGradientLayer: CAGradientLayer {
|
|
|
5282
5447
|
/// pill with its label touching the edges. The padding is added to its size
|
|
5283
5448
|
/// and the label laid out inside it, at the alignment the style asked for.
|
|
5284
5449
|
private final class CraftNativeButton: UIButton {
|
|
5450
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
5451
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
5452
|
+
}
|
|
5453
|
+
|
|
5285
5454
|
var padding: UIEdgeInsets = .zero {
|
|
5286
5455
|
didSet {
|
|
5287
5456
|
guard padding != oldValue else { return }
|
|
@@ -5323,6 +5492,10 @@ private final class CraftNativeButton: UIButton {
|
|
|
5323
5492
|
/// Opt-in solid switch. Draws only public UIControl content; it neither walks
|
|
5324
5493
|
/// UISwitch's private views nor changes UIKit animations elsewhere in the app.
|
|
5325
5494
|
final class CraftNativeSolidSwitch: UIControl {
|
|
5495
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
5496
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
5497
|
+
}
|
|
5498
|
+
|
|
5326
5499
|
private(set) var isOn = false
|
|
5327
5500
|
var onColor: UIColor? { didSet { setNeedsDisplay() } }
|
|
5328
5501
|
var offColor: UIColor? { didSet { setNeedsDisplay() } }
|
|
@@ -5433,10 +5606,18 @@ final class CraftNativeSolidSwitch: UIControl {
|
|
|
5433
5606
|
/// square/checkmark symbols while keeping the control's title and accessibility
|
|
5434
5607
|
/// state native and keyboard/touch reachable.
|
|
5435
5608
|
private final class CraftNativeCheckboxButton: UIButton {
|
|
5609
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
5610
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
5611
|
+
}
|
|
5612
|
+
|
|
5436
5613
|
var indeterminate = false
|
|
5437
5614
|
}
|
|
5438
5615
|
|
|
5439
5616
|
private final class CraftNativeLabel: UILabel {
|
|
5617
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
5618
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
5619
|
+
}
|
|
5620
|
+
|
|
5440
5621
|
var insets = UIEdgeInsets.zero {
|
|
5441
5622
|
didSet {
|
|
5442
5623
|
guard insets != oldValue else { return }
|
|
@@ -5470,6 +5651,10 @@ private final class CraftNativeLabel: UILabel {
|
|
|
5470
5651
|
/// selection. UILabel has no selection model; UITextView supplies UIKit's
|
|
5471
5652
|
/// standard loupe, menu and copy behavior while remaining a plain text leaf.
|
|
5472
5653
|
private final class CraftNativeSelectableTextView: UITextView {
|
|
5654
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
5655
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
5656
|
+
}
|
|
5657
|
+
|
|
5473
5658
|
convenience init() {
|
|
5474
5659
|
self.init(frame: .zero, textContainer: nil)
|
|
5475
5660
|
}
|
|
@@ -5498,6 +5683,10 @@ private final class CraftNativeSelectableTextView: UITextView {
|
|
|
5498
5683
|
/// A one-line TextInput. Its style's padding is where the text and the
|
|
5499
5684
|
/// placeholder start, as in a web input; UITextField has no padding of its own.
|
|
5500
5685
|
final class CraftNativeTextField: UITextField {
|
|
5686
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
5687
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
5688
|
+
}
|
|
5689
|
+
|
|
5501
5690
|
var padding = UIEdgeInsets.zero {
|
|
5502
5691
|
didSet {
|
|
5503
5692
|
guard padding != oldValue else { return }
|
|
@@ -5529,6 +5718,10 @@ private final class CraftNativeFlexSpacer: UIView {}
|
|
|
5529
5718
|
/// An SF Symbol leaf. Its natural size is the symbol's, or a 20 pt square
|
|
5530
5719
|
/// when the style gives it no size at all.
|
|
5531
5720
|
private final class CraftNativeIconView: UIImageView {
|
|
5721
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
5722
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
5723
|
+
}
|
|
5724
|
+
|
|
5532
5725
|
var defaultBox: CGFloat? { didSet { if defaultBox != oldValue { invalidateIntrinsicContentSize() } } }
|
|
5533
5726
|
|
|
5534
5727
|
/// An icon says only what the screen labels it. iOS gives a symbol image
|
|
@@ -5554,6 +5747,10 @@ private final class CraftNativeIconView: UIImageView {
|
|
|
5554
5747
|
}
|
|
5555
5748
|
|
|
5556
5749
|
private final class CraftNativeTextView: UITextView {
|
|
5750
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
5751
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
5752
|
+
}
|
|
5753
|
+
|
|
5557
5754
|
private let placeholderLabel = UILabel()
|
|
5558
5755
|
private var nativeTextContainerInset: UIEdgeInsets?
|
|
5559
5756
|
private var nativeLineFragmentPadding: CGFloat?
|
|
@@ -5869,6 +6066,41 @@ enum CraftNativeBundle {
|
|
|
5869
6066
|
}
|
|
5870
6067
|
}
|
|
5871
6068
|
|
|
6069
|
+
private final class CraftNativePickerView: UIPickerView {
|
|
6070
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
6071
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
6072
|
+
}
|
|
6073
|
+
|
|
6074
|
+
}
|
|
6075
|
+
|
|
6076
|
+
private final class CraftNativeDatePickerView: UIDatePicker {
|
|
6077
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
6078
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
6079
|
+
}
|
|
6080
|
+
|
|
6081
|
+
}
|
|
6082
|
+
|
|
6083
|
+
private final class CraftNativeSwitch: UISwitch {
|
|
6084
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
6085
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
6086
|
+
}
|
|
6087
|
+
|
|
6088
|
+
}
|
|
6089
|
+
|
|
6090
|
+
private final class CraftNativeSlider: UISlider {
|
|
6091
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
6092
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
6093
|
+
}
|
|
6094
|
+
|
|
6095
|
+
}
|
|
6096
|
+
|
|
6097
|
+
private final class CraftNativeImageView: CraftNativePositionedImageView {
|
|
6098
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
6099
|
+
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
6100
|
+
}
|
|
6101
|
+
|
|
6102
|
+
}
|
|
6103
|
+
|
|
5872
6104
|
private final class CraftNativeScreenRootView: UIView {
|
|
5873
6105
|
weak var controller: CraftNativeScreenController?
|
|
5874
6106
|
|
|
@@ -5879,7 +6111,8 @@ private final class CraftNativeScreenRootView: UIView {
|
|
|
5879
6111
|
// overlays remain pass-through.
|
|
5880
6112
|
if let controller {
|
|
5881
6113
|
for modal in controller.view.subviews.compactMap({ $0 as? CraftNativeModalView }).reversed()
|
|
5882
|
-
where !modal.blocksBackground && !modal.isHidden && modal.alpha > 0.01
|
|
6114
|
+
where !modal.blocksBackground && !modal.isHidden && modal.alpha > 0.01
|
|
6115
|
+
&& modal.craftPointerEvents != CraftNativePointerEvents.none && modal.craftPointerEvents != .boxNone {
|
|
5883
6116
|
let local = modal.convert(point, from: self)
|
|
5884
6117
|
if !modal.bounds.contains(local) && (modal.dismissOnOutsideTap || controller.hasBlockingModalBelow(modal)) { return modal }
|
|
5885
6118
|
}
|
|
@@ -6371,7 +6604,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6371
6604
|
appliedTraitDefaults = traits
|
|
6372
6605
|
view.backgroundColor = screenBackgroundColor ?? config.resolvedBackgroundColor
|
|
6373
6606
|
applyNavigationBarAppearance()
|
|
6374
|
-
if let document = mutationDocument.
|
|
6607
|
+
if let document = mutationDocument.document {
|
|
6375
6608
|
renderCommitted(document)
|
|
6376
6609
|
} else {
|
|
6377
6610
|
rootStack.setNeedsLayout()
|
|
@@ -6469,6 +6702,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6469
6702
|
return view.subviews[..<index].contains { candidate in
|
|
6470
6703
|
guard let other = candidate as? CraftNativeModalView else { return false }
|
|
6471
6704
|
return other.blocksBackground && !other.isHidden && other.alpha > 0.01
|
|
6705
|
+
&& other.craftPointerEvents != CraftNativePointerEvents.none && other.craftPointerEvents != .boxNone
|
|
6472
6706
|
}
|
|
6473
6707
|
}
|
|
6474
6708
|
|
|
@@ -7674,6 +7908,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7674
7908
|
#if compiler(>=6.2)
|
|
7675
7909
|
if #available(iOS 26.0, *) {
|
|
7676
7910
|
trackedScrollView?.topEdgeEffect.isHidden = false
|
|
7911
|
+
trackedScrollView?.topEdgeEffect.style = .automatic
|
|
7677
7912
|
trackedScrollView?.bottomEdgeEffect.isHidden = false
|
|
7678
7913
|
}
|
|
7679
7914
|
#endif
|
|
@@ -7684,10 +7919,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7684
7919
|
}
|
|
7685
7920
|
#if compiler(>=6.2)
|
|
7686
7921
|
if #available(iOS 26.0, *) {
|
|
7687
|
-
//
|
|
7688
|
-
//
|
|
7689
|
-
//
|
|
7690
|
-
|
|
7922
|
+
// The clock and navigation title need separation from live
|
|
7923
|
+
// content. Keep UIKit's soft top scrim rather than removing
|
|
7924
|
+
// it or adding an opaque bar. The floating tab pill already
|
|
7925
|
+
// carries its own material, so its full-width wash stays off.
|
|
7926
|
+
scroll?.topEdgeEffect.isHidden = false
|
|
7927
|
+
scroll?.topEdgeEffect.style = topEdgeToEdge && !navigationBarSolid ? .soft : .automatic
|
|
7691
7928
|
scroll?.bottomEdgeEffect.isHidden = tabbedScroll
|
|
7692
7929
|
}
|
|
7693
7930
|
#endif
|
|
@@ -8332,7 +8569,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8332
8569
|
// Internal so simulator-hosted XCTest can assert actual UIView identity.
|
|
8333
8570
|
func render(_ document: [String: Any]) {
|
|
8334
8571
|
mutationDocument.replace(with: document)
|
|
8335
|
-
|
|
8572
|
+
// Import assigns protocol IDs to legacy descendants. Draw that same
|
|
8573
|
+
// committed tree from the first frame, so later patches cannot turn
|
|
8574
|
+
// selector/positional identities into different native controls.
|
|
8575
|
+
guard let committed = mutationDocument.document else { return }
|
|
8576
|
+
renderCommitted(committed)
|
|
8336
8577
|
if !hybridRendered {
|
|
8337
8578
|
hybridRendered = true
|
|
8338
8579
|
// The app script's drawing is never on screen: not a first frame.
|
|
@@ -8973,6 +9214,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8973
9214
|
if let current = image.image { image.image = imageForDisplay(current, view: image) }
|
|
8974
9215
|
let requestedResizeMode = style["resizeMode"] as? String ?? props["resizeMode"] as? String
|
|
8975
9216
|
image.contentMode = imageContentMode(requestedResizeMode)
|
|
9217
|
+
(image as? CraftNativePositionedImageView)?.configureObjectGeometry(props: props, style: style)
|
|
8976
9218
|
applyImageClipping(image, contentMode: image.contentMode, style: style)
|
|
8977
9219
|
updateAuxiliaryHandler(events["onLoadStart"], in: &imageLoadStartHandlers, for: image)
|
|
8978
9220
|
updateAuxiliaryHandler(events["onLoad"], in: &imageLoadHandlers, for: image)
|
|
@@ -8992,6 +9234,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8992
9234
|
if let current = background.image { background.image = imageForDisplay(current, view: background) }
|
|
8993
9235
|
let requestedResizeMode = style["resizeMode"] as? String ?? props["resizeMode"] as? String
|
|
8994
9236
|
background.contentMode = imageContentMode(requestedResizeMode)
|
|
9237
|
+
background.configureObjectGeometry(props: props, style: style)
|
|
8995
9238
|
applyImageClipping(background, contentMode: background.contentMode, style: style)
|
|
8996
9239
|
updateAuxiliaryHandler(events["onLoadStart"], in: &imageLoadStartHandlers, for: background)
|
|
8997
9240
|
updateAuxiliaryHandler(events["onLoad"], in: &imageLoadHandlers, for: background)
|
|
@@ -9133,6 +9376,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9133
9376
|
)
|
|
9134
9377
|
if disabled { result.isUserInteractionEnabled = false }
|
|
9135
9378
|
}
|
|
9379
|
+
result.craftPointerEvents = CraftNativePointerEvents.resolve(props: props, style: style)
|
|
9136
9380
|
applyAccessibility(props, type: type, to: result)
|
|
9137
9381
|
(result as? CraftNativeFlowView)?.configureCelebration(props["celebration"])
|
|
9138
9382
|
return current
|
|
@@ -9179,12 +9423,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9179
9423
|
control.addTarget(self, action: #selector(segmentChanged(_:)), for: .valueChanged)
|
|
9180
9424
|
return control
|
|
9181
9425
|
case "Picker":
|
|
9182
|
-
let picker =
|
|
9426
|
+
let picker = CraftNativePickerView()
|
|
9183
9427
|
picker.dataSource = self
|
|
9184
9428
|
picker.delegate = self
|
|
9185
9429
|
return picker
|
|
9186
9430
|
case "DatePicker":
|
|
9187
|
-
let picker =
|
|
9431
|
+
let picker = CraftNativeDatePickerView()
|
|
9188
9432
|
picker.preferredDatePickerStyle = .compact
|
|
9189
9433
|
picker.addTarget(self, action: #selector(datePicked(_:)), for: .valueChanged)
|
|
9190
9434
|
return picker
|
|
@@ -9196,11 +9440,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9196
9440
|
toggle.addTarget(self, action: #selector(solidSwitchChanged(_:)), for: .valueChanged)
|
|
9197
9441
|
return toggle
|
|
9198
9442
|
}
|
|
9199
|
-
let toggle =
|
|
9443
|
+
let toggle = CraftNativeSwitch()
|
|
9200
9444
|
toggle.addTarget(self, action: #selector(switchChanged(_:)), for: .valueChanged)
|
|
9201
9445
|
return toggle
|
|
9202
9446
|
case "Slider":
|
|
9203
|
-
let slider =
|
|
9447
|
+
let slider = CraftNativeSlider()
|
|
9204
9448
|
slider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)
|
|
9205
9449
|
slider.addTarget(self, action: #selector(sliderFinished(_:)), for: [.touchUpInside, .touchUpOutside, .touchCancel])
|
|
9206
9450
|
return slider
|
|
@@ -9209,7 +9453,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9209
9453
|
case "StatusBar", "RefreshControl":
|
|
9210
9454
|
return CraftNativeFlowView()
|
|
9211
9455
|
case "Image":
|
|
9212
|
-
return
|
|
9456
|
+
return CraftNativeImageView()
|
|
9213
9457
|
case "ImageBackground":
|
|
9214
9458
|
return CraftNativeImageBackgroundView(frame: .zero)
|
|
9215
9459
|
case "KeyboardAvoidingView":
|
|
@@ -10462,7 +10706,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
10462
10706
|
if let image = CGImageSourceCreateThumbnailAtIndex(source, 0, options) {
|
|
10463
10707
|
// At the screen's scale, so its size in points is the
|
|
10464
10708
|
// size it is drawn at.
|
|
10465
|
-
|
|
10709
|
+
let decoded = UIImage(cgImage: image, scale: target.scale, orientation: .up)
|
|
10710
|
+
decoded.craftIntrinsicSize = CGSize(width: width, height: height)
|
|
10711
|
+
return decoded
|
|
10466
10712
|
}
|
|
10467
10713
|
}
|
|
10468
10714
|
}
|
|
@@ -10499,7 +10745,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
10499
10745
|
let authored = (style["overflow"] as? String) == "hidden"
|
|
10500
10746
|
|| (view.craftRequestedCornerRadius ?? 0) > 0
|
|
10501
10747
|
|| view.craftRequestedCornerRadii != nil
|
|
10502
|
-
let clips = authored || contentMode == .scaleAspectFill
|
|
10748
|
+
let clips = authored || contentMode == .scaleAspectFill || (view as? CraftNativePositionedImageView)?.usesObjectGeometry == true
|
|
10503
10749
|
view.clipsToBounds = clips
|
|
10504
10750
|
view.layer.masksToBounds = clips
|
|
10505
10751
|
}
|
|
@@ -10546,7 +10792,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
10546
10792
|
if let url = imageURL(for: uri) {
|
|
10547
10793
|
// Decoded at the size it is drawn, when the style fixes that size.
|
|
10548
10794
|
let pixels = Self.displayPixels(of: view)
|
|
10549
|
-
let fill = view.contentMode == .scaleAspectFill
|
|
10795
|
+
let fill = (view as? CraftNativePositionedImageView)?.objectCovers == true || view.contentMode == .scaleAspectFill
|
|
10550
10796
|
// media.keep owns private/offline copies keyed by the authored URL.
|
|
10551
10797
|
// Read before the decoded/network cache, without retaining private
|
|
10552
10798
|
// pixels there after the app clears its media on sign-out.
|
|
@@ -10654,7 +10900,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
10654
10900
|
}
|
|
10655
10901
|
|
|
10656
10902
|
private func imageForDisplay(_ image: UIImage, view: UIImageView) -> UIImage {
|
|
10657
|
-
image.withRenderingMode(tintedImages.contains(ObjectIdentifier(view)) ? .alwaysTemplate : .alwaysOriginal)
|
|
10903
|
+
let displayed = image.withRenderingMode(tintedImages.contains(ObjectIdentifier(view)) ? .alwaysTemplate : .alwaysOriginal)
|
|
10904
|
+
displayed.craftIntrinsicSize = image.craftIntrinsicSize
|
|
10905
|
+
return displayed
|
|
10658
10906
|
}
|
|
10659
10907
|
|
|
10660
10908
|
private func emitImageEvent(
|
|
@@ -35,6 +35,9 @@ final class CraftNativeMutationDocument {
|
|
|
35
35
|
var style: [String: Any]
|
|
36
36
|
var events: [String: Any]
|
|
37
37
|
var text: [String]
|
|
38
|
+
// Legacy snapshots may interleave bare text and native children.
|
|
39
|
+
// Keep each text fragment's slot among the child nodes on import.
|
|
40
|
+
var textPositions: [Int]? = nil
|
|
38
41
|
var children: [String]
|
|
39
42
|
var parent: String?
|
|
40
43
|
}
|
|
@@ -43,6 +46,11 @@ final class CraftNativeMutationDocument {
|
|
|
43
46
|
private var rootId: String?
|
|
44
47
|
private(set) var revision = 0
|
|
45
48
|
|
|
49
|
+
/// The normalized committed tree, whose root need not be named "root".
|
|
50
|
+
var document: [String: Any]? {
|
|
51
|
+
rootId.flatMap { materialize($0, nodes: nodes) }
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
func replace(with document: [String: Any]) {
|
|
47
55
|
var replacement: [String: Node] = [:]
|
|
48
56
|
let root = importNode(document, preferredId: "root", parent: nil, nodes: &replacement)
|
|
@@ -217,6 +225,7 @@ final class CraftNativeMutationDocument {
|
|
|
217
225
|
if patch.keys.contains("events") { mergePatch(patch["events"], into: &node.events) }
|
|
218
226
|
if patch.keys.contains("children") || patch.keys.contains("text") {
|
|
219
227
|
node.text = try textChildren(patch["children"] ?? patch["text"])
|
|
228
|
+
node.textPositions = nil
|
|
220
229
|
}
|
|
221
230
|
nodes[id] = node
|
|
222
231
|
}
|
|
@@ -422,9 +431,18 @@ final class CraftNativeMutationDocument {
|
|
|
422
431
|
"events": node.events,
|
|
423
432
|
]
|
|
424
433
|
if let key = node.key { value["key"] = key }
|
|
425
|
-
let text: [Any] = node.text
|
|
426
434
|
let children: [Any] = node.children.compactMap { materialize($0, nodes: nodes) }
|
|
427
|
-
|
|
435
|
+
var content: [Any] = []
|
|
436
|
+
var textIndex = 0
|
|
437
|
+
for index in 0...children.count {
|
|
438
|
+
while textIndex < node.text.count,
|
|
439
|
+
min(node.textPositions?[textIndex] ?? 0, children.count) == index {
|
|
440
|
+
content.append(node.text[textIndex])
|
|
441
|
+
textIndex += 1
|
|
442
|
+
}
|
|
443
|
+
if index < children.count { content.append(children[index]) }
|
|
444
|
+
}
|
|
445
|
+
value["children"] = content
|
|
428
446
|
return value
|
|
429
447
|
}
|
|
430
448
|
|
|
@@ -440,6 +458,12 @@ final class CraftNativeMutationDocument {
|
|
|
440
458
|
var id = explicit ?? key.map { "\(parent ?? "root")/key:\($0)" } ?? preferredId
|
|
441
459
|
if nodes[id] != nil { id = "\(preferredId)#\(nodes.count)" }
|
|
442
460
|
let children = value["children"] as? [Any] ?? []
|
|
461
|
+
var textPositions: [Int] = []
|
|
462
|
+
var childCount = 0
|
|
463
|
+
for child in children {
|
|
464
|
+
if child is String { textPositions.append(childCount) }
|
|
465
|
+
else if child is [String: Any] { childCount += 1 }
|
|
466
|
+
}
|
|
443
467
|
nodes[id] = Node(
|
|
444
468
|
id: id,
|
|
445
469
|
key: value["key"] as? String,
|
|
@@ -448,6 +472,7 @@ final class CraftNativeMutationDocument {
|
|
|
448
472
|
style: dictionary(value["style"]),
|
|
449
473
|
events: dictionary(value["events"]),
|
|
450
474
|
text: children.compactMap { $0 as? String },
|
|
475
|
+
textPositions: textPositions,
|
|
451
476
|
children: [],
|
|
452
477
|
parent: parent
|
|
453
478
|
)
|