@octane-xplat/macos-renderer 0.10.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 +153 -9
- 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 +1115 -391
- package/src/intrinsics.d.ts +17 -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,42 +2125,224 @@ 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 {
|
|
1852
2326
|
inputTransparentViews.delete(node.view!)
|
|
1853
2327
|
}
|
|
1854
2328
|
} else if (name === 'objectFit' && node.type === 'image') {
|
|
1855
|
-
//
|
|
1856
|
-
|
|
2329
|
+
// NSImageScaling: 0 ProportionallyDown, 1 AxesIndependently,
|
|
2330
|
+
// 2 None (centered, unscaled), 3 ProportionallyUpOrDown.
|
|
2331
|
+
// AppKit has no cover mode — 'cover' degrades to scale-down.
|
|
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
|
|
1857
2340
|
} else if (name === 'fontSize' && ['label', 'textfield', 'textview'].includes(node.type)) {
|
|
1858
|
-
|
|
1859
|
-
node
|
|
1860
|
-
node.view!.font = fontForFamilyStyle(
|
|
1861
|
-
value,
|
|
1862
|
-
weight,
|
|
1863
|
-
style.fontFamily ?? node.appliedFontFamily,
|
|
1864
|
-
)
|
|
2341
|
+
node.styleFontSize = value
|
|
2342
|
+
syncNodeFont(node)
|
|
1865
2343
|
} else if (name === 'fontFamily' && ['label', 'textfield', 'textview'].includes(node.type)) {
|
|
1866
|
-
node.
|
|
1867
|
-
|
|
1868
|
-
const weight = style.fontWeight ?? node.appliedFontWeight ?? 400
|
|
1869
|
-
node.view!.font = fontForFamilyStyle(size, weight, value)
|
|
2344
|
+
node.styleFontFamily = String(value)
|
|
2345
|
+
syncNodeFont(node)
|
|
1870
2346
|
} else if (name === 'color' && ['label', 'textfield', 'textview'].includes(node.type)) {
|
|
1871
2347
|
node.styleColor = String(value)
|
|
1872
2348
|
node.view!.textColor = nativeColor(value)
|
|
@@ -1874,31 +2350,28 @@ function applyStyle(node: ElementNode, style: PropBag) {
|
|
|
1874
2350
|
// syncText applies this to each paragraph; it is not the label's fixed height.
|
|
1875
2351
|
continue
|
|
1876
2352
|
} else if (name === 'padding' && node.type === 'flexboxlayout') {
|
|
1877
|
-
node.
|
|
2353
|
+
node.stylePaddingBox = parseEdgeInsets(value)
|
|
2354
|
+
syncEdgeInsets(node)
|
|
1878
2355
|
} else if (EDGE_INSET_PROPS.has(name) && node.type === 'flexboxlayout') {
|
|
1879
|
-
const
|
|
2356
|
+
const edges = (node.stylePaddingEdges ??= {})
|
|
1880
2357
|
for (const edge of EDGE_INSET_PROPS.get(name)!) {
|
|
1881
|
-
|
|
2358
|
+
edges[edge] = Number(value) || 0
|
|
1882
2359
|
}
|
|
1883
2360
|
|
|
1884
|
-
node
|
|
2361
|
+
syncEdgeInsets(node)
|
|
1885
2362
|
} else if (name === 'backgroundColor' && node.view) {
|
|
1886
2363
|
node.styleBg = String(value)
|
|
2364
|
+
node.bgApplied = true
|
|
1887
2365
|
node.view.wantsLayer = true
|
|
1888
2366
|
node.view.layer.backgroundColor = nativeColor(value).CGColor
|
|
1889
2367
|
} else if (name === 'borderRadius' && node.view) {
|
|
1890
|
-
node.
|
|
1891
|
-
node
|
|
1892
|
-
node.view.layer.masksToBounds = true
|
|
2368
|
+
node.styleRadius = value
|
|
2369
|
+
syncCornerRadius(node)
|
|
1893
2370
|
} else if ((name === 'width' || name === 'height') && node.view) {
|
|
1894
2371
|
setSizeConstraint(node, name, value)
|
|
1895
2372
|
} else if (name === 'left' && node.view) {
|
|
1896
|
-
if (node.parent?.type === 'gridlayout') {
|
|
1897
|
-
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
if (node.parent?.type === 'absolutelayout') {
|
|
1901
|
-
layoutAbsoluteChildren(node.parent)
|
|
2373
|
+
if (node.parent?.type === 'gridlayout' || node.parent?.type === 'absolutelayout') {
|
|
2374
|
+
queueLayoutReconcile(node.container, node.parent)
|
|
1902
2375
|
}
|
|
1903
2376
|
} else if ((name === 'translateX' || name === 'translateY' || name === 'zIndex') && node.view) {
|
|
1904
2377
|
node.view.wantsLayer = true
|
|
@@ -1915,12 +2388,8 @@ function applyStyle(node: ElementNode, style: PropBag) {
|
|
|
1915
2388
|
} else if (name === 'opacity' && node.view) {
|
|
1916
2389
|
node.view.alphaValue = Number(value)
|
|
1917
2390
|
} else if (name === 'fontWeight' && ['label', 'textfield', 'textview'].includes(node.type)) {
|
|
1918
|
-
node.
|
|
1919
|
-
node
|
|
1920
|
-
node.view!.font.pointSize,
|
|
1921
|
-
value,
|
|
1922
|
-
style.fontFamily ?? node.appliedFontFamily,
|
|
1923
|
-
)
|
|
2391
|
+
node.styleFontWeight = String(value)
|
|
2392
|
+
syncNodeFont(node)
|
|
1924
2393
|
} else if (name === 'textAlign' && node.type === 'label') {
|
|
1925
2394
|
node.view!.alignment =
|
|
1926
2395
|
value === 'left'
|
|
@@ -1940,6 +2409,9 @@ function applyStyle(node: ElementNode, style: PropBag) {
|
|
|
1940
2409
|
console.warn('[macos-style] ignored unsupported style.' + name + ' on <' + node.type + '>')
|
|
1941
2410
|
}
|
|
1942
2411
|
}
|
|
2412
|
+
|
|
2413
|
+
node.appliedStyle = next
|
|
2414
|
+
syncStackLayout(node)
|
|
1943
2415
|
}
|
|
1944
2416
|
|
|
1945
2417
|
function applyClassName(node: ElementNode, value: any) {
|
|
@@ -1947,11 +2419,23 @@ function applyClassName(node: ElementNode, value: any) {
|
|
|
1947
2419
|
.split(/\s+/)
|
|
1948
2420
|
.filter(Boolean)
|
|
1949
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
|
+
|
|
1950
2434
|
if (node.type === 'label') {
|
|
1951
|
-
|
|
2435
|
+
const grabber = classes.includes('vx-sheet-grabber')
|
|
2436
|
+
if (grabber) {
|
|
1952
2437
|
node.classLineHeight = 24
|
|
1953
2438
|
setSizeConstraint(node, 'height', 24)
|
|
1954
|
-
node.view!.alignment = NSTextAlignment.Center
|
|
1955
2439
|
}
|
|
1956
2440
|
|
|
1957
2441
|
const sizes: Record<string, number> = {
|
|
@@ -1984,18 +2468,14 @@ function applyClassName(node: ElementNode, value: any) {
|
|
|
1984
2468
|
for (const name of classes) {
|
|
1985
2469
|
const heading = headingMetrics[name]
|
|
1986
2470
|
if (heading) {
|
|
1987
|
-
node.
|
|
2471
|
+
node.classFontWeight = '700'
|
|
2472
|
+
node.classFontSize = heading.size
|
|
1988
2473
|
node.headingDefaultHeight = heading.height
|
|
1989
|
-
node.view!.font = fontForFamilyStyle(heading.size, 700, node.appliedFontFamily)
|
|
1990
2474
|
}
|
|
1991
2475
|
|
|
1992
2476
|
if (sizes[name]) {
|
|
2477
|
+
node.classFontSize = sizes[name]
|
|
1993
2478
|
node.headingDefaultHeight = undefined
|
|
1994
|
-
node.view!.font = fontForFamilyStyle(
|
|
1995
|
-
sizes[name],
|
|
1996
|
-
node.appliedFontWeight ?? 400,
|
|
1997
|
-
node.appliedFontFamily,
|
|
1998
|
-
)
|
|
1999
2479
|
}
|
|
2000
2480
|
|
|
2001
2481
|
if (lineHeights[name] != null) {
|
|
@@ -2003,13 +2483,11 @@ function applyClassName(node: ElementNode, value: any) {
|
|
|
2003
2483
|
}
|
|
2004
2484
|
|
|
2005
2485
|
if (name === 'font-semibold') {
|
|
2006
|
-
node.
|
|
2007
|
-
node.view!.font = fontForFamilyStyle(node.view!.font.pointSize, 600, node.appliedFontFamily)
|
|
2486
|
+
node.classFontWeight = '600'
|
|
2008
2487
|
}
|
|
2009
2488
|
|
|
2010
2489
|
if (name === 'font-bold') {
|
|
2011
|
-
node.
|
|
2012
|
-
node.view!.font = fontForFamilyStyle(node.view!.font.pointSize, 700, node.appliedFontFamily)
|
|
2490
|
+
node.classFontWeight = '700'
|
|
2013
2491
|
}
|
|
2014
2492
|
|
|
2015
2493
|
if (name === 'text-muted') {
|
|
@@ -2020,6 +2498,28 @@ function applyClassName(node: ElementNode, value: any) {
|
|
|
2020
2498
|
node.colorSlot = 'onprimary'
|
|
2021
2499
|
}
|
|
2022
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
|
+
}
|
|
2023
2523
|
}
|
|
2024
2524
|
|
|
2025
2525
|
if (
|
|
@@ -2038,37 +2538,27 @@ function applyClassName(node: ElementNode, value: any) {
|
|
|
2038
2538
|
|
|
2039
2539
|
for (const name of classes) {
|
|
2040
2540
|
if (gaps[name] !== undefined) {
|
|
2041
|
-
node.
|
|
2541
|
+
node.classSpacing = gaps[name]
|
|
2042
2542
|
}
|
|
2043
2543
|
|
|
2044
2544
|
if (name === 'flex-row') {
|
|
2045
|
-
node.
|
|
2545
|
+
node.classOrientation = NSUserInterfaceLayoutOrientation.Horizontal
|
|
2046
2546
|
}
|
|
2047
2547
|
|
|
2048
2548
|
if (name === 'flex-col') {
|
|
2049
|
-
node.
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
if (name === 'items-center' || name === 'items-start' || name === 'items-end') {
|
|
2053
|
-
node.view!.alignment = stackAlignmentAttribute(node.view, stackAlignItems(node))
|
|
2549
|
+
node.classOrientation = NSUserInterfaceLayoutOrientation.Vertical
|
|
2054
2550
|
}
|
|
2055
2551
|
|
|
2056
2552
|
if (name === 'vx-toast' && node.type === 'flexboxlayout') {
|
|
2057
|
-
|
|
2553
|
+
node.classInsets = { top: 12, right: 12, bottom: 12, left: 12 }
|
|
2058
2554
|
}
|
|
2059
2555
|
|
|
2060
2556
|
if (name === 'vx-toast-viewport') {
|
|
2061
|
-
|
|
2557
|
+
node.classSpacing = 8
|
|
2062
2558
|
}
|
|
2063
2559
|
|
|
2064
2560
|
if (name === 'vx-button') {
|
|
2065
|
-
node.
|
|
2066
|
-
node.view!.alignment = NSLayoutAttribute.CenterY
|
|
2067
|
-
node.view!.distribution = NSStackViewDistribution.GravityAreas
|
|
2068
|
-
}
|
|
2069
|
-
|
|
2070
|
-
if (name === 'justify-between') {
|
|
2071
|
-
node.view!.distribution = NSStackViewDistribution.EqualSpacing
|
|
2561
|
+
node.classOrientation = NSUserInterfaceLayoutOrientation.Horizontal
|
|
2072
2562
|
}
|
|
2073
2563
|
|
|
2074
2564
|
if (name === 'flex-1') {
|
|
@@ -2091,37 +2581,21 @@ function applyClassName(node: ElementNode, value: any) {
|
|
|
2091
2581
|
}
|
|
2092
2582
|
|
|
2093
2583
|
if (name === 'rounded-full' || name.startsWith('rounded-')) {
|
|
2094
|
-
node.
|
|
2095
|
-
node.view!.layer.cornerRadius = name === 'rounded-full' ? 12 : 8
|
|
2096
|
-
node.view!.layer.masksToBounds = true
|
|
2584
|
+
node.classRadius = name === 'rounded-full' ? 12 : 8
|
|
2097
2585
|
}
|
|
2098
2586
|
|
|
2099
2587
|
if (name === 'bg-primary' || name === 'btn') {
|
|
2100
2588
|
node.bgSlot = 'primary'
|
|
2101
|
-
node.
|
|
2102
|
-
node.view!.layer.backgroundColor = nativeColor(
|
|
2103
|
-
SCHEME_COLORS[nodeScheme(node)].primary,
|
|
2104
|
-
).CGColor
|
|
2105
|
-
|
|
2106
|
-
node.view!.edgeInsets = { top: 6, left: 10, bottom: 6, right: 10 }
|
|
2589
|
+
node.classInsets = { top: 6, right: 10, bottom: 6, left: 10 }
|
|
2107
2590
|
}
|
|
2108
2591
|
|
|
2109
2592
|
if (name === 'bg-danger') {
|
|
2110
2593
|
node.bgSlot = 'danger'
|
|
2111
|
-
node.view!.wantsLayer = true
|
|
2112
|
-
node.view!.layer.backgroundColor = nativeColor(
|
|
2113
|
-
SCHEME_COLORS[nodeScheme(node)].danger,
|
|
2114
|
-
).CGColor
|
|
2115
2594
|
}
|
|
2116
2595
|
|
|
2117
2596
|
if (name === 'btn-secondary' || name === 'chip' || name === 'chip-off') {
|
|
2118
2597
|
node.bgSlot = 'secondary'
|
|
2119
|
-
node.
|
|
2120
|
-
node.view!.layer.backgroundColor = nativeColor(
|
|
2121
|
-
SCHEME_COLORS[nodeScheme(node)].secondary,
|
|
2122
|
-
).CGColor
|
|
2123
|
-
|
|
2124
|
-
node.view!.edgeInsets = { top: 4, left: 8, bottom: 4, right: 8 }
|
|
2598
|
+
node.classInsets = { top: 4, right: 8, bottom: 4, left: 8 }
|
|
2125
2599
|
}
|
|
2126
2600
|
|
|
2127
2601
|
// Panels that paint the web body's --color-surface. On this host the
|
|
@@ -2134,22 +2608,18 @@ function applyClassName(node: ElementNode, value: any) {
|
|
|
2134
2608
|
name === 'modal-panel'
|
|
2135
2609
|
) {
|
|
2136
2610
|
node.bgSlot = 'surface'
|
|
2137
|
-
node.view!.wantsLayer = true
|
|
2138
|
-
node.view!.layer.backgroundColor = nativeColor(
|
|
2139
|
-
SCHEME_COLORS[nodeScheme(node)].surface,
|
|
2140
|
-
).CGColor
|
|
2141
2611
|
}
|
|
2142
2612
|
}
|
|
2613
|
+
|
|
2614
|
+
syncStackLayout(node)
|
|
2615
|
+
|
|
2616
|
+
syncEdgeInsets(node)
|
|
2617
|
+
syncCornerRadius(node)
|
|
2143
2618
|
}
|
|
2144
2619
|
|
|
2145
2620
|
if (node.type === 'textfield' || node.type === 'textview') {
|
|
2146
2621
|
if (classes.includes('vx-input') || classes.includes('vx-textarea')) {
|
|
2147
|
-
node.
|
|
2148
|
-
14,
|
|
2149
|
-
node.appliedFontWeight ?? 400,
|
|
2150
|
-
node.appliedFontFamily,
|
|
2151
|
-
)
|
|
2152
|
-
|
|
2622
|
+
node.classFontSize = 14
|
|
2153
2623
|
node.colorSlot = 'text'
|
|
2154
2624
|
node.view!.drawsBackground = false
|
|
2155
2625
|
if (node.type === 'textfield') {
|
|
@@ -2160,6 +2630,8 @@ function applyClassName(node: ElementNode, value: any) {
|
|
|
2160
2630
|
node.view!.textContainerInset = { width: 0, height: 0 }
|
|
2161
2631
|
}
|
|
2162
2632
|
}
|
|
2633
|
+
|
|
2634
|
+
syncNodeFont(node)
|
|
2163
2635
|
}
|
|
2164
2636
|
|
|
2165
2637
|
applyThemeColors(node)
|
|
@@ -2174,7 +2646,7 @@ function applyClassName(node: ElementNode, value: any) {
|
|
|
2174
2646
|
}
|
|
2175
2647
|
|
|
2176
2648
|
if (node.parent) {
|
|
2177
|
-
|
|
2649
|
+
queueLayoutReconcile(node.container, node.parent)
|
|
2178
2650
|
}
|
|
2179
2651
|
|
|
2180
2652
|
updateStackDistribution(node)
|
|
@@ -2192,6 +2664,38 @@ function textContent(node: ElementNode): string {
|
|
|
2192
2664
|
return node.children.map(textContent).join('')
|
|
2193
2665
|
}
|
|
2194
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
|
+
|
|
2195
2699
|
function setLabelText(node: ElementNode, text: any) {
|
|
2196
2700
|
const lineHeight = Number(node.props?.style?.lineHeight)
|
|
2197
2701
|
if (!Number.isFinite(lineHeight) || lineHeight <= 0) {
|
|
@@ -2200,6 +2704,7 @@ function setLabelText(node: ElementNode, text: any) {
|
|
|
2200
2704
|
}
|
|
2201
2705
|
|
|
2202
2706
|
const paragraphStyle = NSMutableParagraphStyle.alloc().init()
|
|
2707
|
+
paragraphStyle.lineBreakMode = (node.view as NSTextField).cell.lineBreakMode
|
|
2203
2708
|
paragraphStyle.minimumLineHeight = lineHeight
|
|
2204
2709
|
paragraphStyle.maximumLineHeight = lineHeight
|
|
2205
2710
|
node.view!.attributedStringValue = NSAttributedString.alloc().initWithStringAttributes(text, {
|
|
@@ -2233,7 +2738,7 @@ function setAction(node: ElementNode, value: any) {
|
|
|
2233
2738
|
actionIdsByView.set(node.view!, node.actionId)
|
|
2234
2739
|
actionHandlers.set(node.actionId, null)
|
|
2235
2740
|
node.view!.addGestureRecognizer(
|
|
2236
|
-
NSClickGestureRecognizer.alloc().initWithTargetAction(buttonActionTarget, 'viewPressed'),
|
|
2741
|
+
NSClickGestureRecognizer.alloc().initWithTargetAction(buttonActionTarget, 'viewPressed:'),
|
|
2237
2742
|
)
|
|
2238
2743
|
}
|
|
2239
2744
|
|
|
@@ -2255,7 +2760,7 @@ function setPanAction(node: ElementNode, value: any) {
|
|
|
2255
2760
|
if (typeof value === 'function' && !node.panGestureRecognizer) {
|
|
2256
2761
|
node.panGestureRecognizer = NSPanGestureRecognizer.alloc().initWithTargetAction(
|
|
2257
2762
|
buttonActionTarget,
|
|
2258
|
-
'viewPanned',
|
|
2763
|
+
'viewPanned:',
|
|
2259
2764
|
)
|
|
2260
2765
|
|
|
2261
2766
|
node.view!.addGestureRecognizer(node.panGestureRecognizer)
|
|
@@ -2294,6 +2799,35 @@ function setControlAction(node: ElementNode, value: any, readValue: () => any) {
|
|
|
2294
2799
|
)
|
|
2295
2800
|
}
|
|
2296
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
|
+
|
|
2297
2831
|
function applyAccessibility(node: ElementNode, name: string, value: any) {
|
|
2298
2832
|
if (node.type === 'flexboxlayout' && node.actionId !== undefined) {
|
|
2299
2833
|
const props = stackAccessibilityPropsByView.get(node.view!) ?? {}
|
|
@@ -2310,6 +2844,20 @@ function applyAccessibility(node: ElementNode, name: string, value: any) {
|
|
|
2310
2844
|
if (node.type === 'label' && name === 'accessibilityLabel') {
|
|
2311
2845
|
node.view!.setAccessibilityLabel?.(String(value ?? ''))
|
|
2312
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
|
+
}
|
|
2313
2861
|
}
|
|
2314
2862
|
|
|
2315
2863
|
function applyProps(node: ElementNode, props: PropBag) {
|
|
@@ -2327,7 +2875,20 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2327
2875
|
return
|
|
2328
2876
|
}
|
|
2329
2877
|
|
|
2878
|
+
const layoutChildProps: Record<string, readonly string[]> = {
|
|
2879
|
+
absolutelayout: ['left', 'top', 'right', 'bottom'],
|
|
2880
|
+
gridlayout: ['row', 'col', 'rowSpan', 'colSpan', 'horizontalAlignment', 'verticalAlignment'],
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2330
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
|
+
|
|
2331
2892
|
if (name === 'onLayoutChanged') {
|
|
2332
2893
|
setLayoutAction(node, value)
|
|
2333
2894
|
continue
|
|
@@ -2336,7 +2897,7 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2336
2897
|
switch (node.type) {
|
|
2337
2898
|
case 'stack':
|
|
2338
2899
|
if (name === 'spacing') {
|
|
2339
|
-
node
|
|
2900
|
+
syncStackLayout(node)
|
|
2340
2901
|
} else if (name === 'style') {
|
|
2341
2902
|
applyStyle(node, value)
|
|
2342
2903
|
} else if (name === 'className') {
|
|
@@ -2349,33 +2910,12 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2349
2910
|
|
|
2350
2911
|
break
|
|
2351
2912
|
case 'flexboxlayout':
|
|
2352
|
-
if (name === '
|
|
2353
|
-
node
|
|
2354
|
-
} else if (name === 'spacing') {
|
|
2355
|
-
node.view!.spacing = Number(value ?? 0)
|
|
2356
|
-
} else if (name === 'flexDirection') {
|
|
2357
|
-
node.view!.orientation =
|
|
2358
|
-
value === 'row'
|
|
2359
|
-
? NSUserInterfaceLayoutOrientation.Horizontal
|
|
2360
|
-
: NSUserInterfaceLayoutOrientation.Vertical
|
|
2361
|
-
|
|
2362
|
-
node.view!.distribution = NSStackViewDistribution.GravityAreas
|
|
2363
|
-
node.view!.alignment = stackAlignmentAttribute(node.view, stackAlignItems(node))
|
|
2364
|
-
moveStackChildren(node)
|
|
2365
|
-
updateCrossAxisConstraints(node)
|
|
2366
|
-
} else if (name === 'alignItems') {
|
|
2367
|
-
node.view!.alignment = stackAlignmentAttribute(node.view, stackAlignItems(node))
|
|
2368
|
-
updateCrossAxisConstraints(node)
|
|
2369
|
-
} else if (name === 'justifyContent') {
|
|
2370
|
-
node.view!.distribution = NSStackViewDistribution.GravityAreas
|
|
2371
|
-
moveStackChildren(node)
|
|
2913
|
+
if (name === 'spacing') {
|
|
2914
|
+
syncStackLayout(node)
|
|
2372
2915
|
} else if (name === 'style') {
|
|
2373
2916
|
applyStyle(node, value)
|
|
2374
2917
|
} else if (name === 'className') {
|
|
2375
2918
|
applyClassName(node, value)
|
|
2376
|
-
moveStackChildren(node)
|
|
2377
|
-
node.view!.alignment = stackAlignmentAttribute(node.view, stackAlignItems(node))
|
|
2378
|
-
updateCrossAxisConstraints(node)
|
|
2379
2919
|
} else if (name === 'id') {
|
|
2380
2920
|
continue
|
|
2381
2921
|
} else if (name === 'onTap') {
|
|
@@ -2397,18 +2937,10 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2397
2937
|
'alignItems',
|
|
2398
2938
|
'justifyContent',
|
|
2399
2939
|
'flexWrap',
|
|
2400
|
-
'row',
|
|
2401
|
-
'col',
|
|
2402
|
-
'rowSpan',
|
|
2403
|
-
'colSpan',
|
|
2404
|
-
'left',
|
|
2405
|
-
'top',
|
|
2406
2940
|
'flexGrow',
|
|
2407
2941
|
'flexShrink',
|
|
2408
2942
|
'alignSelf',
|
|
2409
2943
|
'order',
|
|
2410
|
-
'horizontalAlignment',
|
|
2411
|
-
'verticalAlignment',
|
|
2412
2944
|
].includes(name)
|
|
2413
2945
|
) {
|
|
2414
2946
|
continue
|
|
@@ -2439,7 +2971,7 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2439
2971
|
} else if (name === 'accessible' || name.startsWith('accessibility')) {
|
|
2440
2972
|
applyAccessibility(node, name, value)
|
|
2441
2973
|
} else if (['rows', 'columns'].includes(name)) {
|
|
2442
|
-
|
|
2974
|
+
queueLayoutReconcile(node.container, node)
|
|
2443
2975
|
} else {
|
|
2444
2976
|
console.warn('[macos-host] ignored gridlayout prop ' + name)
|
|
2445
2977
|
}
|
|
@@ -2465,11 +2997,8 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2465
2997
|
if (name === 'text') {
|
|
2466
2998
|
setLabelText(node, String(value ?? ''))
|
|
2467
2999
|
} else if (name === 'fontSize') {
|
|
2468
|
-
node.
|
|
2469
|
-
|
|
2470
|
-
node.appliedFontWeight ?? 400,
|
|
2471
|
-
node.appliedFontFamily,
|
|
2472
|
-
)
|
|
3000
|
+
node.propFontSize = Number(value ?? 16)
|
|
3001
|
+
syncNodeFont(node)
|
|
2473
3002
|
} else if (name === 'style') {
|
|
2474
3003
|
applyStyle(node, value)
|
|
2475
3004
|
syncText(node)
|
|
@@ -2477,9 +3006,9 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2477
3006
|
applyClassName(node, value)
|
|
2478
3007
|
} else if (name === 'id') {
|
|
2479
3008
|
continue
|
|
2480
|
-
} else if (['maxLines', 'whiteSpace', 'textOverflow'
|
|
3009
|
+
} else if (['maxLines', 'whiteSpace', 'textOverflow'].includes(name)) {
|
|
2481
3010
|
continue
|
|
2482
|
-
} else if (name.startsWith('accessibility')) {
|
|
3011
|
+
} else if (name === 'accessible' || name.startsWith('accessibility')) {
|
|
2483
3012
|
applyAccessibility(node, name, value)
|
|
2484
3013
|
} else if (name.startsWith('on') && value == null) {
|
|
2485
3014
|
continue
|
|
@@ -2561,6 +3090,30 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2561
3090
|
setControlAction(node, value, () =>
|
|
2562
3091
|
String(node.type === 'textview' ? node.view!.string : (node.view!.stringValue ?? '')),
|
|
2563
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
|
+
}
|
|
2564
3117
|
} else if (name === 'style') {
|
|
2565
3118
|
applyStyle(node, value)
|
|
2566
3119
|
} else if (name === 'className') {
|
|
@@ -2569,21 +3122,11 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2569
3122
|
continue
|
|
2570
3123
|
} else if (name === 'rows' && node.type === 'textview') {
|
|
2571
3124
|
continue
|
|
2572
|
-
} else if (name.startsWith('accessibility')) {
|
|
3125
|
+
} else if (name === 'accessible' || name.startsWith('accessibility')) {
|
|
2573
3126
|
applyAccessibility(node, name, value)
|
|
2574
3127
|
} else if (name.startsWith('on') && value == null) {
|
|
2575
3128
|
continue
|
|
2576
|
-
} else if (
|
|
2577
|
-
[
|
|
2578
|
-
'editable',
|
|
2579
|
-
'enabled',
|
|
2580
|
-
'secure',
|
|
2581
|
-
'keyboardType',
|
|
2582
|
-
'returnKeyType',
|
|
2583
|
-
'autoGrow',
|
|
2584
|
-
'maxRows',
|
|
2585
|
-
].includes(name)
|
|
2586
|
-
) {
|
|
3129
|
+
} else if (['keyboardType', 'returnKeyType', 'autoGrow', 'maxRows'].includes(name)) {
|
|
2587
3130
|
continue
|
|
2588
3131
|
} else {
|
|
2589
3132
|
console.warn('[macos-host] ignored text control prop ' + name)
|
|
@@ -2642,7 +3185,7 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2642
3185
|
break
|
|
2643
3186
|
case 'image':
|
|
2644
3187
|
if (name === 'src') {
|
|
2645
|
-
node.view
|
|
3188
|
+
updateImage(node.view as NSImageView, value)
|
|
2646
3189
|
} else if (name === 'style') {
|
|
2647
3190
|
applyStyle(node, value)
|
|
2648
3191
|
} else if (name === 'className' || name === 'id' || name === 'alt') {
|
|
@@ -2658,6 +3201,13 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2658
3201
|
}
|
|
2659
3202
|
}
|
|
2660
3203
|
|
|
3204
|
+
if (
|
|
3205
|
+
(node.type === 'flexboxlayout' || node.type === 'stack') &&
|
|
3206
|
+
Object.keys(props).some(isStackLayoutInput)
|
|
3207
|
+
) {
|
|
3208
|
+
syncStackLayout(node)
|
|
3209
|
+
}
|
|
3210
|
+
|
|
2661
3211
|
if (node.type === 'webview') {
|
|
2662
3212
|
updateWebView(node, props, (height) => {
|
|
2663
3213
|
node.props.style = { ...node.props.style, height }
|
|
@@ -2670,8 +3220,17 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2670
3220
|
}
|
|
2671
3221
|
|
|
2672
3222
|
if (node.type === 'label') {
|
|
3223
|
+
syncLabelOverflow(node)
|
|
3224
|
+
syncText(node)
|
|
2673
3225
|
const style = node.props.style ?? {}
|
|
2674
|
-
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) {
|
|
2675
3234
|
const size = Number(node.view!.font?.pointSize ?? 16)
|
|
2676
3235
|
const height =
|
|
2677
3236
|
style.fontSize == null && node.headingDefaultHeight != null
|
|
@@ -2698,14 +3257,10 @@ function applyProps(node: ElementNode, props: PropBag) {
|
|
|
2698
3257
|
}
|
|
2699
3258
|
}
|
|
2700
3259
|
|
|
2701
|
-
if (node.type === 'gridlayout') {
|
|
2702
|
-
|
|
2703
|
-
} else if (node.type === 'absolutelayout') {
|
|
2704
|
-
|
|
2705
|
-
} else if (node.parent?.type === 'gridlayout') {
|
|
2706
|
-
layoutGridChildren(node.parent)
|
|
2707
|
-
} else if (node.parent?.type === 'absolutelayout') {
|
|
2708
|
-
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)
|
|
2709
3264
|
}
|
|
2710
3265
|
}
|
|
2711
3266
|
|
|
@@ -2723,6 +3278,7 @@ function detach(container: RootContainer, node: ElementNode) {
|
|
|
2723
3278
|
|
|
2724
3279
|
node.crossAxisConstraint = null
|
|
2725
3280
|
deactivateSizeConstraints(node)
|
|
3281
|
+
releasePlacementPins(node)
|
|
2726
3282
|
if (node.view) {
|
|
2727
3283
|
const parentView = previousParent?.childHost ?? previousParent?.view
|
|
2728
3284
|
if (parentView?.removeArrangedSubview) {
|
|
@@ -2732,20 +3288,53 @@ function detach(container: RootContainer, node: ElementNode) {
|
|
|
2732
3288
|
arrangedView(node).removeFromSuperview()
|
|
2733
3289
|
}
|
|
2734
3290
|
|
|
2735
|
-
|
|
2736
|
-
|
|
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
|
|
2737
3305
|
}
|
|
2738
3306
|
|
|
2739
|
-
|
|
2740
|
-
|
|
3307
|
+
const pending = container?.layoutDirty
|
|
3308
|
+
if (pending) {
|
|
3309
|
+
pending.add(parent)
|
|
3310
|
+
return
|
|
2741
3311
|
}
|
|
2742
3312
|
|
|
2743
|
-
|
|
2744
|
-
|
|
3313
|
+
reconcileLayoutParent(parent)
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
function reconcileLayoutParent(parent: ElementNode) {
|
|
3317
|
+
if (parent.type === 'gridlayout') {
|
|
3318
|
+
layoutGridChildren(parent)
|
|
3319
|
+
return
|
|
2745
3320
|
}
|
|
2746
3321
|
|
|
2747
|
-
|
|
2748
|
-
|
|
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)
|
|
2749
3338
|
}
|
|
2750
3339
|
|
|
2751
3340
|
function insert(
|
|
@@ -2754,62 +3343,54 @@ function insert(
|
|
|
2754
3343
|
node: ElementNode,
|
|
2755
3344
|
beforeId: number | null,
|
|
2756
3345
|
) {
|
|
2757
|
-
|
|
3346
|
+
// Validate before detaching — a rejected insert must not leave the node
|
|
3347
|
+
// orphaned from its current parent.
|
|
2758
3348
|
const parent = (parentId === null ? null : container.nodes.get(parentId)) ?? null
|
|
2759
3349
|
if (parentId !== null && !parent) {
|
|
2760
3350
|
throw new Error('Unknown AppKit parent ' + parentId)
|
|
2761
3351
|
}
|
|
2762
3352
|
|
|
2763
|
-
if (
|
|
2764
|
-
parent &&
|
|
2765
|
-
node.view &&
|
|
2766
|
-
parent.type !== 'stack' &&
|
|
2767
|
-
parent.type !== 'flexboxlayout' &&
|
|
2768
|
-
parent.type !== 'scrollview' &&
|
|
2769
|
-
parent.type !== 'gridlayout' &&
|
|
2770
|
-
parent.type !== 'absolutelayout'
|
|
2771
|
-
) {
|
|
3353
|
+
if (parent && node.view && !VIEW_PARENT_TYPES.has(parent.type)) {
|
|
2772
3354
|
throw new Error('AppKit <' + parent.type + '> cannot contain child views')
|
|
2773
3355
|
}
|
|
2774
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)
|
|
2775
3363
|
const siblings = parent ? parent.children : container.children
|
|
2776
3364
|
const beforeIndex = beforeId === null ? -1 : siblings.findIndex((child) => child.id === beforeId)
|
|
2777
3365
|
const index = beforeIndex < 0 ? siblings.length : beforeIndex
|
|
2778
3366
|
siblings.splice(index, 0, node)
|
|
2779
3367
|
node.parent = parent
|
|
2780
3368
|
if (node.view) {
|
|
2781
|
-
const parentView = parent?.childHost ?? parent?.view ?? container.hostView
|
|
2782
|
-
if (!parentView) {
|
|
2783
|
-
throw new Error('AppKit host has no parent view for node ' + node.id)
|
|
2784
|
-
}
|
|
2785
|
-
|
|
2786
3369
|
if (parent) {
|
|
2787
3370
|
if (parent.type === 'gridlayout') {
|
|
2788
3371
|
parentView.addSubview(node.view)
|
|
2789
3372
|
node.view.translatesAutoresizingMaskIntoConstraints = false
|
|
2790
3373
|
deactivateSizeConstraints(node)
|
|
2791
|
-
|
|
3374
|
+
queueLayoutReconcile(container, parent)
|
|
2792
3375
|
} else if (parent.type === 'absolutelayout') {
|
|
2793
3376
|
parentView.addSubview(node.view)
|
|
2794
3377
|
node.view.translatesAutoresizingMaskIntoConstraints = false
|
|
2795
3378
|
deactivateSizeConstraints(node)
|
|
2796
|
-
|
|
3379
|
+
queueLayoutReconcile(container, parent)
|
|
2797
3380
|
} else {
|
|
2798
3381
|
if (marginInsetsOf(node)) {
|
|
2799
3382
|
makeMarginHost(node)
|
|
3383
|
+
setLayoutAction(node, node.props?.onLayoutChanged)
|
|
2800
3384
|
}
|
|
2801
3385
|
|
|
2802
3386
|
// AppKit insertion indices are local to a gravity area. Honor the
|
|
2803
3387
|
// renderer's before edge instead of appending moved keyed rows.
|
|
2804
3388
|
const gravity = stackGravity(parent, node)
|
|
2805
|
-
const arrangedIndex =
|
|
2806
|
-
.slice(0, index)
|
|
2807
|
-
.filter((child) => child.view && stackGravity(parent, child) === gravity).length
|
|
3389
|
+
const arrangedIndex = gravityInsertIndex(parent, node, gravity)
|
|
2808
3390
|
|
|
2809
3391
|
parentView.insertViewAtIndexInGravity(arrangedView(node), arrangedIndex, gravity)
|
|
2810
3392
|
applySizeConstraints(node)
|
|
2811
|
-
|
|
2812
|
-
updateCrossAxisConstraints(parent)
|
|
3393
|
+
queueLayoutReconcile(container, parent)
|
|
2813
3394
|
}
|
|
2814
3395
|
} else {
|
|
2815
3396
|
parentView.addSubview(node.view)
|
|
@@ -2844,6 +3425,7 @@ function remove(container: RootContainer, parentId: number | null, node: Element
|
|
|
2844
3425
|
|
|
2845
3426
|
node.crossAxisConstraint = null
|
|
2846
3427
|
deactivateSizeConstraints(node)
|
|
3428
|
+
releasePlacementPins(node)
|
|
2847
3429
|
if (node.view) {
|
|
2848
3430
|
const parentView = expectedParent?.childHost ?? expectedParent?.view
|
|
2849
3431
|
if (parentView?.removeArrangedSubview) {
|
|
@@ -2854,31 +3436,27 @@ function remove(container: RootContainer, parentId: number | null, node: Element
|
|
|
2854
3436
|
node.marginHost?.removeFromSuperview()
|
|
2855
3437
|
node.marginHost = null
|
|
2856
3438
|
node.marginConstraints = null
|
|
3439
|
+
setLayoutAction(node, node.props?.onLayoutChanged)
|
|
2857
3440
|
}
|
|
2858
3441
|
|
|
2859
|
-
|
|
2860
|
-
layoutGridChildren(expectedParent)
|
|
2861
|
-
}
|
|
2862
|
-
|
|
2863
|
-
if (expectedParent?.type === 'absolutelayout') {
|
|
2864
|
-
layoutAbsoluteChildren(expectedParent)
|
|
2865
|
-
}
|
|
2866
|
-
|
|
2867
|
-
if (expectedParent) {
|
|
2868
|
-
updateStackDistribution(expectedParent)
|
|
2869
|
-
}
|
|
2870
|
-
|
|
3442
|
+
queueLayoutReconcile(container, expectedParent)
|
|
2871
3443
|
node.parent = null
|
|
2872
3444
|
syncText(expectedParent)
|
|
2873
3445
|
}
|
|
2874
3446
|
|
|
2875
3447
|
function destroy(node: ElementNode) {
|
|
2876
|
-
|
|
2877
|
-
|
|
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)
|
|
2878
3456
|
}
|
|
2879
3457
|
|
|
2880
|
-
if (node.type === 'scrollview' && node.scrollObserverInstalled) {
|
|
2881
|
-
const clipView = node.view
|
|
3458
|
+
if (node.type === 'scrollview' && node.scrollObserverInstalled && node.view) {
|
|
3459
|
+
const clipView = node.view.contentView
|
|
2882
3460
|
scrollHandlers.delete(clipView)
|
|
2883
3461
|
NSNotificationCenter.defaultCenter.removeObserver(node.scrollObserver)
|
|
2884
3462
|
node.scrollObserver = null
|
|
@@ -2890,6 +3468,9 @@ function destroy(node: ElementNode) {
|
|
|
2890
3468
|
node.layoutObserver = null
|
|
2891
3469
|
}
|
|
2892
3470
|
|
|
3471
|
+
layoutHandlers.delete(node.layoutObservedView)
|
|
3472
|
+
node.layoutObservedView = null
|
|
3473
|
+
|
|
2893
3474
|
if (node.actionId !== undefined) {
|
|
2894
3475
|
actionHandlers.delete(node.actionId)
|
|
2895
3476
|
accessibilityLabels.delete(node.actionId)
|
|
@@ -2897,18 +3478,127 @@ function destroy(node: ElementNode) {
|
|
|
2897
3478
|
}
|
|
2898
3479
|
}
|
|
2899
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
|
+
|
|
2900
3565
|
function applyCommand(container: RootContainer, command: UniversalHostCommand) {
|
|
2901
3566
|
switch (command.op) {
|
|
2902
|
-
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
|
+
|
|
2903
3576
|
container.nodes.set(command.id, makeNode(container, command.id, command.type, command.props))
|
|
2904
3577
|
return
|
|
3578
|
+
}
|
|
2905
3579
|
case 'update': {
|
|
2906
3580
|
const node = container.nodes.get(command.id)
|
|
2907
3581
|
if (!node) {
|
|
2908
3582
|
throw new Error('Unknown AppKit node ' + command.id)
|
|
2909
3583
|
}
|
|
2910
3584
|
|
|
2911
|
-
|
|
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
|
+
|
|
2912
3602
|
return
|
|
2913
3603
|
}
|
|
2914
3604
|
case 'insert':
|
|
@@ -2954,8 +3644,14 @@ function applyCommand(container: RootContainer, command: UniversalHostCommand) {
|
|
|
2954
3644
|
throw new Error('Unknown AppKit node ' + command.id)
|
|
2955
3645
|
}
|
|
2956
3646
|
|
|
2957
|
-
const replacement = makeNode(container, command.id, command.type, command.props)
|
|
2958
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)
|
|
2959
3655
|
const siblings = parent ? parent.children : container.children
|
|
2960
3656
|
const index = siblings.indexOf(node)
|
|
2961
3657
|
const beforeId = index < 0 ? null : (siblings[index + 1]?.id ?? null)
|
|
@@ -2974,26 +3670,41 @@ const macOSDriver: UniversalHostDriver<RootContainer, any> = {
|
|
|
2974
3670
|
id: 'macos',
|
|
2975
3671
|
capabilities: { text: 'host' },
|
|
2976
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)
|
|
2977
3677
|
return {
|
|
2978
3678
|
apply() {
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
'
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
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
|
+
}
|
|
2997
3708
|
}
|
|
2998
3709
|
}
|
|
2999
3710
|
},
|
|
@@ -3747,7 +4458,7 @@ function attachContextMenu(
|
|
|
3747
4458
|
|
|
3748
4459
|
const menuItem = NSMenuItem.alloc().initWithTitleActionKeyEquivalent(
|
|
3749
4460
|
String(item?.title ?? ''),
|
|
3750
|
-
'menuItemSelected',
|
|
4461
|
+
'menuItemSelected:',
|
|
3751
4462
|
'',
|
|
3752
4463
|
)
|
|
3753
4464
|
|
|
@@ -3880,7 +4591,7 @@ function attachDatePicker(view: NSView, options: PropBag, onChange: (event: any)
|
|
|
3880
4591
|
const actionId = nextActionId++
|
|
3881
4592
|
picker.tag = actionId
|
|
3882
4593
|
picker.target = buttonActionTarget
|
|
3883
|
-
picker.action = 'controlChanged'
|
|
4594
|
+
picker.action = 'controlChanged:'
|
|
3884
4595
|
actionHandlers.set(actionId, () => {
|
|
3885
4596
|
const date = picker.dateValue
|
|
3886
4597
|
const millis =
|
|
@@ -4086,10 +4797,11 @@ function presentSheet(view: NSView, options: PropBag) {
|
|
|
4086
4797
|
}
|
|
4087
4798
|
|
|
4088
4799
|
const appKitBridge = (globalThis.__xplatAppKit ??= {})
|
|
4800
|
+
installAppearanceBridge(appKitBridge)
|
|
4089
4801
|
appKitBridge.observeHover = observeHover
|
|
4090
4802
|
appKitBridge.showAnchoredPopup = showAnchoredPopup
|
|
4091
4803
|
appKitBridge.showLayer = (options: PropBag) =>
|
|
4092
|
-
showWindowLayer(options, {
|
|
4804
|
+
showWindowLayer(options as WindowLayerOptions, {
|
|
4093
4805
|
createRoot: (view: NSView, anchor: NSView) =>
|
|
4094
4806
|
createMacOSRoot(view, { fontFamily: fontFamilyForView(anchor) }),
|
|
4095
4807
|
fittingSize: popupFittingSize,
|
|
@@ -4103,7 +4815,7 @@ installPresentationBridge(appKitBridge, createMacOSRoot, fontFamilyForView)
|
|
|
4103
4815
|
const rootFontFamilies = new WeakMap<object, string | undefined>()
|
|
4104
4816
|
|
|
4105
4817
|
function fontFamilyForView(view: NSView | undefined): string | undefined {
|
|
4106
|
-
for (let current = view; current; current = current.superview) {
|
|
4818
|
+
for (let current: NSView | null | undefined = view; current; current = current.superview) {
|
|
4107
4819
|
if (rootFontFamilies.has(current)) {
|
|
4108
4820
|
return rootFontFamilies.get(current)
|
|
4109
4821
|
}
|
|
@@ -4125,6 +4837,7 @@ export function createMacOSRoot(hostView: NSView, { fontFamily }: MacOSRootOptio
|
|
|
4125
4837
|
nodes: new Map(),
|
|
4126
4838
|
children: [],
|
|
4127
4839
|
root: null,
|
|
4840
|
+
layoutDirty: null,
|
|
4128
4841
|
}
|
|
4129
4842
|
|
|
4130
4843
|
const root = createUniversalRoot(container, macOSDriver, {
|
|
@@ -4301,7 +5014,7 @@ export function createMacOSRoot(hostView: NSView, { fontFamily }: MacOSRootOptio
|
|
|
4301
5014
|
try {
|
|
4302
5015
|
// Row box relative to the clip's top edge, computed in doc
|
|
4303
5016
|
// coordinates so it stays docH-independent like `offset`.
|
|
4304
|
-
const inDoc = child.view.superview
|
|
5017
|
+
const inDoc = child.view.superview!.convertRectToView(child.view.frame, doc)
|
|
4305
5018
|
const h = Number(inDoc.size.height)
|
|
4306
5019
|
const y = Number(inDoc.origin.y)
|
|
4307
5020
|
const top = flipped ? y - originY : originY + clipH - y - h
|
|
@@ -4365,7 +5078,18 @@ export function createMacOSRoot(hostView: NSView, { fontFamily }: MacOSRootOptio
|
|
|
4365
5078
|
throw new Error('No AppKit node with id ' + id)
|
|
4366
5079
|
}
|
|
4367
5080
|
|
|
4368
|
-
|
|
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)
|
|
4369
5093
|
container.hostView.layoutSubtreeIfNeeded?.()
|
|
4370
5094
|
return this.frameInWindow(id)
|
|
4371
5095
|
},
|
|
@@ -4586,7 +5310,7 @@ export function createMacOSRoot(hostView: NSView, { fontFamily }: MacOSRootOptio
|
|
|
4586
5310
|
buttonActionTarget.textDidChange({ object: node.view } as NSNotification)
|
|
4587
5311
|
} else {
|
|
4588
5312
|
node.view!.stringValue = String(value)
|
|
4589
|
-
buttonActionTarget.
|
|
5313
|
+
buttonActionTarget.controlTextDidChange({ object: node.view } as NSNotification)
|
|
4590
5314
|
}
|
|
4591
5315
|
},
|
|
4592
5316
|
pressAccessibilityLabel(label: string) {
|