@octane-xplat/macos-renderer 0.11.1 → 0.12.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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +109 -26
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.12.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,34 @@ 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
+ if (typeof queueMicrotask === 'function') {
1488
+ queueMicrotask(mark)
1489
+ } else {
1490
+ mark()
1491
+ }
1409
1492
  }
1410
1493
 
1411
1494
  /** Drop the pins a grid/absolute parent installed — a constraint that keeps
@@ -3378,7 +3461,7 @@ function insert(
3378
3461
  deactivateSizeConstraints(node)
3379
3462
  queueLayoutReconcile(container, parent)
3380
3463
  } else {
3381
- if (marginInsetsOf(node)) {
3464
+ if (marginInsetsOf(node) && !node.marginHost) {
3382
3465
  makeMarginHost(node)
3383
3466
  setLayoutAction(node, node.props?.onLayoutChanged)
3384
3467
  }