react-x11 1.2.0 → 2.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/README.md +330 -81
- package/package.json +132 -14
- package/src/ClickToComponent.js +9 -2
- package/src/DevToolsIntegration.js +440 -32
- package/src/Reconciler.js +463 -165
- package/src/a11y.js +1299 -0
- package/src/acceleratorhooks.js +86 -0
- package/src/accelerators.js +128 -0
- package/src/activate.js +168 -0
- package/src/anchor.js +360 -0
- package/src/appcontext.js +161 -0
- package/src/appearance.js +748 -0
- package/src/appearancehooks.js +96 -0
- package/src/apphooks.js +73 -0
- package/src/application.js +748 -0
- package/src/atspi.js +1953 -0
- package/src/bus.js +545 -0
- package/src/bushooks.js +136 -0
- package/src/clientmessage.js +140 -0
- package/src/clipboard.js +121 -0
- package/src/components/Button.js +148 -33
- package/src/components/Checkbox.js +66 -32
- package/src/components/Dialog.js +96 -29
- package/src/components/FileDialog.js +491 -0
- package/src/components/Icon.js +329 -0
- package/src/components/Menu.js +797 -133
- package/src/components/PasswordInput.js +434 -0
- package/src/components/ProgressBar.js +91 -12
- package/src/components/Radio.js +78 -35
- package/src/components/Select.js +255 -65
- package/src/components/Slider.js +102 -41
- package/src/components/SplitPane.js +206 -0
- package/src/components/Switch.js +100 -22
- package/src/components/Table.js +529 -0
- package/src/components/Tabs.js +211 -0
- package/src/components/Tooltip.js +528 -54
- package/src/components/anchor.js +154 -107
- package/src/components/change.js +34 -0
- package/src/components/dnd.js +103 -0
- package/src/components/index.js +16 -3
- package/src/components/keys.js +17 -20
- package/src/components/scribble.js +247 -0
- package/src/components/theme.js +350 -42
- package/src/components/typeahead.js +12 -2
- package/src/compose.js +868 -0
- package/src/compositing.js +224 -0
- package/src/dbusmenu.js +384 -0
- package/src/debug.d.ts +63 -0
- package/src/debug.js +642 -0
- package/src/decorations.js +486 -0
- package/src/desktopsettings.js +209 -0
- package/src/desktopsettingshooks.js +62 -0
- package/src/dnd.js +1725 -0
- package/src/editmenu.js +272 -0
- package/src/errors.js +98 -0
- package/src/events.js +1406 -162
- package/src/extensions.js +45 -0
- package/src/filedialog.js +375 -0
- package/src/filedialoghooks.js +132 -0
- package/src/fonthooks.js +64 -0
- package/src/fonts.js +301 -0
- package/src/foreignnodes.js +519 -0
- package/src/frame/child.js +33 -0
- package/src/frame/childmain.js +274 -0
- package/src/frame/env.js +140 -0
- package/src/frame/index.js +444 -0
- package/src/frame/lifecycle.js +67 -0
- package/src/frame/protocol.js +179 -0
- package/src/frames.js +73 -0
- package/src/glbackend.js +114 -0
- package/src/glnodes.js +125 -67
- package/src/globalmenu.js +683 -0
- package/src/host.d.ts +88 -0
- package/src/host.js +42 -0
- package/src/idle.js +490 -0
- package/src/idlehooks.js +100 -0
- package/src/imagesource.js +349 -0
- package/src/index.d.ts +475 -0
- package/src/index.js +83 -9
- package/src/inputtime.js +206 -0
- package/src/jsx-dev-runtime.d.ts +21 -0
- package/src/jsx-dev-runtime.js +2 -0
- package/src/jsx-runtime.d.ts +39 -0
- package/src/jsx-runtime.js +6 -0
- package/src/keyboard.js +256 -0
- package/src/keyboardstate.js +278 -0
- package/src/keyboardstatehooks.js +58 -0
- package/src/keysyms.d.ts +149 -0
- package/src/keysyms.js +270 -0
- package/src/locale.js +170 -0
- package/src/localehooks.js +47 -0
- package/src/menuitem.js +223 -0
- package/src/node.d.ts +597 -0
- package/src/node.js +44 -0
- package/src/nodes.js +9546 -690
- package/src/ntk.d.ts +44 -0
- package/src/ntk.js +25 -0
- package/src/paintcache.js +366 -0
- package/src/palette.js +380 -0
- package/src/pastestate.js +66 -0
- package/src/portal.js +461 -0
- package/src/priority.js +26 -0
- package/src/refresh/index.d.ts +40 -0
- package/src/refresh/index.js +122 -0
- package/src/refresh/loader.d.ts +37 -0
- package/src/refresh/loader.js +401 -0
- package/src/refresh/register.d.ts +5 -0
- package/src/refresh/register.js +13 -0
- package/src/registry.js +232 -0
- package/src/scale.js +626 -0
- package/src/scalehooks.js +27 -0
- package/src/screencolor.js +640 -0
- package/src/screencolorhooks.js +101 -0
- package/src/screens.js +754 -0
- package/src/screenshooks.js +137 -0
- package/src/startup.js +302 -0
- package/src/style.d.ts +126 -0
- package/src/style.js +33 -0
- package/src/styles.js +1482 -6
- package/src/svgnodes.js +306 -0
- package/src/testing/a11y.js +484 -0
- package/src/testing/components.js +414 -0
- package/src/testing/events.js +407 -0
- package/src/testing/harness.js +455 -0
- package/src/testing/index.d.ts +558 -0
- package/src/testing/index.js +85 -0
- package/src/testing/mock-app.js +463 -0
- package/src/testing/pixels.js +152 -0
- package/src/testing/queries.js +224 -0
- package/src/textrange.js +83 -0
- package/src/textselection.js +439 -0
- package/src/trace-registry.js +63 -0
- package/src/transfer.js +93 -0
- package/src/types/appearance.d.ts +84 -0
- package/src/types/application.d.ts +173 -0
- package/src/types/components.d.ts +820 -0
- package/src/types/dbus.d.ts +177 -0
- package/src/types/elements.d.ts +897 -0
- package/src/types/events.d.ts +549 -0
- package/src/types/filedialog.d.ts +260 -0
- package/src/types/fonts.d.ts +124 -0
- package/src/types/frame.d.ts +146 -0
- package/src/types/globalmenu.d.ts +42 -0
- package/src/types/nodes.d.ts +199 -0
- package/src/types/screencolor.d.ts +84 -0
- package/src/types/style.d.ts +434 -0
- package/src/types/system.d.ts +287 -0
- package/src/windowid.js +151 -0
- package/src/windowstate.js +393 -0
- package/src/xsettings.js +336 -0
- package/src/yoga.d.ts +55 -0
- package/src/yoga.js +135 -0
- package/src/components/Canvas3D.js +0 -28
- package/src/geometry3d.js +0 -223
- package/src/pointer3d.js +0 -158
- package/src/raycast3d.js +0 -146
- package/src/richnodes.js +0 -436
- package/src/scene3d.js +0 -683
package/src/richnodes.js
DELETED
|
@@ -1,436 +0,0 @@
|
|
|
1
|
-
// Rich-content elements: thin retained-node wrappers over ntk's document
|
|
2
|
-
// widgets in standalone mode (`layout(width)` + `draw(ctx, x, y)` +
|
|
3
|
-
// `contentHeight`). One yoga measure function per node feeds the widget's
|
|
4
|
-
// own layout into flexbox; async content (mermaid models, images) arrives
|
|
5
|
-
// through the widget's `onInvalidate` hook (ntk >= 3.4.0).
|
|
6
|
-
import { MarkdownView, HtmlView, SvgView, layoutTex } from 'ntk';
|
|
7
|
-
|
|
8
|
-
import { Node } from './nodes.js';
|
|
9
|
-
import { Yoga, isLayoutProp, isPaintProp } from './styles.js';
|
|
10
|
-
|
|
11
|
-
/** Joined text of string children (react-markdown style), or null when the
|
|
12
|
-
* element has none — then the `source` prop is the content. */
|
|
13
|
-
function stringChildrenOf(node) {
|
|
14
|
-
let text = null;
|
|
15
|
-
for (const child of node.children) {
|
|
16
|
-
if (child.kind === 'textchunk') text = (text ?? '') + child.text;
|
|
17
|
-
}
|
|
18
|
-
return text;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Fallback width for measuring under an unconstrained yoga width
|
|
22
|
-
// (position: absolute without width, unbounded row). Flowed documents have
|
|
23
|
-
// no natural width, so pick something readable.
|
|
24
|
-
const PREFERRED_WIDTH = 480;
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Base for <markdown> and <html>: width-driven document flow. Subclasses
|
|
28
|
-
* implement _createView() (a widget with layout/draw/contentHeight) and
|
|
29
|
-
* _setSource(view). Content is a string child (like react-markdown:
|
|
30
|
-
* <markdown>{`# Hi`}</markdown> — use a template-literal expression, JSX
|
|
31
|
-
* literal text collapses newlines) or the `source` prop; children win.
|
|
32
|
-
* The view is created lazily because the headless mock app in smoke tests
|
|
33
|
-
* has no font manager — everything measures 0×0 there, like <text>.
|
|
34
|
-
*/
|
|
35
|
-
class DocumentViewNode extends Node {
|
|
36
|
-
constructor(kind, props, app) {
|
|
37
|
-
super(kind, props, app);
|
|
38
|
-
this.view = null;
|
|
39
|
-
this._laidOutWidth = -1;
|
|
40
|
-
this.yoga.setMeasureFunc((width, widthMode) => {
|
|
41
|
-
const view = this._ensureView();
|
|
42
|
-
if (!view) return { width: 0, height: 0 };
|
|
43
|
-
const unconstrained =
|
|
44
|
-
widthMode === Yoga.MEASURE_MODE_UNDEFINED ||
|
|
45
|
-
!Number.isFinite(width) ||
|
|
46
|
-
width <= 0;
|
|
47
|
-
const w = Math.max(1, Math.ceil(unconstrained ? PREFERRED_WIDTH : width));
|
|
48
|
-
const height = this._layoutAt(w);
|
|
49
|
-
return { width: w, height: Math.ceil(height) };
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
_ensureView() {
|
|
54
|
-
if (this.view || !this.app?.fonts) return this.view;
|
|
55
|
-
this.view = this._createView();
|
|
56
|
-
if (this.view) {
|
|
57
|
-
// ntk >= 3.4.0 notifies here when async content (a mermaid model, an
|
|
58
|
-
// image) invalidates the widget layout; harmless no-op before that
|
|
59
|
-
this.view.onInvalidate = () => this._contentInvalidated();
|
|
60
|
-
this._setSource(this.view);
|
|
61
|
-
}
|
|
62
|
-
return this.view;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
_contentInvalidated() {
|
|
66
|
-
if (this.destroyed) return;
|
|
67
|
-
this._laidOutWidth = -1;
|
|
68
|
-
this.yoga?.markDirty();
|
|
69
|
-
this.root?.invalidate(true);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
_source() {
|
|
73
|
-
return stringChildrenOf(this) ?? this.props.source ?? '';
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** String children changed (chunk added/removed/edited). */
|
|
77
|
-
_textContentChanged() {
|
|
78
|
-
if (this.view) this._setSource(this.view);
|
|
79
|
-
this._contentInvalidated();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
_layoutAt(width) {
|
|
83
|
-
if (this._laidOutWidth !== width) {
|
|
84
|
-
this.view.layout(width);
|
|
85
|
-
this._laidOutWidth = width;
|
|
86
|
-
}
|
|
87
|
-
return this.view.contentHeight;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Props that require rebuilding the view (theme and loader options are
|
|
91
|
-
* constructor-only in the ntk widgets). Subclasses extend. */
|
|
92
|
-
_viewProps() {
|
|
93
|
-
return ['theme'];
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
applyProps(newProps, oldProps) {
|
|
97
|
-
const before = oldProps ?? this.props;
|
|
98
|
-
super.applyProps(newProps, oldProps);
|
|
99
|
-
if (!this.view) return;
|
|
100
|
-
if (this._viewProps().some((key) => newProps[key] !== before[key])) {
|
|
101
|
-
this.view = null; // rebuilt (and re-sourced) on next measure/paint
|
|
102
|
-
this._contentInvalidated();
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
if (newProps.source !== before.source) {
|
|
106
|
-
this._setSource(this.view);
|
|
107
|
-
this._contentInvalidated();
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
_paintContent(ctx) {
|
|
112
|
-
const view = this._ensureView();
|
|
113
|
-
if (!view) return;
|
|
114
|
-
const content = this.contentBox();
|
|
115
|
-
if (content.width <= 0) return;
|
|
116
|
-
this._layoutAt(Math.max(1, Math.ceil(content.width)));
|
|
117
|
-
ctx.save();
|
|
118
|
-
ctx.beginPath();
|
|
119
|
-
ctx.rect(this.abs.x, this.abs.y, this.abs.width, this.abs.height);
|
|
120
|
-
ctx.clip();
|
|
121
|
-
// links recorded by draw() are in window coordinates, so linkAt can
|
|
122
|
-
// take synthetic-event coordinates directly (_defaultMouseDown)
|
|
123
|
-
view.draw(ctx, content.x, content.y);
|
|
124
|
-
ctx.restore();
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
_defaultMouseDown(ev) {
|
|
128
|
-
if (ev.button !== 1 || !this.view || !this.props.onLink) return;
|
|
129
|
-
const hit = this.view.linkAt(ev.x, ev.y);
|
|
130
|
-
if (!hit) return;
|
|
131
|
-
// MarkdownView.linkAt returns the href, HtmlView an { href, element }
|
|
132
|
-
if (typeof hit === 'string') this.props.onLink(hit, ev);
|
|
133
|
-
else this.props.onLink(hit.href, ev, hit.element);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* <markdown source>: ntk MarkdownView — headings, emphasis, lists, quotes,
|
|
139
|
-
* tables, syntax-highlighted fences, math fences, async mermaid fences.
|
|
140
|
-
* Props: source, theme, onLink(href, ev). Spacing comes from the box model
|
|
141
|
-
* (padding prop), not the widget's own page padding.
|
|
142
|
-
*/
|
|
143
|
-
export class MarkdownNode extends DocumentViewNode {
|
|
144
|
-
constructor(props, app) {
|
|
145
|
-
super('markdown', props, app);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
_createView() {
|
|
149
|
-
return new MarkdownView(null, {
|
|
150
|
-
fonts: this.app.fonts,
|
|
151
|
-
theme: this.props.theme,
|
|
152
|
-
padding: 0,
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
_setSource(view) {
|
|
157
|
-
view.setMarkdown(this._source());
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* <html source>: ntk HtmlView — its own CSS cascade (document <style>s plus
|
|
163
|
-
* the `stylesheet` prop), flexbox/block layout, images. Props: source,
|
|
164
|
-
* stylesheet, baseUrl, loadResource, theme, onLink(href, ev, element).
|
|
165
|
-
*/
|
|
166
|
-
export class HtmlNode extends DocumentViewNode {
|
|
167
|
-
constructor(props, app) {
|
|
168
|
-
super('html', props, app);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
_viewProps() {
|
|
172
|
-
return ['theme', 'stylesheet', 'baseUrl', 'loadResource'];
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
_createView() {
|
|
176
|
-
return new HtmlView(null, {
|
|
177
|
-
fonts: this.app.fonts,
|
|
178
|
-
theme: this.props.theme,
|
|
179
|
-
stylesheet: this.props.stylesheet,
|
|
180
|
-
baseUrl: this.props.baseUrl,
|
|
181
|
-
loadResource: this.props.loadResource,
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
_setSource(view) {
|
|
186
|
-
view.setHtml(this._source(), { baseUrl: this.props.baseUrl });
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// SVG attributes that are camelCase in SVG itself; every other camelCase
|
|
191
|
-
// prop (strokeWidth, fillRule, textAnchor, …) kebab-cases, like React DOM.
|
|
192
|
-
const SVG_CAMEL_ATTRS = new Set([
|
|
193
|
-
'viewBox',
|
|
194
|
-
'preserveAspectRatio',
|
|
195
|
-
'gradientUnits',
|
|
196
|
-
'gradientTransform',
|
|
197
|
-
'spreadMethod',
|
|
198
|
-
'patternUnits',
|
|
199
|
-
'patternContentUnits',
|
|
200
|
-
'patternTransform',
|
|
201
|
-
'clipPathUnits',
|
|
202
|
-
'maskUnits',
|
|
203
|
-
'maskContentUnits',
|
|
204
|
-
'markerWidth',
|
|
205
|
-
'markerHeight',
|
|
206
|
-
'markerUnits',
|
|
207
|
-
'refX',
|
|
208
|
-
'refY',
|
|
209
|
-
'textLength',
|
|
210
|
-
'lengthAdjust',
|
|
211
|
-
'stdDeviation',
|
|
212
|
-
]);
|
|
213
|
-
|
|
214
|
-
const toSvgAttrib = (name) =>
|
|
215
|
-
SVG_CAMEL_ATTRS.has(name)
|
|
216
|
-
? name
|
|
217
|
-
: name.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase());
|
|
218
|
-
|
|
219
|
-
function svgAttribs(props, skip) {
|
|
220
|
-
const attribs = {};
|
|
221
|
-
for (const key of Object.keys(props)) {
|
|
222
|
-
const value = props[key];
|
|
223
|
-
if (key === 'children' || value == null) continue;
|
|
224
|
-
if (typeof value === 'function') continue;
|
|
225
|
-
if (skip && skip(key)) continue;
|
|
226
|
-
attribs[toSvgAttrib(key)] = String(value);
|
|
227
|
-
}
|
|
228
|
-
return attribs;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* An element inside <svg>: no yoga node, no painting of its own — it only
|
|
233
|
-
* carries a tag name and props, and is serialized into the htmlparser2-style
|
|
234
|
-
* DOM that SvgView consumes. Prop/child changes bubble to the owning
|
|
235
|
-
* SvgNode via the _textContentChanged channel (also used by text chunks,
|
|
236
|
-
* so string children of an SVG <text> invalidate the same way).
|
|
237
|
-
*/
|
|
238
|
-
export class SvgChildNode extends Node {
|
|
239
|
-
constructor(tag, props, app) {
|
|
240
|
-
super(tag, props, app, { yoga: false });
|
|
241
|
-
this.isSvgChild = true;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
_textContentChanged() {
|
|
245
|
-
this.parent?._textContentChanged();
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
applyProps(newProps, oldProps) {
|
|
249
|
-
super.applyProps(newProps, oldProps);
|
|
250
|
-
this._textContentChanged();
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
toDom() {
|
|
254
|
-
return {
|
|
255
|
-
type: 'tag',
|
|
256
|
-
name: this.kind,
|
|
257
|
-
attribs: svgAttribs(this.props),
|
|
258
|
-
children: this.children
|
|
259
|
-
.map((c) =>
|
|
260
|
-
c.kind === 'textchunk'
|
|
261
|
-
? { type: 'text', data: c.text }
|
|
262
|
-
: c.isSvgChild
|
|
263
|
-
? c.toDom()
|
|
264
|
-
: null,
|
|
265
|
-
)
|
|
266
|
-
.filter(Boolean),
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* <svg>: ntk SvgView. Content is either JSX children (React-DOM style —
|
|
273
|
-
* <svg viewBox="0 0 24 24"><circle cx={12} … />; children win when both
|
|
274
|
-
* are present) or a `source` markup string. Sized like <image>: natural
|
|
275
|
-
* (viewBox) size, kept to its aspect ratio when only the width is
|
|
276
|
-
* constrained; scales to the content box.
|
|
277
|
-
*/
|
|
278
|
-
export class SvgNode extends Node {
|
|
279
|
-
constructor(props, app) {
|
|
280
|
-
super('svg', props, app);
|
|
281
|
-
this.view = null;
|
|
282
|
-
this._stale = true;
|
|
283
|
-
this._configureMeasure();
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/** Any change to the svg subtree (props, children, text) lands here. */
|
|
287
|
-
_textContentChanged() {
|
|
288
|
-
this._stale = true;
|
|
289
|
-
// markDirty is only legal on nodes with a measure function (it is
|
|
290
|
-
// unset when width and height are both fixed)
|
|
291
|
-
if (this._hasMeasure) this.yoga?.markDirty();
|
|
292
|
-
this.root?.invalidate(true);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
_ensureView() {
|
|
296
|
-
if (!this._stale) return this.view;
|
|
297
|
-
this._stale = false;
|
|
298
|
-
this.view = null;
|
|
299
|
-
try {
|
|
300
|
-
const children = this.children.filter(
|
|
301
|
-
(c) => c.isSvgChild || c.kind === 'textchunk',
|
|
302
|
-
);
|
|
303
|
-
if (children.length > 0) {
|
|
304
|
-
const skip = (key) =>
|
|
305
|
-
key === 'source' ||
|
|
306
|
-
isLayoutProp(key) ||
|
|
307
|
-
isPaintProp(key) ||
|
|
308
|
-
key === 'cursor' ||
|
|
309
|
-
key === 'focusable' ||
|
|
310
|
-
key === 'pointerEvents';
|
|
311
|
-
this.view = new SvgView(null).setSvgDom({
|
|
312
|
-
type: 'tag',
|
|
313
|
-
name: 'svg',
|
|
314
|
-
attribs: svgAttribs(this.props, skip),
|
|
315
|
-
children: children.map((c) =>
|
|
316
|
-
c.kind === 'textchunk' ? { type: 'text', data: c.text } : c.toDom(),
|
|
317
|
-
),
|
|
318
|
-
});
|
|
319
|
-
} else if (this.props.source) {
|
|
320
|
-
this.view = new SvgView(null).setSvg(String(this.props.source));
|
|
321
|
-
}
|
|
322
|
-
} catch (err) {
|
|
323
|
-
console.error('react-x11: <svg> failed to parse:', err.message);
|
|
324
|
-
}
|
|
325
|
-
return this.view;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
_configureMeasure() {
|
|
329
|
-
if (this.props.width != null && this.props.height != null) {
|
|
330
|
-
if (this._hasMeasure) this.yoga.unsetMeasureFunc();
|
|
331
|
-
this._hasMeasure = false;
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
this._hasMeasure = true;
|
|
335
|
-
this.yoga.setMeasureFunc((width, widthMode) => {
|
|
336
|
-
const view = this._ensureView();
|
|
337
|
-
const natW = view?.naturalWidth ?? 0;
|
|
338
|
-
const natH = view?.naturalHeight ?? 0;
|
|
339
|
-
let w = natW;
|
|
340
|
-
if (
|
|
341
|
-
widthMode !== Yoga.MEASURE_MODE_UNDEFINED &&
|
|
342
|
-
Number.isFinite(width) &&
|
|
343
|
-
width < w
|
|
344
|
-
) {
|
|
345
|
-
w = width;
|
|
346
|
-
}
|
|
347
|
-
return { width: w, height: natW > 0 ? (w * natH) / natW : natH };
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
applyProps(newProps, oldProps) {
|
|
352
|
-
super.applyProps(newProps, oldProps);
|
|
353
|
-
this._configureMeasure();
|
|
354
|
-
// attributes live in props (children form) or in `source`; either way
|
|
355
|
-
// the SvgView is cheap to rebuild on the next measure/paint
|
|
356
|
-
this._textContentChanged();
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
_paintContent(ctx) {
|
|
360
|
-
const view = this._ensureView();
|
|
361
|
-
if (!view) return;
|
|
362
|
-
const content = this.contentBox();
|
|
363
|
-
if (content.width <= 0 || content.height <= 0) return;
|
|
364
|
-
view.draw(ctx, content.x, content.y, content.width, content.height);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* <tex source displayMode size color>: a KaTeX formula via ntk layoutTex.
|
|
370
|
-
* Layout is synchronous and headless; the box has an intrinsic size (no
|
|
371
|
-
* wrapping). Invalid TeX renders nothing and logs once.
|
|
372
|
-
*/
|
|
373
|
-
export class TexNode extends Node {
|
|
374
|
-
constructor(props, app) {
|
|
375
|
-
super('tex', props, app);
|
|
376
|
-
this.box = null;
|
|
377
|
-
this._boxKey = null;
|
|
378
|
-
this.yoga.setMeasureFunc(() => {
|
|
379
|
-
const box = this._ensureBox();
|
|
380
|
-
if (!box) return { width: 0, height: 0 };
|
|
381
|
-
return { width: Math.ceil(box.width), height: Math.ceil(box.height) };
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/** Formula text changed via string children. */
|
|
386
|
-
_textContentChanged() {
|
|
387
|
-
this._boxKey = null;
|
|
388
|
-
this.yoga?.markDirty();
|
|
389
|
-
this.root?.invalidate(true);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
_ensureBox() {
|
|
393
|
-
const { size, color, displayMode, katex } = this.props;
|
|
394
|
-
const source = stringChildrenOf(this) ?? this.props.source;
|
|
395
|
-
if (!source) return null;
|
|
396
|
-
const key = `${source}|${size}|${color}|${displayMode}`;
|
|
397
|
-
if (this._boxKey === key) return this.box;
|
|
398
|
-
this._boxKey = key;
|
|
399
|
-
this.box = null;
|
|
400
|
-
try {
|
|
401
|
-
this.box = layoutTex(String(source), {
|
|
402
|
-
size,
|
|
403
|
-
color,
|
|
404
|
-
displayMode: displayMode ?? false,
|
|
405
|
-
katex,
|
|
406
|
-
});
|
|
407
|
-
} catch (err) {
|
|
408
|
-
console.error('react-x11: <tex> failed to layout:', err.message);
|
|
409
|
-
}
|
|
410
|
-
return this.box;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
applyProps(newProps, oldProps) {
|
|
414
|
-
const before = oldProps ?? this.props;
|
|
415
|
-
super.applyProps(newProps, oldProps);
|
|
416
|
-
if (
|
|
417
|
-
newProps.source !== before.source ||
|
|
418
|
-
newProps.size !== before.size ||
|
|
419
|
-
newProps.color !== before.color ||
|
|
420
|
-
newProps.displayMode !== before.displayMode
|
|
421
|
-
) {
|
|
422
|
-
this.yoga.markDirty();
|
|
423
|
-
this.root?.invalidate(true);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
_paintContent(ctx) {
|
|
428
|
-
const box = this._ensureBox();
|
|
429
|
-
// TexBox.draw issues raw XRender requests through ctx.window.app —
|
|
430
|
-
// it needs a real ntk 2d context (headless mock contexts skip)
|
|
431
|
-
if (!box || !ctx.window?.app?.display) return;
|
|
432
|
-
const content = this.contentBox();
|
|
433
|
-
ctx.fillStyle = this.props.color ?? '#222222';
|
|
434
|
-
box.draw(ctx, content.x, content.y);
|
|
435
|
-
}
|
|
436
|
-
}
|