craft-native 0.0.436 → 0.0.442
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/LICENSE.md +30 -0
- package/README.md +35 -0
- package/dist/android/src/index.d.ts +8 -0
- package/dist/android/src/index.js +17 -2
- package/dist/android/templates/CraftHealthServices.kt.template +2 -0
- package/dist/android/templates/CraftNativeCapabilities.kt.template +42 -1
- package/dist/android/templates/CraftNativeMotion.kt.template +269 -0
- package/dist/android/templates/CraftNativeServices.kt.template +6 -1
- package/dist/android/templates/CraftPassiveSteps.kt.template +159 -0
- package/dist/android/templates/CraftRecordingPressure.kt.template +156 -0
- package/dist/android/templates/LocationRecordingService.kt.template +77 -9
- package/dist/android/templates/MainActivityNative.kt.template +129 -1
- package/dist/cli.js +289 -108
- package/dist/components/index.d.ts +25 -0
- package/dist/index.cjs +207 -27
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +321 -140
- package/dist/installer-background.d.ts +2 -0
- package/dist/installer-layout.d.ts +1 -0
- package/dist/ios/src/index.d.ts +10 -0
- package/dist/ios/src/index.js +28 -2
- package/dist/ios/templates/CraftApp.swift +14 -0
- package/dist/ios/templates/CraftHealth.swift +2 -1
- package/dist/ios/templates/CraftHealthBackground.swift +308 -0
- package/dist/ios/templates/CraftNativeActions.swift +153 -0
- package/dist/ios/templates/CraftNativePedometer.swift +181 -0
- package/dist/ios/templates/CraftNativeScreen.swift +76 -0
- package/dist/macos/src/index.js +1 -1
- package/dist/macos/src/toolchain.d.ts +1 -1
- package/dist/macos/templates/CraftMacApplication.swift +124 -39
- package/dist/macos/templates/CraftMacRenderer.swift +111 -33
- package/dist/macos/templates/CraftMacRuntime.swift +270 -81
- package/dist/package.d.ts +40 -0
- package/dist/types.d.ts +26 -0
- package/package.json +1 -1
|
@@ -21,6 +21,22 @@ enum CraftMacColor {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/// SF Symbols, one image per name and description. An image keeps what it rasterized, so views that
|
|
25
|
+
/// share one draw it once rather than each looking the glyph up in the catalog and rendering it again.
|
|
26
|
+
enum CraftMacSymbols {
|
|
27
|
+
private struct Key: Hashable { let name: String, description: String? }
|
|
28
|
+
private static var images: [Key: NSImage?] = [:]
|
|
29
|
+
static func image(_ name: String, description: String? = nil) -> NSImage? {
|
|
30
|
+
let key = Key(name: name, description: description)
|
|
31
|
+
if let image = images[key] { return image }
|
|
32
|
+
// Descriptions can carry changing text (a button's title), so the cache starts over rather than grow without end.
|
|
33
|
+
if images.count >= 512 { images.removeAll(keepingCapacity: true) }
|
|
34
|
+
let image = NSImage(systemSymbolName: name, accessibilityDescription: description)
|
|
35
|
+
images[key] = image
|
|
36
|
+
return image
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
24
40
|
/// The same C ABI used by Craft's mobile hosts. AppKit supplies text metrics;
|
|
25
41
|
/// Zig supplies flex sizing and justification, without a second layout rule.
|
|
26
42
|
enum CraftMacFlex {
|
|
@@ -56,7 +72,6 @@ final class CraftMacFlow: NSView {
|
|
|
56
72
|
// scopes must preserve the root's allocation on that later pass too.
|
|
57
73
|
if componentScope, boxes.count == 1, boxes[0].kind != "Modal", boxes[0].superview === self {
|
|
58
74
|
boxes[0].frame = CGRect(origin: .zero, size: size)
|
|
59
|
-
boxes[0].layoutSubtreeIfNeeded()
|
|
60
75
|
return
|
|
61
76
|
}
|
|
62
77
|
let horizontal = style["flexDirection"] as? String == "row"
|
|
@@ -102,9 +117,10 @@ final class CraftMacFlow: NSView {
|
|
|
102
117
|
let measured = horizontal ? box.measure(width: frames[index].0, stretch: true) : sizes[index]
|
|
103
118
|
let cross = min(crossAvailable, crossExplicit ?? (alignment == "stretch" ? crossAvailable : (horizontal ? measured.height : measured.width)))
|
|
104
119
|
let offset = margins.start + (alignment == "center" ? (crossAvailable - cross) / 2 : alignment == "flex-end" ? crossAvailable - cross : 0)
|
|
120
|
+
// No layout pass of its own: a child whose size changed is marked for layout, and AppKit lays it out
|
|
121
|
+
// after this one returns, in the same pass. Asking here walked its subtree again for every ancestor.
|
|
105
122
|
box.frame = horizontal ? CGRect(x: left + frames[index].1, y: top + offset, width: frames[index].0, height: cross)
|
|
106
123
|
: CGRect(x: left + offset, y: top + frames[index].1, width: cross, height: frames[index].0)
|
|
107
|
-
box.layoutSubtreeIfNeeded()
|
|
108
124
|
}
|
|
109
125
|
for box in boxes where !box.isHidden && box.layoutStyle["position"] as? String == "absolute" {
|
|
110
126
|
let measured = box.measure(width: size.width)
|
|
@@ -113,7 +129,6 @@ final class CraftMacFlow: NSView {
|
|
|
113
129
|
let width = dimension(box.layoutStyle["width"], base: size.width) ?? (leading != nil && trailing != nil ? max(0, size.width - leading! - trailing!) : measured.width)
|
|
114
130
|
let height = dimension(box.layoutStyle["height"], base: size.height) ?? (above != nil && below != nil ? max(0, size.height - above! - below!) : measured.height)
|
|
115
131
|
box.frame = CGRect(x: (leading ?? (trailing.map { size.width - $0 - width } ?? 0)), y: (above ?? (below.map { size.height - $0 - height } ?? 0)), width: width, height: height)
|
|
116
|
-
box.layoutSubtreeIfNeeded()
|
|
117
132
|
}
|
|
118
133
|
}
|
|
119
134
|
func fitting(width: CGFloat, stretchChildren: Bool = true) -> CGSize {
|
|
@@ -289,8 +304,11 @@ final class CraftMacBox: NSView, NSTextFieldDelegate, NSTextViewDelegate, NSDrag
|
|
|
289
304
|
private struct MeasureKey: Hashable { let width: CGFloat, stretch: Bool }
|
|
290
305
|
private var measured: [MeasureKey: CGSize] = [:]
|
|
291
306
|
/// What this box measured is stale: it or something inside it changed (CraftMacRenderer.update).
|
|
292
|
-
func forgetMeasurements() { measured.removeAll(keepingCapacity: true) }
|
|
307
|
+
func forgetMeasurements() { measured.removeAll(keepingCapacity: true); controlSize = nil }
|
|
293
308
|
private var measuredGeneration = -1
|
|
309
|
+
/// A control's own size (a button, a switch, a picker), asked once rather than at every width the box is
|
|
310
|
+
/// measured at: a button answers through SwiftUI's layout on current macOS. Forgotten with the measurements.
|
|
311
|
+
private var controlSize: CGSize?
|
|
294
312
|
var componentRoot: CraftMacBox? { props["data-stx-component-wrapper"] != nil && flow?.boxes.count == 1 && flow?.boxes.first?.kind != "Modal" ? flow?.boxes.first : nil }
|
|
295
313
|
var layoutStyle: [String: Any] {
|
|
296
314
|
guard let child = componentRoot else { return style }
|
|
@@ -313,8 +331,13 @@ final class CraftMacBox: NSView, NSTextFieldDelegate, NSTextViewDelegate, NSDrag
|
|
|
313
331
|
text.isSelectable = true; text.maximumNumberOfLines = 0
|
|
314
332
|
control = text
|
|
315
333
|
case "Button", "Link":
|
|
316
|
-
|
|
317
|
-
|
|
334
|
+
// What `NSButton(title:target:action:)` sets up, without the `sizeToFit` it ends with: a button's
|
|
335
|
+
// size goes through SwiftUI's layout on current macOS, and the box sets its frame anyway.
|
|
336
|
+
let button = NSButton(frame: .zero)
|
|
337
|
+
button.setButtonType(.momentaryPushIn); button.bezelStyle = .rounded
|
|
338
|
+
button.lineBreakMode = .byTruncatingTail; button.imageScaling = .scaleProportionallyDown
|
|
339
|
+
button.title = ""; button.target = self; button.action = #selector(pressed)
|
|
340
|
+
control = button
|
|
318
341
|
case "TextInput":
|
|
319
342
|
let input = NSTextField(); input.delegate = self; control = input
|
|
320
343
|
case "Picker":
|
|
@@ -348,6 +371,7 @@ final class CraftMacBox: NSView, NSTextFieldDelegate, NSTextViewDelegate, NSDrag
|
|
|
348
371
|
required init?(coder: NSCoder) { fatalError("Native controls are created from STX") }
|
|
349
372
|
|
|
350
373
|
func configure(_ node: [String: Any]) {
|
|
374
|
+
controlSize = nil
|
|
351
375
|
props = node["props"] as? [String: Any] ?? [:]
|
|
352
376
|
flow?.componentScope = props["data-stx-component-wrapper"] != nil
|
|
353
377
|
style = node["style"] as? [String: Any] ?? [:]
|
|
@@ -378,10 +402,15 @@ final class CraftMacBox: NSView, NSTextFieldDelegate, NSTextViewDelegate, NSDrag
|
|
|
378
402
|
} else { layer?.shadowOpacity = 0; layer?.shadowPath = nil; control?.layer?.cornerRadius = 0 }
|
|
379
403
|
layer?.cornerCurve = .continuous
|
|
380
404
|
for edge in ["Top", "Right", "Bottom", "Left"] {
|
|
405
|
+
let hidden = (number(style["border\(edge)Width"]) ?? 0) <= 0
|
|
406
|
+
// An edge's layer is made the first time it shows: most boxes never draw one, and four hidden
|
|
407
|
+
// layers on every box still cost Core Animation a commit each. An input makes its four at once,
|
|
408
|
+
// as it always did, since swapping its control later puts the new one above layers made before.
|
|
409
|
+
if edgeBorders[edge] == nil, hidden, kind != "TextInput" { continue }
|
|
381
410
|
let border = edgeBorders[edge] ?? CALayer()
|
|
382
411
|
if edgeBorders[edge] == nil { edgeBorders[edge] = border; layer?.addSublayer(border) }
|
|
383
412
|
border.backgroundColor = CraftMacColor.parse(style["borderColor"], fallback: .separatorColor).cgColor
|
|
384
|
-
border.isHidden =
|
|
413
|
+
border.isHidden = hidden
|
|
385
414
|
}
|
|
386
415
|
if let name = props["material"] as? String {
|
|
387
416
|
if material == nil { let effect = NSVisualEffectView(); effect.state = .followsWindowActiveState; addSubview(effect, positioned: .below, relativeTo: control); material = effect }
|
|
@@ -428,7 +457,7 @@ final class CraftMacBox: NSView, NSTextFieldDelegate, NSTextViewDelegate, NSDrag
|
|
|
428
457
|
button.bezelStyle = props["variant"] as? String == "toolbar" ? .texturedRounded : .rounded
|
|
429
458
|
button.isBordered = !["toolbar", "ghost"].contains(props["variant"] as? String ?? "")
|
|
430
459
|
button.contentTintColor = CraftMacColor.parse(style["color"], fallback: .controlAccentColor)
|
|
431
|
-
if let symbol = props["symbol"] as? String { button.image =
|
|
460
|
+
if let symbol = props["symbol"] as? String { button.image = CraftMacSymbols.image(symbol, description: button.title); button.imagePosition = button.title.isEmpty ? .imageOnly : .imageLeading }
|
|
432
461
|
if button.image != nil { button.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: number(style["fontSize"]) ?? 16, weight: .regular) }
|
|
433
462
|
if props["variant"] as? String == "primary" { button.bezelColor = .controlAccentColor }
|
|
434
463
|
if props["variant"] as? String == "danger" { button.contentTintColor = .systemRed }
|
|
@@ -457,7 +486,7 @@ final class CraftMacBox: NSView, NSTextFieldDelegate, NSTextViewDelegate, NSDrag
|
|
|
457
486
|
picker.isEnabled = props["disabled"] as? Bool != true
|
|
458
487
|
}
|
|
459
488
|
if let image = control as? NSImageView {
|
|
460
|
-
if kind == "Icon" { image.image =
|
|
489
|
+
if kind == "Icon" { image.image = CraftMacSymbols.image(props["symbol"] as? String ?? props["name"] as? String ?? "questionmark", description: label); image.contentTintColor = CraftMacColor.parse(style["color"]) }
|
|
461
490
|
else { configureImage(image) }
|
|
462
491
|
}
|
|
463
492
|
if let video = control as? AVPlayerView, let source = (props["source"] as? String ?? props["src"] as? String), source != imageSource {
|
|
@@ -556,7 +585,7 @@ final class CraftMacBox: NSView, NSTextFieldDelegate, NSTextViewDelegate, NSDrag
|
|
|
556
585
|
image?.image = data.flatMap(NSImage.init(data:))
|
|
557
586
|
self.fire(image?.image == nil ? "onError" : "onLoad")
|
|
558
587
|
}
|
|
559
|
-
}; imageTask?.resume()
|
|
588
|
+
}; imageTask.map { renderer?.runtime.network.resume($0) }
|
|
560
589
|
}
|
|
561
590
|
|
|
562
591
|
/// `style` with the pointer's layers on top: `hoverStyle` while hovered, `pressStyle` while pressed.
|
|
@@ -727,8 +756,9 @@ final class CraftMacBox: NSView, NSTextFieldDelegate, NSTextViewDelegate, NSDrag
|
|
|
727
756
|
return NSEdgeInsets(top: number(style["paddingTop"]) ?? vertical, left: number(style["paddingLeft"]) ?? horizontal, bottom: number(style["paddingBottom"]) ?? vertical, right: number(style["paddingRight"]) ?? horizontal)
|
|
728
757
|
}
|
|
729
758
|
func measure(width: CGFloat, stretch: Bool = false) -> CGSize {
|
|
730
|
-
if measuredGeneration != CraftMacBox.measureGeneration || measured.count > 64 {
|
|
731
|
-
|
|
759
|
+
if measuredGeneration != CraftMacBox.measureGeneration || measured.count > 64 { forgetMeasurements(); measuredGeneration = CraftMacBox.measureGeneration }
|
|
760
|
+
// Only a box with children can stretch to the width offered; a leaf measures the same either way.
|
|
761
|
+
let key = MeasureKey(width: width, stretch: stretch && flow != nil)
|
|
732
762
|
if let size = measured[key] { return size }
|
|
733
763
|
let size = measureSubtree(width: width, stretch: stretch)
|
|
734
764
|
measured[key] = size
|
|
@@ -756,7 +786,15 @@ final class CraftMacBox: NSView, NSTextFieldDelegate, NSTextViewDelegate, NSDrag
|
|
|
756
786
|
else if kind == "Icon" { size = CGSize(width: 18, height: 18) }
|
|
757
787
|
else if kind == "Image" || kind == "Video" { size = CGSize(width: limit, height: 160) }
|
|
758
788
|
else if kind == "TextInput" { size = CGSize(width: limit, height: props["multiline"] as? Bool == true ? 48 : 28) }
|
|
759
|
-
else if let control = control as? NSControl {
|
|
789
|
+
else if let control = control as? NSControl {
|
|
790
|
+
// A control its style sizes in full is not asked at all: the width and height below replace its answer.
|
|
791
|
+
if specifiedWidth != nil, dimension(style["height"], base: 1000) != nil { size = CGSize(width: 24, height: 26) }
|
|
792
|
+
else {
|
|
793
|
+
let natural = controlSize ?? control.intrinsicContentSize
|
|
794
|
+
controlSize = natural
|
|
795
|
+
size = natural; size.width = max(24, size.width); size.height = max(26, size.height)
|
|
796
|
+
}
|
|
797
|
+
}
|
|
760
798
|
else if let flow { size = flow.fitting(width: limit, stretchChildren: false) }
|
|
761
799
|
size.width = specifiedWidth ?? (flow != nil && stretch ? limit : min(limit, size.width))
|
|
762
800
|
if let maximum = dimension(style["maxWidth"], base: width) { size.width = min(size.width, maximum) }
|
|
@@ -808,7 +846,7 @@ final class CraftMacBox: NSView, NSTextFieldDelegate, NSTextViewDelegate, NSDrag
|
|
|
808
846
|
}
|
|
809
847
|
}
|
|
810
848
|
if let child = componentRoot {
|
|
811
|
-
child.frame = bounds
|
|
849
|
+
child.frame = bounds
|
|
812
850
|
} else if let scroll = control as? NSScrollView {
|
|
813
851
|
if let flow {
|
|
814
852
|
let width = scroll.contentSize.width
|
|
@@ -950,7 +988,7 @@ final class CraftMacSidebar: NSScrollView, NSOutlineViewDataSource, NSOutlineVie
|
|
|
950
988
|
let badge = NSTextField(labelWithString: item.badge); badge.font = NSFont.systemFont(ofSize: 11); badge.textColor = .secondaryLabelColor
|
|
951
989
|
badge.translatesAutoresizingMaskIntoConstraints = false; badge.setContentHuggingPriority(.required, for: .horizontal); cell.addSubview(badge)
|
|
952
990
|
NSLayoutConstraint.activate([label.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: item.icon.isEmpty ? 16 : 39), label.trailingAnchor.constraint(lessThanOrEqualTo: badge.leadingAnchor, constant: -8), label.centerYAnchor.constraint(equalTo: cell.centerYAnchor), badge.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -16), badge.centerYAnchor.constraint(equalTo: cell.centerYAnchor)])
|
|
953
|
-
if !item.icon.isEmpty { let image = NSImageView(image:
|
|
991
|
+
if !item.icon.isEmpty { let image = NSImageView(image: CraftMacSymbols.image(item.icon) ?? NSImage()); image.contentTintColor = item.color; image.frame = CGRect(x: 16, y: (rowHeight - 16) / 2, width: 16, height: 16); cell.imageView = image; cell.addSubview(image) }
|
|
954
992
|
cell.setAccessibilityIdentifier(item.id); cell.setAccessibilityLabel(item.label)
|
|
955
993
|
return cell
|
|
956
994
|
}
|
|
@@ -967,28 +1005,62 @@ final class CraftMacRenderer {
|
|
|
967
1005
|
var boxes: [String: CraftMacBox] = [:]
|
|
968
1006
|
weak var window: NSWindow?
|
|
969
1007
|
var sheets: [String: NSPanel] = [:]
|
|
1008
|
+
/// Boxes whose nodes went away: hidden at once, and taken out of the window once the batch is drawn.
|
|
1009
|
+
/// Leaving a window walks a view's whole subtree, every control in it hearing so, and a batch that
|
|
1010
|
+
/// replaced a list's rows spent most of its time on the rows it dropped, before any new row showed.
|
|
1011
|
+
private var leaving: [CraftMacBox] = []
|
|
970
1012
|
init(runtime: CraftMacRuntime) { self.runtime = runtime }
|
|
1013
|
+
|
|
1014
|
+
private func discard(_ box: CraftMacBox) {
|
|
1015
|
+
box.imageTask?.cancel()
|
|
1016
|
+
guard box.superview != nil else { return }
|
|
1017
|
+
box.isHidden = true
|
|
1018
|
+
if leaving.isEmpty { DispatchQueue.main.async { [weak self] in self?.sweep() } }
|
|
1019
|
+
leaving.append(box)
|
|
1020
|
+
}
|
|
1021
|
+
private func sweep() {
|
|
1022
|
+
let gone = leaving; leaving.removeAll()
|
|
1023
|
+
CATransaction.begin(); CATransaction.setDisableActions(true)
|
|
1024
|
+
gone.forEach { $0.removeFromSuperview() }
|
|
1025
|
+
CATransaction.commit()
|
|
1026
|
+
}
|
|
971
1027
|
/// The box for `node`, made or reconfigured, and its children's in order; every id drawn goes into `retained`.
|
|
972
1028
|
func reconcile(_ node: [String: Any], retained: inout Set<String>) -> CraftMacBox {
|
|
1029
|
+
let box = configured(node, retained: &retained)
|
|
1030
|
+
reconcileChildren(of: box, node, retained: &retained)
|
|
1031
|
+
return box
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/// The box for `node`, made or reconfigured, without its children.
|
|
1035
|
+
private func configured(_ node: [String: Any], retained: inout Set<String>) -> CraftMacBox {
|
|
973
1036
|
let id = node["id"] as? String ?? "root", kind = node["type"] as? String ?? "View"
|
|
974
1037
|
retained.insert(id)
|
|
975
1038
|
let box = boxes[id] ?? CraftMacBox(id: id, kind: kind, renderer: self)
|
|
976
1039
|
boxes[id] = box; box.configure(node)
|
|
977
|
-
if let flow = box.flow, kind != "Button" {
|
|
978
|
-
let children = (node["children"] as? [Any] ?? []).compactMap { $0 as? [String: Any] }.map { reconcile($0, retained: &retained) }
|
|
979
|
-
for child in flow.boxes where !children.contains(where: { $0 === child }) { child.removeFromSuperview() }
|
|
980
|
-
flow.boxes = children
|
|
981
|
-
for child in children where child.superview !== flow && sheets[child.id] == nil { flow.addSubview(child) }
|
|
982
|
-
// `zIndex` draws a child above its siblings (an overlay over a web page), highest last.
|
|
983
|
-
let raised = children.filter { (number($0.layoutStyle["zIndex"]) ?? 0) > 0 && $0.superview === flow }
|
|
984
|
-
.sorted { (number($0.layoutStyle["zIndex"]) ?? 0) < (number($1.layoutStyle["zIndex"]) ?? 0) }
|
|
985
|
-
if !raised.isEmpty, Array(flow.subviews.suffix(raised.count)) != raised {
|
|
986
|
-
for child in raised { flow.addSubview(child, positioned: .above, relativeTo: nil) }
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
1040
|
return box
|
|
990
1041
|
}
|
|
991
1042
|
|
|
1043
|
+
/// A box's children in order, then theirs. Each new child joins its parent before its own children are
|
|
1044
|
+
/// made, so views join the window one at a time: AppKit takes about twice as long over a whole new
|
|
1045
|
+
/// subtree joining at once.
|
|
1046
|
+
private func reconcileChildren(of box: CraftMacBox, _ node: [String: Any], retained: inout Set<String>) {
|
|
1047
|
+
guard let flow = box.flow, box.kind != "Button" else { return }
|
|
1048
|
+
let nodes = (node["children"] as? [Any] ?? []).compactMap { $0 as? [String: Any] }
|
|
1049
|
+
let children = nodes.map { configured($0, retained: &retained) }
|
|
1050
|
+
// A child dropped here either moves to another parent in this pass (adding it there takes it from
|
|
1051
|
+
// here) or is gone, and the caller discards it: it is not taken out of the window twice.
|
|
1052
|
+
flow.boxes = children
|
|
1053
|
+
for child in children where child.superview !== flow && sheets[child.id] == nil { flow.addSubview(child) }
|
|
1054
|
+
for (child, childNode) in zip(children, nodes) { reconcileChildren(of: child, childNode, retained: &retained) }
|
|
1055
|
+
// `zIndex` draws a child above its siblings (an overlay over a web page), highest last. After the
|
|
1056
|
+
// children's own children: a component's zIndex is its root's.
|
|
1057
|
+
let raised = children.filter { (number($0.layoutStyle["zIndex"]) ?? 0) > 0 && $0.superview === flow }
|
|
1058
|
+
.sorted { (number($0.layoutStyle["zIndex"]) ?? 0) < (number($1.layoutStyle["zIndex"]) ?? 0) }
|
|
1059
|
+
if !raised.isEmpty, Array(flow.subviews.suffix(raised.count)) != raised {
|
|
1060
|
+
for child in raised { flow.addSubview(child, positioned: .above, relativeTo: nil) }
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
992
1064
|
/// A batch drawn where it happened rather than across the window: each changed node's subtree
|
|
993
1065
|
/// (or, for a node the batch created, its nearest drawn ancestor's) is reconciled again, and only
|
|
994
1066
|
/// those boxes and the boxes above them measure and lay out again. A hover on one row used to
|
|
@@ -997,10 +1069,14 @@ final class CraftMacRenderer {
|
|
|
997
1069
|
func update(_ ids: [String], parentOf: (String) -> String?, node nodeFor: (String) -> [String: Any]?) -> Bool {
|
|
998
1070
|
guard !boxes.isEmpty else { return false }
|
|
999
1071
|
var targets: [String] = []
|
|
1072
|
+
let top = root.boxes.first?.id
|
|
1000
1073
|
for id in ids {
|
|
1001
1074
|
var current: String? = id
|
|
1002
1075
|
while let candidate = current, boxes[candidate] == nil { current = parentOf(candidate) }
|
|
1003
|
-
|
|
1076
|
+
// A node the batch removed has no parent left (one it made and removed never had a box): the
|
|
1077
|
+
// removal marked the parent it left, which is drawn instead, discarding its box. Declining here
|
|
1078
|
+
// redrew the whole window for any batch that dropped a row it had also updated.
|
|
1079
|
+
guard let target = current, target == top || parentOf(target) != nil else { continue }
|
|
1004
1080
|
if !targets.contains(target) { targets.append(target) }
|
|
1005
1081
|
}
|
|
1006
1082
|
// A target inside another is drawn with it.
|
|
@@ -1019,7 +1095,7 @@ final class CraftMacRenderer {
|
|
|
1019
1095
|
_ = reconcile(node, retained: &retained)
|
|
1020
1096
|
for id in before where !retained.contains(id) {
|
|
1021
1097
|
guard let gone = boxes.removeValue(forKey: id) else { continue }
|
|
1022
|
-
|
|
1098
|
+
discard(gone)
|
|
1023
1099
|
}
|
|
1024
1100
|
for id in retained { if let drawn = boxes[id] { drawn.forgetMeasurements(); drawn.needsLayout = true } }
|
|
1025
1101
|
var above: NSView? = box.superview
|
|
@@ -1049,15 +1125,17 @@ final class CraftMacRenderer {
|
|
|
1049
1125
|
let bottom = bounds.maxY >= (scroll.documentView?.bounds.height ?? 0) - 8
|
|
1050
1126
|
scrolls.append((box, bounds.origin, bottom))
|
|
1051
1127
|
} }
|
|
1052
|
-
|
|
1128
|
+
// The top box joins the window before its children are made (see reconcileChildren).
|
|
1129
|
+
let box = configured(document, retained: &retained)
|
|
1130
|
+
root.style = ["flexDirection": "column"]
|
|
1131
|
+
root.boxes = [box]; if box.superview !== root { root.addSubview(box) }
|
|
1132
|
+
reconcileChildren(of: box, document, retained: &retained)
|
|
1053
1133
|
// Boxes were reconfigured and children replaced: nothing measured before now holds.
|
|
1054
1134
|
CraftMacBox.measureGeneration &+= 1
|
|
1055
1135
|
for (id, box) in boxes where !retained.contains(id) {
|
|
1056
|
-
|
|
1136
|
+
discard(box); boxes.removeValue(forKey: id)
|
|
1057
1137
|
if let sheet = sheets.removeValue(forKey: id) { window?.endSheet(sheet); sheet.close() }
|
|
1058
1138
|
}
|
|
1059
|
-
root.style = ["flexDirection": "column"]
|
|
1060
|
-
root.boxes = [box]; if box.superview !== root { root.addSubview(box) }
|
|
1061
1139
|
box.frame = root.bounds; box.layoutSubtreeIfNeeded(); root.layoutBoxes(in: root.bounds.size)
|
|
1062
1140
|
for modal in boxes.values where modal.kind == "Modal" {
|
|
1063
1141
|
guard let window else { continue }
|