@octane-xplat/macos-renderer 0.11.0 → 0.11.1
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/README.md +152 -8
- package/package.json +4 -3
- package/src/appearance.ts +56 -0
- package/src/appkit.d.ts +303 -30
- package/src/image.ts +92 -0
- package/src/index.ts +1112 -390
- package/src/intrinsics.d.ts +8 -2
- package/src/{layer.mjs → layer.ts} +74 -22
- package/src/presentation.d.mts +29 -0
- package/src/{presentation.mjs → presentation.ts} +102 -17
package/src/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { showWindowLayer } from './layer
|
|
1
|
+
import { showWindowLayer, type WindowLayerOptions } from './layer'
|
|
2
2
|
import '@nativescript/macos-node-api'
|
|
3
|
-
import {
|
|
4
|
-
import { installPresentationBridge } from './presentation
|
|
3
|
+
import { disposeImage, updateImage } from './image'
|
|
4
|
+
import { installPresentationBridge } from './presentation'
|
|
5
|
+
import { installAppearanceBridge } from './appearance'
|
|
5
6
|
import { makeWebView, updateWebView, disposeWebView } from './webview'
|
|
6
7
|
import { createUniversalRoot } from 'octane/universal/native'
|
|
7
8
|
import type {
|
|
@@ -43,8 +44,10 @@ export interface ElementNode {
|
|
|
43
44
|
scrollObserverInstalled: boolean
|
|
44
45
|
text: string
|
|
45
46
|
// Per-kind extras: placeholderView, marginHost, marginConstraints,
|
|
46
|
-
// crossAxisConstraint, sizeConstraints, styleBg, bgSlot,
|
|
47
|
-
// scrollObserver, scheme, textRuns,
|
|
47
|
+
// crossAxisConstraint, sizeConstraints, placementPins, styleBg, bgSlot,
|
|
48
|
+
// layoutObserver, scrollObserver, scheme, textRuns, appliedStyle,
|
|
49
|
+
// styleFont*/classFont*/propFontSize, stylePaddingBox/Edges, classInsets,
|
|
50
|
+
// styleRadius, classRadius, classSpacing, classOrientation, ...
|
|
48
51
|
[key: string]: any
|
|
49
52
|
}
|
|
50
53
|
|
|
@@ -92,6 +95,9 @@ interface RootContainer {
|
|
|
92
95
|
nodes: Map<number, ElementNode>
|
|
93
96
|
children: ElementNode[]
|
|
94
97
|
root: UniversalRoot | null
|
|
98
|
+
/** Parents needing a sibling-wide layout pass; non-null only while a
|
|
99
|
+
* command batch is applying. */
|
|
100
|
+
layoutDirty: Set<ElementNode> | null
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
const actionHandlers = new Map<number, (() => void) | null>()
|
|
@@ -122,6 +128,8 @@ class ButtonActionTarget extends NSObject {
|
|
|
122
128
|
viewPressed: { params: [NSObject], returns: interop.types.void },
|
|
123
129
|
viewPanned: { params: [NSPanGestureRecognizer], returns: interop.types.void },
|
|
124
130
|
controlChanged: { params: [NSObject], returns: interop.types.void },
|
|
131
|
+
controlTextDidChange: { params: [NSNotification], returns: interop.types.void },
|
|
132
|
+
textFieldSubmitted: { params: [NSObject], returns: interop.types.void },
|
|
125
133
|
textDidChange: { params: [NSNotification], returns: interop.types.void },
|
|
126
134
|
}
|
|
127
135
|
|
|
@@ -133,14 +141,28 @@ class ButtonActionTarget extends NSObject {
|
|
|
133
141
|
invokeAction(sender.tag)
|
|
134
142
|
}
|
|
135
143
|
|
|
136
|
-
viewPressed(sender:
|
|
144
|
+
viewPressed(sender: NSClickGestureRecognizer) {
|
|
137
145
|
invokeAction(actionIdsByView.get(sender.view))
|
|
138
146
|
}
|
|
139
147
|
|
|
140
|
-
controlChanged(sender:
|
|
148
|
+
controlChanged(sender: NSControl) {
|
|
141
149
|
invokeAction(sender.tag ?? actionIdsByView.get(sender))
|
|
142
150
|
}
|
|
143
151
|
|
|
152
|
+
// NSControlTextEditingDelegate: fires on every edit while a single-line
|
|
153
|
+
// field is being edited — the per-edit source for onTextChange.
|
|
154
|
+
controlTextDidChange(notification: NSNotification) {
|
|
155
|
+
const node = textNodesByView.get(notification.object)
|
|
156
|
+
invokeAction(node?.actionId ?? actionIdsByView.get(notification.object))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Single-line fields keep target/action for submission (Return). The
|
|
160
|
+
// change handler lives on controlTextDidChange, so the action never
|
|
161
|
+
// masquerades as a value-change event.
|
|
162
|
+
textFieldSubmitted(sender: NSObject) {
|
|
163
|
+
textNodesByView.get(sender)?.submitHandler?.()
|
|
164
|
+
}
|
|
165
|
+
|
|
144
166
|
viewPanned(sender: NSPanGestureRecognizer) {
|
|
145
167
|
const handler = panHandlersByView.get(sender.view)
|
|
146
168
|
if (!handler) {
|
|
@@ -190,6 +212,16 @@ class ContentAlignedTextField extends NSTextField {
|
|
|
190
212
|
}
|
|
191
213
|
}
|
|
192
214
|
|
|
215
|
+
class ContentAlignedSecureTextField extends NSSecureTextField {
|
|
216
|
+
static {
|
|
217
|
+
NativeClass(this)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
alignmentRectInsets() {
|
|
221
|
+
return { top: 0, left: 0, bottom: 0, right: 0 }
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
193
225
|
const inputTransparentViews = new WeakSet<object>()
|
|
194
226
|
|
|
195
227
|
class AccessibleStackView extends NSStackView {
|
|
@@ -400,12 +432,74 @@ function stackAlignmentAttribute(view: any, value: string) {
|
|
|
400
432
|
return horizontal ? NSLayoutAttribute.Top : NSLayoutAttribute.Left
|
|
401
433
|
}
|
|
402
434
|
|
|
435
|
+
// Container layout follows the same source order as fonts and padding. Resolve
|
|
436
|
+
// whole channels so an axis-specific gap never overrides a higher-source gap.
|
|
437
|
+
function syncStackLayout(node: ElementNode) {
|
|
438
|
+
if (node.type !== 'flexboxlayout' && node.type !== 'stack') {
|
|
439
|
+
return
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const stack = node.view!
|
|
443
|
+
const style = node.styleLayout ?? {}
|
|
444
|
+
const previousOrientation = stack.orientation
|
|
445
|
+
stack.orientation =
|
|
446
|
+
style.flexDirection != null
|
|
447
|
+
? style.flexDirection === 'row'
|
|
448
|
+
? NSUserInterfaceLayoutOrientation.Horizontal
|
|
449
|
+
: NSUserInterfaceLayoutOrientation.Vertical
|
|
450
|
+
: (node.classOrientation ??
|
|
451
|
+
(node.props.flexDirection === 'row'
|
|
452
|
+
? NSUserInterfaceLayoutOrientation.Horizontal
|
|
453
|
+
: NSUserInterfaceLayoutOrientation.Vertical))
|
|
454
|
+
|
|
455
|
+
const gapKey =
|
|
456
|
+
stack.orientation === NSUserInterfaceLayoutOrientation.Horizontal ? 'columnGap' : 'rowGap'
|
|
457
|
+
|
|
458
|
+
stack.spacing = Number(
|
|
459
|
+
style[gapKey] ??
|
|
460
|
+
style.gap ??
|
|
461
|
+
node.classSpacing ??
|
|
462
|
+
node.props[gapKey] ??
|
|
463
|
+
node.props.gap ??
|
|
464
|
+
node.props.spacing ??
|
|
465
|
+
0,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
stack.alignment = stackAlignmentAttribute(stack, stackAlignItems(node))
|
|
469
|
+
const justifyContent = stackJustifyContent(node)
|
|
470
|
+
if (stack.orientation !== previousOrientation || justifyContent !== node.appliedStackJustify) {
|
|
471
|
+
moveStackChildren(node)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
node.appliedStackJustify = justifyContent
|
|
475
|
+
updateStackDistribution(node)
|
|
476
|
+
updateCrossAxisConstraints(node)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function isStackLayoutInput(name: string) {
|
|
480
|
+
return ['flexDirection', 'gap', 'rowGap', 'columnGap', 'alignItems', 'justifyContent'].includes(
|
|
481
|
+
name,
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
|
|
403
485
|
function stackAlignItems(node: ElementNode) {
|
|
486
|
+
if (node.styleLayout?.alignItems != null) {
|
|
487
|
+
return node.styleLayout.alignItems
|
|
488
|
+
}
|
|
489
|
+
|
|
404
490
|
const classes = nodeClasses(node)
|
|
405
491
|
if (classes.includes('vx-button') || classes.includes('items-center')) {
|
|
406
492
|
return 'center'
|
|
407
493
|
}
|
|
408
494
|
|
|
495
|
+
if (classes.includes('items-stretch')) {
|
|
496
|
+
return 'stretch'
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (classes.includes('items-baseline')) {
|
|
500
|
+
return 'baseline'
|
|
501
|
+
}
|
|
502
|
+
|
|
409
503
|
if (classes.includes('items-start')) {
|
|
410
504
|
return 'start'
|
|
411
505
|
}
|
|
@@ -418,6 +512,23 @@ function stackAlignItems(node: ElementNode) {
|
|
|
418
512
|
}
|
|
419
513
|
|
|
420
514
|
function stackJustifyContent(node: ElementNode) {
|
|
515
|
+
if (node.styleLayout?.justifyContent != null) {
|
|
516
|
+
return node.styleLayout.justifyContent
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const classes = nodeClasses(node)
|
|
520
|
+
if (classes.includes('justify-center')) {
|
|
521
|
+
return 'center'
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (classes.includes('justify-end')) {
|
|
525
|
+
return 'end'
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (classes.includes('justify-start')) {
|
|
529
|
+
return 'start'
|
|
530
|
+
}
|
|
531
|
+
|
|
421
532
|
if (nodeClasses(node).includes('vx-button')) {
|
|
422
533
|
return 'center'
|
|
423
534
|
}
|
|
@@ -489,7 +600,7 @@ function makeMarginHost(node: ElementNode) {
|
|
|
489
600
|
})
|
|
490
601
|
|
|
491
602
|
host.translatesAutoresizingMaskIntoConstraints = false
|
|
492
|
-
host.addSubview(node.view)
|
|
603
|
+
host.addSubview(node.view!)
|
|
493
604
|
const insets = marginInsetsOf(node)
|
|
494
605
|
const constraints = [
|
|
495
606
|
node.view!.leadingAnchor.constraintEqualToAnchorConstant(host.leadingAnchor, insets.left),
|
|
@@ -526,23 +637,24 @@ function syncMarginHost(node: ElementNode) {
|
|
|
526
637
|
}
|
|
527
638
|
|
|
528
639
|
const insets = marginInsetsOf(node)
|
|
529
|
-
const
|
|
640
|
+
const gravity = stackGravity(parent, node)
|
|
641
|
+
const index = gravityInsertIndex(parent, node, gravity)
|
|
530
642
|
if (insets && !node.marginHost) {
|
|
531
|
-
const gravity = stackGravity(parent, node)
|
|
532
643
|
stack.removeArrangedSubview(node.view)
|
|
533
644
|
node.view.removeFromSuperview()
|
|
534
645
|
const host = makeMarginHost(node)
|
|
535
646
|
stack.insertViewAtIndexInGravity(host, index, gravity)
|
|
536
|
-
|
|
647
|
+
queueLayoutReconcile(node.container, parent)
|
|
648
|
+
setLayoutAction(node, node.props?.onLayoutChanged)
|
|
537
649
|
} else if (!insets && node.marginHost) {
|
|
538
|
-
const gravity = stackGravity(parent, node)
|
|
539
650
|
stack.removeArrangedSubview(node.marginHost)
|
|
540
651
|
node.view.removeFromSuperview()
|
|
541
652
|
node.marginHost.removeFromSuperview()
|
|
542
653
|
node.marginHost = null
|
|
543
654
|
node.marginConstraints = null
|
|
544
655
|
stack.insertViewAtIndexInGravity(node.view, index, gravity)
|
|
545
|
-
|
|
656
|
+
queueLayoutReconcile(node.container, parent)
|
|
657
|
+
setLayoutAction(node, node.props?.onLayoutChanged)
|
|
546
658
|
} else if (insets && node.marginHost) {
|
|
547
659
|
const [leading, trailing, top, bottom] = node.marginConstraints
|
|
548
660
|
leading.constant = insets.left
|
|
@@ -577,7 +689,7 @@ function setMarginStyle(node: ElementNode, name: string, value: any) {
|
|
|
577
689
|
|
|
578
690
|
if (node.parent?.type === 'gridlayout') {
|
|
579
691
|
// Grids lay out children manually from props.style — no wrapper needed.
|
|
580
|
-
|
|
692
|
+
queueLayoutReconcile(node.container, node.parent)
|
|
581
693
|
return
|
|
582
694
|
}
|
|
583
695
|
|
|
@@ -614,6 +726,24 @@ function stackGravity(parent: ElementNode, child: ElementNode) {
|
|
|
614
726
|
return leading
|
|
615
727
|
}
|
|
616
728
|
|
|
729
|
+
/** AppKit insertion indices are local to a gravity area — count the
|
|
730
|
+
* same-gravity siblings ahead of `node`, not its logical child index.
|
|
731
|
+
* Shared by stack insertion and margin-host reinsertion. */
|
|
732
|
+
function gravityInsertIndex(parent: ElementNode, node: ElementNode, gravity: number) {
|
|
733
|
+
let index = 0
|
|
734
|
+
for (const sibling of parent.children) {
|
|
735
|
+
if (sibling === node) {
|
|
736
|
+
break
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (sibling.view && stackGravity(parent, sibling) === gravity) {
|
|
740
|
+
index++
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
return index
|
|
745
|
+
}
|
|
746
|
+
|
|
617
747
|
function moveStackChildren(parent: ElementNode) {
|
|
618
748
|
const stack = parent.childHost ?? parent.view
|
|
619
749
|
if (typeof stack?.addViewInGravity !== 'function') {
|
|
@@ -629,9 +759,13 @@ function moveStackChildren(parent: ElementNode) {
|
|
|
629
759
|
stack.removeArrangedSubview(arranged)
|
|
630
760
|
arranged.removeFromSuperview()
|
|
631
761
|
stack.addViewInGravity(arranged, stackGravity(parent, child))
|
|
762
|
+
// Removing a view drops constraints to its old superview, including
|
|
763
|
+
// percentage dimensions. Reinstall them after reattaching.
|
|
764
|
+
applySizeConstraints(child)
|
|
632
765
|
setStackChildPriorities(parent, child)
|
|
633
766
|
}
|
|
634
767
|
|
|
768
|
+
updateStackDistribution(parent)
|
|
635
769
|
updateCrossAxisConstraints(parent)
|
|
636
770
|
}
|
|
637
771
|
|
|
@@ -672,8 +806,6 @@ function setStackChildPriorities(parent: ElementNode, child: ElementNode) {
|
|
|
672
806
|
target.setContentHuggingPriorityForOrientation(priority, orientation)
|
|
673
807
|
target.setContentCompressionResistancePriorityForOrientation(750, orientation)
|
|
674
808
|
}
|
|
675
|
-
|
|
676
|
-
updateStackDistribution(parent)
|
|
677
809
|
}
|
|
678
810
|
|
|
679
811
|
function makeFlexbox(props: PropBag) {
|
|
@@ -684,11 +816,12 @@ function makeFlexbox(props: PropBag) {
|
|
|
684
816
|
: undefined
|
|
685
817
|
|
|
686
818
|
if (actionId !== undefined) {
|
|
687
|
-
actionIdsByView.set(stack, actionId)
|
|
688
|
-
actionHandlers.set(actionId, null)
|
|
689
819
|
stack.addGestureRecognizer(
|
|
690
|
-
NSClickGestureRecognizer.alloc().initWithTargetAction(buttonActionTarget, 'viewPressed'),
|
|
820
|
+
NSClickGestureRecognizer.alloc().initWithTargetAction(buttonActionTarget, 'viewPressed:'),
|
|
691
821
|
)
|
|
822
|
+
|
|
823
|
+
actionIdsByView.set(stack, actionId)
|
|
824
|
+
actionHandlers.set(actionId, null)
|
|
692
825
|
}
|
|
693
826
|
|
|
694
827
|
return { view: stack, actionId }
|
|
@@ -717,7 +850,7 @@ function makeButton(props: PropBag) {
|
|
|
717
850
|
const button = NSButton.buttonWithTitleTargetAction(
|
|
718
851
|
String(props.title ?? 'Button'),
|
|
719
852
|
buttonActionTarget,
|
|
720
|
-
'buttonPressed',
|
|
853
|
+
'buttonPressed:',
|
|
721
854
|
)
|
|
722
855
|
|
|
723
856
|
button.bezelStyle = NSBezelStyle.Rounded
|
|
@@ -810,6 +943,22 @@ function setScrollAction(node: ElementNode, handler: ((event: any) => void) | un
|
|
|
810
943
|
|
|
811
944
|
const layoutHandlers = new WeakMap<object, () => void>()
|
|
812
945
|
|
|
946
|
+
/** Margin changes swap a node's arranged view without touching the node.
|
|
947
|
+
* Drop the observer and handler bound to the stale target — the caller
|
|
948
|
+
* re-runs `setLayoutAction`, which rebuilds both against the current
|
|
949
|
+
* arranged view so events keep reporting that view's bounds. */
|
|
950
|
+
function syncLayoutObserver(node: ElementNode) {
|
|
951
|
+
const observed = node.layoutObservedView
|
|
952
|
+
if (!observed || observed === arrangedView(node)) {
|
|
953
|
+
return
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
layoutHandlers.delete(observed)
|
|
957
|
+
NSNotificationCenter.defaultCenter.removeObserver(node.layoutObserver)
|
|
958
|
+
node.layoutObserver = null
|
|
959
|
+
node.layoutObservedView = null
|
|
960
|
+
}
|
|
961
|
+
|
|
813
962
|
/** `onLayoutChanged` prop: NSViewFrameDidChangeNotification on the arranged
|
|
814
963
|
* view (margin host when present) — the windowed VirtualList leaf measures
|
|
815
964
|
* rows through it. */
|
|
@@ -819,6 +968,7 @@ function setLayoutAction(node: ElementNode, handler: ((event: any) => void) | un
|
|
|
819
968
|
return
|
|
820
969
|
}
|
|
821
970
|
|
|
971
|
+
syncLayoutObserver(node)
|
|
822
972
|
if (typeof handler !== 'function') {
|
|
823
973
|
layoutHandlers.delete(target)
|
|
824
974
|
return
|
|
@@ -835,6 +985,8 @@ function setLayoutAction(node: ElementNode, handler: ((event: any) => void) | un
|
|
|
835
985
|
null,
|
|
836
986
|
() => layoutHandlers.get(target)?.(),
|
|
837
987
|
)
|
|
988
|
+
|
|
989
|
+
node.layoutObservedView = target
|
|
838
990
|
}
|
|
839
991
|
|
|
840
992
|
layoutHandlers.set(target, () => {
|
|
@@ -878,10 +1030,12 @@ function setTextFieldPlaceholder(field: NSTextField, props: PropBag, scheme = 'l
|
|
|
878
1030
|
function makeTextField(props: PropBag, multiline = false) {
|
|
879
1031
|
const field = multiline
|
|
880
1032
|
? NSTextView.alloc().initWithFrame({ origin: { x: 0, y: 0 }, size: { width: 320, height: 72 } })
|
|
881
|
-
: ContentAlignedTextField
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1033
|
+
: (props.secure ? ContentAlignedSecureTextField : ContentAlignedTextField)
|
|
1034
|
+
.alloc()
|
|
1035
|
+
.initWithFrame({
|
|
1036
|
+
origin: { x: 0, y: 0 },
|
|
1037
|
+
size: { width: 320, height: 28 },
|
|
1038
|
+
})
|
|
885
1039
|
|
|
886
1040
|
field.translatesAutoresizingMaskIntoConstraints = false
|
|
887
1041
|
field.font = fontForStyle(14)
|
|
@@ -892,7 +1046,7 @@ function makeTextField(props: PropBag, multiline = false) {
|
|
|
892
1046
|
field.editable = true
|
|
893
1047
|
field.selectable = true
|
|
894
1048
|
field.sendsActionOnEndEditing = false
|
|
895
|
-
setTextFieldPlaceholder(field, props)
|
|
1049
|
+
setTextFieldPlaceholder(field as NSTextField, props)
|
|
896
1050
|
} else {
|
|
897
1051
|
field.editable = true
|
|
898
1052
|
field.selectable = true
|
|
@@ -911,7 +1065,7 @@ function makeTextField(props: PropBag, multiline = false) {
|
|
|
911
1065
|
}
|
|
912
1066
|
|
|
913
1067
|
function makeSwitch(props: PropBag) {
|
|
914
|
-
const button = NSButton.buttonWithTitleTargetAction('', buttonActionTarget, 'controlChanged')
|
|
1068
|
+
const button = NSButton.buttonWithTitleTargetAction('', buttonActionTarget, 'controlChanged:')
|
|
915
1069
|
button.setButtonType(NSButtonType.Switch)
|
|
916
1070
|
button.state = props.checked ? 1 : 0
|
|
917
1071
|
button.translatesAutoresizingMaskIntoConstraints = false
|
|
@@ -931,7 +1085,7 @@ function makeSlider(props: PropBag) {
|
|
|
931
1085
|
slider.maxValue = Number(props.maxValue ?? 1)
|
|
932
1086
|
slider.doubleValue = Number(props.value ?? 0)
|
|
933
1087
|
slider.target = buttonActionTarget
|
|
934
|
-
slider.action = 'controlChanged'
|
|
1088
|
+
slider.action = 'controlChanged:'
|
|
935
1089
|
slider.translatesAutoresizingMaskIntoConstraints = false
|
|
936
1090
|
const actionId = nextActionId++
|
|
937
1091
|
slider.tag = actionId
|
|
@@ -1217,6 +1371,58 @@ function trackOffset(sizes: number[], index: number) {
|
|
|
1217
1371
|
return offset
|
|
1218
1372
|
}
|
|
1219
1373
|
|
|
1374
|
+
/** Pin a grid/absolute child to the rect its parent's layout pass computed.
|
|
1375
|
+
* Children are translates=false views, so Auto Layout owns their frames —
|
|
1376
|
+
* an imperative frame write only holds until the next solve shrinks the
|
|
1377
|
+
* child back to fitting size. Each axis gets one edge pin against the
|
|
1378
|
+
* parent plus one size pin, so parent geometry stays an input; a pin set
|
|
1379
|
+
* that also implied the parent's size would outrank the window's
|
|
1380
|
+
* 500-priority WindowSizeStayPut hold and lock the window at the content's
|
|
1381
|
+
* fitting size. */
|
|
1382
|
+
function pinLayoutChild(
|
|
1383
|
+
child: ElementNode,
|
|
1384
|
+
parentView: any,
|
|
1385
|
+
x: number,
|
|
1386
|
+
top: number,
|
|
1387
|
+
width: number,
|
|
1388
|
+
height: number,
|
|
1389
|
+
) {
|
|
1390
|
+
const pins = (child.placementPins ??= {})
|
|
1391
|
+
if (!pins.leading) {
|
|
1392
|
+
pins.leading = child.view!.leadingAnchor.constraintEqualToAnchorConstant(
|
|
1393
|
+
parentView.leadingAnchor,
|
|
1394
|
+
0,
|
|
1395
|
+
)
|
|
1396
|
+
|
|
1397
|
+
pins.top = child.view!.topAnchor.constraintEqualToAnchorConstant(parentView.topAnchor, 0)
|
|
1398
|
+
pins.width = child.view!.widthAnchor.constraintEqualToConstant(0)
|
|
1399
|
+
pins.height = child.view!.heightAnchor.constraintEqualToConstant(0)
|
|
1400
|
+
for (const pin of [pins.leading, pins.top, pins.width, pins.height]) {
|
|
1401
|
+
pin.active = true
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
pins.leading.constant = x
|
|
1406
|
+
pins.top.constant = top
|
|
1407
|
+
pins.width.constant = width
|
|
1408
|
+
pins.height.constant = height
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/** Drop the pins a grid/absolute parent installed — a constraint that keeps
|
|
1412
|
+
* referencing the old parent view is invalid once the child reparents. */
|
|
1413
|
+
function releasePlacementPins(node: ElementNode) {
|
|
1414
|
+
const pins = node.placementPins
|
|
1415
|
+
if (!pins) {
|
|
1416
|
+
return
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
for (const pin of Object.values(pins) as { active: boolean }[]) {
|
|
1420
|
+
pin.active = false
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
node.placementPins = null
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1220
1426
|
function layoutGridChildren(parent: ElementNode | undefined) {
|
|
1221
1427
|
if (!parent?.view) {
|
|
1222
1428
|
return
|
|
@@ -1302,10 +1508,7 @@ function layoutGridChildren(parent: ElementNode | undefined) {
|
|
|
1302
1508
|
}
|
|
1303
1509
|
|
|
1304
1510
|
top += layoutLength(style.top, cellHeight, 0) + Number(style.marginTop ?? 0)
|
|
1305
|
-
child.view
|
|
1306
|
-
origin: { x, y: height - top - childHeight },
|
|
1307
|
-
size: { width: childWidth, height: childHeight },
|
|
1308
|
-
}
|
|
1511
|
+
pinLayoutChild(child, parent.view, x, top, childWidth, childHeight)
|
|
1309
1512
|
}
|
|
1310
1513
|
|
|
1311
1514
|
return
|
|
@@ -1346,10 +1549,7 @@ function layoutGridChildren(parent: ElementNode | undefined) {
|
|
|
1346
1549
|
y = height - childHeight - Number(style.marginTop ?? 0)
|
|
1347
1550
|
}
|
|
1348
1551
|
|
|
1349
|
-
child.view
|
|
1350
|
-
origin: { x, y },
|
|
1351
|
-
size: { width: childWidth, height: childHeight },
|
|
1352
|
-
}
|
|
1552
|
+
pinLayoutChild(child, parent.view, x, height - y - childHeight, childWidth, childHeight)
|
|
1353
1553
|
}
|
|
1354
1554
|
}
|
|
1355
1555
|
|
|
@@ -1394,10 +1594,7 @@ function layoutAbsoluteChildren(parent: ElementNode | undefined) {
|
|
|
1394
1594
|
? height - bottom - childHeight
|
|
1395
1595
|
: 0
|
|
1396
1596
|
|
|
1397
|
-
child.view
|
|
1398
|
-
origin: { x, y: height - offsetTop - childHeight },
|
|
1399
|
-
size: { width: childWidth, height: childHeight },
|
|
1400
|
-
}
|
|
1597
|
+
pinLayoutChild(child, parent.view, x, offsetTop, childWidth, childHeight)
|
|
1401
1598
|
}
|
|
1402
1599
|
}
|
|
1403
1600
|
|
|
@@ -1433,140 +1630,236 @@ function makeImageView(props: PropBag) {
|
|
|
1433
1630
|
})
|
|
1434
1631
|
|
|
1435
1632
|
image.translatesAutoresizingMaskIntoConstraints = false
|
|
1436
|
-
image
|
|
1633
|
+
updateImage(image, props.src)
|
|
1437
1634
|
|
|
1438
1635
|
return image
|
|
1439
1636
|
}
|
|
1440
1637
|
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1638
|
+
/** Types whose views arrange child views; mirrors the check in insert(). */
|
|
1639
|
+
const VIEW_PARENT_TYPES = new Set([
|
|
1640
|
+
'stack',
|
|
1641
|
+
'flexboxlayout',
|
|
1642
|
+
'scrollview',
|
|
1643
|
+
'gridlayout',
|
|
1644
|
+
'absolutelayout',
|
|
1645
|
+
])
|
|
1646
|
+
|
|
1647
|
+
/** Every type the factory switch below can build. */
|
|
1648
|
+
const SUPPORTED_TYPES = new Set([
|
|
1649
|
+
...VIEW_PARENT_TYPES,
|
|
1650
|
+
'label',
|
|
1651
|
+
'#text',
|
|
1652
|
+
'button',
|
|
1653
|
+
'textfield',
|
|
1654
|
+
'textview',
|
|
1655
|
+
'switch',
|
|
1656
|
+
'slider',
|
|
1657
|
+
'webview',
|
|
1658
|
+
'image',
|
|
1659
|
+
'span',
|
|
1660
|
+
])
|
|
1661
|
+
|
|
1662
|
+
function typeHasView(type: string) {
|
|
1663
|
+
return type !== '#text' && type !== 'span'
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
function assertSupportedType(type: string) {
|
|
1667
|
+
if (!SUPPORTED_TYPES.has(type)) {
|
|
1668
|
+
throw new Error('AppKit spike does not support <' + type + '>')
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
/** Deterministic prop failures that would otherwise throw after views and
|
|
1673
|
+
* registrations exist. `baseProps` supplies already-applied props for
|
|
1674
|
+
* update commands so merged-prop rules (e.g. placeholder color) stay exact. */
|
|
1675
|
+
function validateNodeProps(type: string, props: PropBag | undefined, baseProps?: PropBag) {
|
|
1676
|
+
if (type === '#text' || type === 'span') {
|
|
1677
|
+
return
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
const style = props?.style
|
|
1681
|
+
if (style != null) {
|
|
1682
|
+
if (typeof style !== 'object') {
|
|
1683
|
+
throw new Error('AppKit spike expects style to be an object')
|
|
1472
1684
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
break
|
|
1685
|
+
|
|
1686
|
+
// applyStyle feeds these straight into nativeColor.
|
|
1687
|
+
if (style.backgroundColor != null) {
|
|
1688
|
+
nativeColor(style.backgroundColor)
|
|
1478
1689
|
}
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
case 'textview':
|
|
1483
|
-
view = makeTextField(props, true)
|
|
1484
|
-
break
|
|
1485
|
-
case 'switch': {
|
|
1486
|
-
const control = makeSwitch(props)
|
|
1487
|
-
view = control.view
|
|
1488
|
-
actionId = control.actionId
|
|
1489
|
-
break
|
|
1690
|
+
|
|
1691
|
+
if (style.borderColor != null) {
|
|
1692
|
+
nativeColor(style.borderColor)
|
|
1490
1693
|
}
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
actionId = control.actionId
|
|
1495
|
-
break
|
|
1694
|
+
|
|
1695
|
+
if (['label', 'textfield', 'textview'].includes(type) && style.color != null) {
|
|
1696
|
+
nativeColor(style.color)
|
|
1496
1697
|
}
|
|
1497
|
-
case 'webview':
|
|
1498
|
-
view = makeWebView()
|
|
1499
|
-
break
|
|
1500
|
-
case 'image':
|
|
1501
|
-
view = makeImageView(props)
|
|
1502
|
-
break
|
|
1503
|
-
case 'span':
|
|
1504
|
-
view = null
|
|
1505
|
-
break
|
|
1506
|
-
default:
|
|
1507
|
-
throw new Error('AppKit spike does not support <' + type + '>')
|
|
1508
1698
|
}
|
|
1509
1699
|
|
|
1510
|
-
if (type === '
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1700
|
+
if (type === 'gridlayout') {
|
|
1701
|
+
// layoutGridChildren parses the track lists on every apply.
|
|
1702
|
+
parseGridTracks(props?.rows)
|
|
1703
|
+
parseGridTracks(props?.columns)
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
if (type === 'textfield' && props?.placeholderTextColor != null) {
|
|
1707
|
+
// setTextFieldPlaceholder only reaches the color when text exists.
|
|
1708
|
+
const placeholder = props.placeholder ?? baseProps?.placeholder
|
|
1709
|
+
if (String(placeholder ?? '') !== '') {
|
|
1710
|
+
nativeColor(props.placeholderTextColor)
|
|
1520
1711
|
}
|
|
1521
1712
|
}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
function makeNode(container: RootContainer, id: number, type: string, props: PropBag) {
|
|
1716
|
+
// Reject malformed props before factories allocate views or register
|
|
1717
|
+
// actions — the node only reaches container.nodes after this returns, so
|
|
1718
|
+
// a throwing prop below must unwind whatever was already acquired.
|
|
1719
|
+
assertSupportedType(type)
|
|
1720
|
+
validateNodeProps(type, props)
|
|
1522
1721
|
|
|
1523
1722
|
const node: ElementNode = {
|
|
1524
1723
|
id,
|
|
1525
1724
|
type,
|
|
1526
|
-
view,
|
|
1527
|
-
childHost,
|
|
1725
|
+
view: null,
|
|
1528
1726
|
props: {},
|
|
1529
1727
|
parent: null,
|
|
1530
1728
|
children: [],
|
|
1531
1729
|
container,
|
|
1532
1730
|
appliedFontFamily: container.fontFamily,
|
|
1533
|
-
actionId,
|
|
1534
1731
|
scrollObserverInstalled: false,
|
|
1535
1732
|
text: '',
|
|
1733
|
+
// The native class is fixed at creation; post-mount `secure` updates
|
|
1734
|
+
// are rejected against this in applyProps.
|
|
1735
|
+
secure: type === 'textfield' && !!props.secure,
|
|
1536
1736
|
}
|
|
1537
1737
|
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1738
|
+
try {
|
|
1739
|
+
switch (type) {
|
|
1740
|
+
case 'stack':
|
|
1741
|
+
node.view = makeStack(props)
|
|
1742
|
+
break
|
|
1743
|
+
case 'flexboxlayout': {
|
|
1744
|
+
const flexbox = makeFlexbox(props)
|
|
1745
|
+
node.view = flexbox.view
|
|
1746
|
+
node.actionId = flexbox.actionId
|
|
1747
|
+
break
|
|
1748
|
+
}
|
|
1749
|
+
case 'gridlayout':
|
|
1750
|
+
node.view = makeGridLayout()
|
|
1751
|
+
break
|
|
1752
|
+
case 'absolutelayout':
|
|
1753
|
+
node.view = makeAbsoluteLayout()
|
|
1754
|
+
break
|
|
1755
|
+
case 'label':
|
|
1756
|
+
node.view = makeLabel()
|
|
1757
|
+
break
|
|
1758
|
+
case '#text':
|
|
1759
|
+
break
|
|
1760
|
+
case 'button': {
|
|
1761
|
+
const button = makeButton(props)
|
|
1762
|
+
node.view = button.view
|
|
1763
|
+
node.actionId = button.actionId
|
|
1764
|
+
break
|
|
1765
|
+
}
|
|
1766
|
+
case 'scrollview': {
|
|
1767
|
+
const scroll = makeScrollView()
|
|
1768
|
+
node.view = scroll.view
|
|
1769
|
+
node.childHost = scroll.childHost
|
|
1770
|
+
break
|
|
1771
|
+
}
|
|
1772
|
+
case 'textfield':
|
|
1773
|
+
node.view = makeTextField(props)
|
|
1774
|
+
break
|
|
1775
|
+
case 'textview':
|
|
1776
|
+
node.view = makeTextField(props, true)
|
|
1777
|
+
break
|
|
1778
|
+
case 'switch': {
|
|
1779
|
+
const control = makeSwitch(props)
|
|
1780
|
+
node.view = control.view
|
|
1781
|
+
node.actionId = control.actionId
|
|
1782
|
+
break
|
|
1783
|
+
}
|
|
1784
|
+
case 'slider': {
|
|
1785
|
+
const control = makeSlider(props)
|
|
1786
|
+
node.view = control.view
|
|
1787
|
+
node.actionId = control.actionId
|
|
1788
|
+
break
|
|
1789
|
+
}
|
|
1790
|
+
case 'webview':
|
|
1791
|
+
node.view = makeWebView()
|
|
1792
|
+
break
|
|
1793
|
+
case 'image':
|
|
1794
|
+
node.view = makeImageView(props)
|
|
1795
|
+
break
|
|
1796
|
+
case 'span':
|
|
1797
|
+
break
|
|
1798
|
+
default:
|
|
1799
|
+
throw new Error('AppKit spike does not support <' + type + '>')
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
if (type === 'textfield' || type === 'textview') {
|
|
1803
|
+
node.actionId = nextActionId++
|
|
1804
|
+
actionIdsByView.set(node.view!, node.actionId)
|
|
1805
|
+
actionHandlers.set(node.actionId, null)
|
|
1806
|
+
if (type === 'textfield') {
|
|
1807
|
+
// Per-edit events arrive through controlTextDidChange (delegate);
|
|
1808
|
+
// the target/action is reserved for submission on Return.
|
|
1809
|
+
node.view!.delegate = buttonActionTarget
|
|
1810
|
+
node.view!.target = buttonActionTarget
|
|
1811
|
+
node.view!.action = 'textFieldSubmitted:'
|
|
1812
|
+
} else {
|
|
1813
|
+
node.view!.delegate = buttonActionTarget
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1541
1816
|
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1817
|
+
if (['label', 'textfield', 'textview'].includes(type) && node.view!.font) {
|
|
1818
|
+
node.view!.font = fontForFamilyStyle(node.view!.font.pointSize, 400, container.fontFamily)
|
|
1819
|
+
}
|
|
1545
1820
|
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1821
|
+
if (type === 'gridlayout') {
|
|
1822
|
+
gridLayoutNodesByView.set(node.view!, node)
|
|
1823
|
+
}
|
|
1549
1824
|
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
placeholder.textColor = nativeColor('#666666')
|
|
1554
|
-
placeholder.stringValue = String(props.placeholder ?? '')
|
|
1555
|
-
placeholder.translatesAutoresizingMaskIntoConstraints = false
|
|
1556
|
-
placeholder.heightAnchor.constraintEqualToConstant(
|
|
1557
|
-
Math.ceil(14 * DEFAULT_TEXT_LINE_HEIGHT_RATIO),
|
|
1558
|
-
).active = true
|
|
1825
|
+
if (type === 'absolutelayout') {
|
|
1826
|
+
absoluteLayoutNodesByView.set(node.view!, node)
|
|
1827
|
+
}
|
|
1559
1828
|
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1829
|
+
if (type === 'textfield') {
|
|
1830
|
+
textNodesByView.set(node.view!, node)
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
if (type === 'textview') {
|
|
1834
|
+
const placeholder = makeLabel()
|
|
1835
|
+
placeholder.font = fontForFamilyStyle(14, 400, node.appliedFontFamily)
|
|
1836
|
+
placeholder.textColor = nativeColor('#666666')
|
|
1837
|
+
placeholder.stringValue = String(props.placeholder ?? '')
|
|
1838
|
+
placeholder.translatesAutoresizingMaskIntoConstraints = false
|
|
1839
|
+
placeholder.heightAnchor.constraintEqualToConstant(
|
|
1840
|
+
Math.ceil(14 * DEFAULT_TEXT_LINE_HEIGHT_RATIO),
|
|
1841
|
+
).active = true
|
|
1842
|
+
|
|
1843
|
+
placeholder.hidden = String(props.value ?? '').length > 0
|
|
1844
|
+
node.view!.addSubview(placeholder)
|
|
1845
|
+
placeholder.leadingAnchor.constraintEqualToAnchorConstant(
|
|
1846
|
+
node.view!.leadingAnchor,
|
|
1847
|
+
0,
|
|
1848
|
+
).active = true
|
|
1849
|
+
|
|
1850
|
+
placeholder.topAnchor.constraintEqualToAnchor(node.view!.topAnchor).active = true
|
|
1851
|
+
node.placeholderView = placeholder
|
|
1852
|
+
textNodesByView.set(node.view!, node)
|
|
1853
|
+
}
|
|
1567
1854
|
|
|
1568
|
-
|
|
1569
|
-
|
|
1855
|
+
applyProps(node, props)
|
|
1856
|
+
return node
|
|
1857
|
+
} catch (error) {
|
|
1858
|
+
// Release whatever the failed creation registered — action slots,
|
|
1859
|
+
// observers, WebView delegates — since no caller can destroy it.
|
|
1860
|
+
destroy(node)
|
|
1861
|
+
throw error
|
|
1862
|
+
}
|
|
1570
1863
|
}
|
|
1571
1864
|
|
|
1572
1865
|
const EDGE_INSET_PROPS = new Map([
|
|
@@ -1694,16 +1987,20 @@ function applyThemeColors(node: ElementNode) {
|
|
|
1694
1987
|
}
|
|
1695
1988
|
|
|
1696
1989
|
if (node.type === 'textfield') {
|
|
1697
|
-
setTextFieldPlaceholder(view, node.props, node.scheme)
|
|
1990
|
+
setTextFieldPlaceholder(view as NSTextField, node.props, node.scheme)
|
|
1698
1991
|
}
|
|
1699
1992
|
|
|
1700
1993
|
if (node.type === 'textview' && node.placeholderView) {
|
|
1701
1994
|
node.placeholderView.textColor = nativeColor(colors.placeholder)
|
|
1702
1995
|
}
|
|
1703
1996
|
|
|
1704
|
-
if (node.bgSlot) {
|
|
1997
|
+
if (node.bgSlot || node.styleBg != null) {
|
|
1998
|
+
node.bgApplied = true
|
|
1705
1999
|
view.wantsLayer = true
|
|
1706
2000
|
view.layer.backgroundColor = nativeColor(node.styleBg ?? colors[node.bgSlot]).CGColor
|
|
2001
|
+
} else if (node.bgApplied) {
|
|
2002
|
+
node.bgApplied = false
|
|
2003
|
+
view.layer.backgroundColor = null
|
|
1707
2004
|
}
|
|
1708
2005
|
}
|
|
1709
2006
|
|
|
@@ -1729,28 +2026,25 @@ function setSizeConstraint(node: ElementNode, name: string, value: any) {
|
|
|
1729
2026
|
const specs = (node.sizeConstraintSpecs ??= {})
|
|
1730
2027
|
if (!spec) {
|
|
1731
2028
|
delete specs[name]
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
2029
|
+
// null/undefined is an intentional clear — only malformed values warn.
|
|
2030
|
+
if (value != null) {
|
|
2031
|
+
console.warn(
|
|
2032
|
+
'[macos-style] ignored unsupported style.' + name + ' value ' + JSON.stringify(value),
|
|
2033
|
+
)
|
|
2034
|
+
}
|
|
1735
2035
|
} else {
|
|
1736
2036
|
specs[name] = spec
|
|
1737
2037
|
}
|
|
1738
2038
|
|
|
1739
|
-
if (node.parent?.type === 'gridlayout') {
|
|
1740
|
-
deactivateSizeConstraints(node)
|
|
1741
|
-
layoutGridChildren(node.parent)
|
|
1742
|
-
return
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
if (node.parent?.type === 'absolutelayout') {
|
|
2039
|
+
if (node.parent?.type === 'gridlayout' || node.parent?.type === 'absolutelayout') {
|
|
1746
2040
|
deactivateSizeConstraints(node)
|
|
1747
|
-
|
|
2041
|
+
queueLayoutReconcile(node.container, node.parent)
|
|
1748
2042
|
return
|
|
1749
2043
|
}
|
|
1750
2044
|
|
|
1751
2045
|
applySizeConstraint(node, name)
|
|
1752
2046
|
if (node.parent) {
|
|
1753
|
-
|
|
2047
|
+
queueLayoutReconcile(node.container, node.parent)
|
|
1754
2048
|
}
|
|
1755
2049
|
}
|
|
1756
2050
|
|
|
@@ -1831,21 +2125,201 @@ function deactivateSizeConstraints(node: ElementNode) {
|
|
|
1831
2125
|
}
|
|
1832
2126
|
}
|
|
1833
2127
|
|
|
1834
|
-
|
|
1835
|
-
|
|
2128
|
+
// Style, class, and (for labels) prop sources all write one native font.
|
|
2129
|
+
// Each source is tracked on the node so removing one re-resolves the rest —
|
|
2130
|
+
// explicit style wins over classes, which win over the element defaults.
|
|
2131
|
+
function syncNodeFont(node: ElementNode) {
|
|
2132
|
+
const size =
|
|
2133
|
+
node.styleFontSize ??
|
|
2134
|
+
node.propFontSize ??
|
|
2135
|
+
node.classFontSize ??
|
|
2136
|
+
(node.type === 'label' ? 16 : 14)
|
|
2137
|
+
|
|
2138
|
+
const weight = node.styleFontWeight ?? node.classFontWeight ?? 400
|
|
2139
|
+
node.appliedFontWeight = String(weight)
|
|
2140
|
+
node.appliedFontFamily = node.styleFontFamily ?? node.container.fontFamily
|
|
2141
|
+
node.view!.font = fontForFamilyStyle(size, weight, node.appliedFontFamily)
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
// NSStackView edgeInsets blend padding styles over class padding over zero.
|
|
2145
|
+
// Edges a style never set fall through to the class contribution.
|
|
2146
|
+
function syncEdgeInsets(node: ElementNode) {
|
|
2147
|
+
const view = node.view
|
|
2148
|
+
if (!view) {
|
|
2149
|
+
return
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
const edges = node.stylePaddingEdges
|
|
2153
|
+
const box = node.stylePaddingBox
|
|
2154
|
+
const klass = node.classInsets
|
|
2155
|
+
if (!edges && !box && !klass && !node.insetsApplied) {
|
|
2156
|
+
return
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
node.insetsApplied = true
|
|
2160
|
+
view.edgeInsets = {
|
|
2161
|
+
top: edges?.top ?? box?.top ?? klass?.top ?? 0,
|
|
2162
|
+
right: edges?.right ?? box?.right ?? klass?.right ?? 0,
|
|
2163
|
+
bottom: edges?.bottom ?? box?.bottom ?? klass?.bottom ?? 0,
|
|
2164
|
+
left: edges?.left ?? box?.left ?? klass?.left ?? 0,
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
// borderRadius (style) and rounded-* (class) share cornerRadius; either source
|
|
2169
|
+
// keeps masksToBounds on only while a positive radius is in effect.
|
|
2170
|
+
function syncCornerRadius(node: ElementNode) {
|
|
2171
|
+
const view = node.view
|
|
2172
|
+
if (!view) {
|
|
2173
|
+
return
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
const radius = Number(node.styleRadius ?? node.classRadius ?? 0) || 0
|
|
2177
|
+
if (radius <= 0 && !node.radiusApplied) {
|
|
2178
|
+
return
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
node.radiusApplied = radius > 0
|
|
2182
|
+
view.wantsLayer = true
|
|
2183
|
+
view.layer.cornerRadius = Math.max(0, radius)
|
|
2184
|
+
view.layer.masksToBounds = radius > 0
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
// Zero the margin sides a removed style owned, then let the stack unwrap or
|
|
2188
|
+
// the grid relayout — marginInsetsOf() returns null once every side is zero.
|
|
2189
|
+
function clearMarginStyle(node: ElementNode, name: string) {
|
|
2190
|
+
const insets = node.marginInsets
|
|
2191
|
+
if (!insets) {
|
|
2192
|
+
return
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
if (name === 'margin') {
|
|
2196
|
+
insets.top = insets.right = insets.bottom = insets.left = 0
|
|
2197
|
+
} else {
|
|
2198
|
+
insets[MARGIN_SIDES[name]] = 0
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
if (node.parent?.type === 'gridlayout') {
|
|
2202
|
+
layoutGridChildren(node.parent)
|
|
1836
2203
|
return
|
|
1837
2204
|
}
|
|
1838
2205
|
|
|
1839
|
-
|
|
2206
|
+
syncMarginHost(node)
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
// A style key present in the last bag but absent or nulled in the new one must
|
|
2210
|
+
// put back the native state it installed: constraints deactivate, hit-testing
|
|
2211
|
+
// reverts, and shared properties resolve their class/default source again.
|
|
2212
|
+
function resetStyle(node: ElementNode, name: string) {
|
|
2213
|
+
const view = node.view
|
|
2214
|
+
if (!view) {
|
|
2215
|
+
return
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
const textControl = ['label', 'textfield', 'textview'].includes(node.type)
|
|
2219
|
+
if (name === 'pointerEvents') {
|
|
2220
|
+
inputTransparentViews.delete(view)
|
|
2221
|
+
} else if (name === 'objectFit' && node.type === 'image') {
|
|
2222
|
+
// NSImageScaleProportionallyDown — the makeImageView/AppKit default.
|
|
2223
|
+
view.imageScaling = 0
|
|
2224
|
+
} else if (name === 'fontSize' && textControl) {
|
|
2225
|
+
node.styleFontSize = undefined
|
|
2226
|
+
syncNodeFont(node)
|
|
2227
|
+
} else if (name === 'fontFamily' && textControl) {
|
|
2228
|
+
node.styleFontFamily = undefined
|
|
2229
|
+
syncNodeFont(node)
|
|
2230
|
+
} else if (name === 'fontWeight' && textControl) {
|
|
2231
|
+
node.styleFontWeight = undefined
|
|
2232
|
+
syncNodeFont(node)
|
|
2233
|
+
} else if (name === 'color' && textControl) {
|
|
2234
|
+
node.styleColor = undefined
|
|
2235
|
+
applyThemeColors(node)
|
|
2236
|
+
} else if (name === 'padding' && node.type === 'flexboxlayout') {
|
|
2237
|
+
node.stylePaddingBox = undefined
|
|
2238
|
+
syncEdgeInsets(node)
|
|
2239
|
+
} else if (EDGE_INSET_PROPS.has(name) && node.type === 'flexboxlayout') {
|
|
2240
|
+
for (const edge of EDGE_INSET_PROPS.get(name)!) {
|
|
2241
|
+
delete node.stylePaddingEdges?.[edge]
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
syncEdgeInsets(node)
|
|
2245
|
+
} else if (name === 'backgroundColor') {
|
|
2246
|
+
node.styleBg = undefined
|
|
2247
|
+
applyThemeColors(node)
|
|
2248
|
+
} else if (name === 'borderRadius') {
|
|
2249
|
+
node.styleRadius = undefined
|
|
2250
|
+
syncCornerRadius(node)
|
|
2251
|
+
} else if (name === 'width' || name === 'height') {
|
|
2252
|
+
setSizeConstraint(node, name, undefined)
|
|
2253
|
+
} else if (name === 'left' || name === 'top' || name === 'right' || name === 'bottom') {
|
|
2254
|
+
if (node.parent?.type === 'gridlayout') {
|
|
2255
|
+
layoutGridChildren(node.parent)
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
if (node.parent?.type === 'absolutelayout') {
|
|
2259
|
+
layoutAbsoluteChildren(node.parent)
|
|
2260
|
+
}
|
|
2261
|
+
} else if (name === 'translateX' || name === 'translateY') {
|
|
2262
|
+
if (typeof view.layer?.setValueForKeyPath === 'function') {
|
|
2263
|
+
view.layer.setValueForKeyPath(
|
|
2264
|
+
0,
|
|
2265
|
+
name === 'translateY' ? 'transform.translation.y' : 'transform.translation.x',
|
|
2266
|
+
)
|
|
2267
|
+
}
|
|
2268
|
+
} else if (name === 'zIndex') {
|
|
2269
|
+
if (view.layer) {
|
|
2270
|
+
view.layer.zPosition = 0
|
|
2271
|
+
}
|
|
2272
|
+
} else if (name === 'opacity') {
|
|
2273
|
+
view.alphaValue = 1
|
|
2274
|
+
} else if (name === 'textAlign' && node.type === 'label') {
|
|
2275
|
+
view.alignment = nodeClasses(node).includes('vx-sheet-grabber')
|
|
2276
|
+
? NSTextAlignment.Center
|
|
2277
|
+
: NSTextAlignment.Left
|
|
2278
|
+
} else if (name === 'borderWidth') {
|
|
2279
|
+
if (view.layer) {
|
|
2280
|
+
view.layer.borderWidth = 0
|
|
2281
|
+
}
|
|
2282
|
+
} else if (name === 'borderColor') {
|
|
2283
|
+
if (view.layer) {
|
|
2284
|
+
view.layer.borderColor = null
|
|
2285
|
+
}
|
|
2286
|
+
} else if (MARGIN_SIDES[name] || name === 'margin') {
|
|
2287
|
+
clearMarginStyle(node, name)
|
|
2288
|
+
}
|
|
2289
|
+
// lineHeight folds into setLabelText from props.style — no native reset.
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
function applyStyle(node: ElementNode, style: PropBag) {
|
|
2293
|
+
if (style != null && typeof style !== 'object') {
|
|
1840
2294
|
throw new Error('AppKit spike expects style to be an object')
|
|
1841
2295
|
}
|
|
1842
2296
|
|
|
1843
|
-
|
|
2297
|
+
// The style prop replaces wholesale, so keys dropped or nulled since the
|
|
2298
|
+
// last apply must reset the native state they installed before the current
|
|
2299
|
+
// values go on — otherwise removed styles linger on the view.
|
|
2300
|
+
const next: PropBag = style ?? {}
|
|
2301
|
+
if (node.type === 'flexboxlayout' || node.type === 'stack') {
|
|
2302
|
+
node.styleLayout = Object.fromEntries(
|
|
2303
|
+
Object.entries(next).filter(([name]) => isStackLayoutInput(name)),
|
|
2304
|
+
)
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
const prev: PropBag = node.appliedStyle ?? {}
|
|
2308
|
+
for (const name of Object.keys(prev)) {
|
|
2309
|
+
if (prev[name] != null && next[name] == null) {
|
|
2310
|
+
resetStyle(node, name)
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
for (const [name, value] of Object.entries(next)) {
|
|
1844
2315
|
if (value == null) {
|
|
1845
2316
|
continue
|
|
1846
2317
|
}
|
|
1847
2318
|
|
|
1848
|
-
if (
|
|
2319
|
+
if ((node.type === 'flexboxlayout' || node.type === 'stack') && isStackLayoutInput(name)) {
|
|
2320
|
+
// Applied together below, after all style inputs have reconciled.
|
|
2321
|
+
continue
|
|
2322
|
+
} else if (name === 'pointerEvents') {
|
|
1849
2323
|
if (value === 'none') {
|
|
1850
2324
|
inputTransparentViews.add(node.view!)
|
|
1851
2325
|
} else {
|
|
@@ -1855,20 +2329,20 @@ function applyStyle(node: ElementNode, style: PropBag) {
|
|
|
1855
2329
|
// NSImageScaling: 0 ProportionallyDown, 1 AxesIndependently,
|
|
1856
2330
|
// 2 None (centered, unscaled), 3 ProportionallyUpOrDown.
|
|
1857
2331
|
// AppKit has no cover mode — 'cover' degrades to scale-down.
|
|
1858
|
-
|
|
2332
|
+
const imageScaling: Record<string, number> = {
|
|
2333
|
+
contain: 3,
|
|
2334
|
+
'scale-down': 0,
|
|
2335
|
+
fill: 1,
|
|
2336
|
+
none: 2,
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
node.view!.imageScaling = imageScaling[String(value)] ?? 0
|
|
1859
2340
|
} else if (name === 'fontSize' && ['label', 'textfield', 'textview'].includes(node.type)) {
|
|
1860
|
-
|
|
1861
|
-
node
|
|
1862
|
-
node.view!.font = fontForFamilyStyle(
|
|
1863
|
-
value,
|
|
1864
|
-
weight,
|
|
1865
|
-
style.fontFamily ?? node.appliedFontFamily,
|
|
1866
|
-
)
|
|
2341
|
+
node.styleFontSize = value
|
|
2342
|
+
syncNodeFont(node)
|
|
1867
2343
|
} else if (name === 'fontFamily' && ['label', 'textfield', 'textview'].includes(node.type)) {
|
|
1868
|
-
node.
|
|
1869
|
-
|
|
1870
|
-
const weight = style.fontWeight ?? node.appliedFontWeight ?? 400
|
|
1871
|
-
node.view!.font = fontForFamilyStyle(size, weight, value)
|
|
2344
|
+
node.styleFontFamily = String(value)
|
|
2345
|
+
syncNodeFont(node)
|
|
1872
2346
|
} else if (name === 'color' && ['label', 'textfield', 'textview'].includes(node.type)) {
|
|
1873
2347
|
node.styleColor = String(value)
|
|
1874
2348
|
node.view!.textColor = nativeColor(value)
|
|
@@ -1876,31 +2350,28 @@ function applyStyle(node: ElementNode, style: PropBag) {
|
|
|
1876
2350
|
// syncText applies this to each paragraph; it is not the label's fixed height.
|
|
1877
2351
|
continue
|
|
1878
2352
|
} else if (name === 'padding' && node.type === 'flexboxlayout') {
|
|
1879
|
-
node.
|
|
2353
|
+
node.stylePaddingBox = parseEdgeInsets(value)
|
|
2354
|
+
syncEdgeInsets(node)
|
|
1880
2355
|
} else if (EDGE_INSET_PROPS.has(name) && node.type === 'flexboxlayout') {
|
|
1881
|
-
const
|
|
2356
|
+
const edges = (node.stylePaddingEdges ??= {})
|
|
1882
2357
|
for (const edge of EDGE_INSET_PROPS.get(name)!) {
|
|
1883
|
-
|
|
2358
|
+
edges[edge] = Number(value) || 0
|
|
1884
2359
|
}
|
|
1885
2360
|
|
|
1886
|
-
node
|
|
2361
|
+
syncEdgeInsets(node)
|
|
1887
2362
|
} else if (name === 'backgroundColor' && node.view) {
|
|
1888
2363
|
node.styleBg = String(value)
|
|
2364
|
+
node.bgApplied = true
|
|
1889
2365
|
node.view.wantsLayer = true
|
|
1890
2366
|
node.view.layer.backgroundColor = nativeColor(value).CGColor
|
|
1891
2367
|
} else if (name === 'borderRadius' && node.view) {
|
|
1892
|
-
node.
|
|
1893
|
-
node
|
|
1894
|
-
node.view.layer.masksToBounds = true
|
|
2368
|
+
node.styleRadius = value
|
|
2369
|
+
syncCornerRadius(node)
|
|
1895
2370
|
} else if ((name === 'width' || name === 'height') && node.view) {
|
|
1896
2371
|
setSizeConstraint(node, name, value)
|
|
1897
2372
|
} else if (name === 'left' && node.view) {
|
|
1898
|
-
if (node.parent?.type === 'gridlayout') {
|
|
1899
|
-
|
|
1900
|
-
}
|
|
1901
|
-
|
|
1902
|
-
if (node.parent?.type === 'absolutelayout') {
|
|
1903
|
-
layoutAbsoluteChildren(node.parent)
|
|
2373
|
+
if (node.parent?.type === 'gridlayout' || node.parent?.type === 'absolutelayout') {
|
|
2374
|
+
queueLayoutReconcile(node.container, node.parent)
|
|
1904
2375
|
}
|
|
1905
2376
|
} else if ((name === 'translateX' || name === 'translateY' || name === 'zIndex') && node.view) {
|
|
1906
2377
|
node.view.wantsLayer = true
|
|
@@ -1917,12 +2388,8 @@ function applyStyle(node: ElementNode, style: PropBag) {
|
|
|
1917
2388
|
} else if (name === 'opacity' && node.view) {
|
|
1918
2389
|
node.view.alphaValue = Number(value)
|
|
1919
2390
|
} else if (name === 'fontWeight' && ['label', 'textfield', 'textview'].includes(node.type)) {
|
|
1920
|
-
node.
|
|
1921
|
-
node
|
|
1922
|
-
node.view!.font.pointSize,
|
|
1923
|
-
value,
|
|
1924
|
-
style.fontFamily ?? node.appliedFontFamily,
|
|
1925
|
-
)
|
|
2391
|
+
node.styleFontWeight = String(value)
|
|
2392
|
+
syncNodeFont(node)
|
|
1926
2393
|
} else if (name === 'textAlign' && node.type === 'label') {
|
|
1927
2394
|
node.view!.alignment =
|
|
1928
2395
|
value === 'left'
|
|
@@ -1942,6 +2409,9 @@ function applyStyle(node: ElementNode, style: PropBag) {
|
|
|
1942
2409
|
console.warn('[macos-style] ignored unsupported style.' + name + ' on <' + node.type + '>')
|
|
1943
2410
|
}
|
|
1944
2411
|
}
|
|
2412
|
+
|
|
2413
|
+
node.appliedStyle = next
|
|
2414
|
+
syncStackLayout(node)
|
|
1945
2415
|
}
|
|
1946
2416
|
|
|
1947
2417
|
function applyClassName(node: ElementNode, value: any) {
|
|
@@ -1949,11 +2419,23 @@ function applyClassName(node: ElementNode, value: any) {
|
|
|
1949
2419
|
.split(/\s+/)
|
|
1950
2420
|
.filter(Boolean)
|
|
1951
2421
|
|
|
2422
|
+
// Class effects re-derive from scratch on every apply: the class-owned
|
|
2423
|
+
// fields reset here and re-populate below, so removing a class drops the
|
|
2424
|
+
// native state it installed instead of leaving stale values behind.
|
|
2425
|
+
node.classFontSize = undefined
|
|
2426
|
+
node.classFontWeight = undefined
|
|
2427
|
+
node.classInsets = undefined
|
|
2428
|
+
node.classRadius = undefined
|
|
2429
|
+
node.classSpacing = undefined
|
|
2430
|
+
node.classOrientation = undefined
|
|
2431
|
+
node.colorSlot = undefined
|
|
2432
|
+
node.bgSlot = undefined
|
|
2433
|
+
|
|
1952
2434
|
if (node.type === 'label') {
|
|
1953
|
-
|
|
2435
|
+
const grabber = classes.includes('vx-sheet-grabber')
|
|
2436
|
+
if (grabber) {
|
|
1954
2437
|
node.classLineHeight = 24
|
|
1955
2438
|
setSizeConstraint(node, 'height', 24)
|
|
1956
|
-
node.view!.alignment = NSTextAlignment.Center
|
|
1957
2439
|
}
|
|
1958
2440
|
|
|
1959
2441
|
const sizes: Record<string, number> = {
|
|
@@ -1986,18 +2468,14 @@ function applyClassName(node: ElementNode, value: any) {
|
|
|
1986
2468
|
for (const name of classes) {
|
|
1987
2469
|
const heading = headingMetrics[name]
|
|
1988
2470
|
if (heading) {
|
|
1989
|
-
node.
|
|
2471
|
+
node.classFontWeight = '700'
|
|
2472
|
+
node.classFontSize = heading.size
|
|
1990
2473
|
node.headingDefaultHeight = heading.height
|
|
1991
|
-
node.view!.font = fontForFamilyStyle(heading.size, 700, node.appliedFontFamily)
|
|
1992
2474
|
}
|
|
1993
2475
|
|
|
1994
2476
|
if (sizes[name]) {
|
|
2477
|
+
node.classFontSize = sizes[name]
|
|
1995
2478
|
node.headingDefaultHeight = undefined
|
|
1996
|
-
node.view!.font = fontForFamilyStyle(
|
|
1997
|
-
sizes[name],
|
|
1998
|
-
node.appliedFontWeight ?? 400,
|
|
1999
|
-
node.appliedFontFamily,
|
|
2000
|
-
)
|
|
2001
2479
|
}
|
|
2002
2480
|
|
|
2003
2481
|
if (lineHeights[name] != null) {
|
|
@@ -2005,13 +2483,11 @@ function applyClassName(node: ElementNode, value: any) {
|
|
|
2005
2483
|
}
|
|
2006
2484
|
|
|
2007
2485
|
if (name === 'font-semibold') {
|
|
2008
|
-
node.
|
|
2009
|
-
node.view!.font = fontForFamilyStyle(node.view!.font.pointSize, 600, node.appliedFontFamily)
|
|
2486
|
+
node.classFontWeight = '600'
|
|
2010
2487
|
}
|
|
2011
2488
|
|
|
2012
2489
|
if (name === 'font-bold') {
|
|
2013
|
-
node.
|
|
2014
|
-
node.view!.font = fontForFamilyStyle(node.view!.font.pointSize, 700, node.appliedFontFamily)
|
|
2490
|
+
node.classFontWeight = '700'
|
|
2015
2491
|
}
|
|
2016
2492
|
|
|
2017
2493
|
if (name === 'text-muted') {
|
|
@@ -2022,6 +2498,28 @@ function applyClassName(node: ElementNode, value: any) {
|
|
|
2022
2498
|
node.colorSlot = 'onprimary'
|
|
2023
2499
|
}
|
|
2024
2500
|
}
|
|
2501
|
+
|
|
2502
|
+
syncNodeFont(node)
|
|
2503
|
+
|
|
2504
|
+
if (!grabber && node.props?.style?.height != null) {
|
|
2505
|
+
// The grabber's 24pt pin shares the height spec with style.height —
|
|
2506
|
+
// re-assert the style value once the class is gone.
|
|
2507
|
+
setSizeConstraint(node, 'height', node.props.style.height)
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
if (grabber) {
|
|
2511
|
+
node.view!.alignment = NSTextAlignment.Center
|
|
2512
|
+
} else {
|
|
2513
|
+
// Without the grabber the label returns to its textAlign — or the
|
|
2514
|
+
// Left default when no alignment style is in effect.
|
|
2515
|
+
const textAlign = node.props?.style?.textAlign
|
|
2516
|
+
node.view!.alignment =
|
|
2517
|
+
textAlign == null || textAlign === 'left'
|
|
2518
|
+
? NSTextAlignment.Left
|
|
2519
|
+
: textAlign === 'right'
|
|
2520
|
+
? NSTextAlignment.Right
|
|
2521
|
+
: NSTextAlignment.Center
|
|
2522
|
+
}
|
|
2025
2523
|
}
|
|
2026
2524
|
|
|
2027
2525
|
if (
|
|
@@ -2040,37 +2538,27 @@ function applyClassName(node: ElementNode, value: any) {
|
|
|
2040
2538
|
|
|
2041
2539
|
for (const name of classes) {
|
|
2042
2540
|
if (gaps[name] !== undefined) {
|
|
2043
|
-
node.
|
|
2541
|
+
node.classSpacing = gaps[name]
|
|
2044
2542
|
}
|
|
2045
2543
|
|
|
2046
2544
|
if (name === 'flex-row') {
|
|
2047
|
-
node.
|
|
2545
|
+
node.classOrientation = NSUserInterfaceLayoutOrientation.Horizontal
|
|
2048
2546
|
}
|
|
2049
2547
|
|
|
2050
2548
|
if (name === 'flex-col') {
|
|
2051
|
-
node.
|
|
2052
|
-
}
|
|
2053
|
-
|
|
2054
|
-
if (name === 'items-center' || name === 'items-start' || name === 'items-end') {
|
|
2055
|
-
node.view!.alignment = stackAlignmentAttribute(node.view, stackAlignItems(node))
|
|
2549
|
+
node.classOrientation = NSUserInterfaceLayoutOrientation.Vertical
|
|
2056
2550
|
}
|
|
2057
2551
|
|
|
2058
2552
|
if (name === 'vx-toast' && node.type === 'flexboxlayout') {
|
|
2059
|
-
|
|
2553
|
+
node.classInsets = { top: 12, right: 12, bottom: 12, left: 12 }
|
|
2060
2554
|
}
|
|
2061
2555
|
|
|
2062
2556
|
if (name === 'vx-toast-viewport') {
|
|
2063
|
-
|
|
2557
|
+
node.classSpacing = 8
|
|
2064
2558
|
}
|
|
2065
2559
|
|
|
2066
2560
|
if (name === 'vx-button') {
|
|
2067
|
-
node.
|
|
2068
|
-
node.view!.alignment = NSLayoutAttribute.CenterY
|
|
2069
|
-
node.view!.distribution = NSStackViewDistribution.GravityAreas
|
|
2070
|
-
}
|
|
2071
|
-
|
|
2072
|
-
if (name === 'justify-between') {
|
|
2073
|
-
node.view!.distribution = NSStackViewDistribution.EqualSpacing
|
|
2561
|
+
node.classOrientation = NSUserInterfaceLayoutOrientation.Horizontal
|
|
2074
2562
|
}
|
|
2075
2563
|
|
|
2076
2564
|
if (name === 'flex-1') {
|
|
@@ -2093,37 +2581,21 @@ function applyClassName(node: ElementNode, value: any) {
|
|
|
2093
2581
|
}
|
|
2094
2582
|
|
|
2095
2583
|
if (name === 'rounded-full' || name.startsWith('rounded-')) {
|
|
2096
|
-
node.
|
|
2097
|
-
node.view!.layer.cornerRadius = name === 'rounded-full' ? 12 : 8
|
|
2098
|
-
node.view!.layer.masksToBounds = true
|
|
2584
|
+
node.classRadius = name === 'rounded-full' ? 12 : 8
|
|
2099
2585
|
}
|
|
2100
2586
|
|
|
2101
2587
|
if (name === 'bg-primary' || name === 'btn') {
|
|
2102
2588
|
node.bgSlot = 'primary'
|
|
2103
|
-
node.
|
|
2104
|
-
node.view!.layer.backgroundColor = nativeColor(
|
|
2105
|
-
SCHEME_COLORS[nodeScheme(node)].primary,
|
|
2106
|
-
).CGColor
|
|
2107
|
-
|
|
2108
|
-
node.view!.edgeInsets = { top: 6, left: 10, bottom: 6, right: 10 }
|
|
2589
|
+
node.classInsets = { top: 6, right: 10, bottom: 6, left: 10 }
|
|
2109
2590
|
}
|
|
2110
2591
|
|
|
2111
2592
|
if (name === 'bg-danger') {
|
|
2112
2593
|
node.bgSlot = 'danger'
|
|
2113
|
-
node.view!.wantsLayer = true
|
|
2114
|
-
node.view!.layer.backgroundColor = nativeColor(
|
|
2115
|
-
SCHEME_COLORS[nodeScheme(node)].danger,
|
|
2116
|
-
).CGColor
|
|
2117
2594
|
}
|
|
2118
2595
|
|
|
2119
2596
|
if (name === 'btn-secondary' || name === 'chip' || name === 'chip-off') {
|
|
2120
2597
|
node.bgSlot = 'secondary'
|
|
2121
|
-
node.
|
|
2122
|
-
node.view!.layer.backgroundColor = nativeColor(
|
|
2123
|
-
SCHEME_COLORS[nodeScheme(node)].secondary,
|
|
2124
|
-
).CGColor
|
|
2125
|
-
|
|
2126
|
-
node.view!.edgeInsets = { top: 4, left: 8, bottom: 4, right: 8 }
|
|
2598
|
+
node.classInsets = { top: 4, right: 8, bottom: 4, left: 8 }
|
|
2127
2599
|
}
|
|
2128
2600
|
|
|
2129
2601
|
// Panels that paint the web body's --color-surface. On this host the
|
|
@@ -2136,22 +2608,18 @@ function applyClassName(node: ElementNode, value: any) {
|
|
|
2136
2608
|
name === 'modal-panel'
|
|
2137
2609
|
) {
|
|
2138
2610
|
node.bgSlot = 'surface'
|
|
2139
|
-
node.view!.wantsLayer = true
|
|
2140
|
-
node.view!.layer.backgroundColor = nativeColor(
|
|
2141
|
-
SCHEME_COLORS[nodeScheme(node)].surface,
|
|
2142
|
-
).CGColor
|
|
2143
2611
|
}
|
|
2144
2612
|
}
|
|
2613
|
+
|
|
2614
|
+
syncStackLayout(node)
|
|
2615
|
+
|
|
2616
|
+
syncEdgeInsets(node)
|
|
2617
|
+
syncCornerRadius(node)
|
|
2145
2618
|
}
|
|
2146
2619
|
|
|
2147
2620
|
if (node.type === 'textfield' || node.type === 'textview') {
|
|
2148
2621
|
if (classes.includes('vx-input') || classes.includes('vx-textarea')) {
|
|
2149
|
-
node.
|
|
2150
|
-
14,
|
|
2151
|
-
node.appliedFontWeight ?? 400,
|
|
2152
|
-
node.appliedFontFamily,
|
|
2153
|
-
)
|
|
2154
|
-
|
|
2622
|
+
node.classFontSize = 14
|
|
2155
2623
|
node.colorSlot = 'text'
|
|
2156
2624
|
node.view!.drawsBackground = false
|
|
2157
2625
|
if (node.type === 'textfield') {
|
|
@@ -2162,6 +2630,8 @@ function applyClassName(node: ElementNode, value: any) {
|
|
|
2162
2630
|
node.view!.textContainerInset = { width: 0, height: 0 }
|
|
2163
2631
|
}
|
|
2164
2632
|
}
|
|
2633
|
+
|
|
2634
|
+
syncNodeFont(node)
|
|
2165
2635
|
}
|
|
2166
2636
|
|
|
2167
2637
|
applyThemeColors(node)
|
|
@@ -2176,7 +2646,7 @@ function applyClassName(node: ElementNode, value: any) {
|
|
|
2176
2646
|
}
|
|
2177
2647
|
|
|
2178
2648
|
if (node.parent) {
|
|
2179
|
-
|
|
2649
|
+
queueLayoutReconcile(node.container, node.parent)
|
|
2180
2650
|
}
|
|
2181
2651
|
|
|
2182
2652
|
updateStackDistribution(node)
|
|
@@ -2194,6 +2664,38 @@ function textContent(node: ElementNode): string {
|
|
|
2194
2664
|
return node.children.map(textContent).join('')
|
|
2195
2665
|
}
|
|
2196
2666
|
|
|
2667
|
+
// Keep the native value intact: AppKit truncates only when drawing in the frame.
|
|
2668
|
+
function syncLabelOverflow(node: ElementNode) {
|
|
2669
|
+
const { maxLines, whiteSpace, textOverflow } = node.props
|
|
2670
|
+
if (whiteSpace != null && !['normal', 'nowrap'].includes(whiteSpace)) {
|
|
2671
|
+
throw new Error('[macos-host] label whiteSpace supports only normal and nowrap')
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
if (textOverflow != null && !['clip', 'ellipsis'].includes(textOverflow)) {
|
|
2675
|
+
throw new Error('[macos-host] label textOverflow supports only clip and ellipsis')
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
if (maxLines != null && (!Number.isInteger(maxLines) || maxLines < 0)) {
|
|
2679
|
+
throw new Error('[macos-host] label maxLines must be a non-negative integer')
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
const singleLine = whiteSpace === 'nowrap' || maxLines === 1
|
|
2683
|
+
const view = node.view as NSTextField
|
|
2684
|
+
|
|
2685
|
+
view.maximumNumberOfLines = singleLine ? 1 : (maxLines ?? 0)
|
|
2686
|
+
view.cell.wraps = !singleLine
|
|
2687
|
+
view.cell.scrollable = false
|
|
2688
|
+
view.cell.usesSingleLineMode = singleLine
|
|
2689
|
+
view.cell.lineBreakMode = singleLine
|
|
2690
|
+
? textOverflow === 'ellipsis'
|
|
2691
|
+
? NSLineBreakMode.TruncatingTail
|
|
2692
|
+
: NSLineBreakMode.Clipping
|
|
2693
|
+
: NSLineBreakMode.WordWrapping
|
|
2694
|
+
|
|
2695
|
+
view.cell.truncatesLastVisibleLine = !singleLine && textOverflow === 'ellipsis'
|
|
2696
|
+
view.invalidateIntrinsicContentSize()
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2197
2699
|
function setLabelText(node: ElementNode, text: any) {
|
|
2198
2700
|
const lineHeight = Number(node.props?.style?.lineHeight)
|
|
2199
2701
|
if (!Number.isFinite(lineHeight) || lineHeight <= 0) {
|
|
@@ -2202,6 +2704,7 @@ function setLabelText(node: ElementNode, text: any) {
|
|
|
2202
2704
|
}
|
|
2203
2705
|
|
|
2204
2706
|
const paragraphStyle = NSMutableParagraphStyle.alloc().init()
|
|
2707
|
+
paragraphStyle.lineBreakMode = (node.view as NSTextField).cell.lineBreakMode
|
|
2205
2708
|
paragraphStyle.minimumLineHeight = lineHeight
|
|
2206
2709
|
paragraphStyle.maximumLineHeight = lineHeight
|
|
2207
2710
|
node.view!.attributedStringValue = NSAttributedString.alloc().initWithStringAttributes(text, {
|
|
@@ -2235,7 +2738,7 @@ function setAction(node: ElementNode, value: any) {
|
|
|
2235
2738
|
actionIdsByView.set(node.view!, node.actionId)
|
|
2236
2739
|
actionHandlers.set(node.actionId, null)
|
|
2237
2740
|
node.view!.addGestureRecognizer(
|
|
2238
|
-
NSClickGestureRecognizer.alloc().initWithTargetAction(buttonActionTarget, 'viewPressed'),
|
|
2741
|
+
NSClickGestureRecognizer.alloc().initWithTargetAction(buttonActionTarget, 'viewPressed:'),
|
|
2239
2742
|
)
|
|
2240
2743
|
}
|
|
2241
2744
|
|
|
@@ -2257,7 +2760,7 @@ function setPanAction(node: ElementNode, value: any) {
|
|
|
2257
2760
|
if (typeof value === 'function' && !node.panGestureRecognizer) {
|
|
2258
2761
|
node.panGestureRecognizer = NSPanGestureRecognizer.alloc().initWithTargetAction(
|
|
2259
2762
|
buttonActionTarget,
|
|
2260
|
-
'viewPanned',
|
|
2763
|
+
'viewPanned:',
|
|
2261
2764
|
)
|
|
2262
2765
|
|
|
2263
2766
|
node.view!.addGestureRecognizer(node.panGestureRecognizer)
|
|
@@ -2296,6 +2799,35 @@ function setControlAction(node: ElementNode, value: any, readValue: () => any) {
|
|
|
2296
2799
|
)
|
|
2297
2800
|
}
|
|
2298
2801
|
|
|
2802
|
+
/** Return-key submission for single-line fields, invoked by the field's
|
|
2803
|
+
* target/action — kept distinct from per-edit `onTextChange`. */
|
|
2804
|
+
function setTextSubmitAction(node: ElementNode, value: any) {
|
|
2805
|
+
node.submitHandler =
|
|
2806
|
+
typeof value === 'function'
|
|
2807
|
+
? () => {
|
|
2808
|
+
try {
|
|
2809
|
+
node.container.root!.eventScope('discrete', value)
|
|
2810
|
+
} catch (error) {
|
|
2811
|
+
console.error('[macos-event] text submit handler failed', error)
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
: null
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
/** `isEnabled`/`editable` → AppKit state. Disabled fields reject edits and
|
|
2818
|
+
* selection; read-only fields stay selectable so text can be copied. */
|
|
2819
|
+
function syncTextControlState(node: ElementNode) {
|
|
2820
|
+
const enabled = node.props.isEnabled !== false && node.props.enabled !== false
|
|
2821
|
+
const editable = enabled && node.props.editable !== false
|
|
2822
|
+
const view = node.view!
|
|
2823
|
+
if (node.type === 'textfield') {
|
|
2824
|
+
view.enabled = enabled
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
view.editable = editable
|
|
2828
|
+
view.selectable = editable || enabled
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2299
2831
|
function applyAccessibility(node: ElementNode, name: string, value: any) {
|
|
2300
2832
|
if (node.type === 'flexboxlayout' && node.actionId !== undefined) {
|
|
2301
2833
|
const props = stackAccessibilityPropsByView.get(node.view!) ?? {}
|
|
@@ -2312,6 +2844,20 @@ function applyAccessibility(node: ElementNode, name: string, value: any) {
|
|
|
2312
2844
|
if (node.type === 'label' && name === 'accessibilityLabel') {
|
|
2313
2845
|
node.view!.setAccessibilityLabel?.(String(value ?? ''))
|
|
2314
2846
|
}
|
|
2847
|
+
|
|
2848
|
+
if (node.type === 'textfield' || node.type === 'textview') {
|
|
2849
|
+
if (name === 'accessibilityLabel') {
|
|
2850
|
+
node.view!.setAccessibilityLabel?.(String(value ?? ''))
|
|
2851
|
+
} else if (name === 'accessibilityHint') {
|
|
2852
|
+
node.view!.setAccessibilityHint?.(String(value ?? ''))
|
|
2853
|
+
} else if (name === 'accessible') {
|
|
2854
|
+
node.view!.setAccessibilityElement?.(value !== false)
|
|
2855
|
+
} else if (name === 'accessibilityState' && node.type === 'textview') {
|
|
2856
|
+
// NSTextField reports AX enabled through its `enabled` flag;
|
|
2857
|
+
// NSTextView has no enabled state, so it is mirrored here.
|
|
2858
|
+
node.view!.setAccessibilityEnabled?.(value?.disabled !== true)
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2315
2861
|
}
|
|
2316
2862
|
|
|
2317
2863
|
function applyProps(node: ElementNode, props: PropBag) {
|
|
@@ -2329,7 +2875,20 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2329
2875
|
return
|
|
2330
2876
|
}
|
|
2331
2877
|
|
|
2878
|
+
const layoutChildProps: Record<string, readonly string[]> = {
|
|
2879
|
+
absolutelayout: ['left', 'top', 'right', 'bottom'],
|
|
2880
|
+
gridlayout: ['row', 'col', 'rowSpan', 'colSpan', 'horizontalAlignment', 'verticalAlignment'],
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2332
2883
|
for (const [name, value] of Object.entries(props)) {
|
|
2884
|
+
if (node.parent && layoutChildProps[node.parent.type]?.includes(name)) {
|
|
2885
|
+
continue
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
if ((node.type === 'flexboxlayout' || node.type === 'stack') && isStackLayoutInput(name)) {
|
|
2889
|
+
continue
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2333
2892
|
if (name === 'onLayoutChanged') {
|
|
2334
2893
|
setLayoutAction(node, value)
|
|
2335
2894
|
continue
|
|
@@ -2338,7 +2897,7 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2338
2897
|
switch (node.type) {
|
|
2339
2898
|
case 'stack':
|
|
2340
2899
|
if (name === 'spacing') {
|
|
2341
|
-
node
|
|
2900
|
+
syncStackLayout(node)
|
|
2342
2901
|
} else if (name === 'style') {
|
|
2343
2902
|
applyStyle(node, value)
|
|
2344
2903
|
} else if (name === 'className') {
|
|
@@ -2351,33 +2910,12 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2351
2910
|
|
|
2352
2911
|
break
|
|
2353
2912
|
case 'flexboxlayout':
|
|
2354
|
-
if (name === '
|
|
2355
|
-
node
|
|
2356
|
-
} else if (name === 'spacing') {
|
|
2357
|
-
node.view!.spacing = Number(value ?? 0)
|
|
2358
|
-
} else if (name === 'flexDirection') {
|
|
2359
|
-
node.view!.orientation =
|
|
2360
|
-
value === 'row'
|
|
2361
|
-
? NSUserInterfaceLayoutOrientation.Horizontal
|
|
2362
|
-
: NSUserInterfaceLayoutOrientation.Vertical
|
|
2363
|
-
|
|
2364
|
-
node.view!.distribution = NSStackViewDistribution.GravityAreas
|
|
2365
|
-
node.view!.alignment = stackAlignmentAttribute(node.view, stackAlignItems(node))
|
|
2366
|
-
moveStackChildren(node)
|
|
2367
|
-
updateCrossAxisConstraints(node)
|
|
2368
|
-
} else if (name === 'alignItems') {
|
|
2369
|
-
node.view!.alignment = stackAlignmentAttribute(node.view, stackAlignItems(node))
|
|
2370
|
-
updateCrossAxisConstraints(node)
|
|
2371
|
-
} else if (name === 'justifyContent') {
|
|
2372
|
-
node.view!.distribution = NSStackViewDistribution.GravityAreas
|
|
2373
|
-
moveStackChildren(node)
|
|
2913
|
+
if (name === 'spacing') {
|
|
2914
|
+
syncStackLayout(node)
|
|
2374
2915
|
} else if (name === 'style') {
|
|
2375
2916
|
applyStyle(node, value)
|
|
2376
2917
|
} else if (name === 'className') {
|
|
2377
2918
|
applyClassName(node, value)
|
|
2378
|
-
moveStackChildren(node)
|
|
2379
|
-
node.view!.alignment = stackAlignmentAttribute(node.view, stackAlignItems(node))
|
|
2380
|
-
updateCrossAxisConstraints(node)
|
|
2381
2919
|
} else if (name === 'id') {
|
|
2382
2920
|
continue
|
|
2383
2921
|
} else if (name === 'onTap') {
|
|
@@ -2399,18 +2937,10 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2399
2937
|
'alignItems',
|
|
2400
2938
|
'justifyContent',
|
|
2401
2939
|
'flexWrap',
|
|
2402
|
-
'row',
|
|
2403
|
-
'col',
|
|
2404
|
-
'rowSpan',
|
|
2405
|
-
'colSpan',
|
|
2406
|
-
'left',
|
|
2407
|
-
'top',
|
|
2408
2940
|
'flexGrow',
|
|
2409
2941
|
'flexShrink',
|
|
2410
2942
|
'alignSelf',
|
|
2411
2943
|
'order',
|
|
2412
|
-
'horizontalAlignment',
|
|
2413
|
-
'verticalAlignment',
|
|
2414
2944
|
].includes(name)
|
|
2415
2945
|
) {
|
|
2416
2946
|
continue
|
|
@@ -2441,7 +2971,7 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2441
2971
|
} else if (name === 'accessible' || name.startsWith('accessibility')) {
|
|
2442
2972
|
applyAccessibility(node, name, value)
|
|
2443
2973
|
} else if (['rows', 'columns'].includes(name)) {
|
|
2444
|
-
|
|
2974
|
+
queueLayoutReconcile(node.container, node)
|
|
2445
2975
|
} else {
|
|
2446
2976
|
console.warn('[macos-host] ignored gridlayout prop ' + name)
|
|
2447
2977
|
}
|
|
@@ -2467,11 +2997,8 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2467
2997
|
if (name === 'text') {
|
|
2468
2998
|
setLabelText(node, String(value ?? ''))
|
|
2469
2999
|
} else if (name === 'fontSize') {
|
|
2470
|
-
node.
|
|
2471
|
-
|
|
2472
|
-
node.appliedFontWeight ?? 400,
|
|
2473
|
-
node.appliedFontFamily,
|
|
2474
|
-
)
|
|
3000
|
+
node.propFontSize = Number(value ?? 16)
|
|
3001
|
+
syncNodeFont(node)
|
|
2475
3002
|
} else if (name === 'style') {
|
|
2476
3003
|
applyStyle(node, value)
|
|
2477
3004
|
syncText(node)
|
|
@@ -2479,9 +3006,9 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2479
3006
|
applyClassName(node, value)
|
|
2480
3007
|
} else if (name === 'id') {
|
|
2481
3008
|
continue
|
|
2482
|
-
} else if (['maxLines', 'whiteSpace', 'textOverflow'
|
|
3009
|
+
} else if (['maxLines', 'whiteSpace', 'textOverflow'].includes(name)) {
|
|
2483
3010
|
continue
|
|
2484
|
-
} else if (name.startsWith('accessibility')) {
|
|
3011
|
+
} else if (name === 'accessible' || name.startsWith('accessibility')) {
|
|
2485
3012
|
applyAccessibility(node, name, value)
|
|
2486
3013
|
} else if (name.startsWith('on') && value == null) {
|
|
2487
3014
|
continue
|
|
@@ -2563,6 +3090,30 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2563
3090
|
setControlAction(node, value, () =>
|
|
2564
3091
|
String(node.type === 'textview' ? node.view!.string : (node.view!.stringValue ?? '')),
|
|
2565
3092
|
)
|
|
3093
|
+
} else if (name === 'onSubmit') {
|
|
3094
|
+
if (node.type === 'textfield') {
|
|
3095
|
+
setTextSubmitAction(node, value)
|
|
3096
|
+
} else if (typeof value === 'function') {
|
|
3097
|
+
console.warn('[macos-host] onSubmit is unsupported on multiline textview')
|
|
3098
|
+
}
|
|
3099
|
+
} else if (name === 'editable' || name === 'isEnabled' || name === 'enabled') {
|
|
3100
|
+
syncTextControlState(node)
|
|
3101
|
+
} else if (name === 'secure') {
|
|
3102
|
+
// The native class is chosen in makeTextField; a secure change
|
|
3103
|
+
// would require a new view, so it is rejected rather than
|
|
3104
|
+
// silently downgraded to plain text.
|
|
3105
|
+
if (node.type !== 'textfield') {
|
|
3106
|
+
if (value) {
|
|
3107
|
+
console.warn(
|
|
3108
|
+
'[macos-host] secure is unsupported on <' + node.type + '>; rendering plain text',
|
|
3109
|
+
)
|
|
3110
|
+
}
|
|
3111
|
+
} else if (!!value !== node.secure) {
|
|
3112
|
+
console.warn(
|
|
3113
|
+
'[macos-host] changing "secure" after mount is unsupported; the field keeps its initial secure=' +
|
|
3114
|
+
node.secure,
|
|
3115
|
+
)
|
|
3116
|
+
}
|
|
2566
3117
|
} else if (name === 'style') {
|
|
2567
3118
|
applyStyle(node, value)
|
|
2568
3119
|
} else if (name === 'className') {
|
|
@@ -2571,21 +3122,11 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2571
3122
|
continue
|
|
2572
3123
|
} else if (name === 'rows' && node.type === 'textview') {
|
|
2573
3124
|
continue
|
|
2574
|
-
} else if (name.startsWith('accessibility')) {
|
|
3125
|
+
} else if (name === 'accessible' || name.startsWith('accessibility')) {
|
|
2575
3126
|
applyAccessibility(node, name, value)
|
|
2576
3127
|
} else if (name.startsWith('on') && value == null) {
|
|
2577
3128
|
continue
|
|
2578
|
-
} else if (
|
|
2579
|
-
[
|
|
2580
|
-
'editable',
|
|
2581
|
-
'enabled',
|
|
2582
|
-
'secure',
|
|
2583
|
-
'keyboardType',
|
|
2584
|
-
'returnKeyType',
|
|
2585
|
-
'autoGrow',
|
|
2586
|
-
'maxRows',
|
|
2587
|
-
].includes(name)
|
|
2588
|
-
) {
|
|
3129
|
+
} else if (['keyboardType', 'returnKeyType', 'autoGrow', 'maxRows'].includes(name)) {
|
|
2589
3130
|
continue
|
|
2590
3131
|
} else {
|
|
2591
3132
|
console.warn('[macos-host] ignored text control prop ' + name)
|
|
@@ -2644,7 +3185,7 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2644
3185
|
break
|
|
2645
3186
|
case 'image':
|
|
2646
3187
|
if (name === 'src') {
|
|
2647
|
-
node.view
|
|
3188
|
+
updateImage(node.view as NSImageView, value)
|
|
2648
3189
|
} else if (name === 'style') {
|
|
2649
3190
|
applyStyle(node, value)
|
|
2650
3191
|
} else if (name === 'className' || name === 'id' || name === 'alt') {
|
|
@@ -2660,6 +3201,13 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2660
3201
|
}
|
|
2661
3202
|
}
|
|
2662
3203
|
|
|
3204
|
+
if (
|
|
3205
|
+
(node.type === 'flexboxlayout' || node.type === 'stack') &&
|
|
3206
|
+
Object.keys(props).some(isStackLayoutInput)
|
|
3207
|
+
) {
|
|
3208
|
+
syncStackLayout(node)
|
|
3209
|
+
}
|
|
3210
|
+
|
|
2663
3211
|
if (node.type === 'webview') {
|
|
2664
3212
|
updateWebView(node, props, (height) => {
|
|
2665
3213
|
node.props.style = { ...node.props.style, height }
|
|
@@ -2672,8 +3220,17 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2672
3220
|
}
|
|
2673
3221
|
|
|
2674
3222
|
if (node.type === 'label') {
|
|
3223
|
+
syncLabelOverflow(node)
|
|
3224
|
+
syncText(node)
|
|
2675
3225
|
const style = node.props.style ?? {}
|
|
2676
|
-
if (
|
|
3226
|
+
if (
|
|
3227
|
+
style.height == null &&
|
|
3228
|
+
node.props.maxLines != null &&
|
|
3229
|
+
node.props.maxLines !== 1 &&
|
|
3230
|
+
node.props.whiteSpace !== 'nowrap'
|
|
3231
|
+
) {
|
|
3232
|
+
setSizeConstraint(node, 'height', undefined)
|
|
3233
|
+
} else if (style.lineHeight == null && style.height == null) {
|
|
2677
3234
|
const size = Number(node.view!.font?.pointSize ?? 16)
|
|
2678
3235
|
const height =
|
|
2679
3236
|
style.fontSize == null && node.headingDefaultHeight != null
|
|
@@ -2700,14 +3257,10 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2700
3257
|
}
|
|
2701
3258
|
}
|
|
2702
3259
|
|
|
2703
|
-
if (node.type === 'gridlayout') {
|
|
2704
|
-
|
|
2705
|
-
} else if (node.type === 'absolutelayout') {
|
|
2706
|
-
|
|
2707
|
-
} else if (node.parent?.type === 'gridlayout') {
|
|
2708
|
-
layoutGridChildren(node.parent)
|
|
2709
|
-
} else if (node.parent?.type === 'absolutelayout') {
|
|
2710
|
-
layoutAbsoluteChildren(node.parent)
|
|
3260
|
+
if (node.type === 'gridlayout' || node.type === 'absolutelayout') {
|
|
3261
|
+
queueLayoutReconcile(node.container, node)
|
|
3262
|
+
} else if (node.parent?.type === 'gridlayout' || node.parent?.type === 'absolutelayout') {
|
|
3263
|
+
queueLayoutReconcile(node.container, node.parent)
|
|
2711
3264
|
}
|
|
2712
3265
|
}
|
|
2713
3266
|
|
|
@@ -2725,6 +3278,7 @@ function detach(container: RootContainer, node: ElementNode) {
|
|
|
2725
3278
|
|
|
2726
3279
|
node.crossAxisConstraint = null
|
|
2727
3280
|
deactivateSizeConstraints(node)
|
|
3281
|
+
releasePlacementPins(node)
|
|
2728
3282
|
if (node.view) {
|
|
2729
3283
|
const parentView = previousParent?.childHost ?? previousParent?.view
|
|
2730
3284
|
if (parentView?.removeArrangedSubview) {
|
|
@@ -2734,20 +3288,53 @@ function detach(container: RootContainer, node: ElementNode) {
|
|
|
2734
3288
|
arrangedView(node).removeFromSuperview()
|
|
2735
3289
|
}
|
|
2736
3290
|
|
|
2737
|
-
|
|
2738
|
-
|
|
3291
|
+
queueLayoutReconcile(container, previousParent)
|
|
3292
|
+
syncText(previousParent)
|
|
3293
|
+
node.parent = null
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
/** Mark a parent for one sibling-wide reconcile after the batch's commands.
|
|
3297
|
+
* Insert/remove/update each used to rescan siblings, rebuild distribution,
|
|
3298
|
+
* and recreate every child's cross-axis constraint on the spot — n inserts
|
|
3299
|
+
* into one stack made that quadratic. Structural commands queue their dirty
|
|
3300
|
+
* parents here; `prepareBatch.apply` drains the set once at the end. Outside
|
|
3301
|
+
* a batch (no `layoutDirty` set) the reconcile runs immediately. */
|
|
3302
|
+
function queueLayoutReconcile(container: RootContainer, parent: ElementNode | null | undefined) {
|
|
3303
|
+
if (!parent) {
|
|
3304
|
+
return
|
|
2739
3305
|
}
|
|
2740
3306
|
|
|
2741
|
-
|
|
2742
|
-
|
|
3307
|
+
const pending = container?.layoutDirty
|
|
3308
|
+
if (pending) {
|
|
3309
|
+
pending.add(parent)
|
|
3310
|
+
return
|
|
2743
3311
|
}
|
|
2744
3312
|
|
|
2745
|
-
|
|
2746
|
-
|
|
3313
|
+
reconcileLayoutParent(parent)
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
function reconcileLayoutParent(parent: ElementNode) {
|
|
3317
|
+
if (parent.type === 'gridlayout') {
|
|
3318
|
+
layoutGridChildren(parent)
|
|
3319
|
+
return
|
|
2747
3320
|
}
|
|
2748
3321
|
|
|
2749
|
-
|
|
2750
|
-
|
|
3322
|
+
if (parent.type === 'absolutelayout') {
|
|
3323
|
+
layoutAbsoluteChildren(parent)
|
|
3324
|
+
return
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
const stack = parent.childHost ?? parent.view
|
|
3328
|
+
if (typeof stack?.addViewInGravity !== 'function') {
|
|
3329
|
+
return
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
for (const child of parent.children) {
|
|
3333
|
+
setStackChildPriorities(parent, child)
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
updateStackDistribution(parent)
|
|
3337
|
+
updateCrossAxisConstraints(parent)
|
|
2751
3338
|
}
|
|
2752
3339
|
|
|
2753
3340
|
function insert(
|
|
@@ -2756,62 +3343,54 @@ function insert(
|
|
|
2756
3343
|
node: ElementNode,
|
|
2757
3344
|
beforeId: number | null,
|
|
2758
3345
|
) {
|
|
2759
|
-
|
|
3346
|
+
// Validate before detaching — a rejected insert must not leave the node
|
|
3347
|
+
// orphaned from its current parent.
|
|
2760
3348
|
const parent = (parentId === null ? null : container.nodes.get(parentId)) ?? null
|
|
2761
3349
|
if (parentId !== null && !parent) {
|
|
2762
3350
|
throw new Error('Unknown AppKit parent ' + parentId)
|
|
2763
3351
|
}
|
|
2764
3352
|
|
|
2765
|
-
if (
|
|
2766
|
-
parent &&
|
|
2767
|
-
node.view &&
|
|
2768
|
-
parent.type !== 'stack' &&
|
|
2769
|
-
parent.type !== 'flexboxlayout' &&
|
|
2770
|
-
parent.type !== 'scrollview' &&
|
|
2771
|
-
parent.type !== 'gridlayout' &&
|
|
2772
|
-
parent.type !== 'absolutelayout'
|
|
2773
|
-
) {
|
|
3353
|
+
if (parent && node.view && !VIEW_PARENT_TYPES.has(parent.type)) {
|
|
2774
3354
|
throw new Error('AppKit <' + parent.type + '> cannot contain child views')
|
|
2775
3355
|
}
|
|
2776
3356
|
|
|
3357
|
+
const parentView = parent?.childHost ?? parent?.view ?? container.hostView
|
|
3358
|
+
if (node.view && !parentView) {
|
|
3359
|
+
throw new Error('AppKit host has no parent view for node ' + node.id)
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
detach(container, node)
|
|
2777
3363
|
const siblings = parent ? parent.children : container.children
|
|
2778
3364
|
const beforeIndex = beforeId === null ? -1 : siblings.findIndex((child) => child.id === beforeId)
|
|
2779
3365
|
const index = beforeIndex < 0 ? siblings.length : beforeIndex
|
|
2780
3366
|
siblings.splice(index, 0, node)
|
|
2781
3367
|
node.parent = parent
|
|
2782
3368
|
if (node.view) {
|
|
2783
|
-
const parentView = parent?.childHost ?? parent?.view ?? container.hostView
|
|
2784
|
-
if (!parentView) {
|
|
2785
|
-
throw new Error('AppKit host has no parent view for node ' + node.id)
|
|
2786
|
-
}
|
|
2787
|
-
|
|
2788
3369
|
if (parent) {
|
|
2789
3370
|
if (parent.type === 'gridlayout') {
|
|
2790
3371
|
parentView.addSubview(node.view)
|
|
2791
3372
|
node.view.translatesAutoresizingMaskIntoConstraints = false
|
|
2792
3373
|
deactivateSizeConstraints(node)
|
|
2793
|
-
|
|
3374
|
+
queueLayoutReconcile(container, parent)
|
|
2794
3375
|
} else if (parent.type === 'absolutelayout') {
|
|
2795
3376
|
parentView.addSubview(node.view)
|
|
2796
3377
|
node.view.translatesAutoresizingMaskIntoConstraints = false
|
|
2797
3378
|
deactivateSizeConstraints(node)
|
|
2798
|
-
|
|
3379
|
+
queueLayoutReconcile(container, parent)
|
|
2799
3380
|
} else {
|
|
2800
3381
|
if (marginInsetsOf(node)) {
|
|
2801
3382
|
makeMarginHost(node)
|
|
3383
|
+
setLayoutAction(node, node.props?.onLayoutChanged)
|
|
2802
3384
|
}
|
|
2803
3385
|
|
|
2804
3386
|
// AppKit insertion indices are local to a gravity area. Honor the
|
|
2805
3387
|
// renderer's before edge instead of appending moved keyed rows.
|
|
2806
3388
|
const gravity = stackGravity(parent, node)
|
|
2807
|
-
const arrangedIndex =
|
|
2808
|
-
.slice(0, index)
|
|
2809
|
-
.filter((child) => child.view && stackGravity(parent, child) === gravity).length
|
|
3389
|
+
const arrangedIndex = gravityInsertIndex(parent, node, gravity)
|
|
2810
3390
|
|
|
2811
3391
|
parentView.insertViewAtIndexInGravity(arrangedView(node), arrangedIndex, gravity)
|
|
2812
3392
|
applySizeConstraints(node)
|
|
2813
|
-
|
|
2814
|
-
updateCrossAxisConstraints(parent)
|
|
3393
|
+
queueLayoutReconcile(container, parent)
|
|
2815
3394
|
}
|
|
2816
3395
|
} else {
|
|
2817
3396
|
parentView.addSubview(node.view)
|
|
@@ -2846,6 +3425,7 @@ function remove(container: RootContainer, parentId: number | null, node: Element
|
|
|
2846
3425
|
|
|
2847
3426
|
node.crossAxisConstraint = null
|
|
2848
3427
|
deactivateSizeConstraints(node)
|
|
3428
|
+
releasePlacementPins(node)
|
|
2849
3429
|
if (node.view) {
|
|
2850
3430
|
const parentView = expectedParent?.childHost ?? expectedParent?.view
|
|
2851
3431
|
if (parentView?.removeArrangedSubview) {
|
|
@@ -2856,31 +3436,27 @@ function remove(container: RootContainer, parentId: number | null, node: Element
|
|
|
2856
3436
|
node.marginHost?.removeFromSuperview()
|
|
2857
3437
|
node.marginHost = null
|
|
2858
3438
|
node.marginConstraints = null
|
|
3439
|
+
setLayoutAction(node, node.props?.onLayoutChanged)
|
|
2859
3440
|
}
|
|
2860
3441
|
|
|
2861
|
-
|
|
2862
|
-
layoutGridChildren(expectedParent)
|
|
2863
|
-
}
|
|
2864
|
-
|
|
2865
|
-
if (expectedParent?.type === 'absolutelayout') {
|
|
2866
|
-
layoutAbsoluteChildren(expectedParent)
|
|
2867
|
-
}
|
|
2868
|
-
|
|
2869
|
-
if (expectedParent) {
|
|
2870
|
-
updateStackDistribution(expectedParent)
|
|
2871
|
-
}
|
|
2872
|
-
|
|
3442
|
+
queueLayoutReconcile(container, expectedParent)
|
|
2873
3443
|
node.parent = null
|
|
2874
3444
|
syncText(expectedParent)
|
|
2875
3445
|
}
|
|
2876
3446
|
|
|
2877
3447
|
function destroy(node: ElementNode) {
|
|
2878
|
-
|
|
2879
|
-
|
|
3448
|
+
// Nodes destroyed by a failed makeNode may have a null view — every
|
|
3449
|
+
// branch below must tolerate partial construction.
|
|
3450
|
+
if (node.type === 'webview' && node.view) {
|
|
3451
|
+
disposeWebView(node.view as WKWebView)
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
if (node.type === 'image' && node.view) {
|
|
3455
|
+
disposeImage(node.view as NSImageView)
|
|
2880
3456
|
}
|
|
2881
3457
|
|
|
2882
|
-
if (node.type === 'scrollview' && node.scrollObserverInstalled) {
|
|
2883
|
-
const clipView = node.view
|
|
3458
|
+
if (node.type === 'scrollview' && node.scrollObserverInstalled && node.view) {
|
|
3459
|
+
const clipView = node.view.contentView
|
|
2884
3460
|
scrollHandlers.delete(clipView)
|
|
2885
3461
|
NSNotificationCenter.defaultCenter.removeObserver(node.scrollObserver)
|
|
2886
3462
|
node.scrollObserver = null
|
|
@@ -2892,6 +3468,9 @@ function destroy(node: ElementNode) {
|
|
|
2892
3468
|
node.layoutObserver = null
|
|
2893
3469
|
}
|
|
2894
3470
|
|
|
3471
|
+
layoutHandlers.delete(node.layoutObservedView)
|
|
3472
|
+
node.layoutObservedView = null
|
|
3473
|
+
|
|
2895
3474
|
if (node.actionId !== undefined) {
|
|
2896
3475
|
actionHandlers.delete(node.actionId)
|
|
2897
3476
|
accessibilityLabels.delete(node.actionId)
|
|
@@ -2899,18 +3478,127 @@ function destroy(node: ElementNode) {
|
|
|
2899
3478
|
}
|
|
2900
3479
|
}
|
|
2901
3480
|
|
|
3481
|
+
/** Pre-flight a whole batch without mutating, so a deterministic failure —
|
|
3482
|
+
* bad props, unknown nodes, illegal parents, unsupported ops — rejects the
|
|
3483
|
+
* batch before any command applies. Residual mid-apply failures are then
|
|
3484
|
+
* limited to unexpected native errors, and each command unwinds itself. */
|
|
3485
|
+
function validateBatch(container: RootContainer, commands: readonly UniversalHostCommand[]) {
|
|
3486
|
+
// Ids created by this batch are not in container.nodes yet — track them so
|
|
3487
|
+
// later commands validate against the intended tree.
|
|
3488
|
+
const pending = new Map<number, { type: string; props: PropBag }>()
|
|
3489
|
+
const destroyed = new Set<number>()
|
|
3490
|
+
const known = (id: number): { type: string; props: PropBag } | undefined =>
|
|
3491
|
+
pending.get(id) ?? (destroyed.has(id) ? undefined : container.nodes.get(id))
|
|
3492
|
+
|
|
3493
|
+
for (const command of commands) {
|
|
3494
|
+
switch (command.op) {
|
|
3495
|
+
case 'create':
|
|
3496
|
+
assertSupportedType(command.type)
|
|
3497
|
+
validateNodeProps(command.type, command.props)
|
|
3498
|
+
pending.set(command.id, { type: command.type, props: command.props })
|
|
3499
|
+
destroyed.delete(command.id)
|
|
3500
|
+
continue
|
|
3501
|
+
case 'recreate': {
|
|
3502
|
+
const entry = known(command.id)
|
|
3503
|
+
if (!entry) {
|
|
3504
|
+
throw new Error('Unknown AppKit node ' + command.id)
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
assertSupportedType(command.type)
|
|
3508
|
+
validateNodeProps(command.type, command.props)
|
|
3509
|
+
const existing = container.nodes.get(command.id)
|
|
3510
|
+
if (
|
|
3511
|
+
existing?.parent &&
|
|
3512
|
+
typeHasView(command.type) &&
|
|
3513
|
+
!VIEW_PARENT_TYPES.has(existing.parent.type)
|
|
3514
|
+
) {
|
|
3515
|
+
throw new Error('AppKit <' + existing.parent.type + '> cannot contain child views')
|
|
3516
|
+
}
|
|
3517
|
+
|
|
3518
|
+
pending.set(command.id, { type: command.type, props: command.props })
|
|
3519
|
+
continue
|
|
3520
|
+
}
|
|
3521
|
+
case 'update': {
|
|
3522
|
+
const entry = known(command.id)
|
|
3523
|
+
if (!entry) {
|
|
3524
|
+
throw new Error('Unknown AppKit node ' + command.id)
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
validateNodeProps(entry.type, command.props, entry.props)
|
|
3528
|
+
continue
|
|
3529
|
+
}
|
|
3530
|
+
case 'insert':
|
|
3531
|
+
case 'move': {
|
|
3532
|
+
const entry = known(command.id)
|
|
3533
|
+
if (!entry) {
|
|
3534
|
+
throw new Error('Unknown AppKit node ' + command.id)
|
|
3535
|
+
}
|
|
3536
|
+
|
|
3537
|
+
const parentId = command.parent as number | null
|
|
3538
|
+
if (parentId !== null) {
|
|
3539
|
+
const parent = known(parentId)
|
|
3540
|
+
if (!parent) {
|
|
3541
|
+
throw new Error('Unknown AppKit parent ' + parentId)
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
if (typeHasView(entry.type) && !VIEW_PARENT_TYPES.has(parent.type)) {
|
|
3545
|
+
throw new Error('AppKit <' + parent.type + '> cannot contain child views')
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3549
|
+
continue
|
|
3550
|
+
}
|
|
3551
|
+
case 'destroy':
|
|
3552
|
+
pending.delete(command.id)
|
|
3553
|
+
destroyed.add(command.id)
|
|
3554
|
+
continue
|
|
3555
|
+
case 'remove':
|
|
3556
|
+
case 'visibility':
|
|
3557
|
+
case 'ensure-public-instance':
|
|
3558
|
+
continue
|
|
3559
|
+
default:
|
|
3560
|
+
throw new Error('AppKit spike does not support host command ' + command.op)
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
|
|
2902
3565
|
function applyCommand(container: RootContainer, command: UniversalHostCommand) {
|
|
2903
3566
|
switch (command.op) {
|
|
2904
|
-
case 'create':
|
|
3567
|
+
case 'create': {
|
|
3568
|
+
// A retried batch can re-create an id the earlier attempt already
|
|
3569
|
+
// registered — release the previous node instead of orphaning it.
|
|
3570
|
+
const existing = container.nodes.get(command.id)
|
|
3571
|
+
if (existing) {
|
|
3572
|
+
detach(container, existing)
|
|
3573
|
+
destroy(existing)
|
|
3574
|
+
}
|
|
3575
|
+
|
|
2905
3576
|
container.nodes.set(command.id, makeNode(container, command.id, command.type, command.props))
|
|
2906
3577
|
return
|
|
3578
|
+
}
|
|
2907
3579
|
case 'update': {
|
|
2908
3580
|
const node = container.nodes.get(command.id)
|
|
2909
3581
|
if (!node) {
|
|
2910
3582
|
throw new Error('Unknown AppKit node ' + command.id)
|
|
2911
3583
|
}
|
|
2912
3584
|
|
|
2913
|
-
|
|
3585
|
+
// node.props records what applied — restore it when a prop throws
|
|
3586
|
+
// mid-update so the bookkeeping doesn't claim unapplied props.
|
|
3587
|
+
const previousProps = node.props
|
|
3588
|
+
try {
|
|
3589
|
+
// Host updates carry the complete current prop bag. Clear omitted
|
|
3590
|
+
// label modes before merging so spread removals restore defaults.
|
|
3591
|
+
applyProps(
|
|
3592
|
+
node,
|
|
3593
|
+
node.type === 'label'
|
|
3594
|
+
? { maxLines: undefined, whiteSpace: undefined, textOverflow: undefined, ...command.props }
|
|
3595
|
+
: command.props,
|
|
3596
|
+
)
|
|
3597
|
+
} catch (error) {
|
|
3598
|
+
node.props = previousProps
|
|
3599
|
+
throw error
|
|
3600
|
+
}
|
|
3601
|
+
|
|
2914
3602
|
return
|
|
2915
3603
|
}
|
|
2916
3604
|
case 'insert':
|
|
@@ -2956,8 +3644,14 @@ function applyCommand(container: RootContainer, command: UniversalHostCommand) {
|
|
|
2956
3644
|
throw new Error('Unknown AppKit node ' + command.id)
|
|
2957
3645
|
}
|
|
2958
3646
|
|
|
2959
|
-
const replacement = makeNode(container, command.id, command.type, command.props)
|
|
2960
3647
|
const parent = node.parent
|
|
3648
|
+
if (parent && typeHasView(command.type) && !VIEW_PARENT_TYPES.has(parent.type)) {
|
|
3649
|
+
// insert would reject the replacement after the original was
|
|
3650
|
+
// already removed and destroyed — fail while it is intact.
|
|
3651
|
+
throw new Error('AppKit <' + parent.type + '> cannot contain child views')
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
const replacement = makeNode(container, command.id, command.type, command.props)
|
|
2961
3655
|
const siblings = parent ? parent.children : container.children
|
|
2962
3656
|
const index = siblings.indexOf(node)
|
|
2963
3657
|
const beforeId = index < 0 ? null : (siblings[index + 1]?.id ?? null)
|
|
@@ -2976,26 +3670,41 @@ const macOSDriver: UniversalHostDriver<RootContainer, any> = {
|
|
|
2976
3670
|
id: 'macos',
|
|
2977
3671
|
capabilities: { text: 'host' },
|
|
2978
3672
|
prepareBatch(container, batch) {
|
|
3673
|
+
// Reject deterministic failures before any command mutates — a
|
|
3674
|
+
// partially applied batch cannot be rolled back once commit() marks
|
|
3675
|
+
// it accepted. Prepare acquires nothing, so abort() has no cleanup.
|
|
3676
|
+
validateBatch(container, batch.commands)
|
|
2979
3677
|
return {
|
|
2980
3678
|
apply() {
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
'
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
3679
|
+
container.layoutDirty = new Set()
|
|
3680
|
+
try {
|
|
3681
|
+
for (const command of batch.commands) {
|
|
3682
|
+
try {
|
|
3683
|
+
applyCommand(container, command)
|
|
3684
|
+
} catch (error) {
|
|
3685
|
+
// The universal root can swallow a mid-batch failure when it
|
|
3686
|
+
// retries or aborts the attempt — log it here so an
|
|
3687
|
+
// unsupported element cannot silently stall the mount.
|
|
3688
|
+
console.error(
|
|
3689
|
+
'[macos-host] command ' +
|
|
3690
|
+
command.op +
|
|
3691
|
+
' failed for <' +
|
|
3692
|
+
((command as { type?: string }).type ?? '?') +
|
|
3693
|
+
'> id=' +
|
|
3694
|
+
('id' in command ? command.id : '?'),
|
|
3695
|
+
error,
|
|
3696
|
+
)
|
|
3697
|
+
|
|
3698
|
+
throw error
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
} finally {
|
|
3702
|
+
const dirty = container.layoutDirty
|
|
3703
|
+
container.layoutDirty = null
|
|
3704
|
+
if (dirty) {
|
|
3705
|
+
for (const parent of dirty) {
|
|
3706
|
+
reconcileLayoutParent(parent)
|
|
3707
|
+
}
|
|
2999
3708
|
}
|
|
3000
3709
|
}
|
|
3001
3710
|
},
|
|
@@ -3749,7 +4458,7 @@ function attachContextMenu(
|
|
|
3749
4458
|
|
|
3750
4459
|
const menuItem = NSMenuItem.alloc().initWithTitleActionKeyEquivalent(
|
|
3751
4460
|
String(item?.title ?? ''),
|
|
3752
|
-
'menuItemSelected',
|
|
4461
|
+
'menuItemSelected:',
|
|
3753
4462
|
'',
|
|
3754
4463
|
)
|
|
3755
4464
|
|
|
@@ -3882,7 +4591,7 @@ function attachDatePicker(view: NSView, options: PropBag, onChange: (event: any)
|
|
|
3882
4591
|
const actionId = nextActionId++
|
|
3883
4592
|
picker.tag = actionId
|
|
3884
4593
|
picker.target = buttonActionTarget
|
|
3885
|
-
picker.action = 'controlChanged'
|
|
4594
|
+
picker.action = 'controlChanged:'
|
|
3886
4595
|
actionHandlers.set(actionId, () => {
|
|
3887
4596
|
const date = picker.dateValue
|
|
3888
4597
|
const millis =
|
|
@@ -4088,10 +4797,11 @@ function presentSheet(view: NSView, options: PropBag) {
|
|
|
4088
4797
|
}
|
|
4089
4798
|
|
|
4090
4799
|
const appKitBridge = (globalThis.__xplatAppKit ??= {})
|
|
4800
|
+
installAppearanceBridge(appKitBridge)
|
|
4091
4801
|
appKitBridge.observeHover = observeHover
|
|
4092
4802
|
appKitBridge.showAnchoredPopup = showAnchoredPopup
|
|
4093
4803
|
appKitBridge.showLayer = (options: PropBag) =>
|
|
4094
|
-
showWindowLayer(options, {
|
|
4804
|
+
showWindowLayer(options as WindowLayerOptions, {
|
|
4095
4805
|
createRoot: (view: NSView, anchor: NSView) =>
|
|
4096
4806
|
createMacOSRoot(view, { fontFamily: fontFamilyForView(anchor) }),
|
|
4097
4807
|
fittingSize: popupFittingSize,
|
|
@@ -4105,7 +4815,7 @@ installPresentationBridge(appKitBridge, createMacOSRoot, fontFamilyForView)
|
|
|
4105
4815
|
const rootFontFamilies = new WeakMap<object, string | undefined>()
|
|
4106
4816
|
|
|
4107
4817
|
function fontFamilyForView(view: NSView | undefined): string | undefined {
|
|
4108
|
-
for (let current = view; current; current = current.superview) {
|
|
4818
|
+
for (let current: NSView | null | undefined = view; current; current = current.superview) {
|
|
4109
4819
|
if (rootFontFamilies.has(current)) {
|
|
4110
4820
|
return rootFontFamilies.get(current)
|
|
4111
4821
|
}
|
|
@@ -4127,6 +4837,7 @@ export function createMacOSRoot(hostView: NSView, { fontFamily }: MacOSRootOptio
|
|
|
4127
4837
|
nodes: new Map(),
|
|
4128
4838
|
children: [],
|
|
4129
4839
|
root: null,
|
|
4840
|
+
layoutDirty: null,
|
|
4130
4841
|
}
|
|
4131
4842
|
|
|
4132
4843
|
const root = createUniversalRoot(container, macOSDriver, {
|
|
@@ -4303,7 +5014,7 @@ export function createMacOSRoot(hostView: NSView, { fontFamily }: MacOSRootOptio
|
|
|
4303
5014
|
try {
|
|
4304
5015
|
// Row box relative to the clip's top edge, computed in doc
|
|
4305
5016
|
// coordinates so it stays docH-independent like `offset`.
|
|
4306
|
-
const inDoc = child.view.superview
|
|
5017
|
+
const inDoc = child.view.superview!.convertRectToView(child.view.frame, doc)
|
|
4307
5018
|
const h = Number(inDoc.size.height)
|
|
4308
5019
|
const y = Number(inDoc.origin.y)
|
|
4309
5020
|
const top = flipped ? y - originY : originY + clipH - y - h
|
|
@@ -4367,7 +5078,18 @@ export function createMacOSRoot(hostView: NSView, { fontFamily }: MacOSRootOptio
|
|
|
4367
5078
|
throw new Error('No AppKit node with id ' + id)
|
|
4368
5079
|
}
|
|
4369
5080
|
|
|
4370
|
-
|
|
5081
|
+
// Single-key writes merge into the node's style bag — applyStyle
|
|
5082
|
+
// diffs against the last applied bag, so a partial object here
|
|
5083
|
+
// would reset every other style.
|
|
5084
|
+
const style = { ...node.props?.style }
|
|
5085
|
+
if (value == null) {
|
|
5086
|
+
delete style[name]
|
|
5087
|
+
} else {
|
|
5088
|
+
style[name] = value
|
|
5089
|
+
}
|
|
5090
|
+
|
|
5091
|
+
node.props = { ...node.props, style }
|
|
5092
|
+
applyStyle(node, style)
|
|
4371
5093
|
container.hostView.layoutSubtreeIfNeeded?.()
|
|
4372
5094
|
return this.frameInWindow(id)
|
|
4373
5095
|
},
|
|
@@ -4588,7 +5310,7 @@ export function createMacOSRoot(hostView: NSView, { fontFamily }: MacOSRootOptio
|
|
|
4588
5310
|
buttonActionTarget.textDidChange({ object: node.view } as NSNotification)
|
|
4589
5311
|
} else {
|
|
4590
5312
|
node.view!.stringValue = String(value)
|
|
4591
|
-
buttonActionTarget.
|
|
5313
|
+
buttonActionTarget.controlTextDidChange({ object: node.view } as NSNotification)
|
|
4592
5314
|
}
|
|
4593
5315
|
},
|
|
4594
5316
|
pressAccessibilityLabel(label: string) {
|