ntk 4.2.0 → 5.0.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.
package/lib/index.js CHANGED
@@ -4,6 +4,7 @@ import App from './app.js';
4
4
  import Clipboard from './clipboard.js';
5
5
  import { BLANK_CURSOR, CursorCache, cursorNames, cursorShapes, resolveCursorShape } from './cursor.js';
6
6
  import Window from './window.js';
7
+ import { loadLayout } from './yoga.js';
7
8
  import { decodeKey, groupForState } from './keyboard.js';
8
9
  import Pixmap from './pixmap.js';
9
10
  import Picture from './picture.js';
@@ -31,6 +32,16 @@ import './renderingcontext_x11.js';
31
32
  import './renderingcontext_2d.js';
32
33
  import './renderingcontext_opengl.js';
33
34
 
35
+ // One socket write per frame instead of one per request (x11 >= 3.6). A frame
36
+ // is emitted in one synchronous run of _runFrame() and ends with the frame
37
+ // fence's GetInputFocus, which x11 flushes on because it expects a reply — so
38
+ // the batch goes out at the frame boundary without ntk flushing anything by
39
+ // hand. 64 KB holds a heavy frame (a 72 KB frame is 2 writes, a typical one
40
+ // is 1); the client's own gates — the size cap, a 5 ms age limit and a flush
41
+ // before the event loop polls — bound how long anything can wait, so this
42
+ // cannot delay a frame or a reply. `bufferRequests: false` opts out.
43
+ const DEFAULT_BUFFER_REQUESTS = { maxSize: 64 * 1024 };
44
+
34
45
  /**
35
46
  * Connect to the X server and resolve with an App.
36
47
  *
@@ -46,7 +57,9 @@ import './renderingcontext_opengl.js';
46
57
  * { glxVisual } — a visual id for getContext('opengl') to use instead of
47
58
  * querying the server for one — and
48
59
  * { onXError } — called with X protocol errors no request callback
49
- * claimed (default: console.warn)
60
+ * claimed (default: console.warn).
61
+ * x11's { bufferRequests } defaults to a 64 KB output buffer here, which
62
+ * makes a frame one socket write; pass false to write per request.
50
63
  * @param {function} [callback] optional node-style callback
51
64
  * @returns {Promise<App>}
52
65
  */
@@ -56,8 +69,15 @@ export function createClient(options, callback) {
56
69
  options = {};
57
70
  }
58
71
 
