@swiftbrowser/swift 0.3.1 → 0.5.0
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/Sources/FoundationBridge/ItemProvider.swift +89 -19
- package/Sources/SwiftUI/API/EditMode.swift +24 -12
- package/Sources/SwiftUI/API/Inspector.swift +64 -0
- package/Sources/SwiftUI/API/Lists.swift +22 -2
- package/Sources/SwiftUI/API/Navigation.swift +17 -3
- package/Sources/SwiftUI/API/Picker.swift +1 -1
- package/Sources/SwiftUI/API/Primitives.swift +2 -0
- package/Sources/SwiftUI/API/ScrollModifiers.swift +50 -4
- package/Sources/SwiftUI/API/StatusGaps.swift +15 -4
- package/Sources/SwiftUI/API/Toolbar.swift +8 -2
- package/Sources/SwiftUI/API/Transfer.swift +223 -15
- package/Sources/SwiftUI/Runtime/Reconciler.swift +15 -3
- package/Sources/SwiftUI/Runtime/Replay.swift +16 -1
- package/Sources/SwiftUI/Runtime/Runtime.swift +96 -21
- package/Sources/UIKit/Font.swift +5 -6
- package/Sources/UIKit/TextView.swift +3 -1
- package/package.json +1 -1
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
// file representation is a copy in the temporary directory, removed once the
|
|
8
8
|
// handler returns, as on iOS.
|
|
9
9
|
//
|
|
10
|
-
// SwiftUI's drag-and-drop API
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// (
|
|
10
|
+
// SwiftUI's provider-based drag-and-drop API lives here (the SwiftUI shim
|
|
11
|
+
// cannot see this module): `onDrop`, `onDrag`, `DropInfo`, `DropDelegate`,
|
|
12
|
+
// over the shim's `draggable` / `dropdestination` elements. A drag's
|
|
13
|
+
// provider is made when it begins; a drop hands the destination the
|
|
14
|
+
// source's provider (a `Transferable` payload becomes one) and one provider
|
|
15
|
+
// per item dragged in from outside the page (docs/ops-protocol.md, "Drag
|
|
16
|
+
// and drop").
|
|
15
17
|
|
|
16
18
|
import Foundation
|
|
17
19
|
import SwiftUI
|
|
@@ -572,7 +574,6 @@ struct _UnsafeBox<Value>: @unchecked Sendable {
|
|
|
572
574
|
// MARK: - SwiftUI: drag and drop
|
|
573
575
|
|
|
574
576
|
/// What a drop offers: where it is and the providers of what was dropped.
|
|
575
|
-
/// The page has no drag and drop yet, so no drop is ever delivered.
|
|
576
577
|
public struct DropInfo {
|
|
577
578
|
public let location: SwiftUI.CGPoint
|
|
578
579
|
let providers: [NSItemProvider]
|
|
@@ -610,7 +611,7 @@ public struct DropProposal: Sendable {
|
|
|
610
611
|
}
|
|
611
612
|
|
|
612
613
|
/// What a drop target is told as a drag moves over it and drops. Every
|
|
613
|
-
/// method has a default, as on iOS.
|
|
614
|
+
/// method has a default, as on iOS.
|
|
614
615
|
@MainActor
|
|
615
616
|
public protocol DropDelegate {
|
|
616
617
|
func validateDrop(info: DropInfo) -> Bool
|
|
@@ -627,28 +628,97 @@ extension DropDelegate {
|
|
|
627
628
|
public func dropExited(info: DropInfo) {}
|
|
628
629
|
}
|
|
629
630
|
|
|
631
|
+
extension Transferable {
|
|
632
|
+
/// A provider of `self` in every type it exports.
|
|
633
|
+
fileprivate func _itemProvider() -> NSItemProvider {
|
|
634
|
+
let provider = NSItemProvider()
|
|
635
|
+
let boxed = _UnsafeBox(self)
|
|
636
|
+
provider.register(boxed.value)
|
|
637
|
+
return provider
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
extension DropInfo {
|
|
642
|
+
/// What a drop offers as providers: the dragged view's provider (its
|
|
643
|
+
/// `onDrag` provider as it is, a `draggable` payload registered in the
|
|
644
|
+
/// types it exports) and one per item from outside the page.
|
|
645
|
+
init(_ items: _DropItems) {
|
|
646
|
+
var providers: [NSItemProvider] = []
|
|
647
|
+
if let provider = items.source as? NSItemProvider {
|
|
648
|
+
providers.append(provider)
|
|
649
|
+
} else if let transferable = items.source as? any Transferable {
|
|
650
|
+
providers.append(transferable._itemProvider())
|
|
651
|
+
}
|
|
652
|
+
for content in items.contents {
|
|
653
|
+
let provider = NSItemProvider()
|
|
654
|
+
let data = content.data ?? Data()
|
|
655
|
+
provider.registerDataRepresentation(for: content.contentType) { completion in
|
|
656
|
+
completion(data, nil)
|
|
657
|
+
return nil
|
|
658
|
+
}
|
|
659
|
+
provider.suggestedName = content.suggestedName
|
|
660
|
+
providers.append(provider)
|
|
661
|
+
}
|
|
662
|
+
self.init(_location: items.location, providers: providers)
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
630
666
|
extension View {
|
|
631
|
-
///
|
|
667
|
+
/// The view takes drops that carry one of `supportedContentTypes`: a
|
|
668
|
+
/// dragged view's provider, or text, links and files dragged in from
|
|
669
|
+
/// outside the page. `isTargeted` is set while a drag is over the view;
|
|
670
|
+
/// `action` gets the providers and says whether it took them.
|
|
632
671
|
public func onDrop(of supportedContentTypes: [UTType], isTargeted: Binding<Bool>?,
|
|
633
|
-
perform action: @escaping ([NSItemProvider]) -> Bool) -> some View {
|
|
672
|
+
perform action: @escaping ([NSItemProvider]) -> Bool) -> some View {
|
|
673
|
+
onDrop(of: supportedContentTypes, isTargeted: isTargeted) { providers, _ in action(providers) }
|
|
674
|
+
}
|
|
634
675
|
|
|
635
|
-
///
|
|
676
|
+
/// As above, with the drop's point in the view's coordinates.
|
|
636
677
|
public func onDrop(of supportedContentTypes: [UTType], isTargeted: Binding<Bool>?,
|
|
637
|
-
perform action: @escaping ([NSItemProvider], SwiftUI.CGPoint) -> Bool) -> some View {
|
|
678
|
+
perform action: @escaping ([NSItemProvider], SwiftUI.CGPoint) -> Bool) -> some View {
|
|
679
|
+
_DropDestinationView(content: self, accepts: _dropAccepts(supportedContentTypes),
|
|
680
|
+
targeted: { targeted, _ in isTargeted?.wrappedValue = targeted }) { items in
|
|
681
|
+
let info = DropInfo(items)
|
|
682
|
+
let providers = info.itemProviders(for: supportedContentTypes)
|
|
683
|
+
guard !providers.isEmpty else { return false }
|
|
684
|
+
return action(providers, items.location)
|
|
685
|
+
}
|
|
686
|
+
}
|
|
638
687
|
|
|
639
|
-
///
|
|
640
|
-
|
|
688
|
+
/// The delegate hears the drag enter and leave (`dropEntered`,
|
|
689
|
+
/// `dropExited`, with what it carries) and takes the drop
|
|
690
|
+
/// (`validateDrop`, then `performDrop`).
|
|
691
|
+
public func onDrop(of supportedContentTypes: [UTType], delegate: any DropDelegate) -> some View {
|
|
692
|
+
_DropDestinationView(content: self, accepts: _dropAccepts(supportedContentTypes),
|
|
693
|
+
targeted: { targeted, items in
|
|
694
|
+
if targeted { delegate.dropEntered(info: DropInfo(items)) } else { delegate.dropExited(info: DropInfo(items)) }
|
|
695
|
+
}) { items in
|
|
696
|
+
let info = DropInfo(items)
|
|
697
|
+
guard info.hasItemsConforming(to: supportedContentTypes), delegate.validateDrop(info: info) else { return false }
|
|
698
|
+
return delegate.performDrop(info: info)
|
|
699
|
+
}
|
|
700
|
+
}
|
|
641
701
|
|
|
642
702
|
public func onDrop(of supportedTypes: [String], isTargeted: Binding<Bool>?,
|
|
643
|
-
perform action: @escaping ([NSItemProvider]) -> Bool) -> some View {
|
|
703
|
+
perform action: @escaping ([NSItemProvider]) -> Bool) -> some View {
|
|
704
|
+
onDrop(of: supportedTypes.map(NSItemProvider.type), isTargeted: isTargeted, perform: action)
|
|
705
|
+
}
|
|
644
706
|
|
|
645
|
-
public func onDrop(of supportedTypes: [String], delegate: any DropDelegate) -> some View {
|
|
707
|
+
public func onDrop(of supportedTypes: [String], delegate: any DropDelegate) -> some View {
|
|
708
|
+
onDrop(of: supportedTypes.map(NSItemProvider.type), delegate: delegate)
|
|
709
|
+
}
|
|
646
710
|
|
|
647
|
-
///
|
|
648
|
-
|
|
711
|
+
/// The view can be dragged: a long press lifts it on a touch screen, a
|
|
712
|
+
/// drag does with a mouse. `data` runs when the drag begins; its provider
|
|
713
|
+
/// reaches the `onDrop` (or `dropDestination`) the view is dropped on.
|
|
714
|
+
public func onDrag(_ data: @escaping () -> NSItemProvider) -> some View {
|
|
715
|
+
_DraggableView(content: self) { data() }
|
|
716
|
+
}
|
|
649
717
|
|
|
650
|
-
///
|
|
651
|
-
public func onDrag<Preview: View>(_ data: @escaping () -> NSItemProvider, @ViewBuilder preview: () -> Preview) -> some View {
|
|
718
|
+
/// The page drags a copy of the view itself; `preview` is accepted and not drawn.
|
|
719
|
+
public func onDrag<Preview: View>(_ data: @escaping () -> NSItemProvider, @ViewBuilder preview: () -> Preview) -> some View {
|
|
720
|
+
_DraggableView(content: self) { data() }
|
|
721
|
+
}
|
|
652
722
|
|
|
653
723
|
/// Accepted (macOS's services and iOS's drag source): nothing asks for the provider.
|
|
654
724
|
public func itemProvider(_ action: (() -> NSItemProvider?)?) -> some View { self }
|
|
@@ -42,11 +42,14 @@ public protocol DynamicViewContent: View {
|
|
|
42
42
|
|
|
43
43
|
extension ForEach: DynamicViewContent where Content: View {}
|
|
44
44
|
|
|
45
|
-
/// A keyed group whose rows a `List` can delete
|
|
46
|
-
///
|
|
45
|
+
/// A keyed group whose rows a `List` can delete or move. The runtime wraps
|
|
46
|
+
/// each row in a `listrow` with `deletable` / `movable` (docs/ops-protocol.md,
|
|
47
|
+
/// "Reordering rows").
|
|
47
48
|
protocol _DeletableGroup: _TraitBearing {
|
|
48
49
|
/// Calls the group's `onDelete` with the row at `offset`; nil without one.
|
|
49
50
|
var _delete: ((Int) -> Void)? { get }
|
|
51
|
+
/// Calls the group's `onMove` with the row at the first offset and the
|
|
52
|
+
/// destination offset (both in the group's data); nil without one.
|
|
50
53
|
var _move: ((Int, Int) -> Void)? { get }
|
|
51
54
|
}
|
|
52
55
|
|
|
@@ -76,7 +79,10 @@ extension DynamicViewContent {
|
|
|
76
79
|
_EditableForEach(base: self, delete: action.map { action in { action(IndexSet(integer: $0)) } }, move: nil)
|
|
77
80
|
}
|
|
78
81
|
|
|
79
|
-
///
|
|
82
|
+
/// Lets a `List` reorder the rows: in edit mode each row gains a grip at
|
|
83
|
+
/// its trailing edge, and outside it a long press lifts the row; `action`
|
|
84
|
+
/// receives the moved row's offset and the destination offset in `data`,
|
|
85
|
+
/// as `move(fromOffsets:toOffset:)` takes them. Pass `nil` to opt out.
|
|
80
86
|
public func onMove(perform action: ((IndexSet, Int) -> Void)?) -> _EditableForEach<Self> {
|
|
81
87
|
_EditableForEach(base: self, delete: nil, move: action.map { action in { action(IndexSet(integer: $0), $1) } })
|
|
82
88
|
}
|
|
@@ -96,27 +102,33 @@ extension _EditableForEach {
|
|
|
96
102
|
/// `.deleteDisabled` / `.moveDisabled` on a row.
|
|
97
103
|
protocol _RowEditingDisabling: _TraitBearing {
|
|
98
104
|
var _deleteDisabled: Bool { get }
|
|
105
|
+
var _moveDisabled: Bool { get }
|
|
99
106
|
}
|
|
100
107
|
|
|
101
|
-
struct
|
|
108
|
+
struct _RowEditingDisabledView<Content: View> {
|
|
102
109
|
let content: Content
|
|
103
|
-
|
|
110
|
+
var deleteDisabled = false
|
|
111
|
+
var moveDisabled = false
|
|
104
112
|
}
|
|
105
|
-
extension
|
|
106
|
-
extension
|
|
113
|
+
extension _RowEditingDisabledView: _PrimitiveView {}
|
|
114
|
+
extension _RowEditingDisabledView: _GroupView {
|
|
107
115
|
var _children: [any View] { [content] }
|
|
108
116
|
}
|
|
109
|
-
extension
|
|
110
|
-
var _deleteDisabled: Bool {
|
|
117
|
+
extension _RowEditingDisabledView: _RowEditingDisabling {
|
|
118
|
+
var _deleteDisabled: Bool { deleteDisabled }
|
|
119
|
+
var _moveDisabled: Bool { moveDisabled }
|
|
111
120
|
}
|
|
112
121
|
|
|
113
122
|
nonisolated extension View {
|
|
114
123
|
public func deleteDisabled(_ isDisabled: Bool) -> some View {
|
|
115
|
-
|
|
124
|
+
_RowEditingDisabledView(content: self, deleteDisabled: isDisabled)
|
|
116
125
|
}
|
|
117
126
|
|
|
118
|
-
///
|
|
119
|
-
|
|
127
|
+
/// The row keeps its place: no grip in edit mode, and a long press does
|
|
128
|
+
/// not lift it.
|
|
129
|
+
public func moveDisabled(_ isDisabled: Bool) -> some View {
|
|
130
|
+
_RowEditingDisabledView(content: self, moveDisabled: isDisabled)
|
|
131
|
+
}
|
|
120
132
|
}
|
|
121
133
|
|
|
122
134
|
// MARK: - Edit mode
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// `.inspector(isPresented:content:)`: a trailing panel beside the content in
|
|
2
|
+
// the regular size class (an `inspector` element whose last child is the
|
|
3
|
+
// `inspectorpanel`), a sheet in the compact one, as on iPadOS / iOS. See
|
|
4
|
+
// docs/ops-protocol.md, "Inspector".
|
|
5
|
+
|
|
6
|
+
extension View {
|
|
7
|
+
/// Shows `content` in a trailing column beside this view while
|
|
8
|
+
/// `isPresented` is true (regular width), or in a sheet (compact width).
|
|
9
|
+
public func inspector<Content: View>(isPresented: Binding<Bool>,
|
|
10
|
+
@ViewBuilder content: () -> Content) -> some View {
|
|
11
|
+
_InspectorView(base: self, isPresented: isPresented, inspector: content())
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
nonisolated extension View {
|
|
16
|
+
/// The inspector column's width (on the inspector's content).
|
|
17
|
+
public func inspectorColumnWidth(_ width: CGFloat) -> some View {
|
|
18
|
+
_styled { $0.extras["inspectorColumnWidth"] = .number(width) }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// The inspector column's width: the ideal one (the column does not resize).
|
|
22
|
+
public func inspectorColumnWidth(min: CGFloat? = nil, ideal: CGFloat, max: CGFloat? = nil) -> some View {
|
|
23
|
+
inspectorColumnWidth(ideal)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
struct _InspectorView<Base: View, Inspector: View>: View {
|
|
28
|
+
let base: Base
|
|
29
|
+
let isPresented: Binding<Bool>
|
|
30
|
+
let inspector: Inspector
|
|
31
|
+
@Environment(\.horizontalSizeClass) private var sizeClass
|
|
32
|
+
|
|
33
|
+
var body: some View {
|
|
34
|
+
if sizeClass == .regular {
|
|
35
|
+
_InspectorColumns(main: base, panel: _InspectorPanel(content: inspector), presented: isPresented.wrappedValue)
|
|
36
|
+
} else {
|
|
37
|
+
base.sheet(isPresented: isPresented) { inspector }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// The content and, last, the panel; `presented` slides the panel in.
|
|
43
|
+
struct _InspectorColumns<Main: View, Panel: View> {
|
|
44
|
+
let main: Main
|
|
45
|
+
let panel: Panel
|
|
46
|
+
let presented: Bool
|
|
47
|
+
}
|
|
48
|
+
extension _InspectorColumns: _PrimitiveView {}
|
|
49
|
+
extension _InspectorColumns: _HostView {
|
|
50
|
+
var _kind: String { "inspector" }
|
|
51
|
+
func _props(in environment: EnvironmentValues) -> _JSON { .object(["presented": .bool(presented)]) }
|
|
52
|
+
var _content: [any View] { [main, panel] }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// The inspector column's content.
|
|
56
|
+
struct _InspectorPanel<Content: View> {
|
|
57
|
+
let content: Content
|
|
58
|
+
}
|
|
59
|
+
extension _InspectorPanel: _PrimitiveView {}
|
|
60
|
+
extension _InspectorPanel: _HostView {
|
|
61
|
+
var _kind: String { "inspectorpanel" }
|
|
62
|
+
func _props(in environment: EnvironmentValues) -> _JSON { .object([:]) }
|
|
63
|
+
var _content: [any View] { [content] }
|
|
64
|
+
}
|
|
@@ -3,9 +3,16 @@
|
|
|
3
3
|
public protocol ListStyle {
|
|
4
4
|
var _name: String { get }
|
|
5
5
|
}
|
|
6
|
+
/// `.automatic`: inset grouped, or `sidebar` in a `NavigationSplitView`'s
|
|
7
|
+
/// sidebar in the regular size class (resolved in `List._props`).
|
|
6
8
|
public struct DefaultListStyle: ListStyle {
|
|
7
9
|
public init() {}
|
|
8
|
-
public var _name: String { "
|
|
10
|
+
public var _name: String { "automatic" }
|
|
11
|
+
}
|
|
12
|
+
/// iPadOS's sidebar list: rows on the background, no cards or hairlines.
|
|
13
|
+
public struct SidebarListStyle: ListStyle {
|
|
14
|
+
public init() {}
|
|
15
|
+
public var _name: String { "sidebar" }
|
|
9
16
|
}
|
|
10
17
|
public struct PlainListStyle: ListStyle {
|
|
11
18
|
public init() {}
|
|
@@ -31,6 +38,9 @@ extension ListStyle where Self == InsetGroupedListStyle {
|
|
|
31
38
|
extension ListStyle where Self == GroupedListStyle {
|
|
32
39
|
public static var grouped: GroupedListStyle { GroupedListStyle() }
|
|
33
40
|
}
|
|
41
|
+
extension ListStyle where Self == SidebarListStyle {
|
|
42
|
+
public static var sidebar: SidebarListStyle { SidebarListStyle() }
|
|
43
|
+
}
|
|
34
44
|
|
|
35
45
|
nonisolated extension View {
|
|
36
46
|
public func listStyle<S: ListStyle>(_ style: S) -> some View {
|
|
@@ -203,11 +213,13 @@ extension List: _HostView {
|
|
|
203
213
|
/// `editing` (the environment's edit mode) and `refreshable` / `refreshing`
|
|
204
214
|
/// (`.refreshable`) are present only when true / when refreshable.
|
|
205
215
|
func _props(in environment: EnvironmentValues) -> _JSON {
|
|
206
|
-
var props: [String: _JSON] = ["style": .string(environment
|
|
216
|
+
var props: [String: _JSON] = ["style": .string(Self._resolvedStyle(in: environment))]
|
|
207
217
|
if environment.editMode?.wrappedValue.isEditing == true { props["editing"] = .bool(true) }
|
|
208
218
|
_refresh(in: environment).addProps(to: &props)
|
|
209
219
|
// `.scrollDismissesKeyboard` (ScrollModifiers.swift).
|
|
210
220
|
if environment._scrollDismissesKeyboard { props["dismissesKeyboard"] = .bool(true) }
|
|
221
|
+
// `.scrollEdgeEffectStyle` / `.scrollEdgeEffectHidden` (ScrollModifiers.swift).
|
|
222
|
+
if let edges = environment._scrollEdgeEffects.props { props["edgeEffect"] = edges }
|
|
211
223
|
// `.scrollContentBackground(.hidden)` (ScrollViewReader.swift).
|
|
212
224
|
if environment._scrollContentBackground == .hidden { props["background"] = .string("hidden") }
|
|
213
225
|
// `.environment(\.defaultMinListRowHeight, 1)`: rows as short as their content.
|
|
@@ -217,6 +229,14 @@ extension List: _HostView {
|
|
|
217
229
|
var _content: [any View] { [content] }
|
|
218
230
|
var _handler: ((_JSON) -> Void)? { nil }
|
|
219
231
|
|
|
232
|
+
/// `.automatic` is `sidebar` in a split view's sidebar on a regular-width
|
|
233
|
+
/// screen (as SwiftUI does), else inset grouped.
|
|
234
|
+
static func _resolvedStyle(in environment: EnvironmentValues) -> String {
|
|
235
|
+
let style = environment._listStyle
|
|
236
|
+
guard style == "automatic" else { return style }
|
|
237
|
+
return environment._splitSidebar && environment.horizontalSizeClass == .regular ? "sidebar" : "insetGrouped"
|
|
238
|
+
}
|
|
239
|
+
|
|
220
240
|
func _refresh(in environment: EnvironmentValues) -> _Refresh {
|
|
221
241
|
_Refresh(action: environment._refreshAction, refreshing: $refreshing)
|
|
222
242
|
}
|
|
@@ -226,10 +226,13 @@ extension NavigationLink: _NavigationLinkView {
|
|
|
226
226
|
|
|
227
227
|
// MARK: - NavigationSplitView
|
|
228
228
|
|
|
229
|
-
/// Which columns of a `NavigationSplitView` are shown
|
|
230
|
-
///
|
|
229
|
+
/// Which columns of a `NavigationSplitView` are shown: `detailOnly` hides
|
|
230
|
+
/// the sidebar; every other value shows it beside the detail (the optional
|
|
231
|
+
/// middle column is part of the detail, see below).
|
|
231
232
|
public struct NavigationSplitViewVisibility: Hashable, Sendable {
|
|
232
233
|
let name: String
|
|
234
|
+
/// Whether the sidebar column is shown.
|
|
235
|
+
var _showsSidebar: Bool { name != "detailOnly" }
|
|
233
236
|
public static let automatic = NavigationSplitViewVisibility(name: "automatic")
|
|
234
237
|
public static let all = NavigationSplitViewVisibility(name: "all")
|
|
235
238
|
public static let doubleColumn = NavigationSplitViewVisibility(name: "doubleColumn")
|
|
@@ -266,6 +269,9 @@ protocol _NavigationSplitHost: _TraitBearing {
|
|
|
266
269
|
var _detail: any View { get }
|
|
267
270
|
/// Screens pushed from the compact detail (links inside it).
|
|
268
271
|
var _pushedBox: _StateBox<[_PushedScreen]> { get }
|
|
272
|
+
/// The columns shown in the regular size class: the `columnVisibility`
|
|
273
|
+
/// binding, else the split view's own state. The sidebar toggle sets it.
|
|
274
|
+
var _columnVisibility: Binding<NavigationSplitViewVisibility> { get }
|
|
269
275
|
}
|
|
270
276
|
|
|
271
277
|
/// A two-column navigation interface: a sidebar whose list selection drives
|
|
@@ -274,11 +280,14 @@ protocol _NavigationSplitHost: _TraitBearing {
|
|
|
274
280
|
public struct NavigationSplitView<Sidebar: View, Content: View, Detail: View> {
|
|
275
281
|
let sidebar: Sidebar
|
|
276
282
|
let detail: Detail
|
|
283
|
+
let columnVisibility: Binding<NavigationSplitViewVisibility>?
|
|
277
284
|
@State var pushed: [_PushedScreen] = []
|
|
285
|
+
@State var visibility: NavigationSplitViewVisibility = .automatic
|
|
278
286
|
|
|
279
287
|
public init(@ViewBuilder sidebar: () -> Sidebar, @ViewBuilder detail: () -> Detail) where Content == EmptyView {
|
|
280
288
|
self.sidebar = sidebar()
|
|
281
289
|
self.detail = detail()
|
|
290
|
+
columnVisibility = nil
|
|
282
291
|
}
|
|
283
292
|
|
|
284
293
|
public init(@ViewBuilder sidebar: () -> Sidebar, @ViewBuilder content: () -> Content,
|
|
@@ -286,13 +295,16 @@ public struct NavigationSplitView<Sidebar: View, Content: View, Detail: View> {
|
|
|
286
295
|
self.sidebar = sidebar()
|
|
287
296
|
_ = content()
|
|
288
297
|
self.detail = detail()
|
|
298
|
+
columnVisibility = nil
|
|
289
299
|
}
|
|
290
300
|
|
|
291
|
-
///
|
|
301
|
+
/// `columnVisibility` shows or hides the sidebar (`detailOnly` hides it),
|
|
302
|
+
/// and the sidebar toggle sets it.
|
|
292
303
|
public init(columnVisibility: Binding<NavigationSplitViewVisibility>,
|
|
293
304
|
@ViewBuilder sidebar: () -> Sidebar, @ViewBuilder detail: () -> Detail) where Content == EmptyView {
|
|
294
305
|
self.sidebar = sidebar()
|
|
295
306
|
self.detail = detail()
|
|
307
|
+
self.columnVisibility = columnVisibility
|
|
296
308
|
}
|
|
297
309
|
|
|
298
310
|
public init(columnVisibility: Binding<NavigationSplitViewVisibility>,
|
|
@@ -301,6 +313,7 @@ public struct NavigationSplitView<Sidebar: View, Content: View, Detail: View> {
|
|
|
301
313
|
self.sidebar = sidebar()
|
|
302
314
|
_ = content()
|
|
303
315
|
self.detail = detail()
|
|
316
|
+
self.columnVisibility = columnVisibility
|
|
304
317
|
}
|
|
305
318
|
}
|
|
306
319
|
extension NavigationSplitView: _PrimitiveView {}
|
|
@@ -313,6 +326,7 @@ extension NavigationSplitView: _NavigationSplitHost {
|
|
|
313
326
|
var _sidebar: any View { sidebar }
|
|
314
327
|
var _detail: any View { detail }
|
|
315
328
|
var _pushedBox: _StateBox<[_PushedScreen]> { _pushed.box }
|
|
329
|
+
var _columnVisibility: Binding<NavigationSplitViewVisibility> { columnVisibility ?? $visibility }
|
|
316
330
|
}
|
|
317
331
|
|
|
318
332
|
/// Marks the sidebar subtree of a split view: value links there select
|
|
@@ -46,7 +46,7 @@ nonisolated extension View {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
enum _PickerStyleKey: EnvironmentKey { static let defaultValue = "menu" }
|
|
49
|
-
enum _ListStyleKey: EnvironmentKey { static let defaultValue = "
|
|
49
|
+
enum _ListStyleKey: EnvironmentKey { static let defaultValue = "automatic" }
|
|
50
50
|
|
|
51
51
|
extension EnvironmentValues {
|
|
52
52
|
var _pickerStyle: String {
|
|
@@ -363,6 +363,8 @@ extension ScrollView: _HostView {
|
|
|
363
363
|
if let snap = environment._scrollTargetBehavior { props["snap"] = .string(snap) }
|
|
364
364
|
// `.scrollDismissesKeyboard` (ScrollModifiers.swift).
|
|
365
365
|
if environment._scrollDismissesKeyboard { props["dismissesKeyboard"] = .bool(true) }
|
|
366
|
+
// `.scrollEdgeEffectStyle` / `.scrollEdgeEffectHidden` (ScrollModifiers.swift).
|
|
367
|
+
if let edges = environment._scrollEdgeEffects.props { props["edgeEffect"] = edges }
|
|
366
368
|
return .object(props)
|
|
367
369
|
}
|
|
368
370
|
var _content: [any View] { [content] }
|
|
@@ -7,8 +7,14 @@
|
|
|
7
7
|
// no keyboard to drag down). `.automatic` and `.never` add nothing.
|
|
8
8
|
// - `.scrollIndicators(_:axes:)`: accepted; the page's scroll views never
|
|
9
9
|
// show scroll bars (iOS shows them only while scrolling).
|
|
10
|
-
// - `.scrollEdgeEffectStyle(_:for:)` (
|
|
11
|
-
//
|
|
10
|
+
// - `.scrollEdgeEffectStyle(_:for:)` and `.scrollEdgeEffectHidden(_:for:)`
|
|
11
|
+
// (iOS 26): the lists and scroll views below carry `"edgeEffect": {"top":
|
|
12
|
+
// …, "bottom": …}`, each `"hard"`, `"soft"` or `"hidden"` (absent:
|
|
13
|
+
// automatic, the soft effect). The page draws the effect where a scroll
|
|
14
|
+
// view runs under its screen's bar or a bar covering the bottom (styles.css):
|
|
15
|
+
// soft blurs and fades the content there, hard puts an opaque backing with a
|
|
16
|
+
// dividing line behind the bar. The closest modifier wins per edge; hidden
|
|
17
|
+
// wins over a style; leading and trailing are accepted and ignored.
|
|
12
18
|
// - `.safeAreaInset(edge:alignment:spacing:content:)`: approximated as a
|
|
13
19
|
// stack: the inset view takes its room above or below (before or after)
|
|
14
20
|
// the view, which gets the rest, instead of overlapping a scroll view's
|
|
@@ -39,12 +45,38 @@ public struct ScrollEdgeEffectStyle: Hashable, Sendable {
|
|
|
39
45
|
|
|
40
46
|
enum _ScrollDismissesKeyboardKey: EnvironmentKey { static let defaultValue = false }
|
|
41
47
|
|
|
48
|
+
/// `.scrollEdgeEffectStyle` / `.scrollEdgeEffectHidden` above a scroll view,
|
|
49
|
+
/// for its top and bottom edges.
|
|
50
|
+
struct _ScrollEdgeEffects: Hashable, Sendable {
|
|
51
|
+
/// `"hard"` or `"soft"`; nil is automatic.
|
|
52
|
+
var topStyle: String?
|
|
53
|
+
var bottomStyle: String?
|
|
54
|
+
var topHidden = false
|
|
55
|
+
var bottomHidden = false
|
|
56
|
+
|
|
57
|
+
/// The `edgeEffect` prop of a list or scroll view, nil when both edges are automatic.
|
|
58
|
+
var props: _JSON? {
|
|
59
|
+
var edges: [String: _JSON] = [:]
|
|
60
|
+
if let top = topHidden ? "hidden" : topStyle { edges["top"] = .string(top) }
|
|
61
|
+
if let bottom = bottomHidden ? "hidden" : bottomStyle { edges["bottom"] = .string(bottom) }
|
|
62
|
+
return edges.isEmpty ? nil : .object(edges)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
enum _ScrollEdgeEffectsKey: EnvironmentKey { static let defaultValue = _ScrollEdgeEffects() }
|
|
67
|
+
|
|
42
68
|
extension EnvironmentValues {
|
|
43
69
|
/// `.scrollDismissesKeyboard(.immediately | .interactively)` above a list or scroll view.
|
|
44
70
|
var _scrollDismissesKeyboard: Bool {
|
|
45
71
|
get { self[_ScrollDismissesKeyboardKey.self] }
|
|
46
72
|
set { self[_ScrollDismissesKeyboardKey.self] = newValue }
|
|
47
73
|
}
|
|
74
|
+
|
|
75
|
+
/// `.scrollEdgeEffectStyle` / `.scrollEdgeEffectHidden` above a list or scroll view.
|
|
76
|
+
var _scrollEdgeEffects: _ScrollEdgeEffects {
|
|
77
|
+
get { self[_ScrollEdgeEffectsKey.self] }
|
|
78
|
+
set { self[_ScrollEdgeEffectsKey.self] = newValue }
|
|
79
|
+
}
|
|
48
80
|
}
|
|
49
81
|
|
|
50
82
|
nonisolated extension View {
|
|
@@ -56,9 +88,23 @@ nonisolated extension View {
|
|
|
56
88
|
self
|
|
57
89
|
}
|
|
58
90
|
|
|
59
|
-
|
|
91
|
+
/// The scroll edge effect of the scroll views below on `edges` (top and
|
|
92
|
+
/// bottom): `.hard`, `.soft`, or `.automatic` / nil (soft on iOS).
|
|
93
|
+
public func scrollEdgeEffectStyle(_ style: ScrollEdgeEffectStyle?, for edges: Edge.Set) -> some View {
|
|
94
|
+
transformEnvironment(\._scrollEdgeEffects) { effects in
|
|
95
|
+
let name = style.flatMap { $0 == .automatic ? nil : $0.name }
|
|
96
|
+
if edges.contains(.top) { effects.topStyle = name }
|
|
97
|
+
if edges.contains(.bottom) { effects.bottomStyle = name }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
60
100
|
|
|
61
|
-
|
|
101
|
+
/// Hides (or shows again) the scroll edge effect of the scroll views below on `edges`.
|
|
102
|
+
public func scrollEdgeEffectHidden(_ hidden: Bool = true, for edges: Edge.Set = .all) -> some View {
|
|
103
|
+
transformEnvironment(\._scrollEdgeEffects) { effects in
|
|
104
|
+
if edges.contains(.top) { effects.topHidden = hidden }
|
|
105
|
+
if edges.contains(.bottom) { effects.bottomHidden = hidden }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
62
108
|
|
|
63
109
|
public func safeAreaInset<V: View>(edge: VerticalEdge, alignment: HorizontalAlignment = .center,
|
|
64
110
|
spacing: CGFloat? = nil, @ViewBuilder content: () -> V) -> some View {
|
|
@@ -26,11 +26,22 @@ extension AnyTransition {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/// Space between toolbar items (iOS 26), which separates Liquid Glass
|
|
29
|
-
/// groups
|
|
30
|
-
///
|
|
29
|
+
/// groups: an empty `toolbar` element of its placement with `"spacer":
|
|
30
|
+
/// "flexible"` or `"fixed"`. In a bottom bar flexible spacers share the room
|
|
31
|
+
/// left over and push the groups apart (Toolbar.swift); in the navigation
|
|
32
|
+
/// bar they only separate groups.
|
|
31
33
|
nonisolated public struct ToolbarSpacer: ToolbarContent {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
let sizing: SpacerSizing
|
|
35
|
+
let placement: ToolbarItemPlacement
|
|
36
|
+
|
|
37
|
+
public init(_ sizing: SpacerSizing = .flexible, placement: ToolbarItemPlacement = .automatic) {
|
|
38
|
+
self.sizing = sizing
|
|
39
|
+
self.placement = placement
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
nonisolated public var _toolbarItems: [any View] {
|
|
43
|
+
[_ToolbarGroupView(placement: placement, items: [], spacer: sizing.name)]
|
|
44
|
+
}
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
/// How much room a `ToolbarSpacer` takes.
|
|
@@ -33,15 +33,21 @@ public protocol ToolbarContent {
|
|
|
33
33
|
nonisolated var _toolbarItems: [any View] { get }
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/// A `toolbar` element: the items of one placement
|
|
36
|
+
/// A `toolbar` element: the items of one placement, or a `ToolbarSpacer`
|
|
37
|
+
/// (`spacer`: `"flexible"` or `"fixed"`, no items).
|
|
37
38
|
struct _ToolbarGroupView {
|
|
38
39
|
let placement: ToolbarItemPlacement
|
|
39
40
|
let items: [any View]
|
|
41
|
+
var spacer: String? = nil
|
|
40
42
|
}
|
|
41
43
|
extension _ToolbarGroupView: _PrimitiveView {}
|
|
42
44
|
extension _ToolbarGroupView: _HostView {
|
|
43
45
|
var _kind: String { "toolbar" }
|
|
44
|
-
func _props(in environment: EnvironmentValues) -> _JSON {
|
|
46
|
+
func _props(in environment: EnvironmentValues) -> _JSON {
|
|
47
|
+
var props: [String: _JSON] = ["placement": .string(placement.name)]
|
|
48
|
+
if let spacer { props["spacer"] = .string(spacer) }
|
|
49
|
+
return .object(props)
|
|
50
|
+
}
|
|
45
51
|
var _content: [any View] { items }
|
|
46
52
|
}
|
|
47
53
|
extension _ToolbarGroupView: _EnvironmentModifying {
|
|
@@ -4,9 +4,9 @@ import Foundation
|
|
|
4
4
|
@_exported import CoreTransferable
|
|
5
5
|
|
|
6
6
|
// SwiftUI's side of `Transferable`: `Image` as a transferable value,
|
|
7
|
-
// `ShareLink`'s export of other items,
|
|
8
|
-
// modifiers
|
|
9
|
-
//
|
|
7
|
+
// `ShareLink`'s export of other items (docs/ops-protocol.md, "Sharing and
|
|
8
|
+
// media"), the drag-and-drop modifiers ("Drag and drop") and the clipboard
|
|
9
|
+
// modifiers that take `Transferable` payloads.
|
|
10
10
|
|
|
11
11
|
// MARK: - Image
|
|
12
12
|
|
|
@@ -112,26 +112,234 @@ enum _ShareExport {
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
// MARK: - Drag and drop
|
|
115
|
+
// MARK: - Drag and drop
|
|
116
116
|
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
117
|
+
// A `draggable` element wraps a view the page can lift (a long press on a
|
|
118
|
+
// touch screen, a drag with a mouse) and carry over the screen; a
|
|
119
|
+
// `dropdestination` element hears `drop` events with the phase `entered`,
|
|
120
|
+
// `exited` or `performed`. What travels stays in Swift: the source's payload
|
|
121
|
+
// is kept by `_DragSession` while the drag lasts (from `dragstart` to
|
|
122
|
+
// `dragend`), and a drop names the source element. Text, links and files
|
|
123
|
+
// dragged in from outside the page arrive as `items`, which become
|
|
124
|
+
// `_TransferContent`s the destination's type imports
|
|
125
|
+
// (docs/ops-protocol.md, "Drag and drop").
|
|
126
|
+
|
|
127
|
+
/// The drag in progress: the `draggable` element it started on and the
|
|
128
|
+
/// payload its closure made when it began. Nil between drags.
|
|
129
|
+
@MainActor
|
|
130
|
+
public enum _DragSession {
|
|
131
|
+
public static var current: (source: Int, payload: Any)?
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// `.draggable(_:)` / `onDrag`: the content under a `draggable` element.
|
|
135
|
+
/// `payload` runs when the drag begins, as `onDrag`'s provider closure does.
|
|
136
|
+
public struct _DraggableView<Content: View> {
|
|
137
|
+
let content: Content
|
|
138
|
+
let payload: @MainActor () -> Any
|
|
139
|
+
|
|
140
|
+
public init(content: Content, payload: @escaping @MainActor () -> Any) {
|
|
141
|
+
self.content = content
|
|
142
|
+
self.payload = payload
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
extension _DraggableView: _PrimitiveView {}
|
|
146
|
+
extension _DraggableView: _HostView {
|
|
147
|
+
var _kind: String { "draggable" }
|
|
148
|
+
func _props(in environment: EnvironmentValues) -> _JSON { .object([:]) }
|
|
149
|
+
var _content: [any View] { [content] }
|
|
150
|
+
var _handler: ((_JSON) -> Void)? {
|
|
151
|
+
let payload = self.payload
|
|
152
|
+
return { event in
|
|
153
|
+
guard case .object(let fields) = event, case .string(let type)? = fields["type"],
|
|
154
|
+
case .number(let id)? = fields["id"] else { return }
|
|
155
|
+
MainActor.assumeIsolated {
|
|
156
|
+
switch type {
|
|
157
|
+
case "dragstart":
|
|
158
|
+
_DragSession.current = (source: Int(id), payload: payload())
|
|
159
|
+
case "dragend":
|
|
160
|
+
if _DragSession.current?.source == Int(id) { _DragSession.current = nil }
|
|
161
|
+
default:
|
|
162
|
+
break
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// What a drop (or a drag passing over a destination) offers.
|
|
170
|
+
public struct _DropItems {
|
|
171
|
+
/// Where in the destination's own coordinates (zero while entering and leaving).
|
|
172
|
+
public let location: CGPoint
|
|
173
|
+
/// The payload of the `draggable` / `onDrag` source the drag started on,
|
|
174
|
+
/// when it started in the app; nil for a drag from outside the page.
|
|
175
|
+
public let source: Any?
|
|
176
|
+
/// What came from outside the page: text as UTF-8 plain text, a link as
|
|
177
|
+
/// `public.url`, a file in its type, named after the file.
|
|
178
|
+
public let contents: [_TransferContent]
|
|
179
|
+
|
|
180
|
+
public init(location: CGPoint, source: Any?, contents: [_TransferContent]) {
|
|
181
|
+
self.location = location
|
|
182
|
+
self.source = source
|
|
183
|
+
self.contents = contents
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// The source payload as `T`, else the source exported into a type `T`
|
|
187
|
+
/// imports, then every outside content `T` imports; in that order.
|
|
188
|
+
@MainActor
|
|
189
|
+
public func values<T: Transferable>(of type: T.Type) async -> [T] {
|
|
190
|
+
var values: [T] = []
|
|
191
|
+
if let source {
|
|
192
|
+
if let value = source as? T {
|
|
193
|
+
values.append(value)
|
|
194
|
+
} else if let item = source as? any Transferable,
|
|
195
|
+
let content = try? await item._export(as: _Transfer.importedContentTypes(T.self)),
|
|
196
|
+
let value = try? await _Transfer.import(T.self, from: content) {
|
|
197
|
+
values.append(value)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
for content in contents {
|
|
201
|
+
if let value = try? await _Transfer.import(T.self, from: content) { values.append(value) }
|
|
202
|
+
}
|
|
203
|
+
return values
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/// The `items` of a `drop` event as transfer contents: `{type, text}`
|
|
207
|
+
/// for text and links, `{type, name, data}` (base64) for files.
|
|
208
|
+
static func contents(of items: _JSON?) -> [_TransferContent] {
|
|
209
|
+
guard case .array(let list)? = items else { return [] }
|
|
210
|
+
return list.compactMap { item -> _TransferContent? in
|
|
211
|
+
guard case .object(let fields) = item else { return nil }
|
|
212
|
+
let mime: String = if case .string(let type)? = fields["type"] { type } else { "" }
|
|
213
|
+
if case .string(let text)? = fields["text"] {
|
|
214
|
+
let isLink = mime == "text/uri-list"
|
|
215
|
+
return _TransferContent(contentType: isLink ? .url : .utf8PlainText, data: Data(text.utf8))
|
|
216
|
+
}
|
|
217
|
+
guard case .string(let encoded)? = fields["data"], let data = Data(base64Encoded: encoded) else { return nil }
|
|
218
|
+
let name: String? = if case .string(let name)? = fields["name"] { name } else { nil }
|
|
219
|
+
let type = UTType(mimeType: mime) ?? name.flatMap { UTType(filenameExtension: ($0 as NSString).pathExtension) } ?? .data
|
|
220
|
+
return _TransferContent(contentType: type, data: data, suggestedName: name.map { ($0 as NSString).deletingPathExtension })
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
extension Transferable {
|
|
226
|
+
/// `self` exported in the first of `types` it exports; nil if none.
|
|
227
|
+
nonisolated(nonsending) func _export(as types: [UTType]) async throws -> _TransferContent? {
|
|
228
|
+
for type in types {
|
|
229
|
+
if let content = try await _Transfer.export(self, as: type) { return content }
|
|
230
|
+
}
|
|
231
|
+
return nil
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/// The MIME patterns (`text/plain`, `image/*`, `*/*`) of what dragged in from
|
|
236
|
+
/// outside the page a destination accepting `types` takes: the page matches
|
|
237
|
+
/// them against the drag's types. A type's own MIME type (`application/json`)
|
|
238
|
+
/// when it declares one; any text for a text type, any image for `image`,
|
|
239
|
+
/// anything for `data`, `item`, `content` and files. An app's own type that
|
|
240
|
+
/// no file or text carries matches nothing.
|
|
241
|
+
public func _dropAccepts(_ types: [UTType]) -> [String] {
|
|
242
|
+
var out: [String] = []
|
|
243
|
+
for type in types {
|
|
244
|
+
let pattern: String? =
|
|
245
|
+
if type == .data || type == .item || type == .content || type == .fileURL { "*/*" }
|
|
246
|
+
else if let mime = type.preferredMIMEType { mime }
|
|
247
|
+
else if type.conforms(to: .text) { "text/plain" }
|
|
248
|
+
else if type.conforms(to: .url) { "text/uri-list" }
|
|
249
|
+
else if type.conforms(to: .image) { "image/*" }
|
|
250
|
+
else { nil }
|
|
251
|
+
if let pattern, !out.contains(pattern) { out.append(pattern) }
|
|
252
|
+
}
|
|
253
|
+
return out
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// `.dropDestination` / `onDrop`: the content under a `dropdestination`
|
|
257
|
+
/// element. `targeted` is told when a drag enters and leaves, `perform` when
|
|
258
|
+
/// one drops; both receive what is offered.
|
|
259
|
+
public struct _DropDestinationView<Content: View> {
|
|
260
|
+
let content: Content
|
|
261
|
+
let accepts: [String]
|
|
262
|
+
let targeted: @MainActor (Bool, _DropItems) -> Void
|
|
263
|
+
let perform: @MainActor (_DropItems) -> Bool
|
|
264
|
+
|
|
265
|
+
public init(content: Content, accepts: [String],
|
|
266
|
+
targeted: @escaping @MainActor (Bool, _DropItems) -> Void,
|
|
267
|
+
perform: @escaping @MainActor (_DropItems) -> Bool) {
|
|
268
|
+
self.content = content
|
|
269
|
+
self.accepts = accepts
|
|
270
|
+
self.targeted = targeted
|
|
271
|
+
self.perform = perform
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
extension _DropDestinationView: _PrimitiveView {}
|
|
275
|
+
extension _DropDestinationView: _HostView {
|
|
276
|
+
var _kind: String { "dropdestination" }
|
|
277
|
+
func _props(in environment: EnvironmentValues) -> _JSON {
|
|
278
|
+
.object(["accepts": .array(accepts.map { .string($0) })])
|
|
279
|
+
}
|
|
280
|
+
var _content: [any View] { [content] }
|
|
281
|
+
var _handler: ((_JSON) -> Void)? {
|
|
282
|
+
let targeted = self.targeted
|
|
283
|
+
let perform = self.perform
|
|
284
|
+
return { event in
|
|
285
|
+
guard case .object(let fields) = event, case .string("drop")? = fields["type"],
|
|
286
|
+
case .string(let phase)? = fields["phase"] else { return }
|
|
287
|
+
var location = CGPoint.zero
|
|
288
|
+
if case .number(let x)? = fields["x"], case .number(let y)? = fields["y"] { location = CGPoint(x: x, y: y) }
|
|
289
|
+
MainActor.assumeIsolated {
|
|
290
|
+
var source: Any?
|
|
291
|
+
if case .number(let id)? = fields["source"], let current = _DragSession.current, current.source == Int(id) {
|
|
292
|
+
source = current.payload
|
|
293
|
+
}
|
|
294
|
+
let items = _DropItems(location: location, source: source, contents: _DropItems.contents(of: fields["items"]))
|
|
295
|
+
switch phase {
|
|
296
|
+
case "entered": targeted(true, items)
|
|
297
|
+
case "exited": targeted(false, items)
|
|
298
|
+
case "performed":
|
|
299
|
+
targeted(false, items)
|
|
300
|
+
_ = perform(items)
|
|
301
|
+
default: break
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
121
307
|
|
|
122
308
|
extension View {
|
|
123
|
-
///
|
|
124
|
-
|
|
309
|
+
/// The view can be dragged: a long press lifts it on a touch screen, a
|
|
310
|
+
/// drag does with a mouse. `payload` is made when the drag begins and
|
|
311
|
+
/// reaches the `dropDestination` (or `onDrop`) it is dropped on.
|
|
312
|
+
public func draggable<T: Transferable>(_ payload: @autoclosure @escaping () -> T) -> some View {
|
|
313
|
+
_DraggableView(content: self) { payload() }
|
|
314
|
+
}
|
|
125
315
|
|
|
126
|
-
///
|
|
316
|
+
/// The page drags a copy of the view itself; `preview` is accepted and not drawn.
|
|
127
317
|
public func draggable<T: Transferable, Preview: View>(_ payload: @autoclosure @escaping () -> T,
|
|
128
|
-
@ViewBuilder preview: () -> Preview) -> some View {
|
|
318
|
+
@ViewBuilder preview: () -> Preview) -> some View {
|
|
319
|
+
_DraggableView(content: self) { payload() }
|
|
320
|
+
}
|
|
129
321
|
|
|
130
|
-
///
|
|
131
|
-
///
|
|
322
|
+
/// The view takes drops of `T`: a dragged view's payload when it is a `T`
|
|
323
|
+
/// (or exports a type `T` imports), and text, links or files dragged in
|
|
324
|
+
/// from outside the page that `T` imports. `isTargeted` is told while a
|
|
325
|
+
/// drag is over the view; `action` gets the values and the drop's point
|
|
326
|
+
/// in the view's coordinates. Values that import asynchronously reach
|
|
327
|
+
/// `action` after the drop.
|
|
132
328
|
public func dropDestination<T: Transferable>(for payloadType: T.Type = T.self,
|
|
133
329
|
action: @escaping ([T], CGPoint) -> Bool,
|
|
134
|
-
isTargeted: @escaping (Bool) -> Void = { _ in }) -> some View {
|
|
330
|
+
isTargeted: @escaping (Bool) -> Void = { _ in }) -> some View {
|
|
331
|
+
_DropDestinationView(content: self, accepts: _dropAccepts(_Transfer.importedContentTypes(T.self)),
|
|
332
|
+
targeted: { targeted, _ in isTargeted(targeted) }) { items in
|
|
333
|
+
if let value = items.source as? T, items.contents.isEmpty { return action([value], items.location) }
|
|
334
|
+
let boxed = _UncheckedSendableBox((items, action))
|
|
335
|
+
Task { @MainActor in
|
|
336
|
+
let (items, action) = boxed.value
|
|
337
|
+
let values = await items.values(of: T.self)
|
|
338
|
+
if !values.isEmpty { _ = action(values, items.location) }
|
|
339
|
+
}
|
|
340
|
+
return true
|
|
341
|
+
}
|
|
342
|
+
}
|
|
135
343
|
|
|
136
344
|
/// Accepted; the browser's copy command copies the page's selected text.
|
|
137
345
|
public func copyable<T: Transferable>(_ payload: @autoclosure @escaping () -> [T]) -> some View { self }
|
|
@@ -27,16 +27,22 @@ struct _Element {
|
|
|
27
27
|
|
|
28
28
|
/// Row metadata the runtime attaches while producing elements and consumes
|
|
29
29
|
/// when it emits a `list`: a `.tag` (or a `ForEach` id, the implicit tag), a
|
|
30
|
-
/// `NavigationLink(value:)`'s value and handler, and the `onDelete`
|
|
31
|
-
/// row's `ForEach`.
|
|
30
|
+
/// `NavigationLink(value:)`'s value and handler, and the `onDelete` /
|
|
31
|
+
/// `onMove` of the row's `ForEach`.
|
|
32
32
|
struct _RowMark {
|
|
33
33
|
var tag: AnyHashable?
|
|
34
34
|
var linkValue: AnyHashable?
|
|
35
35
|
var linkHandler: ((_JSON) -> Void)?
|
|
36
36
|
var delete: (() -> Void)?
|
|
37
37
|
var deleteDisabled = false
|
|
38
|
+
/// The `ForEach`'s `onMove`: (the row's offset, the destination offset).
|
|
39
|
+
var move: ((Int, Int) -> Void)?
|
|
40
|
+
/// The `ForEach` node the row belongs to, and the row's offset in its data.
|
|
41
|
+
var moveGroup: ObjectIdentifier?
|
|
42
|
+
var moveOffset = 0
|
|
43
|
+
var moveDisabled = false
|
|
38
44
|
|
|
39
|
-
var isEmpty: Bool { tag == nil && linkValue == nil && delete == nil && !deleteDisabled }
|
|
45
|
+
var isEmpty: Bool { tag == nil && linkValue == nil && delete == nil && !deleteDisabled && move == nil && !moveDisabled }
|
|
40
46
|
|
|
41
47
|
/// Fills what `self` lacks from an inner element's mark.
|
|
42
48
|
func merged(with inner: _RowMark) -> _RowMark {
|
|
@@ -48,6 +54,12 @@ struct _RowMark {
|
|
|
48
54
|
}
|
|
49
55
|
if result.delete == nil { result.delete = inner.delete }
|
|
50
56
|
result.deleteDisabled = result.deleteDisabled || inner.deleteDisabled
|
|
57
|
+
if result.move == nil {
|
|
58
|
+
result.move = inner.move
|
|
59
|
+
result.moveGroup = inner.moveGroup
|
|
60
|
+
result.moveOffset = inner.moveOffset
|
|
61
|
+
}
|
|
62
|
+
result.moveDisabled = result.moveDisabled || inner.moveDisabled
|
|
51
63
|
return result
|
|
52
64
|
}
|
|
53
65
|
}
|
|
@@ -72,7 +72,7 @@ extension String {
|
|
|
72
72
|
/// The event types whose first field is a target.
|
|
73
73
|
private let _targetedEvents: Set<String> = [
|
|
74
74
|
"tap", "text", "toggle", "select", "step", "slide", "date", "geometry",
|
|
75
|
-
"drag", "longpress", "delete", "refresh",
|
|
75
|
+
"drag", "longpress", "delete", "move", "drop", "refresh",
|
|
76
76
|
]
|
|
77
77
|
|
|
78
78
|
/// The element kinds a selector picks for each event type, unless it names
|
|
@@ -84,6 +84,9 @@ private let _targetKinds: [String: Set<String>] = [
|
|
|
84
84
|
"longpress": ["gesture"],
|
|
85
85
|
"drag": ["gesture"],
|
|
86
86
|
"delete": ["listrow"],
|
|
87
|
+
"move": ["listrow"],
|
|
88
|
+
"drop": ["dropdestination"],
|
|
89
|
+
"dragstart": ["draggable"],
|
|
87
90
|
"text": ["textfield", "searchfield", "texteditor"],
|
|
88
91
|
"toggle": ["toggle"],
|
|
89
92
|
"select": ["picker", "tabview"],
|
|
@@ -350,6 +353,18 @@ extension _Reconciler {
|
|
|
350
353
|
return find(in: roots)
|
|
351
354
|
}
|
|
352
355
|
|
|
356
|
+
/// The index of the element with this id among its parent's children, if it is on the tree.
|
|
357
|
+
func containerIndex(of id: Int) -> Int? {
|
|
358
|
+
func find(in elements: [Rendered]) -> Int? {
|
|
359
|
+
if let index = elements.firstIndex(where: { $0.id == id }) { return index }
|
|
360
|
+
for element in elements {
|
|
361
|
+
if let found = find(in: element.children) { return found }
|
|
362
|
+
}
|
|
363
|
+
return nil
|
|
364
|
+
}
|
|
365
|
+
return find(in: roots)
|
|
366
|
+
}
|
|
367
|
+
|
|
353
368
|
/// `back`: the visible screen's back button.
|
|
354
369
|
func backTarget() throws(_ScriptError) -> Rendered {
|
|
355
370
|
try resolve(_Selector(kind: "navscreen"), for: "tap")
|
|
@@ -250,7 +250,7 @@ public final class _Runtime {
|
|
|
250
250
|
private var measuringRoot: _MountedNode?
|
|
251
251
|
/// Set when measurements arrive: the next render re-runs every body, none skipped as unchanged.
|
|
252
252
|
private var renderEverythingPass = false
|
|
253
|
-
|
|
253
|
+
let reconciler = _Reconciler()
|
|
254
254
|
private var needsRender = false
|
|
255
255
|
/// The next render re-runs every body (environment changes, `invalidate()`).
|
|
256
256
|
private var fullRender = true
|
|
@@ -644,11 +644,14 @@ public final class _Runtime {
|
|
|
644
644
|
// Host measurement of a custom `Layout`'s children; the handler
|
|
645
645
|
// parses the whole event (see CustomLayout.swift).
|
|
646
646
|
if let id { reconciler.handlers[id]?(event) }
|
|
647
|
-
case "drag", "longpress", "delete", "refresh", "focus", "submit", "video", "lazy", "menu"
|
|
647
|
+
case "drag", "longpress", "delete", "move", "refresh", "focus", "submit", "video", "lazy", "menu",
|
|
648
|
+
"dragstart", "dragend", "drop":
|
|
648
649
|
// Phase 8: the handler receives the whole event and reads `type`
|
|
649
|
-
// (a `listrow` handles
|
|
650
|
+
// (a `listrow` handles `tap`, `delete` and `move`; Phase 12: a
|
|
650
651
|
// `textfield` handles `text`, `focus` and `submit`; a `video`
|
|
651
|
-
// reads its `event`, docs/ops-protocol.md, "Video and audio"
|
|
652
|
+
// reads its `event`, docs/ops-protocol.md, "Video and audio"; a
|
|
653
|
+
// `draggable` hears `dragstart` / `dragend` and a
|
|
654
|
+
// `dropdestination` `drop`, "Drag and drop").
|
|
652
655
|
if let id { reconciler.handlers[id]?(event) }
|
|
653
656
|
case "response":
|
|
654
657
|
// Phase 13: the host's answer to a `request` op.
|
|
@@ -997,6 +1000,14 @@ public final class _Runtime {
|
|
|
997
1000
|
if !clean { node.splitSelection = nil }
|
|
998
1001
|
let sidebar = _SplitSidebarView(content: split._sidebar, split: node)
|
|
999
1002
|
if context.environment.horizontalSizeClass == .regular {
|
|
1003
|
+
// The sidebar toggle (a `tap` on the split view) shows or
|
|
1004
|
+
// hides the sidebar, sliding the columns.
|
|
1005
|
+
let visibility = split._columnVisibility
|
|
1006
|
+
node.handler = { _ in
|
|
1007
|
+
withAnimation(.default) {
|
|
1008
|
+
visibility.wrappedValue = visibility.wrappedValue._showsSidebar ? .detailOnly : .all
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1000
1011
|
// Two columns, each its own stack; they are not nested in a
|
|
1001
1012
|
// screen even when the split view is, so they are not flattened.
|
|
1002
1013
|
context.navStack = nil
|
|
@@ -1585,11 +1596,12 @@ public final class _Runtime {
|
|
|
1585
1596
|
// `navstack` whose screens are the sidebar, the detail and what
|
|
1586
1597
|
// the detail pushed.
|
|
1587
1598
|
let regular = node.environment.horizontalSizeClass == .regular
|
|
1599
|
+
let shows = (node.view as? any _NavigationSplitHost)?._columnVisibility.wrappedValue._showsSidebar ?? true
|
|
1588
1600
|
produced = [_Element(
|
|
1589
1601
|
kind: regular ? "navsplit" : "navstack",
|
|
1590
|
-
props: .object([:]),
|
|
1602
|
+
props: regular ? .object(["sidebarVisible": .bool(shows)]) : .object([:]),
|
|
1591
1603
|
children: node.children.flatMap(elements(of:)),
|
|
1592
|
-
handler: nil
|
|
1604
|
+
handler: regular ? node.handler : nil
|
|
1593
1605
|
)]
|
|
1594
1606
|
} else if traits.contains(.navigationStack), node.navFlattened {
|
|
1595
1607
|
produced = node.children.flatMap(elements(of:))
|
|
@@ -1681,9 +1693,11 @@ public final class _Runtime {
|
|
|
1681
1693
|
produced[index].row = (produced[index].row ?? _RowMark()).merged(with: _RowMark(tag: tag))
|
|
1682
1694
|
}
|
|
1683
1695
|
}
|
|
1684
|
-
if traits.contains(.rowEditingDisabling), let disabling = node.view as? any _RowEditingDisabling,
|
|
1696
|
+
if traits.contains(.rowEditingDisabling), let disabling = node.view as? any _RowEditingDisabling,
|
|
1697
|
+
disabling._deleteDisabled || disabling._moveDisabled {
|
|
1698
|
+
let mark = _RowMark(deleteDisabled: disabling._deleteDisabled, moveDisabled: disabling._moveDisabled)
|
|
1685
1699
|
for index in produced.indices {
|
|
1686
|
-
produced[index].row = (produced[index].row ?? _RowMark()).merged(with:
|
|
1700
|
+
produced[index].row = (produced[index].row ?? _RowMark()).merged(with: mark)
|
|
1687
1701
|
}
|
|
1688
1702
|
}
|
|
1689
1703
|
if traits.contains(.layoutValueProviding), let providing = node.view as? any _LayoutValueProviding {
|
|
@@ -1722,11 +1736,15 @@ public final class _Runtime {
|
|
|
1722
1736
|
/// the siblings. Without that the element diff matched the two groups'
|
|
1723
1737
|
/// elements to each other on alternate passes, swapping their props.
|
|
1724
1738
|
private func childElements(of node: _MountedNode) -> [_Element] {
|
|
1725
|
-
let
|
|
1739
|
+
let editable = _ViewTraits.of(node.view).contains(.deletableGroup) ? node.view as? any _DeletableGroup : nil
|
|
1740
|
+
let delete = editable?._delete
|
|
1741
|
+
let move = editable?._move
|
|
1742
|
+
let group = ObjectIdentifier(node)
|
|
1726
1743
|
return node.children.enumerated().flatMap { position, child -> [_Element] in
|
|
1727
1744
|
var items = elements(of: child)
|
|
1728
|
-
// A `ForEach` id is the row's implicit tag; an `onDelete`
|
|
1729
|
-
// child at `position` is the row at that offset
|
|
1745
|
+
// A `ForEach` id is the row's implicit tag; an `onDelete` /
|
|
1746
|
+
// `onMove` group's child at `position` is the row at that offset
|
|
1747
|
+
// of its data.
|
|
1730
1748
|
if let key = child.key {
|
|
1731
1749
|
for index in items.indices where items[index].row?.tag == nil {
|
|
1732
1750
|
items[index].row = (items[index].row ?? _RowMark()).merged(with: _RowMark(tag: key))
|
|
@@ -1737,6 +1755,11 @@ public final class _Runtime {
|
|
|
1737
1755
|
items[index].row = (items[index].row ?? _RowMark()).merged(with: _RowMark(delete: { delete(position) }))
|
|
1738
1756
|
}
|
|
1739
1757
|
}
|
|
1758
|
+
if let move {
|
|
1759
|
+
for index in items.indices where items[index].row?.move == nil {
|
|
1760
|
+
items[index].row = (items[index].row ?? _RowMark()).merged(with: _RowMark(move: move, moveGroup: group, moveOffset: position))
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1740
1763
|
if child.key == nil, _ViewTraits.of(child.view).contains(.group), let group = child.view as? any _GroupView, group._keys != nil {
|
|
1741
1764
|
for index in items.indices {
|
|
1742
1765
|
if let key = items[index].key {
|
|
@@ -1748,15 +1771,19 @@ public final class _Runtime {
|
|
|
1748
1771
|
}
|
|
1749
1772
|
}
|
|
1750
1773
|
|
|
1751
|
-
/// Wraps a `list`'s rows (also those of its `section`s) that can be selected
|
|
1752
|
-
/// or
|
|
1774
|
+
/// Wraps a `list`'s rows (also those of its `section`s) that can be selected,
|
|
1775
|
+
/// deleted or moved in `listrow` elements (docs/ops-protocol.md, Phase 8). A row
|
|
1753
1776
|
/// is selectable when the list has a selection and the row's `.tag`,
|
|
1754
1777
|
/// `ForEach` id or `NavigationLink(value:)` is of the selection's type;
|
|
1755
1778
|
/// deletable when its `ForEach` has `onDelete` and the row is not
|
|
1756
|
-
/// `deleteDisabled
|
|
1779
|
+
/// `deleteDisabled`; movable when its `ForEach` has `onMove` and the row
|
|
1780
|
+
/// is not `moveDisabled`. The row's key moves to the wrapper.
|
|
1757
1781
|
private func wrapListRows(_ elements: [_Element], in node: _MountedNode) -> [_Element] {
|
|
1758
1782
|
let selection = node.environment._listSelection
|
|
1759
1783
|
let editMode = node.environment.editMode
|
|
1784
|
+
// Which `ForEach` each child belongs to, by position: a `move` event's
|
|
1785
|
+
// destination counts the children of the moved row's group before it.
|
|
1786
|
+
let groups = elements.map { $0.row?.moveGroup }
|
|
1760
1787
|
return elements.map { element in
|
|
1761
1788
|
if element.kind == "section" {
|
|
1762
1789
|
var section = element
|
|
@@ -1767,7 +1794,8 @@ public final class _Runtime {
|
|
|
1767
1794
|
let value = mark.linkValue ?? mark.tag
|
|
1768
1795
|
let selectable: Bool = if let selection, let value { selection.accepts(value) } else { false }
|
|
1769
1796
|
let deletable = mark.delete != nil && !mark.deleteDisabled
|
|
1770
|
-
|
|
1797
|
+
let movable = mark.move != nil && !mark.moveDisabled
|
|
1798
|
+
guard selectable || deletable || movable else { return element }
|
|
1771
1799
|
let selected = selectable && selection!.contains(value!)
|
|
1772
1800
|
|
|
1773
1801
|
var inner = element
|
|
@@ -1778,6 +1806,18 @@ public final class _Runtime {
|
|
|
1778
1806
|
if deletable { mark.delete?() }
|
|
1779
1807
|
return
|
|
1780
1808
|
}
|
|
1809
|
+
if case .object(let fields) = payload, case .string("move")? = fields["type"] {
|
|
1810
|
+
// `to`: the index among the container's children before
|
|
1811
|
+
// which the row goes, counted before it leaves (as
|
|
1812
|
+
// `move(fromOffsets:toOffset:)` takes its destination).
|
|
1813
|
+
// In the group's data that is the number of the group's
|
|
1814
|
+
// rows before that index.
|
|
1815
|
+
guard movable, let move = mark.move, case .number(let to)? = fields["to"] else { return }
|
|
1816
|
+
let bound = Swift.max(0, Swift.min(Int(to), groups.count))
|
|
1817
|
+
let destination = groups[0..<bound].filter { $0 == mark.moveGroup }.count
|
|
1818
|
+
move(mark.moveOffset, destination)
|
|
1819
|
+
return
|
|
1820
|
+
}
|
|
1781
1821
|
guard selectable, let selection, let value else {
|
|
1782
1822
|
mark.linkHandler?(.null)
|
|
1783
1823
|
return
|
|
@@ -1792,13 +1832,15 @@ public final class _Runtime {
|
|
|
1792
1832
|
selection.set(value)
|
|
1793
1833
|
}
|
|
1794
1834
|
}
|
|
1835
|
+
var props: [String: _JSON] = [
|
|
1836
|
+
"selectable": .bool(selectable),
|
|
1837
|
+
"selected": .bool(selected),
|
|
1838
|
+
"deletable": .bool(deletable),
|
|
1839
|
+
]
|
|
1840
|
+
if movable { props["movable"] = .bool(true) }
|
|
1795
1841
|
return _Element(
|
|
1796
1842
|
kind: "listrow",
|
|
1797
|
-
props: .object(
|
|
1798
|
-
"selectable": .bool(selectable),
|
|
1799
|
-
"selected": .bool(selected),
|
|
1800
|
-
"deletable": .bool(deletable),
|
|
1801
|
-
]),
|
|
1843
|
+
props: .object(props),
|
|
1802
1844
|
children: [inner],
|
|
1803
1845
|
handler: handler,
|
|
1804
1846
|
key: element.key
|
|
@@ -1843,7 +1885,9 @@ public final class _Runtime {
|
|
|
1843
1885
|
/// `text:"Name":Milk`, `toggle:"Done":true`, `select:"Sort":"Title"`,
|
|
1844
1886
|
/// `env:dark`, `type:xxxLarge`, `size:regular`, `step:"8 hours":1`,
|
|
1845
1887
|
/// `slide:7:0.35`, `date:7:1700000000`, `drag:12:changed:40x-3`,
|
|
1846
|
-
/// `longpress:12`, `delete:"Coffee"`, `
|
|
1888
|
+
/// `longpress:12`, `delete:"Coffee"`, `move:"Coffee":2` /
|
|
1889
|
+
/// `move:"Coffee":"Lunch"`, `drop:"Trash":"Coffee"` / `drop:"Trash":Hello`,
|
|
1890
|
+
/// `refresh:40`, `back`, `dismiss`.
|
|
1847
1891
|
/// Each is echoed, with the id its target resolved to, before its ops.
|
|
1848
1892
|
/// Returns false, after printing an error op, at the first event that
|
|
1849
1893
|
/// cannot be parsed or whose selector does not name exactly one element.
|
|
@@ -1863,6 +1907,13 @@ public final class _Runtime {
|
|
|
1863
1907
|
print(_JSON.object(["op": .string("error"), "message": .string(error.message)]).serialized)
|
|
1864
1908
|
return false
|
|
1865
1909
|
}
|
|
1910
|
+
if event["type"] == .string("drop"), case .number(let source)? = event["source"] {
|
|
1911
|
+
dispatch(event: .object(["type": .string("dragstart"), "id": .number(source)]))
|
|
1912
|
+
host.apply([.event(.object(event))])
|
|
1913
|
+
dispatch(event: .object(event))
|
|
1914
|
+
dispatch(event: .object(["type": .string("dragend"), "id": .number(source)]))
|
|
1915
|
+
continue
|
|
1916
|
+
}
|
|
1866
1917
|
host.apply([.event(.object(event))])
|
|
1867
1918
|
dispatch(event: .object(event))
|
|
1868
1919
|
}
|
|
@@ -1938,6 +1989,30 @@ public final class _Runtime {
|
|
|
1938
1989
|
guard rest.count == 2 else { throw unrecognized }
|
|
1939
1990
|
event["phase"] = .string(rest[0])
|
|
1940
1991
|
event["translation"] = try size(rest[1])
|
|
1992
|
+
case "move":
|
|
1993
|
+
// move:"Coffee":2 (the index among the row's container's children
|
|
1994
|
+
// before which it goes) or move:"Coffee":"Lunch" (before that row).
|
|
1995
|
+
if parsed.valueQuoted, let text = value {
|
|
1996
|
+
let row = try reconciler.resolve(_Selector(text: text), for: "move")
|
|
1997
|
+
guard let index = reconciler.containerIndex(of: row.id) else { throw unrecognized }
|
|
1998
|
+
event["to"] = .number(Double(index))
|
|
1999
|
+
} else {
|
|
2000
|
+
event["to"] = try number(value)
|
|
2001
|
+
}
|
|
2002
|
+
case "drop":
|
|
2003
|
+
// drop:"Trash":"Coffee" (the draggable showing Coffee, dropped on
|
|
2004
|
+
// the destination showing Trash) or drop:"Trash":Hello (the text
|
|
2005
|
+
// "Hello" dropped from outside the page). A dragged source starts
|
|
2006
|
+
// and ends its drag around the drop (replayed as three events).
|
|
2007
|
+
event["phase"] = .string("performed")
|
|
2008
|
+
event["x"] = .number(0)
|
|
2009
|
+
event["y"] = .number(0)
|
|
2010
|
+
if parsed.valueQuoted, let text = value {
|
|
2011
|
+
let source = try reconciler.resolve(_Selector(text: text), for: "dragstart")
|
|
2012
|
+
event["source"] = .number(Double(source.id))
|
|
2013
|
+
} else {
|
|
2014
|
+
event["items"] = .array([.object(["type": .string("text/plain"), "text": .string(value ?? "")])])
|
|
2015
|
+
}
|
|
1941
2016
|
default:
|
|
1942
2017
|
throw unrecognized
|
|
1943
2018
|
}
|
package/Sources/UIKit/Font.swift
CHANGED
|
@@ -34,13 +34,14 @@ open class UIFont: NSObject, NSSecureCoding, @unchecked Sendable {
|
|
|
34
34
|
// MARK: Metrics (SF Pro's, per point)
|
|
35
35
|
|
|
36
36
|
public var familyName: String { design == .rounded ? ".AppleSystemUIFontRounded" : ".AppleSystemUIFont" }
|
|
37
|
-
public var ascender: CGFloat { pointSize *
|
|
38
|
-
public var descender: CGFloat { -pointSize *
|
|
37
|
+
public var ascender: CGFloat { pointSize * 1950 / 2048 }
|
|
38
|
+
public var descender: CGFloat { -pointSize * 494 / 2048 }
|
|
39
39
|
public var capHeight: CGFloat { pointSize * 0.705 }
|
|
40
40
|
public var xHeight: CGFloat { pointSize * 0.53 }
|
|
41
41
|
public var leading: CGFloat { 0 }
|
|
42
|
-
/// The
|
|
43
|
-
|
|
42
|
+
/// The ascender plus the descender, as on iOS (17 → 20.29); SwiftUI makes
|
|
43
|
+
/// a line of `Text` this tall, rounded up to a pixel (typography.ts).
|
|
44
|
+
public var lineHeight: CGFloat { ascender - descender }
|
|
44
45
|
|
|
45
46
|
public var fontDescriptor: UIFontDescriptor {
|
|
46
47
|
UIFontDescriptor(fontName: fontName, pointSize: pointSize, weight: weight, design: design)
|
|
@@ -128,8 +129,6 @@ open class UIFont: NSObject, NSSecureCoding, @unchecked Sendable {
|
|
|
128
129
|
super.init()
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
static let standardLineHeights: [CGFloat: CGFloat] = [34: 41, 28: 34, 22: 28, 20: 25, 17: 22, 16: 21, 15: 20, 13: 18, 12: 16, 11: 13]
|
|
132
|
-
|
|
133
132
|
// MARK: Nested types
|
|
134
133
|
|
|
135
134
|
public struct Weight: RawRepresentable, Hashable, Sendable {
|
|
@@ -159,7 +159,9 @@ enum _TextEstimate {
|
|
|
159
159
|
}
|
|
160
160
|
if maximumLines > 0 { lines = min(lines, maximumLines) }
|
|
161
161
|
let resolvedWidth = width.flatMap { $0 > 0 && $0.isFinite ? min($0, widest) : nil } ?? widest
|
|
162
|
-
|
|
162
|
+
// Whole 3x pixels, as iOS rounds a text's size up.
|
|
163
|
+
let height = (CGFloat(max(1, lines)) * font.lineHeight * 3).rounded(.up) / 3
|
|
164
|
+
return CGSize(width: resolvedWidth.rounded(.up), height: height)
|
|
163
165
|
}
|
|
164
166
|
}
|
|
165
167
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swiftbrowser/swift",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "The SwiftBrowser Swift package (the SwiftUI shim, Charts, SwiftData and FoundationBridge) as an npm package, so @swiftbrowser/cli builds apps against the exact version it ships with.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|