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 +32 -7
- package/lib/widgets/htmlview.js +1 -1
- package/lib/window.js +60 -0
- package/lib/yoga.js +119 -0
- package/package.json +2 -2
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
|
|
60
|
-
|
|
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
|
-
//
|
|
149
|
-
//
|
|
150
|
-
// second WASM
|
|
151
|
-
|
|
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 };
|
package/lib/widgets/htmlview.js
CHANGED
|
@@ -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
|
|
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": "
|
|
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.
|
|
51
|
+
"x11": "^3.6.0",
|
|
52
52
|
"yoga-layout": "^3.2.1"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|