59
- const promise = new Promise((resolve, reject) => {
60
- x11.createClient(options || {}, (error, display) => {
72
+ const x11Options = { ...(options || {}) };
73
+ if (x11Options.bufferRequests === undefined) x11Options.bufferRequests = DEFAULT_BUFFER_REQUESTS;
74
+
75
+ // the layout engine's WASM loads alongside the connection, so widgets are
76
+ // usable synchronously by the time the App exists (see lib/yoga.js)
77
+ const layout = loadLayout();
78
+
79
+ const connecting = new Promise((resolve, reject) => {
80
+ x11.createClient(x11Options, (error, display) => {
61
81
  if (error) return reject(error);
62
82
 
63
83
  display.client.require('glx', (glxError, GLX) => {
@@ -98,6 +118,8 @@ export function createClient(options, callback) {
98
118
  });
99
119
  });
100
120
 
121
+ const promise = Promise.all([connecting, layout]).then(([app]) => app);
122
+
101
123
  if (callback) {
102
124
  promise.then(
103
125
  (app) => callback(null, app),
@@ -145,8 +167,11 @@ export {
145
167
  decodeKey,
146
168
  groupForState
147
169
  };
148
- // the yoga-layout instance ntk lays HtmlView out with — downstream layout
149
- // consumers (e.g. the react-x11 renderer) must share it to avoid loading a
150
- // second WASM copy with potentially mismatched enums
151
- export { default as Yoga } from 'yoga-layout';
170
+ // The layout engine ntk lays HtmlView out with — downstream layout consumers
171
+ // (e.g. the react-x11 renderer) must import it from here rather than from
172
+ // `yoga-layout`, or they get a second WASM instance whose Nodes cannot be
173
+ // mixed with ntk's. Its enum constants are readable as soon as ntk is
174
+ // imported; `Node`/`Config` need the WASM, which `createClient()` loads —
175
+ // `loadLayout()` is there for widgets used without an App.
176
+ export { default as Yoga, loadLayout, layoutLoaded } from './yoga.js';
152
177
  export default { createClient };
@@ -1,7 +1,7 @@
1
1
  import { selectAll } from 'css-select';
2
2
  import { textContent } from 'domutils';
3
3
  import { parseDocument } from 'htmlparser2';
4
- import Yoga from 'yoga-layout';
4
+ import Yoga from '../yoga.js';
5
5
 
6
6
  import { decodeImage, Image } from '../image.js';
7
7
  import { TextLayout } from '../text/layout.js';
package/lib/window.js CHANGED
@@ -693,6 +693,66 @@ export default class Window extends Drawable {
693
693
  });
694
694
  }
695
695
 
696
+ /**
697
+ * Scroll the pixels of `rect` (window coordinates, `{x, y, width, height}`)
698
+ * by (dx, dy) within the retained backing store, server-side: one CopyArea
699
+ * of the band that survives the shift, in place of the caller re-drawing
700
+ * everything that merely moved. Returns true when the blit was issued;
701
+ * false means "not possible here", and the caller repaints `rect` exactly
702
+ * as it would have without this method — every refusal is the status quo.
703
+ *
704
+ * Refused when there is no (valid) backing store, when the delta is
705
+ * fractional (a sub-pixel shift changes every pixel, so there is nothing
706
+ * to copy), when it is zero, or when nothing of `rect` survives the shift
707
+ * after clamping to window ∩ backing.
708
+ *
709
+ * Backing-store only, deliberately: pixmap contents cannot be occluded, so
710
+ * an overlapping self-copy is fully defined (the server fetches the source
711
+ * region before storing) and the GraphicsExpose handling a window-drawable
712
+ * scroll would need never exists. The copy goes out with the present GC —
713
+ * graphicsExposures: 0, where the 2d context's GC would emit a NoExposure
714
+ * packet per copy — and in-order with the caller's follow-up drawing of
715
+ * the exposed strip on the same connection.
716
+ *
717
+ * The whole of `rect` is marked dirty, so the next present shows the
718
+ * scrolled band through the normal fence-aware path; the caller only has
719
+ * to draw the strip the shift exposed, plus any chrome that moved with
720
+ * the content (a scrollbar thumb, say).
721
+ */
722
+ scrollRegion(rect, dx, dy) {
723
+ if (!this._backing || !this._backingValid) return false;
724
+ if (!Number.isInteger(dx) || !Number.isInteger(dy) || (dx === 0 && dy === 0)) return false;
725
+ // clamp like a present: the backing is grow-only, so it can be larger
726
+ // than the window after a shrink
727
+ const w = Math.min(this.width, this._backing.width);
728
+ const h = Math.min(this.height, this._backing.height);
729
+ const x0 = Math.max(0, Math.floor(rect.x));
730
+ const y0 = Math.max(0, Math.floor(rect.y));
731
+ const x1 = Math.min(w, Math.ceil(rect.x + rect.width));
732
+ const y1 = Math.min(h, Math.ceil(rect.y + rect.height));
733
+ // the band that survives: dest = clamped rect ∩ (clamped rect + delta)
734
+ const dstX0 = Math.max(x0, x0 + dx);
735
+ const dstY0 = Math.max(y0, y0 + dy);
736
+ const dstX1 = Math.min(x1, x1 + dx);
737
+ const dstY1 = Math.min(y1, y1 + dy);
738
+ if (dstX1 <= dstX0 || dstY1 <= dstY0) return false;
739
+ safeRelease(this.X, () => {
740
+ this.X.CopyArea(
741
+ this._backing.id,
742
+ this._backing.id,
743
+ this._presentGc,
744
+ dstX0 - dx,
745
+ dstY0 - dy,
746
+ dstX0,
747
+ dstY0,
748
+ dstX1 - dstX0,
749
+ dstY1 - dstY0
750
+ );
751
+ });
752
+ this._markDirty({ x: x0, y: y0, w: x1 - x0, h: y1 - y0 });
753
+ return true;
754
+ }
755
+
696
756
  /*
697
757
  * Frame clock. Noisy events (see events_map coalesce), synthetic redraws
698
758
  * and requestAnimationFrame callbacks are delivered in "frames", paced by
package/lib/yoga.js ADDED
@@ -0,0 +1,119 @@
1
+ // The layout engine, without a top-level await.
2
+ //
3
+ // `yoga-layout`'s default entry is `const Yoga = wrapAssembly(await
4
+ // loadYoga())`. That one `await` is contagious: every bundle containing ntk
5
+ // inherits it, esbuild then refuses to emit CommonJS ("Top-level await is
6
+ // currently not supported with the cjs output format"), and Node's single
7
+ // executable format runs its embedded main as CommonJS — so an ntk app could
8
+ // not be shipped as one binary at all.
9
+ //
10
+ // So ntk imports the half of the package that has no WASM in it —
11
+ // `yoga-layout/load` exports the enums as plain JavaScript and the assembly
12
+ // behind an async function — and loads the assembly during `createClient()`,
13
+ // which is already asynchronous.
14
+ //
15
+ // The object exported here keeps yoga's own shape, and that is the point:
16
+ // the flat SCREAMING_CASE constants are present from the first tick, so a
17
+ // consumer that builds a lookup table at module scope (ntk's own HtmlView,
18
+ // react-x11's styles.js) still reads them at import time. `Node`, `Config`
19
+ // and the rest of the assembly appear when `loadLayout()` resolves.
20
+
21
+ import {
22
+ loadYoga,
23
+ Align,
24
+ BoxSizing,
25
+ Dimension,
26
+ Direction,
27
+ Display,
28
+ Edge,
29
+ Errata,
30
+ ExperimentalFeature,
31
+ FlexDirection,
32
+ Gutter,
33
+ Justify,
34
+ LogLevel,
35
+ MeasureMode,
36
+ NodeType,
37
+ Overflow,
38
+ PositionType,
39
+ Unit,
40
+ Wrap
41
+ } from 'yoga-layout/load';
42
+
43
+ const ENUMS = {
44
+ Align,
45
+ BoxSizing,
46
+ Dimension,
47
+ Direction,
48
+ Display,
49
+ Edge,
50
+ Errata,
51
+ ExperimentalFeature,
52
+ FlexDirection,
53
+ Gutter,
54
+ Justify,
55
+ LogLevel,
56
+ MeasureMode,
57
+ NodeType,
58
+ Overflow,
59
+ PositionType,
60
+ Unit,
61
+ Wrap
62
+ };
63
+
64
+ // yoga's generator names each constant <ENUM>_<MEMBER>, both snake-cased from
65
+ // PascalCase: FlexDirection.ColumnReverse -> FLEX_DIRECTION_COLUMN_REVERSE.
66
+ // yoga-export.test.js pins every name against the real assembly, so a rename
67
+ // upstream fails loudly instead of yielding an undefined constant.
68
+ const screamingSnake = (name) => name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase();
69
+
70
+ /** The layout engine: enums now, assembly after `loadLayout()`. */
71
+ const Yoga = {};
72
+ for (const [enumName, members] of Object.entries(ENUMS)) {
73
+ // a bundler may stub yoga out entirely — the browser playground does,
74
+ // since no demo uses HtmlView — and importing ntk must still work
75
+ if (!members || typeof members !== 'object') continue;
76
+ const prefix = screamingSnake(enumName);
77
+ for (const [member, value] of Object.entries(members)) {
78
+ if (typeof value === 'number') Yoga[`${prefix}_${screamingSnake(member)}`] = value;
79
+ }
80
+ }
81
+
82
+ const notLoaded = (what) => () => {
83
+ throw new Error(
84
+ `ntk: the layout engine is not loaded, so Yoga.${what} is not available yet. ` +
85
+ 'createClient() loads it; widgets used without an App need `await loadLayout()` first.'
86
+ );
87
+ };
88
+
89
+ // A useful message instead of "Cannot read properties of undefined"
90
+ for (const name of ['Node', 'Config']) {
91
+ Object.defineProperty(Yoga, name, { configurable: true, get: notLoaded(name) });
92
+ }
93
+
94
+ let loading = null;
95
+
96
+ /**
97
+ * Load the layout engine's WebAssembly. Idempotent, and resolves with the
98
+ * same `Yoga` object this module exports — `createClient()` awaits it, so
99
+ * applications rarely call it themselves. Widgets used without an App
100
+ * (`new HtmlView(null, …)`) need it.
101
+ */
102
+ export function loadLayout() {
103
+ if (!loading) {
104
+ loading = loadYoga().then((assembly) => {
105
+ for (const name of ['Node', 'Config']) delete Yoga[name]; // drop the throwing getters
106
+ Object.assign(Yoga, assembly);
107
+ return Yoga;
108
+ });
109
+ }
110
+ return loading;
111
+ }
112
+
113
+ /** Whether the assembly is in place — layout will not throw. */
114
+ export function layoutLoaded() {
115
+ return Object.getOwnPropertyDescriptor(Yoga, 'Node')?.value !== undefined;
116
+ }
117
+
118
+ export { Yoga };
119
+ export default Yoga;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "4.2.0",
3
+ "version": "5.0.0",
4
4
  "description": "Desktop UI toolkit for X11 with canvas-like 2d and OpenGL rendering",
5
5
  "author": "Andrey Sidorov <sidorares@yandex.ru>",
6
6
  "license": "MIT",
@@ -48,7 +48,7 @@
48
48
  "parse-color": "^1.0.0",
49
49
  "pngjs": "^7.0.0",
50
50
  "postcss": "^8.5.23",
51
- "x11": "^3.4.0",
51
+ "x11": "^3.6.0",
52
52
  "yoga-layout": "^3.2.1"
53
53
  },
54
54
  "scripts": {