@solidrt/core 0.0.49 → 0.0.50
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/AGENTS.md +11 -0
- package/examples/README.md +1 -1
- package/examples/sound.tsx +23 -3
- package/jsx-runtime.d.ts +4 -0
- package/package.json +2 -2
- package/src/core.ts +69 -0
- package/src/gpu.ts +12 -8
- package/src/index.ts +3 -3
- package/src/renderer.ts +5 -3
- package/src/sound.ts +31 -3
- package/src/types.d.ts +62 -2
package/AGENTS.md
CHANGED
|
@@ -159,6 +159,17 @@ Peer deps @solidjs/signals and @solidjs/universal must match (currently
|
|
|
159
159
|
expression container preserves it - `<d-text>{"one two"}</d-text>` - and
|
|
160
160
|
`\n` inside one produces a hard line break.
|
|
161
161
|
|
|
162
|
+
- Rich text: `<span>` inside `<text>` restyles a run (`color`, `fontFamily`,
|
|
163
|
+
`fontSize`, `fontWeight`, `fontStyle`, `lineHeight`); spans nest and
|
|
164
|
+
inherit inward from the `<text>`. Never lay a paragraph out word by word in
|
|
165
|
+
a wrapping row to mix styles - one `<text>` with spans wraps as a whole.
|
|
166
|
+
A span is content, not a box (no layout or `d-` form, no size, no
|
|
167
|
+
bounding box; it takes its parent's form). A `<span>` takes
|
|
168
|
+
pointer handlers (a link is a span, hit per line it spans), and any other
|
|
169
|
+
element child of `<text>` (`<view>`, `<texture>`, `<path>`, ...) is an
|
|
170
|
+
inline atom flowing with the words as one unbreakable box on the baseline;
|
|
171
|
+
give it margins for spacing, since JSX trims the whitespace around it.
|
|
172
|
+
|
|
162
173
|
- Events: there is NO `onClick`/`onPress`. A "button" is a `<view>`/`<rect>`
|
|
163
174
|
with `onPointerDown`. Handlers: onPointerDown/Up/Move/Enter/Leave, onWheel,
|
|
164
175
|
onKeyDown/Up, onTextInput, onFocus/onBlur. Text entry: focus a node with an
|
package/examples/README.md
CHANGED
|
@@ -48,7 +48,7 @@ for the element/prop model see `@solidrt/core/AGENTS.md`.
|
|
|
48
48
|
- `window-shader-history.tsx` - the window shader's frame history: `previous` binds last frame as uPrevious, drawn as a one-frame motion echo behind an orbiting square; click toggles the echo term.
|
|
49
49
|
|
|
50
50
|
## Sound
|
|
51
|
-
- `sound.tsx` - `createSound`: decode a clip once from bytes (here a binary import), replay cheaply; `overlap` stacking vs single-voice, `playing()` signal, release on unmount. Points to `createSoundStream` for long tracks streamed from a path.
|
|
51
|
+
- `sound.tsx` - `createSound`: decode a clip once from bytes (here a binary import), replay cheaply; `overlap` stacking vs single-voice, `playing()` signal, release on unmount. `createPcmSound` for a synthesised clip from raw samples (a generated sine sweep). Points to `createSoundStream` for long tracks streamed from a path.
|
|
52
52
|
|
|
53
53
|
## Vector graphics
|
|
54
54
|
- `parse-svg.tsx` - `parseSvg` turns a whole SVG *document string* (not HTML/JSX children) into plain draw data mapped to `<d-path>` inside a `viewBox`-fitted view; per-shape hover highlighting shows the payoff (exact-outline hit testing, recolor without re-parse), plus a `currentColor` icon recolored via the `color` option. This is how to use existing icon libraries (Lucide, Heroicons, etc.) - hand their SVG source to `parseSvg`.
|
package/examples/sound.tsx
CHANGED
|
@@ -14,14 +14,32 @@
|
|
|
14
14
|
// selection tick. playing() is a signal: true from play() until stop() (it
|
|
15
15
|
// does not flip back when a clip ends naturally).
|
|
16
16
|
//
|
|
17
|
+
// Sounds you can compute need no bytes at all: createPcmSound takes raw
|
|
18
|
+
// samples (here a 60 ms sine sweep in an Int16Array) and gives the same handle
|
|
19
|
+
// - generate it, do not ship it. Same reactive ownership as createSound.
|
|
20
|
+
//
|
|
17
21
|
// For a long track (music, ambience) do not load bytes at all: pass a file
|
|
18
22
|
// path to createSoundStream from the same module, which decodes from disk on
|
|
19
23
|
// demand and stays off the heap. Same play()/stop()/playing() surface,
|
|
20
24
|
// always single-voice.
|
|
21
25
|
import { render } from "@solidrt/core"
|
|
22
|
-
import { createSound } from "@solidrt/core/sound"
|
|
26
|
+
import { createPcmSound, createSound } from "@solidrt/core/sound"
|
|
23
27
|
import blipBytes from "./blip.wav" with { type: "binary" }
|
|
24
28
|
|
|
29
|
+
const RATE = 44100
|
|
30
|
+
// A short sweep from 880 Hz down to 440 Hz with a linear fade-out.
|
|
31
|
+
function sweep(): Int16Array {
|
|
32
|
+
let n = Math.round(RATE * 0.06)
|
|
33
|
+
let out = new Int16Array(n)
|
|
34
|
+
let phase = 0
|
|
35
|
+
for (let i = 0; i < n; i++) {
|
|
36
|
+
let t = i / n
|
|
37
|
+
phase += (2 * Math.PI * (880 - 440 * t)) / RATE
|
|
38
|
+
out[i] = Math.round(Math.sin(phase) * (1 - t) * 0.6 * 32767)
|
|
39
|
+
}
|
|
40
|
+
return out
|
|
41
|
+
}
|
|
42
|
+
|
|
25
43
|
function Button(props: { label: string; onTap: () => void }) {
|
|
26
44
|
return (
|
|
27
45
|
<view onPointerDown={props.onTap} padding={12} clipRadius={8}>
|
|
@@ -34,13 +52,15 @@ function Button(props: { label: string; onTap: () => void }) {
|
|
|
34
52
|
function App() {
|
|
35
53
|
let blip = createSound(blipBytes, { gain: 0.8 })
|
|
36
54
|
let tick = createSound(blipBytes, { overlap: false })
|
|
55
|
+
let synth = createPcmSound(sweep(), RATE)
|
|
37
56
|
|
|
38
57
|
return (
|
|
39
58
|
<window padding={20} gap={8} alignItems="flex-start">
|
|
40
59
|
<Button label="Blip (tap fast to stack voices)" onTap={() => blip.play()} />
|
|
41
60
|
<Button label="Tick (overlap: false, restarts)" onTap={() => tick.play()} />
|
|
42
|
-
<Button label="
|
|
43
|
-
<
|
|
61
|
+
<Button label="Sweep (createPcmSound, generated)" onTap={() => synth.play()} />
|
|
62
|
+
<Button label="Stop" onTap={() => { blip.stop(); tick.stop(); synth.stop() }} />
|
|
63
|
+
<text color="#888">{blip.playing() || tick.playing() || synth.playing() ? "playing" : "silent"}</text>
|
|
44
64
|
</window>
|
|
45
65
|
)
|
|
46
66
|
}
|
package/jsx-runtime.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
ViewProps,
|
|
8
8
|
ViewOwnProps,
|
|
9
9
|
TextProps,
|
|
10
|
+
SpanProps,
|
|
10
11
|
TextureProps,
|
|
11
12
|
LayoutProps,
|
|
12
13
|
PositionProps,
|
|
@@ -54,5 +55,8 @@ export namespace JSX {
|
|
|
54
55
|
"d-path": PathProps & PositionProps & ElementRef
|
|
55
56
|
"d-texture": TextureProps & GeometryProps & ElementRef
|
|
56
57
|
"d-text": TextProps & TextGeometryProps & ElementRef
|
|
58
|
+
// A styled run inside <text>/<d-text>; never has a layout box, so there is
|
|
59
|
+
// no d- form.
|
|
60
|
+
span: SpanProps & ElementRef
|
|
57
61
|
}
|
|
58
62
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.50",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Antoine van Wel",
|
|
6
6
|
"type": "module",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"colord": "^2.9.3"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@solidrt/flux-types": "0.0.
|
|
30
|
+
"@solidrt/flux-types": "0.0.50"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"@solidjs/signals": "2.0.0-rc.0",
|
package/src/core.ts
CHANGED
|
@@ -331,4 +331,73 @@ export function getBoundingBoxViewport(node: { id: number }): BoundingBox | null
|
|
|
331
331
|
*/
|
|
332
332
|
export function measureText(text: string, options?: tree.MeasureTextOptions): { width: number, height: number } {
|
|
333
333
|
return tree.measureText(text, options)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Segments `text` into wrap units (words with their trailing whitespace) and
|
|
338
|
+
* shapes each in the given font, once, for laying lines out in app code with
|
|
339
|
+
* layoutNextLine or arithmetic of your own over `units`. For the non-standard
|
|
340
|
+
* case (text into a shape, around a moving obstacle, handed between columns,
|
|
341
|
+
* fitted by size); regular text of any length is a <text>.
|
|
342
|
+
*/
|
|
343
|
+
export function prepareText(text: string, options?: tree.MeasureTextOptions): tree.PreparedText {
|
|
344
|
+
return tree.prepareText(text, options)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** One laid-out line from layoutNextLine. */
|
|
348
|
+
export type TextLine = {
|
|
349
|
+
/** Unit range [from, to) into prepared.units. */
|
|
350
|
+
from: number
|
|
351
|
+
to: number
|
|
352
|
+
/** Character range into prepared.text: `text.slice(start, end)` is the line's text (break characters included). */
|
|
353
|
+
start: number
|
|
354
|
+
end: number
|
|
355
|
+
/** Ink width: the units' advances plus the last unit's width, without its trailing whitespace. */
|
|
356
|
+
width: number
|
|
357
|
+
/** Tallest ascent plus tallest descent on the line. */
|
|
358
|
+
height: number
|
|
359
|
+
ascent: number
|
|
360
|
+
/** The line ended at a hard break rather than by running out of width. */
|
|
361
|
+
hardBreak: boolean
|
|
362
|
+
/** Where the next line starts; equal to `to`. */
|
|
363
|
+
cursor: number
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The next line of `prepared` from unit `cursor` that fits `width`, or null
|
|
368
|
+
* when the text is used up. Greedy: units go on the line while the pen plus
|
|
369
|
+
* the unit's ink stays within `width`; a hard break ends the line; a unit
|
|
370
|
+
* wider than `width` on its own goes on the line whole and overflows. Draw a
|
|
371
|
+
* line as `<d-text x y w={line.width + 1}>{prepared.text.slice(line.start, line.end)}</d-text>`
|
|
372
|
+
* with the same font options; its words are already shaped, so that is
|
|
373
|
+
* cheap. Floats, balancing and ellipsis are <text> features, not this.
|
|
374
|
+
*/
|
|
375
|
+
export function layoutNextLine(prepared: tree.PreparedText, cursor: number, width: number): TextLine | null {
|
|
376
|
+
let units = prepared.units
|
|
377
|
+
if (cursor >= units.length) return null
|
|
378
|
+
let pen = 0
|
|
379
|
+
let ascent = 0
|
|
380
|
+
let descent = 0
|
|
381
|
+
let i = cursor
|
|
382
|
+
while (i < units.length) {
|
|
383
|
+
let unit = units[i]!
|
|
384
|
+
if (i > cursor && pen + unit.width > width) break
|
|
385
|
+
pen += unit.advance
|
|
386
|
+
if (unit.ascent > ascent) ascent = unit.ascent
|
|
387
|
+
if (unit.descent > descent) descent = unit.descent
|
|
388
|
+
i++
|
|
389
|
+
if (unit.hardBreak) break
|
|
390
|
+
}
|
|
391
|
+
let last = units[i - 1]!
|
|
392
|
+
return {
|
|
393
|
+
from: cursor,
|
|
394
|
+
to: i,
|
|
395
|
+
start: units[cursor]!.start,
|
|
396
|
+
end: last.end,
|
|
397
|
+
width: pen - last.advance + last.width,
|
|
398
|
+
height: ascent + descent,
|
|
399
|
+
ascent,
|
|
400
|
+
hardBreak: last.hardBreak,
|
|
401
|
+
cursor: i,
|
|
402
|
+
}
|
|
334
403
|
}
|
package/src/gpu.ts
CHANGED
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
// `<texture>` elements and set their `blendMode` (e.g. `blendMode="plus"` for
|
|
22
22
|
// an additive pass over a base pass) instead of writing a pass that samples
|
|
23
23
|
// both. WITHIN one pipeline draw, `blend: "add"` accumulates overlapping
|
|
24
|
-
// geometry additively
|
|
25
|
-
//
|
|
24
|
+
// geometry additively and `blend: "multiply"` scales it (both
|
|
25
|
+
// order-independent, no sorting); `blend: "alpha"` composites over in
|
|
26
|
+
// draw-list order (order-dependent: the app or a scene layer sorts); anything
|
|
27
|
+
// else draws with GL blending disabled and overwrites.
|
|
26
28
|
//
|
|
27
29
|
// The pixel contract. Three facts hold for every texture and target:
|
|
28
30
|
//
|
|
@@ -40,8 +42,8 @@
|
|
|
40
42
|
// default transparent black needs no thought.
|
|
41
43
|
// - Values are non-linear RGBA8, with no color-space concept. Every texture
|
|
42
44
|
// and target holds 8-bit RGBA UNORM exactly as written; nothing converts to
|
|
43
|
-
// or from linear light. `filter: "linear"` averages and `blend
|
|
44
|
-
//
|
|
45
|
+
// or from linear light. `filter: "linear"` averages and the `blend` modes
|
|
46
|
+
// accumulate non-linear values - the usual approximation, stated so
|
|
45
47
|
// shaders written today stay correct if a format vocabulary arrives.
|
|
46
48
|
|
|
47
49
|
import { createEffect, createSignal, getOwner, onCleanup, untrack } from "@solidjs/signals"
|
|
@@ -512,10 +514,12 @@ function toUint8(data: ArrayBuffer | ArrayBufferView): Uint8Array {
|
|
|
512
514
|
* `opts.depth` attaches a private depth buffer (cleared + tested per render);
|
|
513
515
|
* `opts.depthWrite: false` (requires depth) keeps the test but stops the
|
|
514
516
|
* draw from writing depth. `opts.blend: "add"` makes the draw accumulate
|
|
515
|
-
* overlapping geometry additively
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
* the
|
|
517
|
+
* overlapping geometry additively and `"multiply"` makes it scale (darken)
|
|
518
|
+
* what is already there, both order-independent (no sorting) instead of
|
|
519
|
+
* overwriting; `"alpha"` composites over in draw-list order (premultiplied
|
|
520
|
+
* output, back-to-front is the caller's job). A depth-tested blended pass is
|
|
521
|
+
* `{ depth: true, blend: "add", depthWrite: false }` - each option only does
|
|
522
|
+
* what it says, neither implies the other. The draw range (`firstVertex`, `vertexCount`, `instanceCount` -
|
|
519
523
|
* see DrawRange) defaults to the whole buffer drawn once and can be changed
|
|
520
524
|
* later with `setDraw`; `instanceCount` is the standard answer to particles
|
|
521
525
|
* and repeated meshes, N copies of the range told apart by `gl_InstanceID`
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * from "./renderer"
|
|
2
|
-
export { setFocus, focusedNode, startTextInput, textInputActive, getFocusables, measureText, getBoundingBox, getBoundingBoxViewport, onPointerMove } from "./core"
|
|
3
|
-
export type { BoundingBox, GlobalPointerEvent } from "./core"
|
|
2
|
+
export { setFocus, focusedNode, startTextInput, textInputActive, getFocusables, measureText, prepareText, layoutNextLine, getBoundingBox, getBoundingBoxViewport, onPointerMove } from "./core"
|
|
3
|
+
export type { BoundingBox, GlobalPointerEvent, TextLine } from "./core"
|
|
4
4
|
export { parseColor, mixColors, brightness, createLinearGradient, createRadialGradient } from "./color"
|
|
5
5
|
export type { Gradient, GradientStop } from "./color"
|
|
6
6
|
export { onFrame, onLayout, onResize, onWindowFocus, onWindowBlur, onBack, exit } from "./window"
|
|
@@ -49,7 +49,7 @@ export type {
|
|
|
49
49
|
Color,
|
|
50
50
|
Pct,
|
|
51
51
|
} from "./types"
|
|
52
|
-
export type { MeasureTextOptions } from "flux:rendertree"
|
|
52
|
+
export type { MeasureTextOptions, PreparedText, TextUnit } from "flux:rendertree"
|
|
53
53
|
|
|
54
54
|
// A percentage value for dimensional props (e.g. transformOrigin): `pct(50)` is
|
|
55
55
|
// half the element box. Keeps percentages a first-class branded value rather
|
package/src/renderer.ts
CHANGED
|
@@ -233,10 +233,12 @@ export let {
|
|
|
233
233
|
return proxy
|
|
234
234
|
},
|
|
235
235
|
|
|
236
|
+
// A string child of <text> or <span>: a run of its parent's content, with
|
|
237
|
+
// no element form of its own (the DOM's "#text" node name).
|
|
236
238
|
createTextNode: (value: string): ProxyNode => {
|
|
237
|
-
let proxy = createProxyNode("
|
|
239
|
+
let proxy = createProxyNode("#text")
|
|
238
240
|
// console.debug("[srt] createTextNode", proxy.id, value)
|
|
239
|
-
tree.createNode(proxy.id, "
|
|
241
|
+
tree.createNode(proxy.id, "#text")
|
|
240
242
|
tree.setProperty(proxy.id, "text", "" + value)
|
|
241
243
|
return proxy
|
|
242
244
|
},
|
|
@@ -246,7 +248,7 @@ export let {
|
|
|
246
248
|
tree.setProperty(node.id, "text", "" + value)
|
|
247
249
|
},
|
|
248
250
|
|
|
249
|
-
isTextNode: (node: ProxyNode): boolean => node?.elementType === "
|
|
251
|
+
isTextNode: (node: ProxyNode): boolean => node?.elementType === "#text",
|
|
250
252
|
setProperty: <T>(node: ProxyNode, name: string, value: T): void => {
|
|
251
253
|
// console.debug("[srt] setProperty", node.id, name, value)
|
|
252
254
|
applyProp(node, name, value)
|
package/src/sound.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
// Sound playback, reactive (SolidJS) layer. `createSound` decodes an encoded
|
|
2
2
|
// clip (Ogg/Vorbis or WAV) once and owns its lifecycle: the decoded clip is
|
|
3
3
|
// released, and any playing voices stopped, when the reactive owner is disposed.
|
|
4
|
-
// Each play() is cheap (no re-decode). `
|
|
5
|
-
//
|
|
4
|
+
// Each play() is cheap (no re-decode). `createPcmSound` is the same over raw
|
|
5
|
+
// samples the app generated itself; `createSoundStream` reads a large track
|
|
6
|
+
// from a file path on demand instead of decoding it into memory.
|
|
6
7
|
//
|
|
7
8
|
// The imperative primitive lives in the `flux:audio` module; import
|
|
8
9
|
// { play, load, loadPcm, stream } from "flux:audio" for non-reactive use.
|
|
9
10
|
|
|
10
11
|
import { createSignal, onCleanup } from "@solidjs/signals"
|
|
11
|
-
import { load, stream } from "flux:audio"
|
|
12
|
+
import { load, loadPcm, stream } from "flux:audio"
|
|
12
13
|
import { file } from "flux:fs"
|
|
13
14
|
|
|
14
15
|
type FluxFile = ReturnType<typeof file>
|
|
@@ -34,6 +35,12 @@ export type SoundOptions = {
|
|
|
34
35
|
overlap?: boolean
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
/** Options for a PCM sound: `SoundOptions` plus the channel count. */
|
|
39
|
+
export type PcmSoundOptions = SoundOptions & {
|
|
40
|
+
/** Channel count, interleaved samples when 2. Defaults to 1 (mono). */
|
|
41
|
+
channels?: 1 | 2
|
|
42
|
+
}
|
|
43
|
+
|
|
37
44
|
/** Options for a streamed sound. Streams are always single-voice. */
|
|
38
45
|
export type SoundStreamOptions = {
|
|
39
46
|
/** Repeat the track until stopped. Defaults to false. */
|
|
@@ -140,6 +147,27 @@ export function createSound(source: Uint8Array, options: SoundOptions = {}): Sou
|
|
|
140
147
|
})
|
|
141
148
|
}
|
|
142
149
|
|
|
150
|
+
/**
|
|
151
|
+
* A sound over raw PCM samples the app generated itself - no decoding, no
|
|
152
|
+
* container. The typed array is the sample format (Uint8Array = unsigned
|
|
153
|
+
* 8-bit, Int16Array = signed 16-bit, Float32Array = 32-bit float), interleaved
|
|
154
|
+
* when `channels` is 2. Same handle and lifecycle as createSound; on a box with
|
|
155
|
+
* no audio device the clip fails to load, so `error()` is set and play() is a
|
|
156
|
+
* no-op, exactly like createSound there. For imperative use, call loadPcm()
|
|
157
|
+
* from "flux:audio".
|
|
158
|
+
*/
|
|
159
|
+
export function createPcmSound(
|
|
160
|
+
samples: Uint8Array | Int16Array | Float32Array,
|
|
161
|
+
sampleRate: number,
|
|
162
|
+
options: PcmSoundOptions = {},
|
|
163
|
+
): Sound {
|
|
164
|
+
return reactiveSound(() => loadPcm(samples, sampleRate, { channels: options.channels }), options.overlap ?? true, {
|
|
165
|
+
loop: options.loop,
|
|
166
|
+
gain: options.gain,
|
|
167
|
+
pan: options.pan,
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
143
171
|
/**
|
|
144
172
|
* Streams a large track, decoding on demand instead of loading it into memory.
|
|
145
173
|
* Single-voice: each play() restarts it. Pass a path (resolved like flux:fs,
|
package/src/types.d.ts
CHANGED
|
@@ -131,6 +131,21 @@ export interface LayoutProps extends FlexboxProps, GridProps {
|
|
|
131
131
|
overflow?: "visible" | "clip" | "hidden" | "scroll"
|
|
132
132
|
overflowX?: "visible" | "clip" | "hidden" | "scroll"
|
|
133
133
|
overflowY?: "visible" | "clip" | "hidden" | "scroll"
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* As an inline atom (an element child of <text>): leave the flow and sit
|
|
137
|
+
* against that side of the text, at the top of the line where the atom
|
|
138
|
+
* occurs; the lines it overlaps wrap around its margin box. Same-side
|
|
139
|
+
* floats overlapping vertically sit beside each other. The text's height
|
|
140
|
+
* includes the float. Meaningless outside a <text>.
|
|
141
|
+
*/
|
|
142
|
+
float?: "left" | "right"
|
|
143
|
+
/**
|
|
144
|
+
* As an inline atom: start a new line below the text's earlier floats on
|
|
145
|
+
* that side (a floated atom goes below them instead of beside). An empty
|
|
146
|
+
* `<view clear="both" />` is the section break after an image.
|
|
147
|
+
*/
|
|
148
|
+
clear?: "left" | "right" | "both"
|
|
134
149
|
}
|
|
135
150
|
|
|
136
151
|
/** Colors are CSS color strings, parsed to a packed u32 by `parseColor`. */
|
|
@@ -545,8 +560,12 @@ export interface PathProps extends PaintProps, PointerProps {
|
|
|
545
560
|
fillRule?: "nonzero" | "evenodd"
|
|
546
561
|
}
|
|
547
562
|
|
|
548
|
-
|
|
549
|
-
|
|
563
|
+
/**
|
|
564
|
+
* Per-run text style: the paragraph default on <text>, an override on <span>.
|
|
565
|
+
* The cascade is intra-paragraph only: a span inherits from its enclosing
|
|
566
|
+
* span, then from the <text>; nothing inherits across the tree.
|
|
567
|
+
*/
|
|
568
|
+
export interface TextRunProps {
|
|
550
569
|
fontFamily?: "sans" | "serif" | "mono" | (string & {})
|
|
551
570
|
fontSize?: number
|
|
552
571
|
/**
|
|
@@ -557,8 +576,49 @@ export interface TextProps extends PaintProps, PointerProps {
|
|
|
557
576
|
lineHeight?: number
|
|
558
577
|
fontStyle?: "normal" | "italic"
|
|
559
578
|
fontWeight?: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* A styled run inside <text>. Inline text only: children are text and other
|
|
583
|
+
* spans. `color` takes what a text's color takes (solid or gradient).
|
|
584
|
+
* Pointer handlers fire for the boxes of the run's own text on each line it
|
|
585
|
+
* spans and bubble to the enclosing spans and text.
|
|
586
|
+
*/
|
|
587
|
+
export interface SpanProps extends TextRunProps, PointerProps {
|
|
588
|
+
children?: Children
|
|
589
|
+
color?: Color | Gradient
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export interface TextProps extends PaintProps, PointerProps, TextRunProps {
|
|
593
|
+
children?: Children
|
|
560
594
|
textAlign?: "left" | "right" | "center" | "justify"
|
|
561
595
|
maxLines?: number
|
|
596
|
+
/**
|
|
597
|
+
* What happens to text cut off by maxLines: "clip" (default), "ellipsis"
|
|
598
|
+
* (a U+2026 at the end of the last line), or any other string to use as
|
|
599
|
+
* the ellipsis. Drawn in the paragraph's default style.
|
|
600
|
+
*/
|
|
601
|
+
textOverflow?: "clip" | "ellipsis" | (string & {})
|
|
602
|
+
/**
|
|
603
|
+
* A word (wrap unit) wider than the line: "anywhere" (default) splits it
|
|
604
|
+
* at grapheme boundaries so it stays inside the box, "normal" keeps it
|
|
605
|
+
* whole and lets it overflow (CSS's default).
|
|
606
|
+
*/
|
|
607
|
+
overflowWrap?: "normal" | "anywhere"
|
|
608
|
+
/**
|
|
609
|
+
* First-line indent in pixels. Negative hangs: the first line starts at 0
|
|
610
|
+
* and every following line is indented by the magnitude. A hard break does
|
|
611
|
+
* not start a new first line.
|
|
612
|
+
*/
|
|
613
|
+
textIndent?: number
|
|
614
|
+
/**
|
|
615
|
+
* How lines are chosen beyond greedy fitting (CSS text-wrap): "wrap"
|
|
616
|
+
* (default) is greedy; "balance" evens the line lengths while keeping the
|
|
617
|
+
* line count (headings, captions); "pretty" is greedy except that a lone
|
|
618
|
+
* word on the last line pulls one down from the line above. Neither
|
|
619
|
+
* applies once maxLines truncates.
|
|
620
|
+
*/
|
|
621
|
+
textWrap?: "wrap" | "balance" | "pretty"
|
|
562
622
|
}
|
|
563
623
|
|
|
564
624
|
/**
|