@solidrt/core 0.0.51 → 0.0.53
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 +105 -21
- package/README.md +1 -1
- package/docs/index.md +154 -0
- package/docs/reference/detached.md +85 -0
- package/docs/reference/drawing.md +95 -0
- package/docs/reference/elements.md +56 -0
- package/docs/reference/gpu.md +207 -0
- package/docs/reference/index.md +50 -0
- package/docs/reference/input.md +58 -0
- package/docs/reference/layout.md +44 -0
- package/docs/reference/shaders.md +46 -0
- package/docs/reference/text.md +46 -0
- package/docs/reference/transforms.md +35 -0
- package/docs/reference/types.md +34 -0
- package/examples/README.md +5 -3
- package/examples/gpu-pipeline.tsx +2 -2
- package/examples/line-points.tsx +145 -0
- package/examples/parse-svg.tsx +6 -6
- package/examples/responsive-grid.tsx +1 -1
- package/examples/scroll.tsx +2 -2
- package/examples/snapshot-texture.tsx +72 -0
- package/examples/{view-viewbox.tsx → view-design-size.tsx} +33 -15
- package/package.json +6 -5
- package/src/core.ts +19 -1
- package/src/gpu.ts +86 -34
- package/src/index.ts +8 -2
- package/src/logo.tsx +92 -0
- package/src/renderer.ts +189 -34
- package/src/runtime-modules.d.ts +4 -0
- package/src/scroll.ts +50 -14
- package/src/svg.ts +1 -1
- package/src/text-input.ts +0 -1
- package/src/types.d.ts +121 -29
- package/src/window.ts +52 -7
package/src/renderer.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { createRoot, onCleanup } from "@solidjs/signals"
|
|
1
|
+
import { createRoot, onCleanup, NotReadyError } from "@solidjs/signals"
|
|
2
2
|
import { createRenderer } from "@solidjs/universal"
|
|
3
|
+
import { createErrorBoundary } from "solid-js"
|
|
3
4
|
import type { Element } from "solid-js"
|
|
4
5
|
import * as tree from "flux:rendertree"
|
|
5
|
-
import { attachWindow } from "./window"
|
|
6
|
+
import { attachWindow, setWindowRoot } from "./window"
|
|
6
7
|
import { setEventHandler, setFocusable, setTextInputHints, cleanupNode, focusedNode, setFocus } from "./core"
|
|
7
8
|
|
|
8
9
|
export { getEventHandler } from "./core"
|
|
@@ -213,20 +214,7 @@ function applyProp<T>(node: ProxyNode, name: string, value: T): void {
|
|
|
213
214
|
setTreeProperty(node, name, value)
|
|
214
215
|
}
|
|
215
216
|
|
|
216
|
-
|
|
217
|
-
effect,
|
|
218
|
-
memo,
|
|
219
|
-
createComponent,
|
|
220
|
-
createElement,
|
|
221
|
-
createTextNode,
|
|
222
|
-
insertNode,
|
|
223
|
-
insert,
|
|
224
|
-
spread,
|
|
225
|
-
setProp,
|
|
226
|
-
mergeProps,
|
|
227
|
-
applyRef,
|
|
228
|
-
ref,
|
|
229
|
-
} = createRenderer<ProxyNode>({
|
|
217
|
+
let renderer = createRenderer<ProxyNode>({
|
|
230
218
|
createElement: (elementType: string, props?: Record<string, any>): ProxyNode => {
|
|
231
219
|
let proxy = createProxyNode(elementType)
|
|
232
220
|
|
|
@@ -235,11 +223,13 @@ export let {
|
|
|
235
223
|
if (elementType === "window") tree.createRoot(proxy.id)
|
|
236
224
|
else tree.createNode(proxy.id, elementType)
|
|
237
225
|
|
|
238
|
-
// The universal JSX template hands static props here as an object
|
|
239
|
-
//
|
|
226
|
+
// The universal JSX template hands static props here as an object. The
|
|
227
|
+
// compiler routes children/ref expressions through their own hooks, so
|
|
228
|
+
// those names only appear here in degenerate literal forms (children="hi",
|
|
229
|
+
// a bare ref) - let them flow to the tree so the unknown-property warning
|
|
230
|
+
// reports them instead of dropping them silently.
|
|
240
231
|
if (props) {
|
|
241
232
|
for (let name in props) {
|
|
242
|
-
if (name === "children" || name === "ref") continue
|
|
243
233
|
applyProp(proxy, name, props[name])
|
|
244
234
|
}
|
|
245
235
|
}
|
|
@@ -276,6 +266,14 @@ export let {
|
|
|
276
266
|
pendingDestroy.delete(node.id)
|
|
277
267
|
|
|
278
268
|
if (parent) {
|
|
269
|
+
// console.debug("[srt] insertNode", parent.id, node.id, anchor?.id ?? "")
|
|
270
|
+
|
|
271
|
+
// Native first: the tree refuses a laid-out element under a d-* parent
|
|
272
|
+
// (it throws, naming both tags), and the mirror must not record a child
|
|
273
|
+
// the tree does not have.
|
|
274
|
+
if (anchor) tree.insertNode(parent.id, node.id, anchor.id)
|
|
275
|
+
else tree.insertNode(parent.id, node.id)
|
|
276
|
+
|
|
279
277
|
node.parent = parent
|
|
280
278
|
|
|
281
279
|
if (!anchor) {
|
|
@@ -288,11 +286,6 @@ export let {
|
|
|
288
286
|
parent.children.splice(index, 0, node)
|
|
289
287
|
}
|
|
290
288
|
}
|
|
291
|
-
|
|
292
|
-
// console.debug("[srt] insertNode", parent.id, node.id, anchor?.id ?? "")
|
|
293
|
-
|
|
294
|
-
if (anchor) tree.insertNode(parent.id, node.id, anchor.id)
|
|
295
|
-
else tree.insertNode(parent.id, node.id)
|
|
296
289
|
}
|
|
297
290
|
},
|
|
298
291
|
|
|
@@ -309,28 +302,185 @@ export let {
|
|
|
309
302
|
},
|
|
310
303
|
})
|
|
311
304
|
|
|
312
|
-
|
|
313
|
-
|
|
305
|
+
export let { memo, createComponent, createElement, createTextNode, insertNode, spread, setProp, mergeProps, applyRef, ref } =
|
|
306
|
+
renderer
|
|
307
|
+
let { effect: rawEffect, insert: rawInsert } = renderer
|
|
308
|
+
|
|
309
|
+
// ------ Per-node error containment --------
|
|
310
|
+
//
|
|
311
|
+
// Every reactive write into the tree goes through two exports the compiled
|
|
312
|
+
// JSX calls: `effect` (an element's dynamic props) and `insert` (a child
|
|
313
|
+
// expression). An error thrown while computing either is contained right
|
|
314
|
+
// there: the props or children keep their last good value, the effect stays
|
|
315
|
+
// subscribed (the throwing read is tracked, so fixing it recomputes and the
|
|
316
|
+
// node recovers on its own), and the rest of the app keeps running. Without
|
|
317
|
+
// this one unclaimed error halts the whole reactive system. A NotReadyError
|
|
318
|
+
// is not an error but a pending async read on its way to the nearest
|
|
319
|
+
// <Loading>, so it passes through. Reported once per site until it recovers,
|
|
320
|
+
// not once per run. What escapes these two (a user createEffect that throws)
|
|
321
|
+
// still reaches render()'s root boundary.
|
|
322
|
+
const SKIP = Symbol("skip")
|
|
323
|
+
|
|
324
|
+
function guard<T>(fn: (prev?: T) => T, describe: () => string, nested: boolean, empty: T): (prev?: T) => T {
|
|
325
|
+
let last = empty
|
|
326
|
+
let failing = false
|
|
327
|
+
return (prev?: T) => {
|
|
328
|
+
try {
|
|
329
|
+
let value = fn(prev === SKIP ? undefined : prev)
|
|
330
|
+
if (failing) {
|
|
331
|
+
failing = false
|
|
332
|
+
console.warn(`Recovered: ${describe()} computes again`)
|
|
333
|
+
}
|
|
334
|
+
// A child expression resolving to a function is read by an inner
|
|
335
|
+
// effect (universal's insert); that read gets the same containment.
|
|
336
|
+
// Solid's flatten only unwraps zero-arity functions and inserts any
|
|
337
|
+
// other function as a node, so only accessors are wrapped, and the
|
|
338
|
+
// wrapper keeps arity 0.
|
|
339
|
+
if (nested && typeof value === "function" && value.length === 0) {
|
|
340
|
+
let inner = guard(value as any, describe, true, empty)
|
|
341
|
+
value = (() => inner()) as any
|
|
342
|
+
}
|
|
343
|
+
last = value
|
|
344
|
+
return value
|
|
345
|
+
} catch (e) {
|
|
346
|
+
if (e instanceof NotReadyError) throw e
|
|
347
|
+
if (!failing) {
|
|
348
|
+
failing = true
|
|
349
|
+
console.error(`Contained error: ${describe()} threw and keeps its last value until it computes again.`, e)
|
|
350
|
+
}
|
|
351
|
+
return last
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Universal's declarations trail its runtime (effect takes options, insert
|
|
357
|
+
// takes initial and options), so the wrappers carry the runtime signatures.
|
|
358
|
+
type EffectFn = <T>(fn: (prev?: T) => T, effectFn?: (value: T, prev?: T) => void, options?: unknown) => void
|
|
359
|
+
type InsertFn = (parent: ProxyNode, accessor: unknown, marker?: unknown, initial?: unknown, options?: unknown) => ProxyNode
|
|
360
|
+
let effectRaw = rawEffect as unknown as EffectFn
|
|
361
|
+
let insertRaw = rawInsert as unknown as InsertFn
|
|
362
|
+
|
|
363
|
+
export let effect: EffectFn = (fn, effectFn, options) =>
|
|
364
|
+
effectRaw<any>(
|
|
365
|
+
guard<any>(fn, () => "an element's prop expression", false, SKIP),
|
|
366
|
+
effectFn && ((value, prev) => (value === SKIP ? undefined : effectFn(value, prev === SKIP ? undefined : prev))),
|
|
367
|
+
options,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
export let insert: InsertFn = (parent, accessor, marker, initial, options) =>
|
|
371
|
+
insertRaw(
|
|
372
|
+
parent,
|
|
373
|
+
typeof accessor === "function"
|
|
374
|
+
? guard(accessor as any, () => `a child expression of <${parent.elementType}> ${getNodePath(parent.id).join("/")}`, true, undefined)
|
|
375
|
+
: accessor,
|
|
376
|
+
marker,
|
|
377
|
+
initial,
|
|
378
|
+
options,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
// The current <window> node: the app's, or the error window standing in for
|
|
382
|
+
// it. Serves as the default mount target for createPortal (single window by
|
|
383
|
+
// design, so one ambient ref).
|
|
314
384
|
let windowRoot: ProxyNode | undefined
|
|
385
|
+
let rendered = false
|
|
386
|
+
// Ids of error windows built by the root boundary, alive only while shown.
|
|
387
|
+
let errorWindows = new Set<number>()
|
|
315
388
|
|
|
316
389
|
/**
|
|
317
390
|
* Mounts a SolidRT app. Call once at the top level: `render(() => <App />)`.
|
|
318
391
|
* The element returned by `code` MUST be a `<window>` (it becomes the native
|
|
319
392
|
* window and root of the render tree); anything else throws. Runs inside a
|
|
320
393
|
* reactive root, so the whole tree is disposed together on engine reload.
|
|
394
|
+
*
|
|
395
|
+
* The whole app, window included, sits inside an error boundary: an error no
|
|
396
|
+
* <Errored> claims replaces the app's window with an error window (message,
|
|
397
|
+
* stack, a reset button) instead of halting the reactive system for good.
|
|
398
|
+
* The app's subtree stays alive behind it - the boundary keeps it and marks
|
|
399
|
+
* only the failed computations - so reset recomputes those in place and the
|
|
400
|
+
* same window node comes back.
|
|
321
401
|
*/
|
|
322
402
|
export function render(code: () => any) {
|
|
403
|
+
// Once per app: there is no unmount; teardown is engine teardown.
|
|
404
|
+
if (rendered) {
|
|
405
|
+
throw new Error("render() already called; an app has exactly one render()")
|
|
406
|
+
}
|
|
407
|
+
rendered = true
|
|
323
408
|
createRoot(() => {
|
|
324
|
-
let root =
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
409
|
+
let root = createErrorBoundary(
|
|
410
|
+
() => {
|
|
411
|
+
let win = code()
|
|
412
|
+
if (!win || win.elementType !== "window") {
|
|
413
|
+
throw new Error("render() root must be a <window> element")
|
|
414
|
+
}
|
|
415
|
+
return win
|
|
416
|
+
},
|
|
417
|
+
(error, reset) => {
|
|
418
|
+
// The boundary hands the error as an accessor.
|
|
419
|
+
let err = error()
|
|
420
|
+
console.error("Uncaught error: the app is replaced by the error window until reset or reload.", err)
|
|
421
|
+
let win = errorWindow(err, reset)
|
|
422
|
+
errorWindows.add(win.id)
|
|
423
|
+
return win
|
|
424
|
+
},
|
|
425
|
+
)
|
|
426
|
+
rawEffect(
|
|
427
|
+
() => root() as ProxyNode,
|
|
428
|
+
(win, prev) => swapRoot(win, prev),
|
|
429
|
+
)
|
|
331
430
|
})
|
|
332
431
|
}
|
|
333
432
|
|
|
433
|
+
// The boundary's value changed: the app's window on mount and after a
|
|
434
|
+
// successful reset, an error window after an error. Creating a window already
|
|
435
|
+
// made it the native root; setRoot is the way back to an existing one.
|
|
436
|
+
function swapRoot(win: ProxyNode, prev?: ProxyNode) {
|
|
437
|
+
windowRoot = win
|
|
438
|
+
if (prev === undefined) {
|
|
439
|
+
attachWindow(win.id)
|
|
440
|
+
return
|
|
441
|
+
}
|
|
442
|
+
// Creating the error window made it the native root; the app's window
|
|
443
|
+
// coming back is the case that needs the explicit way back.
|
|
444
|
+
if (!errorWindows.has(win.id)) tree.setRoot(win.id)
|
|
445
|
+
setWindowRoot(win.id)
|
|
446
|
+
// Keys route to the focused node; one inside the hidden window must not
|
|
447
|
+
// keep hearing them.
|
|
448
|
+
setFocus(null)
|
|
449
|
+
// The app's window survives behind an error window (the boundary keeps its
|
|
450
|
+
// subtree for reset); an error window replaced by anything is dead.
|
|
451
|
+
if (errorWindows.has(prev.id) || !errorWindows.has(win.id)) {
|
|
452
|
+
errorWindows.delete(prev.id)
|
|
453
|
+
destroyNode(prev)
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// The error window: the in-app sibling of the runtime's startup BSOD, built
|
|
458
|
+
// from the primitives directly (no JSX in core). Static content; the reset
|
|
459
|
+
// button recomputes the failed sources, and a reload replaces everything.
|
|
460
|
+
function errorWindow(err: unknown, reset: () => void): ProxyNode {
|
|
461
|
+
let message = err instanceof Error ? err.message : String(err)
|
|
462
|
+
let stack = err instanceof Error && err.stack ? err.stack : ""
|
|
463
|
+
let text = (content: string, props: Record<string, any>) => {
|
|
464
|
+
let node = createElement("text", props)
|
|
465
|
+
insertNode(node, createTextNode(content))
|
|
466
|
+
return node
|
|
467
|
+
}
|
|
468
|
+
let win = createElement("window", { title: "Application error" })
|
|
469
|
+
insertNode(win, createElement("d-rect", { color: "#1144bb" }))
|
|
470
|
+
let column = createElement("view", { flexGrow: 1, flexDirection: "column", padding: 40, gap: 12 })
|
|
471
|
+
insertNode(column, text(":(", { color: "white", fontSize: 64, fontWeight: 700 }))
|
|
472
|
+
insertNode(column, text("Something went wrong", { color: "white", fontSize: 22 }))
|
|
473
|
+
insertNode(column, text(message, { color: "white", fontSize: 16 }))
|
|
474
|
+
if (stack) insertNode(column, text(stack, { color: "#aac2ff", fontSize: 12, fontFamily: "mono" }))
|
|
475
|
+
insertNode(column, text("Fix the error and save to reload, or reset to retry the failed computations.", { color: "#aac2ff", fontSize: 14 }))
|
|
476
|
+
let button = createElement("view", { alignSelf: "flex-start", padding: 12, onPointerDown: () => reset() })
|
|
477
|
+
insertNode(button, createElement("d-rect", { color: "white", radius: 6 }))
|
|
478
|
+
insertNode(button, text("Reset", { color: "#1144bb", fontSize: 16, fontWeight: 600 }))
|
|
479
|
+
insertNode(column, button)
|
|
480
|
+
insertNode(win, column)
|
|
481
|
+
return win
|
|
482
|
+
}
|
|
483
|
+
|
|
334
484
|
/**
|
|
335
485
|
* Relocates an already-built node out of its lexical position to `mount` (the
|
|
336
486
|
* window root by default), then removes it again when the surrounding reactive
|
|
@@ -362,6 +512,11 @@ export function createPortal(node: Element, mount?: ProxyNode): null {
|
|
|
362
512
|
throw new Error("createPortal: node must be a single built element")
|
|
363
513
|
}
|
|
364
514
|
insertNode(target, node as ProxyNode)
|
|
365
|
-
|
|
515
|
+
// A destroyed mount target has already swept the portaled node with it
|
|
516
|
+
// (destroy walks the mount tree), so detaching then would hand freed ids to
|
|
517
|
+
// the native side, which panics. Gone from the proxy map = already freed.
|
|
518
|
+
onCleanup(() => {
|
|
519
|
+
if (nodes.has((node as ProxyNode).id)) removeNode(target, node as ProxyNode)
|
|
520
|
+
})
|
|
366
521
|
return null
|
|
367
522
|
}
|
package/src/runtime-modules.d.ts
CHANGED
|
@@ -47,6 +47,10 @@ declare module "*.ogg" {
|
|
|
47
47
|
const bytes: Uint8Array
|
|
48
48
|
export default bytes
|
|
49
49
|
}
|
|
50
|
+
declare module "*.glb" {
|
|
51
|
+
const bytes: Uint8Array
|
|
52
|
+
export default bytes
|
|
53
|
+
}
|
|
50
54
|
|
|
51
55
|
// UI event bus (lattice), provided by the runtime as a builtin module.
|
|
52
56
|
// on/once return an unsubscribe function. Notable events: the routed pointer
|
package/src/scroll.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// momentum, scrollbars and styling are policy and belong to the component (the
|
|
5
5
|
// "skin") that composes this, the same way createTextEditorLayout backs TextInput.
|
|
6
6
|
|
|
7
|
-
import { createSignal
|
|
7
|
+
import { createSignal } from "@solidjs/signals"
|
|
8
8
|
import { getBoundingBox } from "./core"
|
|
9
9
|
import { onLayout } from "./window"
|
|
10
10
|
|
|
@@ -12,6 +12,15 @@ export type ScrollAxis = "vertical" | "horizontal" | "both"
|
|
|
12
12
|
|
|
13
13
|
export type ScrollOffset = { x: number; y: number }
|
|
14
14
|
|
|
15
|
+
/** The web's scroll behavior word, simplified: "instant" asks the skin for no
|
|
16
|
+
* motion; "auto" (the default) and "smooth" both mean its usual motion, since
|
|
17
|
+
* the default already animates. */
|
|
18
|
+
export type ScrollBehavior = "auto" | "instant" | "smooth"
|
|
19
|
+
|
|
20
|
+
/** Target of scrollTo/scrollBy. An omitted axis keeps its offset (scrollTo)
|
|
21
|
+
* or moves by 0 (scrollBy). */
|
|
22
|
+
export type ScrollToOptions = { x?: number; y?: number; behavior?: ScrollBehavior }
|
|
23
|
+
|
|
15
24
|
export type ScrollOptions = {
|
|
16
25
|
/** Which axes can scroll. Locked axes are pinned to 0. Default "vertical". */
|
|
17
26
|
axis?: ScrollAxis
|
|
@@ -20,10 +29,18 @@ export type ScrollOptions = {
|
|
|
20
29
|
export type Scroll = {
|
|
21
30
|
/** Current clamped offset, as a reactive accessor. */
|
|
22
31
|
offset(): ScrollOffset
|
|
23
|
-
/**
|
|
24
|
-
|
|
32
|
+
/** Largest reachable offset per axis (content overflow, 0 on a locked axis),
|
|
33
|
+
* as a reactive accessor refreshed each layout. Watching it is how a scroll
|
|
34
|
+
* policy learns that the content or the viewport changed size. */
|
|
35
|
+
range(): ScrollOffset
|
|
36
|
+
/** Behavior of the latest scrollTo/scrollBy, as a reactive accessor: the
|
|
37
|
+
* skin reads it to withhold its motion for an instant write. Layout
|
|
38
|
+
* re-clamps leave it alone. */
|
|
39
|
+
behavior(): ScrollBehavior
|
|
25
40
|
/** Scroll to an absolute offset, clamped to range. */
|
|
26
|
-
scrollTo(
|
|
41
|
+
scrollTo(options: ScrollToOptions): void
|
|
42
|
+
/** Scroll by a delta (positive moves content up/left), clamped to range. */
|
|
43
|
+
scrollBy(options: ScrollToOptions): void
|
|
27
44
|
}
|
|
28
45
|
|
|
29
46
|
/**
|
|
@@ -32,7 +49,9 @@ export type Scroll = {
|
|
|
32
49
|
* current content-vs-viewport overflow, so the view stays valid when content
|
|
33
50
|
* grows or shrinks (e.g. an offset that scrolled to the bottom snaps up when the
|
|
34
51
|
* list gets shorter). scrollBy/scrollTo clamp against the most recently measured
|
|
35
|
-
* range. Pure geometry: no input handling and no visual policy.
|
|
52
|
+
* range. Pure geometry: no input handling and no visual policy. Anything beyond
|
|
53
|
+
* clamping (following a growing log, keeping an item in view) is a policy the
|
|
54
|
+
* caller writes against `range()` and `offset()`.
|
|
36
55
|
*
|
|
37
56
|
* The viewport node is the clipping box (overflow hidden); the content node is
|
|
38
57
|
* the inner wrapper that holds the children and takes their natural size. Apply
|
|
@@ -48,6 +67,11 @@ export function createScroll(
|
|
|
48
67
|
let canY = axis === "vertical" || axis === "both"
|
|
49
68
|
|
|
50
69
|
let [offset, setOffset] = createSignal<ScrollOffset>({ x: 0, y: 0 })
|
|
70
|
+
let [range, setRange] = createSignal<ScrollOffset>({ x: 0, y: 0 })
|
|
71
|
+
let [behavior, setBehavior] = createSignal<ScrollBehavior>("auto")
|
|
72
|
+
// Mirrors the signal: a setter's value is not readable until the flush, and
|
|
73
|
+
// two writes in one batch must still compare against the latest.
|
|
74
|
+
let lastBehavior: ScrollBehavior = "auto"
|
|
51
75
|
|
|
52
76
|
// A scroll viewport with no explicit main-axis size resolves to 0 in flex
|
|
53
77
|
// layout and its content silently vanishes - a classic trap (maxHeight alone
|
|
@@ -59,6 +83,8 @@ export function createScroll(
|
|
|
59
83
|
|
|
60
84
|
// Last measured overflow, refreshed each layout. scrollBy/scrollTo clamp
|
|
61
85
|
// against these between layouts; onLayout re-clamps once new sizes are known.
|
|
86
|
+
// Kept as plain values beside the `range` signal because a signal write is
|
|
87
|
+
// not readable until the flush, and clamping needs the number now.
|
|
62
88
|
let maxX = 0
|
|
63
89
|
let maxY = 0
|
|
64
90
|
|
|
@@ -67,10 +93,14 @@ export function createScroll(
|
|
|
67
93
|
y: canY ? Math.max(0, Math.min(y, maxY)) : 0,
|
|
68
94
|
})
|
|
69
95
|
|
|
70
|
-
let set = (x: number, y: number) => {
|
|
96
|
+
let set = (x: number, y: number, b: ScrollBehavior = "auto") => {
|
|
71
97
|
let cur = offset()
|
|
72
98
|
let next = clamp(x, y)
|
|
73
99
|
if (next.x !== cur.x || next.y !== cur.y) setOffset(next)
|
|
100
|
+
if (b !== lastBehavior) {
|
|
101
|
+
lastBehavior = b
|
|
102
|
+
setBehavior(b)
|
|
103
|
+
}
|
|
74
104
|
}
|
|
75
105
|
|
|
76
106
|
onLayout(() => {
|
|
@@ -94,20 +124,26 @@ export function createScroll(
|
|
|
94
124
|
}
|
|
95
125
|
maxX = Math.max(0, cb.width - vb.width)
|
|
96
126
|
maxY = Math.max(0, cb.height - vb.height)
|
|
127
|
+
let r = range()
|
|
128
|
+
let rx = canX ? maxX : 0
|
|
129
|
+
let ry = canY ? maxY : 0
|
|
130
|
+
if (r.x !== rx || r.y !== ry) setRange({ x: rx, y: ry })
|
|
97
131
|
let cur = offset()
|
|
98
132
|
let next = clamp(cur.x, cur.y)
|
|
99
|
-
if (next.x !== cur.x || next.y !== cur.y)
|
|
100
|
-
setOffset(next)
|
|
101
|
-
flush()
|
|
102
|
-
}
|
|
133
|
+
if (next.x !== cur.x || next.y !== cur.y) setOffset(next)
|
|
103
134
|
})
|
|
104
135
|
|
|
105
136
|
return {
|
|
106
137
|
offset,
|
|
107
|
-
|
|
138
|
+
range,
|
|
139
|
+
behavior,
|
|
140
|
+
scrollTo: (o) => {
|
|
108
141
|
let cur = offset()
|
|
109
|
-
set(
|
|
142
|
+
set(o.x ?? cur.x, o.y ?? cur.y, o.behavior)
|
|
143
|
+
},
|
|
144
|
+
scrollBy: (o) => {
|
|
145
|
+
let cur = offset()
|
|
146
|
+
set(cur.x + (o.x ?? 0), cur.y + (o.y ?? 0), o.behavior)
|
|
110
147
|
},
|
|
111
|
-
scrollTo: (x, y) => set(x, y),
|
|
112
148
|
}
|
|
113
|
-
}
|
|
149
|
+
}
|
package/src/svg.ts
CHANGED
|
@@ -46,7 +46,7 @@ export type SvgDocument = {
|
|
|
46
46
|
* that fits the document's coordinate space into its box:
|
|
47
47
|
*
|
|
48
48
|
* let doc = createMemo(() => parseSvg(src))
|
|
49
|
-
* <view repaintBoundary
|
|
49
|
+
* <view repaintBoundary designSize={[doc().width, doc().height]} width={48} height={48}>
|
|
50
50
|
* {doc().draws.map((draw) => <d-path {...draw} />)}
|
|
51
51
|
* </view>
|
|
52
52
|
*
|
package/src/text-input.ts
CHANGED
|
@@ -397,7 +397,6 @@ export function createTextEditorLayout(
|
|
|
397
397
|
|
|
398
398
|
setScrollX(wrap ? 0 : follow(scrollX(), c.x, caretWidth, vw, contentWidth + caretWidth))
|
|
399
399
|
setScrollY(follow(scrollY(), c.y, c.height, vh, contentHeight))
|
|
400
|
-
flush()
|
|
401
400
|
})
|
|
402
401
|
|
|
403
402
|
return { lines, caret, caretLine, offsetAtX, lineAtY, step, scrollX, scrollY }
|
package/src/types.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/// <reference path="./runtime-modules.d.ts" />
|
|
3
3
|
|
|
4
4
|
import type { Gradient } from "./color"
|
|
5
|
-
import type { ProgramId, TextureId } from "flux:gpu"
|
|
5
|
+
import type { ProgramId, TextureBindings, TextureId } from "flux:gpu"
|
|
6
6
|
import type { TextInputHints } from "flux:rendertree"
|
|
7
7
|
import type { Element } from "solid-js"
|
|
8
8
|
|
|
@@ -155,6 +155,7 @@ export interface PaintProps {
|
|
|
155
155
|
// A solid color, or a gradient from createLinearGradient/createRadialGradient.
|
|
156
156
|
color?: Color | Gradient
|
|
157
157
|
blendMode?: "clear" | "source" | "destination" | "source-over" | "destination-over" | "source-in" | "destination-in" | "source-out" | "destination-out" | "source-atop" | "destination-atop" | "xor" | "plus" | "modulate" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "multiply" | "hue" | "saturation" | "color" | "luminosity"
|
|
158
|
+
/** Default "fill"; "stroke" on line, whose segment has no interior (see LineProps). */
|
|
158
159
|
drawStyle?: "fill" | "stroke" | "stroke-and-fill"
|
|
159
160
|
strokeCap?: "butt" | "round" | "square"
|
|
160
161
|
strokeJoin?: "miter" | "round" | "bevel"
|
|
@@ -365,7 +366,12 @@ export interface TextGeometryProps extends PositionProps {
|
|
|
365
366
|
h?: number
|
|
366
367
|
}
|
|
367
368
|
|
|
368
|
-
/**
|
|
369
|
+
/**
|
|
370
|
+
* See {@link PositionProps}: detached-only, never affects layout. A line's
|
|
371
|
+
* reported bounds (getBoundingBox, the tree, a detached capture) are its
|
|
372
|
+
* painted box: the geometry's extent plus the stroke's reach, not the
|
|
373
|
+
* inherited box.
|
|
374
|
+
*/
|
|
369
375
|
export interface LineGeometryProps {
|
|
370
376
|
/** Endpoints default to spanning the box: (0,0) to (box width, box height). */
|
|
371
377
|
x1?: number
|
|
@@ -468,7 +474,20 @@ export type TransitionPropName =
|
|
|
468
474
|
| "y1"
|
|
469
475
|
| "x2"
|
|
470
476
|
| "y2"
|
|
477
|
+
| "scrollX"
|
|
478
|
+
| "scrollY"
|
|
471
479
|
| "opacity"
|
|
480
|
+
| "originX"
|
|
481
|
+
| "originY"
|
|
482
|
+
| "perspective"
|
|
483
|
+
| "clipRadius"
|
|
484
|
+
| "srcX"
|
|
485
|
+
| "srcY"
|
|
486
|
+
| "srcW"
|
|
487
|
+
| "srcH"
|
|
488
|
+
| "onLength"
|
|
489
|
+
| "offLength"
|
|
490
|
+
| "dashOffset"
|
|
472
491
|
| "rotate"
|
|
473
492
|
| "rotateX"
|
|
474
493
|
| "rotateY"
|
|
@@ -558,8 +577,8 @@ export interface WindowShaderProps {
|
|
|
558
577
|
* type: 2/3/4 for `vec2`/`vec3`/`vec4`, 16 (column-major) for `mat4`.
|
|
559
578
|
*/
|
|
560
579
|
params?: Record<string, number | number[]>
|
|
561
|
-
/** Extra sampler2D inputs: uniform name to texture id. */
|
|
562
|
-
textures?:
|
|
580
|
+
/** Extra sampler2D inputs: uniform name to texture id, or `{ id, filter?, wrap? }` for a per-binding sampling override. */
|
|
581
|
+
textures?: TextureBindings
|
|
563
582
|
/** Vertices drawn (attributeless triangles). Default 3, the covering triangle. */
|
|
564
583
|
vertexCount?: number
|
|
565
584
|
/**
|
|
@@ -580,18 +599,28 @@ export interface ViewOwnProps extends TransformProps, PointerProps {
|
|
|
580
599
|
children?: Children
|
|
581
600
|
trace?: boolean
|
|
582
601
|
/**
|
|
583
|
-
* Design-space size `[w, h]` for the children:
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
|
|
594
|
-
|
|
602
|
+
* Design-space size `[w, h]` (both positive) for the children: everything
|
|
603
|
+
* under the view - layout, paint, input - happens in that coordinate space,
|
|
604
|
+
* which is uniformly scaled to fit and centered in the element's box (SVG's
|
|
605
|
+
* viewBox with its default preserveAspectRatio, generalized off the graphics
|
|
606
|
+
* format and onto a layout element; the fit is always "contain"). Laid-out
|
|
607
|
+
* children resolve flex, percentages and text wrapping against the design
|
|
608
|
+
* size, so a subtree scales into any box without reflowing. The view itself
|
|
609
|
+
* sizes like a replaced element: its intrinsic size is the design size, one
|
|
610
|
+
* sized axis derives the other from the design aspect, layout props
|
|
611
|
+
* override, and it always shrinks to fit (its min-content size is zero). As
|
|
612
|
+
* a flex item it still stretches like any other under the default alignment:
|
|
613
|
+
* a width-only design-size view in a row takes the line's height, not the
|
|
614
|
+
* design height, unless the row's alignItems or its own alignSelf is not
|
|
615
|
+
* "stretch" (CSS's rule for an <img> in a flex row). Composed innermost: the
|
|
616
|
+
* transform props still operate in box space, and pointer events on children
|
|
617
|
+
* arrive in design coordinates. The overflow clip and scrollX/scrollY stay
|
|
618
|
+
* box properties: the clip rect is the layout box and scroll offsets are box
|
|
619
|
+
* pixels, regardless of fit scale. The natural wrapper for parseSvg draws,
|
|
620
|
+
* any d-* subtree authored in fixed design units, or a whole panel that
|
|
621
|
+
* should scale rather than reflow.
|
|
622
|
+
*/
|
|
623
|
+
designSize?: [number, number]
|
|
595
624
|
/**
|
|
596
625
|
* Corner radii for the clip applied when overflow is non-visible (hidden,
|
|
597
626
|
* clip, scroll on both axes). A single number rounds all four corners; an
|
|
@@ -614,6 +643,9 @@ export interface ViewOwnProps extends TransformProps, PointerProps {
|
|
|
614
643
|
* (no multisampled scratch, one render pass), but vector content - svg
|
|
615
644
|
* paths, rounded corners, rotated edges - comes out hard-edged. Text and
|
|
616
645
|
* axis-aligned rects look identical, so prefer it for plain UI panels.
|
|
646
|
+
*
|
|
647
|
+
* A snapshot boundary's pixels are available to the GPU stack as a live
|
|
648
|
+
* texture id through `snapshotTexture(ref)`.
|
|
617
649
|
*/
|
|
618
650
|
repaintBoundary?: boolean | "snapshot" | "snapshot-no-aa"
|
|
619
651
|
/**
|
|
@@ -653,8 +685,8 @@ export interface ViewShaderProps {
|
|
|
653
685
|
* type: 2/3/4 for `vec2`/`vec3`/`vec4`, 16 (column-major) for `mat4`.
|
|
654
686
|
*/
|
|
655
687
|
params?: Record<string, number | number[]>
|
|
656
|
-
/** Extra sampler2D inputs: uniform name to texture id. */
|
|
657
|
-
textures?:
|
|
688
|
+
/** Extra sampler2D inputs: uniform name to texture id, or `{ id, filter?, wrap? }` for a per-binding sampling override. */
|
|
689
|
+
textures?: TextureBindings
|
|
658
690
|
/**
|
|
659
691
|
* Transparent margin in logical px on every side of the layout box, for
|
|
660
692
|
* the effect to write into - glow, drop shadow, blur that bleeds past the
|
|
@@ -698,21 +730,81 @@ export interface RectProps extends PaintProps, PointerProps {
|
|
|
698
730
|
// Strokes paint inside the box, same as `RectProps`.
|
|
699
731
|
export interface OvalProps extends PaintProps, PointerProps {}
|
|
700
732
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
733
|
+
/**
|
|
734
|
+
* A stroke's dash pattern, on `line` and `path`. Both lengths must be set
|
|
735
|
+
* to dash; with either unset, or a gap of 0, the stroke is solid.
|
|
736
|
+
*/
|
|
737
|
+
export interface DashProps {
|
|
738
|
+
/**
|
|
739
|
+
* The drawn length, in local units. The pattern runs continuously along
|
|
740
|
+
* the geometry - through a polyline's vertices and along a path's curves,
|
|
741
|
+
* restarting at each subpath of a path; 0 draws a dot per period (given
|
|
742
|
+
* round or square caps).
|
|
743
|
+
*/
|
|
710
744
|
onLength?: number
|
|
711
|
-
/**
|
|
745
|
+
/** The gap length, in local units. */
|
|
712
746
|
offLength?: number
|
|
747
|
+
/**
|
|
748
|
+
* Distance into the dash pattern at which the stroke starts, in local
|
|
749
|
+
* units (SVG stroke-dashoffset). Wraps around the pattern's period;
|
|
750
|
+
* negative values allowed. Raising it marches the dashes toward the
|
|
751
|
+
* geometry's start: write it every frame for marching ants, or transition
|
|
752
|
+
* it for a one-shot slide. Default 0.
|
|
753
|
+
*/
|
|
754
|
+
dashOffset?: number
|
|
755
|
+
/**
|
|
756
|
+
* What the geometry's length counts as, in the pattern's units (SVG
|
|
757
|
+
* pathLength): when set, `onLength`, `offLength` and `dashOffset` are
|
|
758
|
+
* scaled by the actual length over it. `pathLength={1}` makes them
|
|
759
|
+
* fractions: `onLength={0.77} offLength={1}` draws the first 77%, and
|
|
760
|
+
* transitioning `onLength` from 0 to 1 draws the geometry on. Must be
|
|
761
|
+
* positive; unset, the pattern is in local units.
|
|
762
|
+
*/
|
|
763
|
+
pathLength?: number
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// A line's geometry is numbers, not a path string: the primitive to reach
|
|
767
|
+
// for when the geometry moves (each endpoint is one property write, a
|
|
768
|
+
// polyline is one array write; a path animates by rebuilding its `d` string).
|
|
769
|
+
// Two forms: the segment, whose endpoints (x1/y1/x2/y2) exist on the
|
|
770
|
+
// detached `d-line` only, and the polyline (`points`), which exists on both.
|
|
771
|
+
// A laid-out `<line>` without points is practically a rule - give it a thin
|
|
772
|
+
// box (length x strokeWidth); in general it draws its layout box's
|
|
773
|
+
// top-left-to-bottom-right diagonal. For arbitrary angles and connectors use
|
|
774
|
+
// `d-line`; for curves, a path. A line's stroke is centered on its geometry,
|
|
775
|
+
// so it straddles the box (a rect's paints inside), and the bounds it
|
|
776
|
+
// reports are the painted box: geometry plus stroke, on both forms.
|
|
777
|
+
//
|
|
778
|
+
// The paint defaults to `drawStyle="stroke"` (the box primitives default to
|
|
779
|
+
// fill). On a polyline "fill" and "stroke-and-fill" fill the polygon
|
|
780
|
+
// (nonzero, implicitly closed) and hit-test its interior; on the two-point
|
|
781
|
+
// form fill has no effect, a segment has no interior.
|
|
782
|
+
export interface LineProps extends PaintProps, PointerProps, DashProps {
|
|
783
|
+
/**
|
|
784
|
+
* Polyline vertices as a flat [x0, y0, x1, y1, ...] in the element's local
|
|
785
|
+
* space (the space x1..y2 use). Takes precedence over the endpoints while
|
|
786
|
+
* set. Content, not box geometry: a laid-out <line points> measures its
|
|
787
|
+
* box from the points' extent, like a <path> from `d`, and draws them
|
|
788
|
+
* unscaled. Fewer than two points draws nothing; an odd count throws. Not
|
|
789
|
+
* covered by transitions: animate by writing a new array.
|
|
790
|
+
*/
|
|
791
|
+
points?: number[] | Float32Array | Float64Array
|
|
792
|
+
/**
|
|
793
|
+
* Close the polyline's stroke: the segment back to the first point, joined
|
|
794
|
+
* there instead of capped. A fill always covers the polygon (closed
|
|
795
|
+
* implicitly), so this is a stroke distinction. Default false.
|
|
796
|
+
*/
|
|
797
|
+
closed?: boolean
|
|
713
798
|
}
|
|
714
799
|
|
|
715
|
-
|
|
800
|
+
/**
|
|
801
|
+
* `d` is an SVG path string; the stroke is centered on the geometry. The
|
|
802
|
+
* bounds a path reports (getBoundingBox, the tree, a detached capture) are
|
|
803
|
+
* its painted box: the geometry's tight extent (curve extrema, not control
|
|
804
|
+
* points) plus the stroke's reach, at a `d-path`'s x/y - not its layout box
|
|
805
|
+
* or the inherited one.
|
|
806
|
+
*/
|
|
807
|
+
export interface PathProps extends PaintProps, PointerProps, DashProps {
|
|
716
808
|
d?: string
|
|
717
809
|
fillRule?: "nonzero" | "evenodd"
|
|
718
810
|
}
|