react-x11 2.10.2 → 2.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 (100) hide show
  1. package/README.md +278 -129
  2. package/package.json +10 -3
  3. package/src/Reconciler.js +15 -17
  4. package/src/a11y.js +2 -2
  5. package/src/anchor.js +7 -5
  6. package/src/bootstrap.js +14 -0
  7. package/src/clientmessage.js +1 -1
  8. package/src/cocoa/app.js +304 -49
  9. package/src/cocoa/bezels.js +175 -30
  10. package/src/cocoa/dnd.js +27 -13
  11. package/src/cocoa/fonts.js +3 -3
  12. package/src/cocoa/glarea.js +20 -3
  13. package/src/cocoa/main.d.ts +8 -0
  14. package/src/cocoa/main.js +43 -0
  15. package/src/cocoa/panehost.js +15 -5
  16. package/src/cocoa/presenter.js +13 -9
  17. package/src/cocoa/promotion.js +4 -7
  18. package/src/cocoa/relaunch.js +207 -0
  19. package/src/cocoa/screencolor.js +62 -0
  20. package/src/cocoa/threaded.js +246 -0
  21. package/src/cocoa/window.js +256 -42
  22. package/src/components/Select.js +2 -2
  23. package/src/components/anchor.js +3 -3
  24. package/src/components/native.js +12 -7
  25. package/src/components/theme.js +2 -2
  26. package/src/debug.js +1 -1
  27. package/src/decorations.js +1 -1
  28. package/src/editmenu.js +2 -2
  29. package/src/errors.js +46 -0
  30. package/src/events.js +6 -6
  31. package/src/foreignnodes.js +3 -2
  32. package/src/frames.js +2 -2
  33. package/src/glnodes.js +1 -1
  34. package/src/grid.js +1653 -0
  35. package/src/host.d.ts +230 -0
  36. package/src/host.js +11 -3
  37. package/src/imagesource.js +1 -1
  38. package/src/index.d.ts +21 -4
  39. package/src/index.js +9 -1
  40. package/src/layouts.js +721 -0
  41. package/src/node.d.ts +4 -2
  42. package/src/node.js +19 -21
  43. package/src/nodes/animation.js +644 -0
  44. package/src/nodes/box.js +21 -0
  45. package/src/nodes/boxpaint.js +473 -0
  46. package/src/nodes/canvas.js +269 -0
  47. package/src/nodes/cascade.js +600 -0
  48. package/src/nodes/damage.js +183 -0
  49. package/src/nodes/edithistory.js +124 -0
  50. package/src/nodes/editmenupopup.js +260 -0
  51. package/src/nodes/hittest.js +185 -0
  52. package/src/nodes/image.js +266 -0
  53. package/src/nodes/install.js +75 -0
  54. package/src/nodes/invalidate.js +465 -0
  55. package/src/nodes/kinds.js +31 -0
  56. package/src/nodes/layout.js +439 -0
  57. package/src/nodes/layouthost.js +949 -0
  58. package/src/nodes/node.js +868 -0
  59. package/src/nodes/paint.js +466 -0
  60. package/src/nodes/position.js +366 -0
  61. package/src/nodes/preedit.js +127 -0
  62. package/src/nodes/queries.js +330 -0
  63. package/src/nodes/rects.js +102 -0
  64. package/src/nodes/scrollable.js +891 -0
  65. package/src/nodes/scrollbars.js +138 -0
  66. package/src/nodes/scrollblit.js +1034 -0
  67. package/src/nodes/selectable.js +142 -0
  68. package/src/nodes/styling.js +225 -0
  69. package/src/nodes/text.js +649 -0
  70. package/src/nodes/textarea.js +391 -0
  71. package/src/nodes/textinput.js +1146 -0
  72. package/src/nodes/util.js +17 -0
  73. package/src/nodes/window/anchoring.js +161 -0
  74. package/src/nodes/window/capabilities.js +190 -0
  75. package/src/nodes/window/debugpaint.js +83 -0
  76. package/src/nodes/window/droptarget.js +145 -0
  77. package/src/nodes/window/floors.js +577 -0
  78. package/src/nodes/window/flush.js +334 -0
  79. package/src/nodes/window/hints.js +482 -0
  80. package/src/nodes/window/listeners.js +222 -0
  81. package/src/nodes/window/popup.js +71 -0
  82. package/src/nodes/window/size.js +591 -0
  83. package/src/nodes/window/window.js +945 -0
  84. package/src/palette.js +1 -1
  85. package/src/registry.js +7 -3
  86. package/src/screencolor.js +212 -38
  87. package/src/screencolorhooks.js +6 -2
  88. package/src/styles.js +137 -15
  89. package/src/svgnodes.js +2 -1
  90. package/src/testing/harness.js +2 -2
  91. package/src/textselection.js +5 -3
  92. package/src/trace-registry.js +1 -1
  93. package/src/types/components.d.ts +38 -6
  94. package/src/types/elements.d.ts +11 -1
  95. package/src/types/nodes.d.ts +33 -5
  96. package/src/types/screencolor.d.ts +20 -14
  97. package/src/types/style.d.ts +94 -3
  98. package/src/windowstate.js +1 -1
  99. package/src/yoga.js +1 -1
  100. package/src/nodes.js +0 -13120
