@react-x11/components 0.12.0 → 0.14.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/dist/html/controls.d.ts +41 -5
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +85 -57
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts +134 -20
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +545 -126
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/color.d.ts +18 -0
- package/dist/html/css/color.d.ts.map +1 -1
- package/dist/html/css/color.js +27 -0
- package/dist/html/css/color.js.map +1 -1
- package/dist/html/css/parse.d.ts +47 -6
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +336 -30
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +142 -15
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +563 -147
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/transform.d.ts +85 -0
- package/dist/html/css/transform.d.ts.map +1 -0
- package/dist/html/css/transform.js +425 -0
- package/dist/html/css/transform.js.map +1 -0
- package/dist/html/css/ua.d.ts +11 -1
- package/dist/html/css/ua.d.ts.map +1 -1
- package/dist/html/css/ua.js +73 -9
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/css/values.d.ts +4 -0
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/dom.d.ts.map +1 -1
- package/dist/html/dom.js +3 -1
- package/dist/html/dom.js.map +1 -1
- package/dist/html/fonts.d.ts +78 -4
- package/dist/html/fonts.d.ts.map +1 -1
- package/dist/html/fonts.js +373 -19
- package/dist/html/fonts.js.map +1 -1
- package/dist/html/form.d.ts +200 -0
- package/dist/html/form.d.ts.map +1 -0
- package/dist/html/form.js +801 -0
- package/dist/html/form.js.map +1 -0
- package/dist/html/index.d.ts +12 -0
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +30 -301
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/axes.d.ts +14 -0
- package/dist/html/layout/axes.d.ts.map +1 -0
- package/dist/html/layout/axes.js +28 -0
- package/dist/html/layout/axes.js.map +1 -0
- package/dist/html/layout/block.d.ts +93 -12
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +393 -39
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +90 -2
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +74 -10
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/cache.d.ts +1 -1
- package/dist/html/layout/cache.d.ts.map +1 -1
- package/dist/html/layout/cache.js +6 -0
- package/dist/html/layout/cache.js.map +1 -1
- package/dist/html/layout/css-grid.d.ts.map +1 -1
- package/dist/html/layout/css-grid.js +90 -18
- package/dist/html/layout/css-grid.js.map +1 -1
- package/dist/html/layout/flex.d.ts.map +1 -1
- package/dist/html/layout/flex.js +574 -84
- package/dist/html/layout/flex.js.map +1 -1
- package/dist/html/layout/inline.d.ts +3 -0
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +154 -54
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/layout/multicol.d.ts +42 -0
- package/dist/html/layout/multicol.d.ts.map +1 -0
- package/dist/html/layout/multicol.js +897 -0
- package/dist/html/layout/multicol.js.map +1 -0
- package/dist/html/layout/shaping.d.ts +7 -0
- package/dist/html/layout/shaping.d.ts.map +1 -1
- package/dist/html/layout/shaping.js +25 -20
- package/dist/html/layout/shaping.js.map +1 -1
- package/dist/html/layout/table.d.ts.map +1 -1
- package/dist/html/layout/table.js +62 -18
- package/dist/html/layout/table.js.map +1 -1
- package/dist/html/node.d.ts +90 -15
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +771 -186
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +101 -5
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +1502 -160
- package/dist/html/paint.js.map +1 -1
- package/dist/html/svg.d.ts +15 -2
- package/dist/html/svg.d.ts.map +1 -1
- package/dist/html/svg.js +249 -17
- package/dist/html/svg.js.map +1 -1
- package/dist/html/widgets.d.ts +29 -0
- package/dist/html/widgets.d.ts.map +1 -0
- package/dist/html/widgets.js +700 -0
- package/dist/html/widgets.js.map +1 -0
- package/dist/html/woff2.d.ts +14 -0
- package/dist/html/woff2.d.ts.map +1 -0
- package/dist/html/woff2.js +608 -0
- package/dist/html/woff2.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/maps/controller.d.ts +17 -1
- package/dist/maps/controller.d.ts.map +1 -1
- package/dist/maps/controller.js +27 -7
- package/dist/maps/controller.js.map +1 -1
- package/dist/richtext/node.d.ts +8 -1
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +5 -1
- package/dist/richtext/node.js.map +1 -1
- package/dist/richtext/runs.d.ts +25 -6
- package/dist/richtext/runs.d.ts.map +1 -1
- package/dist/richtext/runs.js +240 -24
- package/dist/richtext/runs.js.map +1 -1
- package/package.json +2 -2
- package/src/html/controls.ts +114 -51
- package/src/html/css/cascade.ts +614 -140
- package/src/html/css/color.ts +29 -0
- package/src/html/css/parse.ts +388 -33
- package/src/html/css/style.ts +688 -150
- package/src/html/css/transform.ts +459 -0
- package/src/html/css/ua.ts +77 -10
- package/src/html/css/values.ts +4 -0
- package/src/html/dom.ts +3 -1
- package/src/html/fonts.ts +417 -24
- package/src/html/form.ts +962 -0
- package/src/html/index.ts +49 -338
- package/src/html/layout/axes.ts +62 -0
- package/src/html/layout/block.ts +447 -37
- package/src/html/layout/boxes.ts +143 -14
- package/src/html/layout/cache.ts +6 -0
- package/src/html/layout/css-grid.ts +95 -22
- package/src/html/layout/flex.ts +675 -78
- package/src/html/layout/inline.ts +159 -55
- package/src/html/layout/multicol.ts +977 -0
- package/src/html/layout/shaping.ts +27 -18
- package/src/html/layout/table.ts +62 -19
- package/src/html/node.ts +798 -172
- package/src/html/paint.ts +1742 -241
- package/src/html/svg.ts +277 -19
- package/src/html/widgets.ts +821 -0
- package/src/html/woff2.ts +612 -0
- package/src/index.ts +1 -0
- package/src/maps/controller.ts +28 -7
- package/src/richtext/node.ts +13 -2
- package/src/richtext/runs.ts +313 -35
package/src/html/index.ts
CHANGED
|
@@ -14,14 +14,7 @@
|
|
|
14
14
|
// is a property of the design rather than a setting.
|
|
15
15
|
import React from 'react';
|
|
16
16
|
import type { ReactElement, ReactNode } from 'react';
|
|
17
|
-
import {
|
|
18
|
-
Button,
|
|
19
|
-
Checkbox,
|
|
20
|
-
Radio,
|
|
21
|
-
RadioGroup,
|
|
22
|
-
Select,
|
|
23
|
-
useTheme,
|
|
24
|
-
} from 'react-x11';
|
|
17
|
+
import { useTheme } from 'react-x11';
|
|
25
18
|
import type { DrawnNode, MouseEvent as X11MouseEvent, Rect } from 'react-x11';
|
|
26
19
|
import { tint } from 'react-x11/style';
|
|
27
20
|
import type { Style } from 'react-x11/style';
|
|
@@ -31,18 +24,13 @@ import type {} from 'react-x11/jsx-runtime';
|
|
|
31
24
|
import { useLinkClicks, useSelectionMenu } from '../richtext/index.js';
|
|
32
25
|
import { useFontPrewarm } from '../internal/prewarm.js';
|
|
33
26
|
import { hx } from './hx.js';
|
|
34
|
-
import { attr, tagOf } from './dom.js';
|
|
35
27
|
import type { Document, Element } from './dom.js';
|
|
36
28
|
import { ELEMENT, HtmlViewNode, registerHtmlView } from './node.js';
|
|
37
29
|
import type { HtmlViewProps, ScriptRequest } from './node.js';
|
|
38
30
|
import type { RootLook } from './css/style.js';
|
|
39
|
-
import {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
selectedOption,
|
|
43
|
-
textareaValue,
|
|
44
|
-
} from './controls.js';
|
|
45
|
-
import type { BareField, ControlRect } from './controls.js';
|
|
31
|
+
import type { ControlRect } from './controls.js';
|
|
32
|
+
import type { FormSubmission } from './form.js';
|
|
33
|
+
import { useForms } from './widgets.js';
|
|
46
34
|
import type { ResourceRequest, ResourceResult } from './resources.js';
|
|
47
35
|
|
|
48
36
|
export {
|
|
@@ -53,6 +41,13 @@ export {
|
|
|
53
41
|
export type { HtmlViewProps, ScriptRequest } from './node.js';
|
|
54
42
|
export type { ResourceRequest, ResourceResult } from './resources.js';
|
|
55
43
|
export type { BareField, ControlRect } from './controls.js';
|
|
44
|
+
export type {
|
|
45
|
+
FormEnctype,
|
|
46
|
+
FormMethod,
|
|
47
|
+
FormSubmission,
|
|
48
|
+
SubmitContext,
|
|
49
|
+
} from './form.js';
|
|
50
|
+
export { formSubmission } from './form.js';
|
|
56
51
|
export type { ComputedStyle, RootLook } from './css/style.js';
|
|
57
52
|
export type {
|
|
58
53
|
AnyNode,
|
|
@@ -154,6 +149,15 @@ export interface HtmlProps {
|
|
|
154
149
|
* calls the handle's `refresh()`.
|
|
155
150
|
*/
|
|
156
151
|
onControlChange?: (element: Element, value: string | boolean) => void;
|
|
152
|
+
/**
|
|
153
|
+
* A form was submitted — a submit button pressed, or Enter in one of its
|
|
154
|
+
* text fields. Handed over as the request it makes: the method, the URL
|
|
155
|
+
* (resolved as `onLink`'s `href` is, and for a GET with the form's entries
|
|
156
|
+
* as its query), a POST's encoded body and its content type, and the
|
|
157
|
+
* entries themselves. Absent means submitting does nothing: this
|
|
158
|
+
* component never sends anything by itself, any more than it navigates.
|
|
159
|
+
*/
|
|
160
|
+
onSubmit?: (submission: FormSubmission) => void;
|
|
157
161
|
/** Base text style. Defaults: theme `fontSize` (14), `sans-serif`. */
|
|
158
162
|
fontSize?: number;
|
|
159
163
|
fontFamily?: string;
|
|
@@ -230,6 +234,10 @@ function deriveLook(
|
|
|
230
234
|
controlPadY: Number(theme.paddingY ?? 6),
|
|
231
235
|
controlBorder: Number(theme.borderWidth ?? 1),
|
|
232
236
|
controlRadius: Number(theme.radius ?? 4),
|
|
237
|
+
// not `fontSize` and `fontFamily`, which a host sets to the web's
|
|
238
|
+
// `medium` and the face it reads pages in
|
|
239
|
+
controlFontSize: Number(theme.fontSize ?? 14),
|
|
240
|
+
controlFontFamily: String(theme.fontFamily ?? 'sans-serif'),
|
|
233
241
|
};
|
|
234
242
|
}
|
|
235
243
|
|
|
@@ -285,14 +293,27 @@ export function Html(props: HtmlProps): ReactElement {
|
|
|
285
293
|
onScript,
|
|
286
294
|
onDocument,
|
|
287
295
|
onControlChange,
|
|
296
|
+
onSubmit,
|
|
288
297
|
style,
|
|
289
298
|
} = props;
|
|
290
299
|
|
|
291
300
|
const theme = useTheme() as unknown as Record<string, unknown>;
|
|
292
301
|
const links = useLinkClicks(onLink);
|
|
293
|
-
const buttons = useButtonPresses(onControlChange);
|
|
294
302
|
const menu = useSelectionMenu(selectable);
|
|
295
303
|
|
|
304
|
+
// The element, for the document's base when a form is submitted, as well
|
|
305
|
+
// as wherever the application's `ref` wants it.
|
|
306
|
+
const viewNode = React.useRef<HtmlViewNode | null>(null);
|
|
307
|
+
const outerRef = props.ref;
|
|
308
|
+
const viewRef = React.useCallback(
|
|
309
|
+
(node: HtmlViewNode | null) => {
|
|
310
|
+
viewNode.current = node;
|
|
311
|
+
if (typeof outerRef === 'function') outerRef(node);
|
|
312
|
+
else if (outerRef) (outerRef as React.RefObject<unknown>).current = node;
|
|
313
|
+
},
|
|
314
|
+
[outerRef],
|
|
315
|
+
);
|
|
316
|
+
|
|
296
317
|
const look = React.useMemo(
|
|
297
318
|
() => deriveLook(theme, props),
|
|
298
319
|
[theme, props.fontSize, props.fontFamily, props.monoFamily],
|
|
@@ -305,6 +326,13 @@ export function Html(props: HtmlProps): ReactElement {
|
|
|
305
326
|
const documentRef = React.useRef<Document | null>(null);
|
|
306
327
|
const [controls, setControls] = React.useState<ControlRect[]>([]);
|
|
307
328
|
const [domRevision, setDomRevision] = React.useState(0);
|
|
329
|
+
const forms = useForms({
|
|
330
|
+
view: viewNode,
|
|
331
|
+
baseUrl,
|
|
332
|
+
onControlChange,
|
|
333
|
+
onSubmit,
|
|
334
|
+
touch: () => setDomRevision((n) => n + 1),
|
|
335
|
+
});
|
|
308
336
|
|
|
309
337
|
const handleDocument = React.useCallback(
|
|
310
338
|
(doc: Document) => {
|
|
@@ -338,7 +366,7 @@ export function Html(props: HtmlProps): ReactElement {
|
|
|
338
366
|
onDocument: handleDocument,
|
|
339
367
|
onControls: handleControls,
|
|
340
368
|
domRevision,
|
|
341
|
-
ref:
|
|
369
|
+
ref: viewRef as React.Ref<unknown>,
|
|
342
370
|
// grown with the component, where an application grows it: the root's
|
|
343
371
|
// background covers the whole of it, as a page's covers the window
|
|
344
372
|
style: { alignSelf: 'stretch', flexGrow: 1 },
|
|
@@ -347,16 +375,7 @@ export function Html(props: HtmlProps): ReactElement {
|
|
|
347
375
|
const children: ReactNode[] = [
|
|
348
376
|
h(ELEMENT, { key: 'view', ...viewProps } as Record<string, unknown>),
|
|
349
377
|
];
|
|
350
|
-
|
|
351
|
-
children.push(
|
|
352
|
-
renderControl(
|
|
353
|
-
rect,
|
|
354
|
-
look,
|
|
355
|
-
() => setDomRevision((n) => n + 1),
|
|
356
|
-
onControlChange,
|
|
357
|
-
),
|
|
358
|
-
);
|
|
359
|
-
}
|
|
378
|
+
children.push(...forms.render(controls, look));
|
|
360
379
|
|
|
361
380
|
const rootStyle: Style = { flexDirection: 'column', position: 'relative' };
|
|
362
381
|
return hx(
|
|
@@ -374,11 +393,11 @@ export function Html(props: HtmlProps): ReactElement {
|
|
|
374
393
|
...links,
|
|
375
394
|
onMouseDown: (ev: X11MouseEvent<DrawnNode>) => {
|
|
376
395
|
links.onMouseDown(ev);
|
|
377
|
-
|
|
396
|
+
forms.onMouseDown(ev);
|
|
378
397
|
},
|
|
379
398
|
onMouseUp: (ev: X11MouseEvent<DrawnNode>) => {
|
|
380
399
|
links.onMouseUp(ev);
|
|
381
|
-
|
|
400
|
+
forms.onMouseUp(ev);
|
|
382
401
|
},
|
|
383
402
|
...menu,
|
|
384
403
|
'data-testname': props['data-testname'],
|
|
@@ -428,314 +447,6 @@ export function useHtmlHandle(): HtmlHandle & { ref: React.Ref<unknown> } {
|
|
|
428
447
|
);
|
|
429
448
|
}
|
|
430
449
|
|
|
431
|
-
/**
|
|
432
|
-
* A press and a release on the same `<button>` the document draws, reported
|
|
433
|
-
* as a pressed widget reports one: `onControlChange` with the button and its
|
|
434
|
-
* `value`. A `<button>` is laid out and drawn like any box (the UA sheet's
|
|
435
|
-
* `button` rule) — its content is the page's, and most of the buttons on
|
|
436
|
-
* the web are an icon or a pill of a page's own design — so it is the
|
|
437
|
-
* element that finds out it was pressed, not a widget. A disabled one, or
|
|
438
|
-
* a press that lands in a link inside one, is not a press of it.
|
|
439
|
-
*/
|
|
440
|
-
function useButtonPresses(onControlChange: HtmlProps['onControlChange']): {
|
|
441
|
-
onMouseDown: (ev: X11MouseEvent<DrawnNode>) => void;
|
|
442
|
-
onMouseUp: (ev: X11MouseEvent<DrawnNode>) => void;
|
|
443
|
-
} {
|
|
444
|
-
const press = React.useRef<Element | null>(null);
|
|
445
|
-
const buttonAt = (ev: X11MouseEvent<DrawnNode>): Element | null => {
|
|
446
|
-
const target = ev.target as {
|
|
447
|
-
elementAtPoint?: (x: number, y: number) => Element | null;
|
|
448
|
-
} | null;
|
|
449
|
-
let node =
|
|
450
|
-
typeof target?.elementAtPoint === 'function'
|
|
451
|
-
? target.elementAtPoint(ev.x, ev.y)
|
|
452
|
-
: null;
|
|
453
|
-
for (
|
|
454
|
-
;
|
|
455
|
-
node;
|
|
456
|
-
node = node.parent?.type === 'tag' ? (node.parent as Element) : null
|
|
457
|
-
) {
|
|
458
|
-
if (tagOf(node) === 'a' && attr(node, 'href') !== undefined) return null;
|
|
459
|
-
if (tagOf(node) === 'button') {
|
|
460
|
-
return attr(node, 'disabled') === undefined ? node : null;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
return null;
|
|
464
|
-
};
|
|
465
|
-
return {
|
|
466
|
-
onMouseDown: (ev) => {
|
|
467
|
-
press.current = onControlChange && ev.button === 1 ? buttonAt(ev) : null;
|
|
468
|
-
},
|
|
469
|
-
onMouseUp: (ev) => {
|
|
470
|
-
const pressed = press.current;
|
|
471
|
-
press.current = null;
|
|
472
|
-
if (!pressed || !onControlChange || ev.button !== 1) return;
|
|
473
|
-
if (buttonAt(ev) !== pressed) return;
|
|
474
|
-
onControlChange(pressed, attr(pressed, 'value') ?? '');
|
|
475
|
-
},
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// --- the widgets ------------------------------------------------------------
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* One form control, as a real widget at the rectangle layout reserved for it.
|
|
483
|
-
*
|
|
484
|
-
* Every one of these is a **core** widget rather than something drawn here:
|
|
485
|
-
* a `<select>` in a document drops the same menu as a `<Select>` in the
|
|
486
|
-
* window around it, a `<textinput>` gets the same caret, the same IME and the
|
|
487
|
-
* same edit menu, and all of them join the window's focus order. The
|
|
488
|
-
* alternative — drawing them in the paint pass — would be a picture of a form.
|
|
489
|
-
*/
|
|
490
|
-
function renderControl(
|
|
491
|
-
rect: ControlRect,
|
|
492
|
-
look: RootLook,
|
|
493
|
-
touch: () => void,
|
|
494
|
-
onChange: HtmlProps['onControlChange'],
|
|
495
|
-
): ReactNode {
|
|
496
|
-
const el = rect.element;
|
|
497
|
-
const key = `${rect.kind}:${rect.x},${rect.y}`;
|
|
498
|
-
const disabled = attr(el, 'disabled') !== undefined;
|
|
499
|
-
const readOnly = attr(el, 'readonly') !== undefined;
|
|
500
|
-
// a field whose box the document draws takes its content box
|
|
501
|
-
const at = rect.bare ?? rect;
|
|
502
|
-
const frame: Style = {
|
|
503
|
-
position: 'absolute',
|
|
504
|
-
left: Math.round(at.x),
|
|
505
|
-
top: Math.round(at.y),
|
|
506
|
-
width: Math.round(at.width),
|
|
507
|
-
height: Math.round(at.height),
|
|
508
|
-
// core's `opacity` is CSS's: the widget faded as a group, and at 0 not
|
|
509
|
-
// drawn and still hit
|
|
510
|
-
...(rect.opacity !== undefined && { opacity: rect.opacity }),
|
|
511
|
-
};
|
|
512
|
-
const field = rect.bare ? bareField(rect.bare) : fieldChrome(look);
|
|
513
|
-
const report = (value: string | boolean): void => {
|
|
514
|
-
onChange?.(el, value);
|
|
515
|
-
touch();
|
|
516
|
-
};
|
|
517
|
-
// A text edit does NOT touch: the value lives in the widget and is echoed
|
|
518
|
-
// into the DOM attribute, and neither changes any box — while `touch()`
|
|
519
|
-
// would re-run the cascade and relayout the whole document *per
|
|
520
|
-
// keystroke*. This also matches HTML's own semantics: typing updates the
|
|
521
|
-
// value property, not the attribute selectors match against. The
|
|
522
|
-
// checkables keep the full touch, because `[checked]` is a selector
|
|
523
|
-
// documents really use.
|
|
524
|
-
const reportText = (value: string): void => {
|
|
525
|
-
onChange?.(el, value);
|
|
526
|
-
};
|
|
527
|
-
|
|
528
|
-
let widget: ReactNode;
|
|
529
|
-
switch (rect.kind) {
|
|
530
|
-
case 'checkbox':
|
|
531
|
-
widget = h(Checkbox, {
|
|
532
|
-
checked: attr(el, 'checked') !== undefined,
|
|
533
|
-
disabled,
|
|
534
|
-
// the element's whole box takes the press, as it does in a browser:
|
|
535
|
-
// a page that sizes one over its label, invisible, means the label
|
|
536
|
-
style: { width: '100%', height: '100%' },
|
|
537
|
-
onChange: (ev) => {
|
|
538
|
-
const next = ev.value;
|
|
539
|
-
if (next) el.attribs.checked = '';
|
|
540
|
-
else delete el.attribs.checked;
|
|
541
|
-
report(next);
|
|
542
|
-
},
|
|
543
|
-
});
|
|
544
|
-
break;
|
|
545
|
-
case 'radio': {
|
|
546
|
-
// Core's radio is a group member and HTML's is a free-standing input
|
|
547
|
-
// that happens to share a `name`. Each one is therefore its own
|
|
548
|
-
// one-member `RadioGroup`, and the exclusivity that makes it a group
|
|
549
|
-
// is done where HTML actually keeps it: in the DOM.
|
|
550
|
-
const value = attr(el, 'value') ?? 'on';
|
|
551
|
-
widget = h(
|
|
552
|
-
RadioGroup,
|
|
553
|
-
{
|
|
554
|
-
value: attr(el, 'checked') !== undefined ? value : undefined,
|
|
555
|
-
onChange: () => {
|
|
556
|
-
clearRadioGroup(el);
|
|
557
|
-
el.attribs.checked = '';
|
|
558
|
-
report(value);
|
|
559
|
-
},
|
|
560
|
-
},
|
|
561
|
-
h(Radio, { key: 'r', value, disabled }),
|
|
562
|
-
);
|
|
563
|
-
break;
|
|
564
|
-
}
|
|
565
|
-
case 'button':
|
|
566
|
-
widget = h(Button, {
|
|
567
|
-
label: buttonLabel(el),
|
|
568
|
-
disabled,
|
|
569
|
-
style: { width: '100%', height: '100%' },
|
|
570
|
-
onPress: () => report(attr(el, 'value') ?? ''),
|
|
571
|
-
});
|
|
572
|
-
break;
|
|
573
|
-
case 'select': {
|
|
574
|
-
const options = optionsOf(el);
|
|
575
|
-
widget = h(Select, {
|
|
576
|
-
options: options.map((o) => ({ value: o.value, label: o.label })),
|
|
577
|
-
value: selectedOption(el) ?? undefined,
|
|
578
|
-
disabled,
|
|
579
|
-
style: rect.bare
|
|
580
|
-
? [BARE_TRIGGER, { width: '100%', height: '100%' }]
|
|
581
|
-
: { width: '100%', height: '100%' },
|
|
582
|
-
// the slots choose the drawn trigger on every backend, and put the
|
|
583
|
-
// caption and the arrow in the page's ink
|
|
584
|
-
...(rect.bare && {
|
|
585
|
-
labelStyle: {
|
|
586
|
-
color: rect.bare.color,
|
|
587
|
-
fontFamily: rect.bare.fontFamily,
|
|
588
|
-
fontSize: rect.bare.fontSize,
|
|
589
|
-
},
|
|
590
|
-
chevronStyle: rect.bare.chevron
|
|
591
|
-
? { color: rect.bare.color }
|
|
592
|
-
: { display: 'none' },
|
|
593
|
-
}),
|
|
594
|
-
onChange: (ev) => {
|
|
595
|
-
const next = String(ev.value ?? '');
|
|
596
|
-
setSelectedOption(el, next);
|
|
597
|
-
report(next);
|
|
598
|
-
},
|
|
599
|
-
});
|
|
600
|
-
break;
|
|
601
|
-
}
|
|
602
|
-
case 'textarea':
|
|
603
|
-
// Uncontrolled on purpose: the widget owns the live text the way a
|
|
604
|
-
// browser's does, and a re-render from any other cause remounts it
|
|
605
|
-
// with whatever was last echoed into the DOM.
|
|
606
|
-
widget = hx('textarea', {
|
|
607
|
-
defaultValue: textareaValue(el),
|
|
608
|
-
style: [field, { width: '100%', height: '100%' }],
|
|
609
|
-
onChange: readOnly ? undefined : (ev) => reportText(ev.value),
|
|
610
|
-
});
|
|
611
|
-
break;
|
|
612
|
-
case 'input': {
|
|
613
|
-
const type = (attr(el, 'type') ?? 'text').toLowerCase();
|
|
614
|
-
widget = hx('textinput', {
|
|
615
|
-
defaultValue: attr(el, 'value') ?? '',
|
|
616
|
-
placeholder: attr(el, 'placeholder'),
|
|
617
|
-
// Core's word for a password field: nothing in it reaches a
|
|
618
|
-
// selection, PRIMARY included.
|
|
619
|
-
sensitive: type === 'password',
|
|
620
|
-
style: [field, { width: '100%', height: '100%' }],
|
|
621
|
-
onChange: readOnly
|
|
622
|
-
? undefined
|
|
623
|
-
: (ev) => {
|
|
624
|
-
el.attribs.value = ev.value;
|
|
625
|
-
reportText(ev.value);
|
|
626
|
-
},
|
|
627
|
-
});
|
|
628
|
-
break;
|
|
629
|
-
}
|
|
630
|
-
default:
|
|
631
|
-
return null;
|
|
632
|
-
}
|
|
633
|
-
return hx('box', { key, style: frame, selectable: false }, widget);
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
/**
|
|
637
|
-
* The chrome a text field needs.
|
|
638
|
-
*
|
|
639
|
-
* `<textinput>` and `<textarea>` are core *elements* rather than components,
|
|
640
|
-
* so they draw no frame of their own — an application supplies one, which is
|
|
641
|
-
* why core's own `<Button>` and `<Select>` are components and these are not.
|
|
642
|
-
* The values are the palette's, so a field in a document and a `<Select>`
|
|
643
|
-
* beside it are the same height with the same corner and the same edge.
|
|
644
|
-
*/
|
|
645
|
-
function fieldChrome(look: RootLook): Style {
|
|
646
|
-
return {
|
|
647
|
-
backgroundColor: look.surface,
|
|
648
|
-
borderWidth: look.controlBorder,
|
|
649
|
-
borderColor: look.borderColor,
|
|
650
|
-
borderRadius: look.controlRadius,
|
|
651
|
-
paddingLeft: 6,
|
|
652
|
-
paddingRight: 6,
|
|
653
|
-
color: look.color,
|
|
654
|
-
fontFamily: look.fontFamily,
|
|
655
|
-
fontSize: look.fontSize,
|
|
656
|
-
};
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
/**
|
|
660
|
-
* A text field whose box the author styled: the document draws the border
|
|
661
|
-
* and the background, so the widget draws neither, and its text is the
|
|
662
|
-
* element's colour and font, which the author chose to go on that
|
|
663
|
-
* background, rather than the theme's.
|
|
664
|
-
*/
|
|
665
|
-
function bareField(bare: BareField): Style {
|
|
666
|
-
return {
|
|
667
|
-
backgroundColor: 'transparent',
|
|
668
|
-
borderWidth: 0,
|
|
669
|
-
borderRadius: 0,
|
|
670
|
-
paddingLeft: 0,
|
|
671
|
-
paddingRight: 0,
|
|
672
|
-
color: bare.color,
|
|
673
|
-
fontFamily: bare.fontFamily,
|
|
674
|
-
fontSize: bare.fontSize,
|
|
675
|
-
};
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
/**
|
|
679
|
-
* The trigger of a `<select>` whose box the page styled: no frame, no fill
|
|
680
|
-
* and none of its own insets, and no wash under the pointer, since the box
|
|
681
|
-
* it would tint is the document's. Core's focus ring still marks it for the
|
|
682
|
-
* keyboard.
|
|
683
|
-
*/
|
|
684
|
-
const BARE_TRIGGER: Style = {
|
|
685
|
-
paddingTop: 0,
|
|
686
|
-
paddingBottom: 0,
|
|
687
|
-
paddingLeft: 0,
|
|
688
|
-
paddingRight: 0,
|
|
689
|
-
borderWidth: 0,
|
|
690
|
-
borderRadius: 0,
|
|
691
|
-
backgroundColor: 'transparent',
|
|
692
|
-
':hover': { backgroundColor: 'transparent' },
|
|
693
|
-
':active': { backgroundColor: 'transparent' },
|
|
694
|
-
};
|
|
695
|
-
|
|
696
|
-
function clearRadioGroup(el: Element): void {
|
|
697
|
-
const name = attr(el, 'name');
|
|
698
|
-
if (!name) return;
|
|
699
|
-
let root: Element | null = el;
|
|
700
|
-
while (root?.parent && root.parent.type === 'tag')
|
|
701
|
-
root = root.parent as Element;
|
|
702
|
-
if (!root) return;
|
|
703
|
-
const walk = (node: Element): void => {
|
|
704
|
-
for (const child of node.children) {
|
|
705
|
-
if (child.type !== 'tag') continue;
|
|
706
|
-
const tag = child.name.toLowerCase();
|
|
707
|
-
if (
|
|
708
|
-
tag === 'input' &&
|
|
709
|
-
(child.attribs.type ?? '').toLowerCase() === 'radio' &&
|
|
710
|
-
child.attribs.name === name
|
|
711
|
-
) {
|
|
712
|
-
delete child.attribs.checked;
|
|
713
|
-
}
|
|
714
|
-
walk(child);
|
|
715
|
-
}
|
|
716
|
-
};
|
|
717
|
-
walk(root);
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
function setSelectedOption(el: Element, value: string): void {
|
|
721
|
-
const walk = (node: Element): void => {
|
|
722
|
-
for (const child of node.children) {
|
|
723
|
-
if (child.type !== 'tag') continue;
|
|
724
|
-
if (child.name.toLowerCase() === 'option') {
|
|
725
|
-
let label = '';
|
|
726
|
-
for (const kid of child.children) {
|
|
727
|
-
if (kid.type === 'text') label += kid.data;
|
|
728
|
-
}
|
|
729
|
-
const own = child.attribs.value ?? label.trim();
|
|
730
|
-
if (own === value) child.attribs.selected = '';
|
|
731
|
-
else delete child.attribs.selected;
|
|
732
|
-
}
|
|
733
|
-
walk(child);
|
|
734
|
-
}
|
|
735
|
-
};
|
|
736
|
-
walk(el);
|
|
737
|
-
}
|
|
738
|
-
|
|
739
450
|
declare module 'react-x11/jsx-runtime' {
|
|
740
451
|
namespace JSX {
|
|
741
452
|
interface IntrinsicElements {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// A text engine that sets a variable web font at the weight CSS says.
|
|
2
|
+
//
|
|
3
|
+
// A style's weight picks a face, and where the face is a variable font it is
|
|
4
|
+
// also a place on the font's `wght` axis — the style's weight, clamped to
|
|
5
|
+
// the range the face's `@font-face` rule declares (CSS Fonts 4, 7.2). The
|
|
6
|
+
// rule is the document's and not the engine's, so which value that is is
|
|
7
|
+
// the document's to say (`WebFonts.wght`), and it is said with the run: a
|
|
8
|
+
// run's `variations`, which both engines take, and which wins over whatever
|
|
9
|
+
// an engine would have done with the weight by itself. ntk moves the axis
|
|
10
|
+
// to the style's weight, past a rule that declared less of it; CoreText,
|
|
11
|
+
// handed a face react-x11 registered, does not move it at all, and every
|
|
12
|
+
// weight of a page set in Geist was the regular on macOS.
|
|
13
|
+
//
|
|
14
|
+
// Only a run whose list leads with such a face is touched, and it is handed
|
|
15
|
+
// on as a copy: the run is the caller's, and the layouts kept from one pass
|
|
16
|
+
// to the next are found by its fields (`TextLayoutCache`), which this is
|
|
17
|
+
// below and adds nothing to. A run in any other family carries no
|
|
18
|
+
// `variations` — an axis value set on the system's own variable font would
|
|
19
|
+
// be this document's rule applied to a font it never declared.
|
|
20
|
+
import type { TextRun } from '../../richtext/index.js';
|
|
21
|
+
import type { FontsLike } from './inline.js';
|
|
22
|
+
import { withLayout } from './shaping.js';
|
|
23
|
+
|
|
24
|
+
/** What says where on its weight axis text is set: `WebFonts`. */
|
|
25
|
+
export interface WeightAxes {
|
|
26
|
+
/** Whether any family has an axis to set. While none has, the engine is
|
|
27
|
+
* called as it is. */
|
|
28
|
+
readonly variable: boolean;
|
|
29
|
+
wght(list: string, weight: number, italic: boolean): { wght: number } | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const NUMERIC: Record<string, number> = { normal: 400, bold: 700 };
|
|
33
|
+
|
|
34
|
+
/** `engine`, laying each run out at the weight axis value `axes` gives its
|
|
35
|
+
* family list, weight and slant. */
|
|
36
|
+
export function weightAxes(engine: FontsLike, axes: WeightAxes): FontsLike {
|
|
37
|
+
return withLayout(engine, (content, style, options) => {
|
|
38
|
+
if (!axes.variable || !Array.isArray(content)) {
|
|
39
|
+
return engine.layout(content, style, options);
|
|
40
|
+
}
|
|
41
|
+
let runs: TextRun[] | null = null;
|
|
42
|
+
for (let i = 0; i < content.length; i += 1) {
|
|
43
|
+
const run = content[i];
|
|
44
|
+
const family = run.family ?? style.family;
|
|
45
|
+
const weight = run.weight ?? style.weight ?? 400;
|
|
46
|
+
const axis =
|
|
47
|
+
typeof family === 'string'
|
|
48
|
+
? axes.wght(
|
|
49
|
+
family,
|
|
50
|
+
typeof weight === 'number'
|
|
51
|
+
? weight
|
|
52
|
+
: (NUMERIC[weight as string] ?? 400),
|
|
53
|
+
(run.style ?? style.style) === 'italic',
|
|
54
|
+
)
|
|
55
|
+
: null;
|
|
56
|
+
if (!axis) continue;
|
|
57
|
+
runs ??= content.slice();
|
|
58
|
+
runs[i] = { ...run, variations: axis } as TextRun;
|
|
59
|
+
}
|
|
60
|
+
return engine.layout(runs ?? content, style, options);
|
|
61
|
+
});
|
|
62
|
+
}
|