@octane-xplat/macos-renderer 0.11.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +7 -7
  2. package/package.json +1 -1
  3. package/src/index.ts +151 -32
package/README.md CHANGED
@@ -9,12 +9,13 @@ Your application owns startup, windows, menus, host services, and custom font
9
9
  assets. The renderer owns views, layout, events, accessibility, and its hosted
10
10
  popups and sheets. It does not load web stylesheets or implement every
11
11
  NativeScript widget. See the [macOS harness](../../apps/macos/README.md) for
12
- the current component and service limits. The image-crop integration test compiles and mounts the public UI barrel.
13
- The full application harness has not been reverified by that test. Registered
14
- action selectors match the assigned target/action strings, and the maintained
15
- consumer checks direct handler dispatch — but OS-level input delivery (a live
12
+ the current component and service limits.
13
+
14
+ Verification status: the image-crop integration test compiles and mounts the
15
+ public UI barrel, and registered action selectors match their target/action
16
+ strings through direct handler dispatch. OS-level input delivery (a live
16
17
  AppKit `performClick`, control edits, gestures) has not been verified end to
17
- end.
18
+ end, and the full application harness has not been reverified by that test.
18
19
 
19
20
  ## Configure an app
20
21
 
@@ -373,5 +374,4 @@ The packed check uses the renderer and CLI tarballs in a temporary app outside
373
374
  the checkout. It checks declarations and production compilation on every
374
375
  host; on Apple Silicon macOS it also packages and launches the app and verifies
375
376
  retained-root HMR. Programmatic action dispatch establishes handler behavior,
376
- not OS input or hit-testing. Publication requires the repository's normal
377
- [npm bootstrap and trusted-publisher setup](../../.agents/docs/releases.md).
377
+ not OS input or hit-testing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octane-xplat/macos-renderer",
3
- "version": "0.11.1",
3
+ "version": "0.13.0",
4
4
  "description": "Experimental AppKit renderer for Octane on macOS",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/index.ts CHANGED
@@ -381,7 +381,7 @@ class GridLayoutView extends NSView {
381
381
  }
382
382
 
383
383
  // Absolute's host — children position via their own left/top/right/bottom
384
- // layout-child props; the layout() pass sets frames directly.
384
+ // layout-child props; the layout() pass updates parent-anchored constraints.
385
385
  class AbsoluteLayoutView extends NSView {
386
386
  static {
387
387
  NativeClass(this)
@@ -637,30 +637,24 @@ function syncMarginHost(node: ElementNode) {
637
637
  }
638
638
 
639
639
  const insets = marginInsetsOf(node)
640
- const gravity = stackGravity(parent, node)
641
- const index = gravityInsertIndex(parent, node, gravity)
642
640
  if (insets && !node.marginHost) {
641
+ const gravity = stackGravity(parent, node)
642
+ const index = gravityInsertIndex(parent, node, gravity)
643
643
  stack.removeArrangedSubview(node.view)
644
644
  node.view.removeFromSuperview()
645
645
  const host = makeMarginHost(node)
646
646
  stack.insertViewAtIndexInGravity(host, index, gravity)
647
647
  queueLayoutReconcile(node.container, parent)
648
648
  setLayoutAction(node, node.props?.onLayoutChanged)
649
- } else if (!insets && node.marginHost) {
650
- stack.removeArrangedSubview(node.marginHost)
651
- node.view.removeFromSuperview()
652
- node.marginHost.removeFromSuperview()
653
- node.marginHost = null
654
- node.marginConstraints = null
655
- stack.insertViewAtIndexInGravity(node.view, index, gravity)
656
- queueLayoutReconcile(node.container, parent)
657
- setLayoutAction(node, node.props?.onLayoutChanged)
658
- } else if (insets && node.marginHost) {
649
+ } else if (node.marginHost) {
650
+ // Once wrapped, the host stays arranged for the node's lifetime —
651
+ // unwrapping would detach a first-responder ancestor and end live
652
+ // editing. Zeroed constants collapse it to the child's frame.
659
653
  const [leading, trailing, top, bottom] = node.marginConstraints
660
- leading.constant = insets.left
661
- trailing.constant = -insets.right
662
- top.constant = insets.top
663
- bottom.constant = -insets.bottom
654
+ leading.constant = insets?.left ?? 0
655
+ trailing.constant = 0 - (insets?.right ?? 0)
656
+ top.constant = insets?.top ?? 0
657
+ bottom.constant = 0 - (insets?.bottom ?? 0)
664
658
  }
665
659
  }
666
660
 
@@ -744,21 +738,86 @@ function gravityInsertIndex(parent: ElementNode, node: ElementNode, gravity: num
744
738
  return index
745
739
  }
746
740
 
741
+ /** Snapshot one gravity area's arranged views. The bridge returns an
742
+ * NSArray; `count` may be a method or a property depending on interop. */
743
+ function stackViewsInGravity(stack: any, gravity: number) {
744
+ const views = typeof stack?.viewsInGravity === 'function' ? stack.viewsInGravity(gravity) : null
745
+ const count = typeof views?.count === 'function' ? views.count() : (views?.count ?? 0)
746
+ const arrangedViews: any[] = []
747
+ for (let index = 0; index < count; index++) {
748
+ arrangedViews.push(views.objectAtIndex(index))
749
+ }
750
+
751
+ return arrangedViews
752
+ }
753
+
754
+ /** Reconcile one gravity area with the children's desired views. Only
755
+ * re-parented children land in `moved` — detaching a view that is (or
756
+ * contains) the first responder ends the active editing session. */
757
+ function syncStackGravityArea(
758
+ parent: ElementNode,
759
+ stack: any,
760
+ gravity: number,
761
+ moved: Set<ElementNode>,
762
+ ) {
763
+ const desired: ElementNode[] = []
764
+ for (const child of parent.children) {
765
+ if (child.view && stackGravity(parent, child) === gravity) {
766
+ desired.push(child)
767
+ }
768
+ }
769
+
770
+ const keep = new Set(desired.map((child) => arrangedView(child)))
771
+ const arrangedViews = stackViewsInGravity(stack, gravity)
772
+ for (let index = arrangedViews.length - 1; index >= 0; index--) {
773
+ if (!keep.has(arrangedViews[index])) {
774
+ stack.removeArrangedSubview(arrangedViews[index])
775
+ arrangedViews[index].removeFromSuperview()
776
+ arrangedViews.splice(index, 1)
777
+ }
778
+ }
779
+
780
+ for (let index = 0; index < desired.length; index++) {
781
+ const child = desired[index]
782
+ const view = arrangedView(child)
783
+ if (arrangedViews[index] === view) {
784
+ continue
785
+ }
786
+
787
+ const from = arrangedViews.indexOf(view)
788
+ if (from >= 0) {
789
+ stack.removeArrangedSubview(view)
790
+ view.removeFromSuperview()
791
+ arrangedViews.splice(from, 1)
792
+ }
793
+
794
+ stack.insertViewAtIndexInGravity(view, index, gravity)
795
+ arrangedViews.splice(index, 0, view)
796
+ moved.add(child)
797
+ }
798
+ }
799
+
747
800
  function moveStackChildren(parent: ElementNode) {
748
801
  const stack = parent.childHost ?? parent.view
749
802
  if (typeof stack?.addViewInGravity !== 'function') {
750
803
  return
751
804
  }
752
805
 
753
- for (const child of parent.children) {
754
- if (!child.view) {
755
- continue
756
- }
806
+ // Diff each gravity area against the live arrangement so unchanged
807
+ // children are never re-parented — AppKit's endEditingFor: fires when a
808
+ // first-responder ancestor leaves its superview. Orientation flips keep
809
+ // identical gravity values (Leading == Top, Trailing == Bottom), so a
810
+ // pure orientation change is a no-op here.
811
+ const moved = new Set<ElementNode>()
812
+ for (const gravity of [
813
+ NSStackViewGravity.Leading,
814
+ NSStackViewGravity.Center,
815
+ NSStackViewGravity.Trailing,
816
+ ]) {
817
+ syncStackGravityArea(parent, stack, gravity, moved)
818
+ }
757
819
 
758
- const arranged = arrangedView(child)
759
- stack.removeArrangedSubview(arranged)
760
- arranged.removeFromSuperview()
761
- stack.addViewInGravity(arranged, stackGravity(parent, child))
820
+ for (const child of moved) {
762
821
  // Removing a view drops constraints to its old superview, including
763
822
  // percentage dimensions. Reinstall them after reattaching.
764
823
  applySizeConstraints(child)
@@ -1402,10 +1461,35 @@ function pinLayoutChild(
1402
1461
  }
1403
1462
  }
1404
1463
 
1464
+ const same = (a: number, b: number) => a === b || (Number.isNaN(a) && Number.isNaN(b))
1465
+ if (
1466
+ same(Number(pins.leading.constant), x) &&
1467
+ same(Number(pins.top.constant), top) &&
1468
+ same(Number(pins.width.constant), width) &&
1469
+ same(Number(pins.height.constant), height)
1470
+ ) {
1471
+ return
1472
+ }
1473
+
1405
1474
  pins.leading.constant = x
1406
1475
  pins.top.constant = top
1407
1476
  pins.width.constant = width
1408
1477
  pins.height.constant = height
1478
+ // Constant writes inside a layout() pass apply only on the next solve, and
1479
+ // a needsLayout mark made now is cleared when that pass ends. Defer the
1480
+ // mark so the next layoutSubtreeIfNeeded/display cycle applies the pins —
1481
+ // without it, real-window resizes leave children at stale frames. The bare
1482
+ // vm slices in tests have no microtask queue, so fall back to a direct
1483
+ // mark there.
1484
+ const mark = () => {
1485
+ parentView.needsLayout = true
1486
+ }
1487
+
1488
+ if (typeof queueMicrotask === 'function') {
1489
+ queueMicrotask(mark)
1490
+ } else {
1491
+ mark()
1492
+ }
1409
1493
  }
1410
1494
 
1411
1495
  /** Drop the pins a grid/absolute parent installed — a constraint that keeps
@@ -2414,10 +2498,47 @@ function applyStyle(node: ElementNode, style: PropBag) {
2414
2498
  syncStackLayout(node)
2415
2499
  }
2416
2500
 
2501
+ // `className` composes clsx-style on the DOM renderer and the NativeScript
2502
+ // driver; this mirrors octane's `normalizeClass` (native dist must not import
2503
+ // bare `octane`, and peers older than 0.11 predate the export) so a
2504
+ // declarative array or object reaches the tokenizer instead of String(value)'s
2505
+ // comma-join.
2506
+ function normalizeClass(value: any): string {
2507
+ if (typeof value === 'string') {
2508
+ return value
2509
+ }
2510
+
2511
+ if (typeof value !== 'object') {
2512
+ return typeof value === 'number' && value ? '' + value : ''
2513
+ }
2514
+
2515
+ if (value === null) {
2516
+ return ''
2517
+ }
2518
+
2519
+ let str = ''
2520
+ if (Array.isArray(value)) {
2521
+ for (const item of value) {
2522
+ if (item) {
2523
+ const inner = normalizeClass(item)
2524
+ if (inner) {
2525
+ str = str ? str + ' ' + inner : inner
2526
+ }
2527
+ }
2528
+ }
2529
+ } else {
2530
+ for (const key in value) {
2531
+ if (value[key]) {
2532
+ str = str ? str + ' ' + key : key
2533
+ }
2534
+ }
2535
+ }
2536
+
2537
+ return str
2538
+ }
2539
+
2417
2540
  function applyClassName(node: ElementNode, value: any) {
2418
- const classes = String(value ?? '')
2419
- .split(/\s+/)
2420
- .filter(Boolean)
2541
+ const classes = normalizeClass(value).split(/\s+/).filter(Boolean)
2421
2542
 
2422
2543
  // Class effects re-derive from scratch on every apply: the class-owned
2423
2544
  // fields reset here and re-populate below, so removing a class drops the
@@ -3378,7 +3499,7 @@ function insert(
3378
3499
  deactivateSizeConstraints(node)
3379
3500
  queueLayoutReconcile(container, parent)
3380
3501
  } else {
3381
- if (marginInsetsOf(node)) {
3502
+ if (marginInsetsOf(node) && !node.marginHost) {
3382
3503
  makeMarginHost(node)
3383
3504
  setLayoutAction(node, node.props?.onLayoutChanged)
3384
3505
  }
@@ -3719,9 +3840,7 @@ const macOSDriver: UniversalHostDriver<RootContainer, any> = {
3719
3840
  const round = (value: number) => Math.round(value * 100) / 100
3720
3841
 
3721
3842
  function nodeClasses(node: ElementNode) {
3722
- return String(node.props?.className ?? '')
3723
- .split(/\s+/)
3724
- .filter(Boolean)
3843
+ return normalizeClass(node.props?.className).split(/\s+/).filter(Boolean)
3725
3844
  }
3726
3845
 
3727
3846
  function descendants(node: ElementNode, out: ElementNode[] = []) {