package/src/styles.js CHANGED
@@ -6,6 +6,11 @@
6
6
  import { cssColorStraight } from 'ntk';
7
7
 
8
8
  import { parseBoxShadow, parseLinearGradient } from './decorations.js';
9
+ import {
10
+ validateGridValue,
11
+ GRID_CONTAINER_PROPS,
12
+ GRID_ITEM_PROPS,
13
+ } from './grid.js';
9
14
  import { Yoga } from './yoga.js';
10
15
 
11
16
  const FLEX_DIRECTION = {
@@ -47,8 +52,34 @@ const POSITION = {
47
52
  absolute: Yoga.POSITION_TYPE_ABSOLUTE,
48
53
  };
49
54
 
55
+ /**
56
+ * Yoga's position type for a `position` value. `static`, `relative` and
57
+ * `absolute` are yoga's own. Everything else — `sticky`, and every position
58
+ * registered with `registerPosition` (src/layouts.js) — is laid out in flow
59
+ * exactly as `relative` is and moved afterwards: placing is not something
60
+ * layout does (nodes/position.js, `WindowNode._placeNodes`). What differs is the
61
+ * insets, which are the placement's to read — thresholds, for sticky — so
62
+ * yoga must never see them as offsets (`applyLayoutStyle`).
63
+ */
64
+ const positionTypeOf = (v) =>
65
+ typeof v === 'string' && Object.hasOwn(POSITION, v)
66
+ ? POSITION[v]
67
+ : Yoga.POSITION_TYPE_RELATIVE;
68
+
69
+ /** Whether a `position` is placed after layout rather than by it. */
70
+ const isPlacedPosition = (v) =>
71
+ v != null && !(typeof v === 'string' && Object.hasOwn(POSITION, v));
72
+
73
+ /** The properties that are an offset under `relative` and `absolute`, and the
74
+ * placement's to read under any other — see `applyLayoutStyle`. */
75
+ const INSETS = new Set(['top', 'right', 'bottom', 'left', 'start', 'end']);
76
+
77
+ /** `'grid'` is a flex item to yoga: the box takes its place in its parent's
78
+ * line like any other, and its own children are the grid's to arrange — a
79
+ * layout host (src/grid.js, nodes/layouthost.js). */
50
80
  const DISPLAY = {
51
81
  flex: Yoga.DISPLAY_FLEX,
82
+ grid: Yoga.DISPLAY_FLEX,
52
83
  none: Yoga.DISPLAY_NONE,
53
84
  };
54
85
 
@@ -129,10 +160,7 @@ const LAYOUT_APPLIERS = {
129
160
  flexGrow: (n, v) => n.setFlexGrow(v ?? 0),
130
161
  flexShrink: (n, v) => n.setFlexShrink(v ?? 1),
131
162
  flexBasis: (n, v) => n.setFlexBasis(v),
132
- position: (n, v) =>
133
- n.setPositionType(
134
- pick(POSITION, v, 'position') ?? Yoga.POSITION_TYPE_RELATIVE,
135
- ),
163
+ position: (n, v) => n.setPositionType(positionTypeOf(v)),
136
164
  // Which way the boxes under this one run. Everything else in this file is
137
165
  // physical; this is the one property that decides what "start" means, and
138
166
  // yoga inherits it down its own tree — so a `<box>` that sets it mirrors
@@ -455,6 +483,18 @@ const STYLE_PROPS = new Set([
455
483
  // the ones that say it. Neither layout nor paint: it changes nothing about
456
484
  // this node, only what the blocks under it resolve against.
457
485
  'container',
486
+ // A registered layout algorithm that arranges this node's children — CSS's
487
+ // `display: layout(name)`, with the algorithm's options written beside the
488
+ // name — and what a child tells the one arranging it, CSS's child input
489
+ // properties (docs/styling.md, "Custom layouts"). Layout in every sense
490
+ // but yoga's: the node side reads them (nodes/layouthost.js).
491
+ 'layout',
492
+ 'layoutItem',
493
+ // CSS grid's own properties, on a `display: 'grid'` box and on each of its
494
+ // children (docs/styling.md, "Grid"). Layout in every sense but yoga's, like
495
+ // the two above: the grid reads them off the style (src/grid.js).
496
+ ...GRID_CONTAINER_PROPS,
497
+ ...GRID_ITEM_PROPS,
458
498
  ]);
459
499
 
460
500
  export const isStyleProp = (name) => STYLE_PROPS.has(name);
@@ -497,7 +537,7 @@ const SIZE_QUERY = /^@(width|height)\s*(>=|<=|>|<)\s*(\d+(?:\.\d+)?)$/;
497
537
  * *when* it is answered — a container's size is what a layout pass
498
538
  * produces, not what one starts from, so the blocks are resolved after the
499
539
  * pass and the tree laid out once more if an answer moved
500
- * (nodes.js, `_resolveContainerQueries`).
540
+ * (nodes/queries.js, `_resolveContainerQueries`).
501
541
  */
502
542
  const CONTAINER_QUERY =
503
543
  /^@container(?:\s+([A-Za-z_][\w-]*))?\s+(width|height)\s*(>=|<=|>|<)\s*(\d+(?:\.\d+)?)$/;
@@ -618,7 +658,7 @@ export function containerQueryNames(style) {
618
658
  * Which of a style's container blocks match, as one string with a character
619
659
  * per block in declaration order. What a node remembers between layout
620
660
  * passes so a re-resolution runs only when an answer actually moved, and
621
- * what the oscillation check compares (nodes.js `_resolveContainerQueries`).
661
+ * what the oscillation check compares (nodes/queries.js `_resolveContainerQueries`).
622
662
  */
623
663
  export function containerAnswers(style, containers) {
624
664
  let out = '';
@@ -690,21 +730,27 @@ function queryMatches(q, size, supports, containers) {
690
730
  }
691
731
  }
692
732
 
733
+ const GRID_VALUES = new Set([...GRID_CONTAINER_PROPS, ...GRID_ITEM_PROPS]);
734
+
693
735
  /**
694
- * The two style values that are a small language rather than a number, and
695
- * therefore the two that can be *wrong* rather than merely absent. Parsed in
736
+ * The style values that are a small language rather than a number — a
737
+ * gradient, a shadow, and grid's track lists, areas and lines and
738
+ * therefore the ones that can be *wrong* rather than merely absent. Parsed in
696
739
  * development wherever they are written — including inside a state block,
697
740
  * which is the half of the surface a `continue` used to skip — so the error
698
741
  * naming the property and the expected spelling arrives at the call site
699
- * instead of as a blank panel three commits later.
742
+ * instead of as a blank panel three commits later. A grid value goes through
743
+ * the parser the layout itself runs (src/grid.js), so the two agree.
700
744
  *
701
745
  * Tokens are still unresolved here (`$accent` is a colour as far as the
702
746
  * grammar is concerned), so this checks the shape and never the colours.
703
747
  */
704
748
  function validateValue(key, value, where) {
705
- if (key !== 'backgroundImage' && key !== 'boxShadow') return;
749
+ const grid = GRID_VALUES.has(key);
750
+ if (!grid && key !== 'backgroundImage' && key !== 'boxShadow') return;
706
751
  try {
707
- if (key === 'backgroundImage') parseLinearGradient(value);
752
+ if (grid) validateGridValue(key, value);
753
+ else if (key === 'backgroundImage') parseLinearGradient(value);
708
754
  else parseBoxShadow(value);
709
755
  } catch (err) {
710
756
  // the parser names the property and the grammar; only the call site is
@@ -714,6 +760,22 @@ function validateValue(key, value, where) {
714
760
  }
715
761
  }
716
762
 
763
+ const ALGORITHM_NAME = /^[A-Za-z_][\w-]*$/;
764
+
765
+ /** A plain object of options — what `layoutItem` is, and what a `layout` or
766
+ * a registered `position` written with its options is. */
767
+ const isOptionBag = (v) =>
768
+ v == null || (typeof v === 'object' && !Array.isArray(v));
769
+
770
+ /** A `layout` / `position` value: a name, or one written with its options —
771
+ * `'masonry'`, `{ name: 'masonry', columns: 3 }`. Whether the name is
772
+ * registered is the node's to find out when it resolves the style: an
773
+ * algorithm may be registered after the style that names it is written. */
774
+ const isAlgorithm = (v) =>
775
+ v == null ||
776
+ (typeof v === 'string' && ALGORITHM_NAME.test(v)) ||
777
+ (isOptionBag(v) && typeof v.name === 'string' && ALGORITHM_NAME.test(v.name));
778
+
717
779
  function validateStyle(style, where) {
718
780
  for (const key of Object.keys(style)) {
719
781
  if (isQuery(key)) {
@@ -765,6 +827,28 @@ function validateStyle(style, where) {
765
827
  "digits, '_' and '-', not starting with a digit)",
766
828
  );
767
829
  }
830
+ if (key === 'layout' && !isAlgorithm(style[key])) {
831
+ throw new Error(
832
+ `react-x11: invalid layout ${JSON.stringify(style[key])} in ${where} ` +
833
+ "(expected a registered name like 'masonry', or { name: 'masonry' } " +
834
+ "with the layout's options beside it)",
835
+ );
836
+ }
837
+ if (key === 'position' && !isAlgorithm(style[key])) {
838
+ throw new Error(
839
+ `react-x11: invalid position ${JSON.stringify(style[key])} in ` +
840
+ `${where} (expected 'static', 'relative', 'absolute', 'sticky', or ` +
841
+ 'a position registered with registerPosition — its name, or ' +
842
+ "{ name: '…' } with its options beside it)",
843
+ );
844
+ }
845
+ if (key === 'layoutItem' && !isOptionBag(style[key])) {
846
+ throw new Error(
847
+ `react-x11: invalid layoutItem ${JSON.stringify(style[key])} in ` +
848
+ `${where} (expected an object of the options the layout arranging ` +
849
+ 'this node reads, like { span: 2 })',
850
+ );
851
+ }
768
852
  if (key === 'flex' && !isFlexShorthand(style[key])) {
769
853
  throw new Error(
770
854
  `react-x11: invalid flex ${JSON.stringify(style[key])} in ${where} ` +
@@ -882,6 +966,10 @@ const NOT_ANIMATABLE = new Set([
882
966
  // nothing is drawn from it, so there is no frame in which a halfway value
883
967
  // would be visible — and it may be an object, which does not lerp
884
968
  'hitSlop',
969
+ // a grid's tracks and a child's lines: a count of 3.5 columns, or line
970
+ // 2.5, is no grid at all, and a track list is a string
971
+ ...GRID_CONTAINER_PROPS,
972
+ ...GRID_ITEM_PROPS,
885
973
  ]);
886
974
 
887
975
  export const isAnimatableProp = (name) =>
@@ -1364,7 +1452,7 @@ export function stripTokens(style) {
1364
1452
  * anyone hears about it: mistakes are pushed onto `problems` and the caller
1365
1453
  * decides what one costs. Resolving itself never throws — it runs from a
1366
1454
  * commit and from an X event alike, and only the caller knows whether React
1367
- * is on the stack to route a throw to a boundary (src/nodes.js).
1455
+ * is on the stack to route a throw to a boundary (src/nodes/cascade.js).
1368
1456
  *
1369
1457
  * A cache hit replays the problems it recorded, so the second node to wear a
1370
1458
  * misspelled shared style is reported like the first.
@@ -1658,13 +1746,21 @@ export const createLayoutNode = () => Yoga.Node.create(layoutConfig());
1658
1746
  export function measuringExactly(measure) {
1659
1747
  const cfg = layoutConfig();
1660
1748
  cfg.setPointScaleFactor(0);
1749
+ exactDepth++;
1661
1750
  try {
1662
1751
  return measure();
1663
1752
  } finally {
1753
+ exactDepth--;
1664
1754
  cfg.setPointScaleFactor(1);
1665
1755
  }
1666
1756
  }
1667
1757
 
1758
+ let exactDepth = 0;
1759
+
1760
+ /** Whether yoga is measuring off the pixel grid right now: a size taken in
1761
+ * here is not the size the same layout comes to on it. */
1762
+ export const isMeasuringExactly = () => exactDepth > 0;
1763
+
1668
1764
  /**
1669
1765
  * The yoga defaults that are not CSS's, written once per node.
1670
1766
  *
@@ -1676,7 +1772,7 @@ export function measuringExactly(measure) {
1676
1772
  * The pair to it is the automatic minimum size — a flex item that may shrink
1677
1773
  * still cannot shrink below its content unless it says so. Yoga has no such
1678
1774
  * rule, so the renderer measures the floors itself; see `Node`'s content
1679
- * floors in nodes.js.
1775
+ * floors in nodes/window/floors.js.
1680
1776
  */
1681
1777
  export function applyLayoutDefaults(yogaNode) {
1682
1778
  yogaNode.setFlexShrink(1);
@@ -1688,15 +1784,41 @@ export function applyLayoutDefaults(yogaNode) {
1688
1784
  */
1689
1785
  export function applyLayoutStyle(yogaNode, props, oldProps = {}) {
1690
1786
  let changed = false;
1787
+ // A placed node's insets — sticky's thresholds, or whatever a registered
1788
+ // position reads them as — are the placement's, never offsets: yoga gets
1789
+ // `undefined` for them, and a node that becomes placed (or stops) has all
1790
+ // six re-sent whether or not they changed. The flip still counts as a
1791
+ // change: the pass it asks for is what places the node.
1792
+ const placed = isPlacedPosition(props.position);
1793
+ const flipped = placed !== isPlacedPosition(oldProps.position);
1691
1794
  for (const key of Object.keys(LAYOUT_APPLIERS)) {
1692
- if (props[key] !== oldProps[key]) {
1693
- LAYOUT_APPLIERS[key](yogaNode, props[key]);
1795
+ const moved =
1796
+ key === 'position'
1797
+ ? !samePosition(props.position, oldProps.position)
1798
+ : props[key] !== oldProps[key];
1799
+ if (moved || (flipped && INSETS.has(key))) {
1800
+ LAYOUT_APPLIERS[key](
1801
+ yogaNode,
1802
+ placed && INSETS.has(key) ? undefined : props[key],
1803
+ );
1694
1804
  changed = true;
1695
1805
  }
1696
1806
  }
1697
1807
  return changed;
1698
1808
  }
1699
1809
 
1810
+ /** Two `position` values asking for the same thing. A registered one written
1811
+ * with its options is an object, and an inline object is a new one every
1812
+ * render — which must not cost a layout pass per commit. */
1813
+ function samePosition(a, b) {
1814
+ if (a === b) return true;
1815
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return false;
1816
+ const keys = Object.keys(a);
1817
+ if (keys.length !== Object.keys(b).length) return false;
1818
+ for (const key of keys) if (a[key] !== b[key]) return false;
1819
+ return true;
1820
+ }
1821
+
1700
1822
  /** @returns true if any paint-only prop changed */
1701
1823
  export function paintPropsChanged(props, oldProps = {}) {
1702
1824
  for (const key of PAINT_PROPS) {
package/src/svgnodes.js CHANGED
@@ -12,7 +12,8 @@
12
12
  // grain for it.
13
13
  import { SvgView } from 'ntk';
14
14
 
15
- import { Node, intrinsicSize } from './nodes.js';
15
+ import { Node } from './nodes/node.js';
16
+ import { intrinsicSize } from './nodes/layout.js';
16
17
  import { isLayoutProp, isPaintProp } from './styles.js';
17
18
 
18
19
  // SVG attributes that are camelCase in SVG itself; every other camelCase
@@ -9,7 +9,7 @@
9
9
 
10
10
  import React from 'react';
11
11
  import { createRoot } from '../Reconciler.js';
12
- import { setAnimationClock } from '../nodes.js';
12
+ import { setAnimationClock } from '../nodes/animation.js';
13
13
  import { setAppearanceForTests } from '../appearance.js';
14
14
 
15
15
  // A test process must not register with the desktop's live AT-SPI registry —
@@ -383,7 +383,7 @@ let clockInstalled = false;
383
383
  /**
384
384
  * Drive transitions from a number you control instead of from wall-clock
385
385
  * time. `setAnimationClock` exists for exactly this; it was reachable only
386
- * through a deep import into `src/nodes.js`.
386
+ * through a deep import into `src/nodes/animation.js`.
387
387
  *
388
388
  * ```js
389
389
  * const clock = withFrameClock();
@@ -9,7 +9,7 @@
9
9
  //
10
10
  // **A surface is a `selectable` element, and its participants are whatever
11
11
  // under it can answer for its own text.** An element joins by implementing
12
- // the four accessors in nodes.js (`textContent`, `textIndexAt`,
12
+ // the four accessors in nodes/selectable.js (`textContent`, `textIndexAt`,
13
13
  // `textCaretRect`, `textRangeRects`) — `<text>` does, and so does a terminal
14
14
  // or a log view written outside this package. There is no registration call
15
15
  // and no list of blessed kinds: a document is a tree, and the tree is walked.
@@ -226,7 +226,7 @@ export class TextSelection {
226
226
  * pointer is: a press landed on the wrong character and a drag stopped
227
227
  * short at half its distance. The X event underneath already carries the
228
228
  * device numbers; an event synthesized without one is multiplied up, the
229
- * way every other pointer consumer in nodes.js does it.
229
+ * way every other pointer consumer in src/nodes/ does it.
230
230
  */
231
231
  devicePoint(ev) {
232
232
  const scale = this.node.scale > 0 ? this.node.scale : 1;
@@ -407,7 +407,9 @@ export class TextSelection {
407
407
  }
408
408
 
409
409
  /** Take PRIMARY, which in X is what "there is a selection here" means to
410
- * every other application on the display. */
410
+ * every other application on the display. macOS has no PRIMARY, and the
411
+ * cocoa backend's clipboard answers the write with nothing
412
+ * (src/cocoa/app.js) — there, a selection is never a copy. */
411
413
  own() {
412
414
  if (this.isCollapsed) return;
413
415
  takeVisibleSelection(this.node);
@@ -1,6 +1,6 @@
1
1
  // The seam between the renderer and the optional debug module.
2
2
  //
3
- // Reconciler.js and nodes.js import this file, so it has to stay safe to
3
+ // Reconciler.js and nodes/window/flush.js import this file, so it has to stay safe to
4
4
  // bundle for the browser playground: no node builtins, no side effects,
5
5
  // nothing but a registry of live connections and a pair of hook slots. The
6
6
  // heavy half (protocol parsing, sinks, file writing) lives in debug.js and
@@ -125,6 +125,20 @@ export interface Theme {
125
125
  direction: 'ltr' | 'rtl';
126
126
  paddingX: number;
127
127
  paddingY: number;
128
+ /**
129
+ * Not a colour: whether the widgets wear the **platform's own control
130
+ * bezels**. `'auto'` (the default) takes them wherever the backend has
131
+ * them — today the Cocoa backend, never X11 — `'native'` asks for them
132
+ * and says so in development where there are none, and `'drawn'` keeps
133
+ * this library's own rendering everywhere.
134
+ *
135
+ * It lives here rather than in `style` because whether an app looks
136
+ * native is an app-identity decision, not a per-node one;
137
+ * {@link NativeControlProp.native} is the per-control override, and
138
+ * `useSupports('nativeControls')` is how application code asks what this
139
+ * backend can do. See docs/components.md and docs/macos.md.
140
+ */
141
+ controls?: 'auto' | 'native' | 'drawn';
128
142
  }
129
143
 
130
144
  export interface ThemeProviderProps {
@@ -194,6 +208,20 @@ export function useDirection(): 'ltr' | 'rtl';
194
208
  /** Props a widget passes through to the `<box>` it renders. */
195
209
  type WidgetProps = Omit<BoxProps, 'children' | 'style' | 'ref'>;
196
210
 
211
+ /**
212
+ * The per-instance escape hatch from the platform's own control bezel, for
213
+ * the one custom-branded control in an otherwise native app.
214
+ *
215
+ * The policy proper is {@link Theme.controls}; this overrides it for this
216
+ * control. `false` keeps the drawn rendering — which is what a control
217
+ * naming its own colours in `style` should say, because a bezel the system
218
+ * draws ignores them. `true` asks for the bezel regardless of the theme's
219
+ * policy, and is a development warning on a backend that has none.
220
+ */
221
+ interface NativeControlProp {
222
+ native?: boolean;
223
+ }
224
+
197
225
  /**
198
226
  * What a value widget's `onChange` receives — one signature across the
199
227
  * library, so `onChange={formik.handleChange}` wires to a `Checkbox` exactly
@@ -279,7 +307,7 @@ export const iconNames: readonly IconName[];
279
307
  /** The default icon box for a given `fontSize`. */
280
308
  export function iconSize(fontSize: number): number;
281
309
 
282
- export interface ButtonProps extends WidgetProps {
310
+ export interface ButtonProps extends WidgetProps, NativeControlProp {
283
311
  children?: ReactNode;
284
312
  label?: string;
285
313
  onPress?: (ev: MouseEvent<DrawnNode>) => void;
@@ -328,7 +356,8 @@ export interface PasswordInputProps extends WidgetProps, NamedWidget {
328
356
  }
329
357
  export const PasswordInput: ComponentType<PasswordInputProps>;
330
358
 
331
- export interface CheckboxProps extends WidgetProps, NamedWidget {
359
+ export interface CheckboxProps
360
+ extends WidgetProps, NamedWidget, NativeControlProp {
332
361
  children?: ReactNode;
333
362
  label?: string;
334
363
  checked?: boolean;
@@ -346,7 +375,7 @@ export interface RadioGroupProps<T = unknown> extends WidgetProps, NamedWidget {
346
375
  }
347
376
  export function RadioGroup<T = unknown>(props: RadioGroupProps<T>): ReactNode;
348
377
 
349
- export interface RadioProps<T = unknown> {
378
+ export interface RadioProps<T = unknown> extends NativeControlProp {
350
379
  value: T;
351
380
  children?: ReactNode;
352
381
  label?: string;
@@ -354,7 +383,8 @@ export interface RadioProps<T = unknown> {
354
383
  }
355
384
  export function Radio<T = unknown>(props: RadioProps<T>): ReactNode;
356
385
 
357
- export interface SwitchProps extends WidgetProps, NamedWidget {
386
+ export interface SwitchProps
387
+ extends WidgetProps, NamedWidget, NativeControlProp {
358
388
  checked?: boolean;
359
389
  onChange?: (ev: WidgetChangeEvent<boolean>) => void;
360
390
  disabled?: boolean;
@@ -378,7 +408,8 @@ export interface ProgressBarProps extends WidgetProps {
378
408
  }
379
409
  export const ProgressBar: ComponentType<ProgressBarProps>;
380
410
 
381
- export interface SliderProps extends WidgetProps, NamedWidget {
411
+ export interface SliderProps
412
+ extends WidgetProps, NamedWidget, NativeControlProp {
382
413
  value?: number;
383
414
  min?: number;
384
415
  max?: number;
@@ -462,7 +493,8 @@ export const Dialog: ComponentType<DialogProps>;
462
493
  /** An option, or a plain value used as both value and label. */
463
494
  export type SelectOption<T = unknown> = { value: T; label: string } | T;
464
495
 
465
- export interface SelectProps<T = unknown> extends WidgetProps, NamedWidget {
496
+ export interface SelectProps<T = unknown>
497
+ extends WidgetProps, NamedWidget, NativeControlProp {
466
498
  value?: T;
467
499
  options?: readonly SelectOption<T>[];
468
500
  onChange?: (ev: WidgetChangeEvent<T>) => void;
@@ -222,7 +222,8 @@ export interface SelectionProps<T = DrawnNode> {
222
222
  /**
223
223
  * `true` makes this element a selection surface: a drag across the text
224
224
  * inside it selects, double and triple clicks take a word and a block,
225
- * Ctrl+A and Ctrl+C work, and a release takes PRIMARY. It also makes the
225
+ * Ctrl+A and Ctrl+C work, and on X11 a release takes PRIMARY (macOS has
226
+ * none: there, only a copy reaches the pasteboard). It also makes the
226
227
  * element a focus target, so the keys have somewhere to arrive —
227
228
  * `tabIndex={-1}` keeps it out of the Tab cycle.
228
229
  *
@@ -649,6 +650,15 @@ export interface BoxProps
649
650
 
650
651
  /** What `overflow: 'scroll'` adds, on a `<box>` or a `<window>`. */
651
652
  export interface ScrollProps {
653
+ /**
654
+ * The offsets moved, by any route. The wheel, the keys, a bar and
655
+ * `scrollTo`/`scrollBy` report inside the call. Layout reports once its
656
+ * pass is over, after the frame that shows the move: a held `scrollTo`
657
+ * landing (one made before the pane's first layout, or past the extent
658
+ * the last pass measured), a `scrollIntoView` resolving, or the offset
659
+ * pulled back when the content shrinks or the viewport grows under it.
660
+ * Each report carries the offsets in force when it is delivered.
661
+ */
652
662
  onScroll?: (ev: ScrollEvent) => void;
653
663
  /** Fired from layout, so it arrives for a list nobody has scrolled yet. */
654
664
  onViewport?: (ev: ViewportEvent) => void;
@@ -18,8 +18,17 @@ export interface DrawnNode {
18
18
  * was registered under. What queries and paint order match on:
19
19
  * `screen.all((n) => n.kind === 'gauge')`. */
20
20
  readonly kind: string;
21
- /** Position and size within the owning window, valid after layout. */
21
+ /** Position and size within the owning window, valid after layout. In
22
+ * **device pixels**, like everything painted — a mouse event's `x`/`y`
23
+ * are logical, `scale` apart (docs/scale.md). `ev.nativeEvent.x`/`y` are
24
+ * the same point as `ev.x`/`y` already in this unit, which is what a
25
+ * pointer measured against a laid-out box wants. */
22
26
  readonly abs: Rect;
27
+ /** Device pixels per logical pixel for this node: the display's scale
28
+ * times any `scale` prop above it, resolved once and constant for the
29
+ * node's life. The number that converts between `abs` and the logical
30
+ * unit an app writes styles and reads events in. */
31
+ readonly scale: number;
23
32
  readonly parent: DrawnNode | null;
24
33
  readonly children: readonly DrawnNode[];
25
34
  /** Take the keyboard focus, if this node is focusable. Returns the node,
@@ -42,14 +51,18 @@ export interface DrawnNode {
42
51
  readonly direction: 'ltr' | 'rtl';
43
52
  /** Whether `node` is this node or a descendant of it (DOM `contains`). */
44
53
  contains(node: DrawnNode | null): boolean;
54
+ /** The boxes this node occupies, in **logical** pixels — already divided
55
+ * by `scale`, so they compare directly with an event's `x`/`y`. One rect
56
+ * for a box; one per line for wrapped text. */
45
57
  getClientRects(): Rect[];
46
58
 
47
59
  // --- text geometry (docs/elements.md, "Selection") ------------------------
48
60
  //
49
61
  // Every drawn node answers these; an element with no text answers `null`,
50
62
  // `0` and `[]`. Indices are **code points** and rectangles are in the
51
- // owning window's coordinates — the same space as `abs` and a mouse
52
- // event's `x`/`y`.
63
+ // owning window's coordinates — the same space as `abs`, which is device
64
+ // pixels: this seam speaks that unit deliberately, so a point taken from
65
+ // an event arrives through `ev.nativeEvent` or multiplied by `scale`.
53
66
 
54
67
  /** This element's text, or null when it has none. */
55
68
  textContent(): string | null;
@@ -110,8 +123,22 @@ export interface ScrollableNode extends DrawnNode {
110
123
  readonly scrollY: number;
111
124
  readonly contentWidth: number;
112
125
  readonly contentHeight: number;
113
- /** A number scrolls the vertical axis; an object moves either or both. */
126
+ /**
127
+ * A number scrolls the vertical axis; an object moves either or both,
128
+ * clamped to the content. It answers at once from the extent the last
129
+ * layout pass measured. While a pass is owed — before the pane's first
130
+ * layout, or in the commit that added its new rows — whatever it asks past
131
+ * that extent is held too, and the pass answers it again against the
132
+ * content it measures, before the frame paints: `scrollTo(Infinity)` from
133
+ * a layout effect follows new rows to the end in the frame that shows
134
+ * them. `onScroll` reports the call's answer at once, and the pass's once
135
+ * that pass is over.
136
+ */
114
137
  scrollTo(to: number | ScrollTarget): void;
138
+ /**
139
+ * The same, by a delta. Made while a `scrollTo` is held for the pass, it
140
+ * moves on from where the pass puts the pane.
141
+ */
115
142
  scrollBy(by: number | ScrollTarget): void;
116
143
  /**
117
144
  * Is there room to move on the axis a delta names? What the wheel's
@@ -122,7 +149,8 @@ export interface ScrollableNode extends DrawnNode {
122
149
  /**
123
150
  * Scroll the minimum amount on both axes that makes a descendant fully
124
151
  * visible. Safe to call right after that node mounts — the request is
125
- * resolved on the next layout pass, when it has geometry.
152
+ * resolved on the next layout pass, when it has geometry, and `onScroll`
153
+ * reports the move once that pass is over.
126
154
  */
127
155
  scrollIntoView(node: DrawnNode): void;
128
156
  }
@@ -6,22 +6,26 @@ import type { AbortSignalLike, WindowTarget } from './filedialog.js';
6
6
  import type { NtkApp } from './nodes.js';
7
7
 
8
8
  /** Which rung of the ladder answered — or would. */
9
- export type ScreenColorBackend = 'portal' | 'x11';
9
+ export type ScreenColorBackend = 'cocoa' | 'portal' | 'x11';
10
10
 
11
11
  export interface PickScreenColorOptions {
12
12
  /**
13
13
  * The window the picker belongs to — `parent_window` for the portal, and
14
- * (when it points at a mounted node) the connection the X11 rung grabs on.
15
- * `useEyedropper()` infers it from the tree.
14
+ * (when it points at a mounted node) the app the cocoa rung samples
15
+ * through or the connection the X11 rung grabs on. `useEyedropper()`
16
+ * infers it from the tree.
16
17
  */
17
18
  parentWindow?: WindowTarget;
18
19
  /** Abort the pick. Closes the portal request, or releases the X11 grab —
19
- * the grab is released **before** the rejection is reported. */
20
+ * the grab is released **before** the rejection is reported. On the cocoa
21
+ * rung it ends the wait only: `NSColorSampler` cannot be dismissed from
22
+ * code, so the loupe stays up until the user answers it. */
20
23
  signal?: AbortSignalLike;
21
24
  /** Force a rung, for kiosks and for tests. */
22
25
  backend?: ScreenColorBackend;
23
26
  /**
24
- * The connection the X11 rung grabs and reads on. Required for that rung
27
+ * The app a pick runs through: the cocoa rung's `colorSampler`, or the
28
+ * connection the X11 rung grabs and reads on. Required for either rung
25
29
  * when `parentWindow` does not resolve to a mounted node — the hook passes
26
30
  * the tree's own.
27
31
  */
@@ -40,23 +44,25 @@ export declare class NoScreenColorError extends Error {
40
44
  }
41
45
 
42
46
  /**
43
- * Sample one pixel from the screen: the desktop's own picker
44
- * (`org.freedesktop.portal.Screenshot.PickColor`, Screenshot interface
45
- * version 2) where there is one, a crosshair pointer grab on plain X11
46
- * everywhere else.
47
+ * Sample one pixel from the screen: the system's own picker where there is
48
+ * one — `NSColorSampler` on the cocoa backend (`@windowkit/appkit` >= 0.9),
49
+ * `org.freedesktop.portal.Screenshot.PickColor` (Screenshot interface
50
+ * version 2) on a desktop that has it — and a crosshair pointer grab on
51
+ * plain X11 everywhere else.
47
52
  *
48
53
  * Resolves to `'#rrggbb'`, or `null` when the user cancelled — Escape on the
49
- * X11 rung, the dialog's own cancel on the portal. Rejects with
50
- * {@link NoScreenColorError} when neither rung is reachable.
54
+ * X11 rung, the dialog's own cancel on the portal, a dismissed sampler on
55
+ * cocoa. Rejects with {@link NoScreenColorError} when no rung is reachable.
51
56
  */
52
57
  export declare function pickScreenColor(
53
58
  options?: PickScreenColorOptions,
54
59
  ): Promise<string | null>;
55
60
 
56
61
  /**
57
- * Which rung this machine lands on, without grabbing anything. `'x11'` needs
58
- * a connection to answer with — pass `app`, or a `parentWindow` pointing at
59
- * a mounted node — and `null` means {@link pickScreenColor} would reject.
62
+ * Which rung this machine lands on, without showing or grabbing anything.
63
+ * `'cocoa'` and `'x11'` both need an app to answer through — pass `app`, or
64
+ * a `parentWindow` pointing at a mounted node — and `null` means
65
+ * {@link pickScreenColor} would reject.
60
66
  */
61
67
  export declare function screenColorBackend(
62
68
  options?: Pick<PickScreenColorOptions, 'app' | 'backend' | 'parentWindow'>,