@solidrt/core 0.0.50 → 0.0.52
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 +102 -21
- package/README.md +1 -1
- package/agents/painting.md +61 -0
- package/agents/performance.md +216 -0
- 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 +204 -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 +7 -5
- package/examples/{sound.tsx → audio.tsx} +1 -1
- package/examples/gpu-pipeline.tsx +2 -2
- package/examples/gpu-sprites.tsx +102 -0
- 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/jsx-runtime.d.ts +15 -14
- package/package.json +11 -9
- package/src/{sound.ts → audio.ts} +68 -15
- package/src/color.ts +17 -18
- package/src/core.ts +40 -3
- package/src/data.ts +99 -0
- package/src/gpu.ts +88 -34
- package/src/index.ts +9 -3
- package/src/logo.tsx +92 -0
- package/src/renderer.ts +219 -53
- package/src/runtime-modules.d.ts +7 -2
- package/src/scroll.ts +51 -15
- package/src/svg.ts +1 -1
- package/src/text-input.ts +297 -61
- package/src/types.d.ts +291 -31
- package/src/window.ts +110 -14
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"
|
|
@@ -236,6 +237,14 @@ export interface PointerEvent {
|
|
|
236
237
|
*/
|
|
237
238
|
parentX: number
|
|
238
239
|
parentY: number
|
|
240
|
+
/**
|
|
241
|
+
* Pointer movement since the previous move event, in logical pixels. Mouse
|
|
242
|
+
* reports hardware deltas (summed, never lost, and the only motion signal
|
|
243
|
+
* while the pointer is locked); touch reports position diffs. 0 on
|
|
244
|
+
* non-move events.
|
|
245
|
+
*/
|
|
246
|
+
movementX: number
|
|
247
|
+
movementY: number
|
|
239
248
|
/** Node id whose handler is currently running (bubbling changes it per call). */
|
|
240
249
|
currentTarget: number
|
|
241
250
|
/** Deepest node id of the event's path (the hit leaf). */
|
|
@@ -357,7 +366,12 @@ export interface TextGeometryProps extends PositionProps {
|
|
|
357
366
|
h?: number
|
|
358
367
|
}
|
|
359
368
|
|
|
360
|
-
/**
|
|
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
|
+
*/
|
|
361
375
|
export interface LineGeometryProps {
|
|
362
376
|
/** Endpoints default to spanning the box: (0,0) to (box width, box height). */
|
|
363
377
|
x1?: number
|
|
@@ -366,6 +380,167 @@ export interface LineGeometryProps {
|
|
|
366
380
|
y2?: number
|
|
367
381
|
}
|
|
368
382
|
|
|
383
|
+
// Native transitions (okf/done/native-transitions.md): declared once on
|
|
384
|
+
// the element, applied by the runtime to every later write of the covered
|
|
385
|
+
// properties. JS hands over targets; Rust interpolates every frame, so a
|
|
386
|
+
// running animation costs no JS per frame.
|
|
387
|
+
|
|
388
|
+
/** A cubic-bezier timing curve: a CSS name or [x1, y1, x2, y2] control values. */
|
|
389
|
+
export type TransitionCurve = "linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out" | [number, number, number, number]
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* A perceptual spring - the default kind: a bare `{ duration }` is a
|
|
393
|
+
* critically damped spring. `duration` (ms) is the perceptual settling
|
|
394
|
+
* time, `bounce` in (-1, 1] the springiness - 0 (the default) settles
|
|
395
|
+
* without overshoot, positive values overshoot, negative values settle
|
|
396
|
+
* sluggishly. A new target while the spring runs keeps position and
|
|
397
|
+
* velocity, so the motion stays continuous - use springs for anything
|
|
398
|
+
* retargeted while moving.
|
|
399
|
+
*/
|
|
400
|
+
export interface TransitionSpring {
|
|
401
|
+
duration: number
|
|
402
|
+
bounce?: number
|
|
403
|
+
/** Hold each write for this long (ms) before it applies; a newer write during the hold replaces it and restarts the delay. */
|
|
404
|
+
delay?: number
|
|
405
|
+
/**
|
|
406
|
+
* Mount-time enter animation: at the element's first attach the property
|
|
407
|
+
* snaps to this value and animates to the value it mounted with. Numbers
|
|
408
|
+
* for the scalar properties; the color property takes a CSS color string
|
|
409
|
+
* or packed number. Per-property entries only (not under `all`); a later
|
|
410
|
+
* move or reorder re-runs nothing.
|
|
411
|
+
*/
|
|
412
|
+
from?: number | string
|
|
413
|
+
/**
|
|
414
|
+
* Removal exit animation: an unmounted element stays visible, animates
|
|
415
|
+
* the property to this value (honoring `delay`), and is freed when its
|
|
416
|
+
* exit animations settle. Same value forms as `from`, per-property only.
|
|
417
|
+
* A move never plays it, the exiting element is hit-test invisible, its
|
|
418
|
+
* whole subtree stays painted with it, and no onTransitionEnd fires (the
|
|
419
|
+
* component is already disposed). An attached element keeps its layout
|
|
420
|
+
* slot until the exit finishes.
|
|
421
|
+
*/
|
|
422
|
+
exit?: number | string
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* A duration/curve tween, opted into by naming the curve (a tween is
|
|
427
|
+
* always a specific curve; without one the spec reads as a spring).
|
|
428
|
+
* Duration in ms. A new target while the tween runs restarts it from the
|
|
429
|
+
* current value with the full duration (CSS semantics) - designer-timed,
|
|
430
|
+
* one-shot motion.
|
|
431
|
+
*/
|
|
432
|
+
export interface TransitionTween {
|
|
433
|
+
duration: number
|
|
434
|
+
curve: TransitionCurve
|
|
435
|
+
/** Hold each write for this long (ms) before it applies; a newer write during the hold replaces it and restarts the delay. */
|
|
436
|
+
delay?: number
|
|
437
|
+
/**
|
|
438
|
+
* Mount-time enter animation: at the element's first attach the property
|
|
439
|
+
* snaps to this value and animates to the value it mounted with. Numbers
|
|
440
|
+
* for the scalar properties; the color property takes a CSS color string
|
|
441
|
+
* or packed number. Per-property entries only (not under `all`); a later
|
|
442
|
+
* move or reorder re-runs nothing.
|
|
443
|
+
*/
|
|
444
|
+
from?: number | string
|
|
445
|
+
/**
|
|
446
|
+
* Removal exit animation: an unmounted element stays visible, animates
|
|
447
|
+
* the property to this value (honoring `delay`), and is freed when its
|
|
448
|
+
* exit animations settle. Same value forms as `from`, per-property only.
|
|
449
|
+
* A move never plays it, the exiting element is hit-test invisible, its
|
|
450
|
+
* whole subtree stays painted with it, and no onTransitionEnd fires (the
|
|
451
|
+
* component is already disposed). An attached element keeps its layout
|
|
452
|
+
* slot until the exit finishes.
|
|
453
|
+
*/
|
|
454
|
+
exit?: number | string
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* The shorthand string: `"<duration>ms [curve] [<delay>ms]"` - `"300ms"` is
|
|
459
|
+
* a bounce-0 spring, `"300ms ease-out"` a tween, `"300ms ease-out 100ms"`
|
|
460
|
+
* delayed (first time value the duration, second the delay; ms only).
|
|
461
|
+
* Bounce, bezier control values and `from` need the object form.
|
|
462
|
+
*/
|
|
463
|
+
export type TransitionShorthand = string
|
|
464
|
+
|
|
465
|
+
export type Transition = TransitionSpring | TransitionTween | TransitionShorthand
|
|
466
|
+
|
|
467
|
+
/** The property names a transition can cover (numeric scalars). */
|
|
468
|
+
export type TransitionPropName =
|
|
469
|
+
| "x"
|
|
470
|
+
| "y"
|
|
471
|
+
| "w"
|
|
472
|
+
| "h"
|
|
473
|
+
| "x1"
|
|
474
|
+
| "y1"
|
|
475
|
+
| "x2"
|
|
476
|
+
| "y2"
|
|
477
|
+
| "scrollX"
|
|
478
|
+
| "scrollY"
|
|
479
|
+
| "opacity"
|
|
480
|
+
| "originX"
|
|
481
|
+
| "originY"
|
|
482
|
+
| "perspective"
|
|
483
|
+
| "clipRadius"
|
|
484
|
+
| "srcX"
|
|
485
|
+
| "srcY"
|
|
486
|
+
| "srcW"
|
|
487
|
+
| "srcH"
|
|
488
|
+
| "onLength"
|
|
489
|
+
| "offLength"
|
|
490
|
+
| "dashOffset"
|
|
491
|
+
| "rotate"
|
|
492
|
+
| "rotateX"
|
|
493
|
+
| "rotateY"
|
|
494
|
+
| "scale"
|
|
495
|
+
| "scaleX"
|
|
496
|
+
| "scaleY"
|
|
497
|
+
| "strokeWidth"
|
|
498
|
+
| "radius"
|
|
499
|
+
| "color"
|
|
500
|
+
|
|
501
|
+
/** Payload of onTransitionEnd: which animated property finished. */
|
|
502
|
+
export interface TransitionEndEvent {
|
|
503
|
+
property: TransitionPropName
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export interface TransitionProps {
|
|
507
|
+
/**
|
|
508
|
+
* A runtime-side transition of one of this element's properties reached
|
|
509
|
+
* its target (natural settles only; a cancelled or retargeted animation
|
|
510
|
+
* does not fire until it finally settles). Delivered to this element
|
|
511
|
+
* only, no bubbling.
|
|
512
|
+
*/
|
|
513
|
+
onTransitionEnd?: (event: TransitionEndEvent) => void
|
|
514
|
+
/**
|
|
515
|
+
* Animate later writes of the listed properties instead of snapping:
|
|
516
|
+
* `transition={{ x: { duration: 400, bounce: 0.2 }, opacity: "200ms ease-out" }}`.
|
|
517
|
+
* `all` covers every animatable property the element has, and a bare
|
|
518
|
+
* string is shorthand for it: `transition="300ms ease-out"`. Only
|
|
519
|
+
* properties the element carries animate (a d-rect has x, a view's x is
|
|
520
|
+
* its transform); the initial value never animates unless the entry sets
|
|
521
|
+
* `from` (an enter animation), and a non-numeric write (e.g. null)
|
|
522
|
+
* cancels the running animation and snaps. `null` clears the
|
|
523
|
+
* declaration; already-running animations finish.
|
|
524
|
+
*/
|
|
525
|
+
transition?:
|
|
526
|
+
| ({
|
|
527
|
+
all?: Omit<TransitionSpring, "from" | "exit"> | Omit<TransitionTween, "from" | "exit"> | TransitionShorthand
|
|
528
|
+
/**
|
|
529
|
+
* Group stagger (ms): every descendant enter (`from`) or exit that
|
|
530
|
+
* begins in the same frame under this element gets `index * stagger`
|
|
531
|
+
* of extra delay, in occurrence order (enters and exits cascade
|
|
532
|
+
* separately). Nearest declaring ancestor wins; it orchestrates
|
|
533
|
+
* descendants only - ordinary writes and this element's own
|
|
534
|
+
* lifecycle are unaffected. Adds on top of a per-entry `delay`.
|
|
535
|
+
*/
|
|
536
|
+
stagger?: number
|
|
537
|
+
} & {
|
|
538
|
+
[P in TransitionPropName]?: Transition
|
|
539
|
+
})
|
|
540
|
+
| TransitionShorthand
|
|
541
|
+
| null
|
|
542
|
+
}
|
|
543
|
+
|
|
369
544
|
// Primitives
|
|
370
545
|
|
|
371
546
|
export interface WindowProps extends LayoutProps, PointerProps {
|
|
@@ -402,8 +577,8 @@ export interface WindowShaderProps {
|
|
|
402
577
|
* type: 2/3/4 for `vec2`/`vec3`/`vec4`, 16 (column-major) for `mat4`.
|
|
403
578
|
*/
|
|
404
579
|
params?: Record<string, number | number[]>
|
|
405
|
-
/** Extra sampler2D inputs: uniform name to texture id. */
|
|
406
|
-
textures?:
|
|
580
|
+
/** Extra sampler2D inputs: uniform name to texture id, or `{ id, filter?, wrap? }` for a per-binding sampling override. */
|
|
581
|
+
textures?: TextureBindings
|
|
407
582
|
/** Vertices drawn (attributeless triangles). Default 3, the covering triangle. */
|
|
408
583
|
vertexCount?: number
|
|
409
584
|
/**
|
|
@@ -424,18 +599,28 @@ export interface ViewOwnProps extends TransformProps, PointerProps {
|
|
|
424
599
|
children?: Children
|
|
425
600
|
trace?: boolean
|
|
426
601
|
/**
|
|
427
|
-
* Design-space size `[w, h]` for the children:
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
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.
|
|
437
622
|
*/
|
|
438
|
-
|
|
623
|
+
designSize?: [number, number]
|
|
439
624
|
/**
|
|
440
625
|
* Corner radii for the clip applied when overflow is non-visible (hidden,
|
|
441
626
|
* clip, scroll on both axes). A single number rounds all four corners; an
|
|
@@ -458,13 +643,17 @@ export interface ViewOwnProps extends TransformProps, PointerProps {
|
|
|
458
643
|
* (no multisampled scratch, one render pass), but vector content - svg
|
|
459
644
|
* paths, rounded corners, rotated edges - comes out hard-edged. Text and
|
|
460
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)`.
|
|
461
649
|
*/
|
|
462
650
|
repaintBoundary?: boolean | "snapshot" | "snapshot-no-aa"
|
|
463
651
|
/**
|
|
464
652
|
* Run this view's rasterized subtree through a GPU program and composite
|
|
465
|
-
* the result in its place. Requires
|
|
466
|
-
*
|
|
467
|
-
*
|
|
653
|
+
* the result in its place. Requires a snapshot boundary
|
|
654
|
+
* (repaintBoundary="snapshot" or "snapshot-no-aa"; the cost is snapshot
|
|
655
|
+
* semantics, kept explicit; declared without one the shader is ignored
|
|
656
|
+
* with a warning). The pass is region-sized and split from content
|
|
468
657
|
* invalidation: a params-only change re-runs just the pass against the
|
|
469
658
|
* cached snapshot, so animating an effect over a static subtree never
|
|
470
659
|
* re-rasterizes it.
|
|
@@ -496,8 +685,8 @@ export interface ViewShaderProps {
|
|
|
496
685
|
* type: 2/3/4 for `vec2`/`vec3`/`vec4`, 16 (column-major) for `mat4`.
|
|
497
686
|
*/
|
|
498
687
|
params?: Record<string, number | number[]>
|
|
499
|
-
/** Extra sampler2D inputs: uniform name to texture id. */
|
|
500
|
-
textures?:
|
|
688
|
+
/** Extra sampler2D inputs: uniform name to texture id, or `{ id, filter?, wrap? }` for a per-binding sampling override. */
|
|
689
|
+
textures?: TextureBindings
|
|
501
690
|
/**
|
|
502
691
|
* Transparent margin in logical px on every side of the layout box, for
|
|
503
692
|
* the effect to write into - glow, drop shadow, blur that bleeds past the
|
|
@@ -541,21 +730,81 @@ export interface RectProps extends PaintProps, PointerProps {
|
|
|
541
730
|
// Strokes paint inside the box, same as `RectProps`.
|
|
542
731
|
export interface OvalProps extends PaintProps, PointerProps {}
|
|
543
732
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
+
*/
|
|
553
744
|
onLength?: number
|
|
554
|
-
/**
|
|
745
|
+
/** The gap length, in local units. */
|
|
555
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
|
|
556
764
|
}
|
|
557
765
|
|
|
558
|
-
|
|
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
|
|
798
|
+
}
|
|
799
|
+
|
|
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 {
|
|
559
808
|
d?: string
|
|
560
809
|
fillRule?: "nonzero" | "evenodd"
|
|
561
810
|
}
|
|
@@ -576,6 +825,17 @@ export interface TextRunProps {
|
|
|
576
825
|
lineHeight?: number
|
|
577
826
|
fontStyle?: "normal" | "italic"
|
|
578
827
|
fontWeight?: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900
|
|
828
|
+
/**
|
|
829
|
+
* Underline in the run's own color, drawn straight through descenders
|
|
830
|
+
* (no skip-ink). Position and thickness come from the font's own metrics
|
|
831
|
+
* unless overridden; a font Impeller resolves through the system fallback
|
|
832
|
+
* gets the shipped Noto values.
|
|
833
|
+
*/
|
|
834
|
+
textDecoration?: "none" | "underline"
|
|
835
|
+
/** Pixels from the baseline to the top of the underline. */
|
|
836
|
+
textUnderlineOffset?: number
|
|
837
|
+
/** Underline thickness in pixels. */
|
|
838
|
+
textDecorationThickness?: number
|
|
579
839
|
}
|
|
580
840
|
|
|
581
841
|
/**
|
package/src/window.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createSignal, onCleanup, onSettled, flush } from "@solidjs/signals"
|
|
2
|
-
import { requestFrame } from "flux:rendertree"
|
|
2
|
+
import { requestFrame, setPointerLock } from "flux:rendertree"
|
|
3
3
|
import { renderFrame } from "srt:render"
|
|
4
4
|
import { on, once } from "srt:events"
|
|
5
5
|
import { exit } from "srt:app"
|
|
@@ -38,9 +38,12 @@ let refreshRate = 60
|
|
|
38
38
|
* the present count, and `rate` is the current refresh rate in Hz. `tick` is paced
|
|
39
39
|
* by the runtime (one refresh period per present, slow-corrected toward the wall
|
|
40
40
|
* clock) so animations driven off it stay smooth even when swap-return times
|
|
41
|
-
* jitter
|
|
42
|
-
*
|
|
43
|
-
*
|
|
41
|
+
* jitter, and it is continuous across hot reloads: every tick an instance
|
|
42
|
+
* sees is on one timebase, so dt is well-defined from the second call on.
|
|
43
|
+
* Timers freeze together with frame callbacks under the dev tools' clock
|
|
44
|
+
* control, but measure their delays against the wall clock, not this paced
|
|
45
|
+
* timeline. performance.now() is not on it either: it is real elapsed time
|
|
46
|
+
* for measuring work; Date.now() is calendar time.
|
|
44
47
|
* Returns a cleanup function; also auto-cleans within a reactive scope.
|
|
45
48
|
*/
|
|
46
49
|
export function onFrame(fn: (tick: number, frame: number, rate: number) => void) {
|
|
@@ -161,6 +164,33 @@ export function windowFocused(): boolean {
|
|
|
161
164
|
return focusedAccessor()
|
|
162
165
|
}
|
|
163
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Lock the pointer to the window (relative mouse mode) or release it. While
|
|
169
|
+
* locked the cursor is hidden and confined, clientX/clientY freeze at the
|
|
170
|
+
* lock point, and mouse motion keeps reporting through the pointer events'
|
|
171
|
+
* movementX/movementY - the mouse-look primitive. Window-level, no
|
|
172
|
+
* permission dance: one app, one window. Observe the applied state through
|
|
173
|
+
* pointerLocked(); the platform can refuse (no relative mode) and the OS
|
|
174
|
+
* may drop the lock, e.g. on focus loss.
|
|
175
|
+
*/
|
|
176
|
+
export function lockPointer(locked: boolean) {
|
|
177
|
+
if (typeof locked !== "boolean") throw new Error(`lockPointer: expected a boolean, got ${typeof locked}`)
|
|
178
|
+
setPointerLock(locked)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let pointerLockedAccessor: (() => boolean) | undefined
|
|
182
|
+
|
|
183
|
+
/** Whether the pointer is currently locked (relative mouse mode), as a reactive accessor. */
|
|
184
|
+
export function pointerLocked(): boolean {
|
|
185
|
+
if (!pointerLockedAccessor) {
|
|
186
|
+
let [locked, setLocked] = createSignal(false)
|
|
187
|
+
// Sticky event: a subscriber after the lock still observes the state.
|
|
188
|
+
on("pointerLock", ({ locked }: { locked: boolean }) => setLocked(locked))
|
|
189
|
+
pointerLockedAccessor = locked
|
|
190
|
+
}
|
|
191
|
+
return pointerLockedAccessor()
|
|
192
|
+
}
|
|
193
|
+
|
|
164
194
|
let keyboardHeightAccessor: (() => number) | undefined
|
|
165
195
|
|
|
166
196
|
/**
|
|
@@ -178,14 +208,49 @@ export function keyboardHeight(): number {
|
|
|
178
208
|
return keyboardHeightAccessor()
|
|
179
209
|
}
|
|
180
210
|
|
|
211
|
+
// Post-layout handlers run in registration order from one bus subscription,
|
|
212
|
+
// then pending reactive writes are flushed once. The postLayout emit is a JS
|
|
213
|
+
// entry inside the frame (layout has run, paint has not), and the runtime's
|
|
214
|
+
// microtask checkpoint only comes after the whole frame closure, so a signal
|
|
215
|
+
// write made by a handler would otherwise reach its node a frame late. The
|
|
216
|
+
// drain lives here, the way runFrame drains before renderFrame, so that no
|
|
217
|
+
// handler has to flush for itself. A throwing handler skips neither the
|
|
218
|
+
// handlers after it nor the flush.
|
|
219
|
+
let layoutHandlers: (() => void)[] = []
|
|
220
|
+
let layoutSubscribed = false
|
|
221
|
+
|
|
222
|
+
function runLayoutHandlers() {
|
|
223
|
+
for (let fn of [...layoutHandlers]) {
|
|
224
|
+
try {
|
|
225
|
+
fn()
|
|
226
|
+
} catch (err) {
|
|
227
|
+
console.error("Error in onLayout handler:", err)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
flush()
|
|
232
|
+
} catch (err) {
|
|
233
|
+
console.error("Error in reactive flush:", err)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
181
237
|
/**
|
|
182
238
|
* Fires after layout has been computed for the current frame but before paint.
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
* paint
|
|
239
|
+
* Property writes made from this callback, direct or through signals (pending
|
|
240
|
+
* reactive writes are flushed once every handler has run), are picked up by a
|
|
241
|
+
* re-layout pass before painting (one extra pass; cascades beyond that paint
|
|
242
|
+
* stale).
|
|
186
243
|
*/
|
|
187
244
|
export function onLayout(fn: () => void) {
|
|
188
|
-
|
|
245
|
+
if (!layoutSubscribed) {
|
|
246
|
+
layoutSubscribed = true
|
|
247
|
+
on("postLayout", runLayoutHandlers)
|
|
248
|
+
}
|
|
249
|
+
layoutHandlers.push(fn)
|
|
250
|
+
let unsubscribe = () => {
|
|
251
|
+
let i = layoutHandlers.indexOf(fn)
|
|
252
|
+
if (i >= 0) layoutHandlers.splice(i, 1)
|
|
253
|
+
}
|
|
189
254
|
onCleanup(unsubscribe)
|
|
190
255
|
return unsubscribe
|
|
191
256
|
}
|
|
@@ -244,10 +309,20 @@ export function onBack(fn: (e: BackEvent) => void) {
|
|
|
244
309
|
|
|
245
310
|
// ------ Window ----------------
|
|
246
311
|
|
|
247
|
-
|
|
312
|
+
// The window root's id: the key-routing fallback target and the pointer
|
|
313
|
+
// interest root. Set by attachWindow, moved by setWindowRoot when render()'s
|
|
314
|
+
// error boundary swaps the app's window for the error window and back.
|
|
315
|
+
let windowRootId = 0
|
|
316
|
+
|
|
317
|
+
export function setWindowRoot(nodeId: number) {
|
|
318
|
+
windowRootId = nodeId
|
|
248
319
|
// The root carries the ambient move-interest bit for global onPointerMove
|
|
249
320
|
// subscribers (it is on every hit path); see core.setInterestRoot.
|
|
250
321
|
setInterestRoot(nodeId)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function attachWindow(nodeId: number) {
|
|
325
|
+
setWindowRoot(nodeId)
|
|
251
326
|
let unsubscribe: () => void = null!
|
|
252
327
|
let unsubDown: () => void = null!
|
|
253
328
|
let unsubUp: () => void = null!
|
|
@@ -255,6 +330,7 @@ export function attachWindow(nodeId: number) {
|
|
|
255
330
|
let unsubEnter: () => void = null!
|
|
256
331
|
let unsubLeave: () => void = null!
|
|
257
332
|
let unsubWheel: () => void = null!
|
|
333
|
+
let unsubTransitionEnd: () => void = null!
|
|
258
334
|
let unsubKeyDown: () => void = null!
|
|
259
335
|
let unsubKeyUp: () => void = null!
|
|
260
336
|
let unsubBack: () => void = null!
|
|
@@ -263,8 +339,16 @@ export function attachWindow(nodeId: number) {
|
|
|
263
339
|
let unsubRefreshRate: () => void = null!
|
|
264
340
|
let unsubFirstResize: (() => void) | null = null
|
|
265
341
|
|
|
266
|
-
|
|
267
|
-
|
|
342
|
+
// `bootstrap` marks the synthetic first frame (see the first-resize
|
|
343
|
+
// subscription below): it flushes and paints the freshly initialized graph
|
|
344
|
+
// but does not invoke onFrame callbacks, because its timestamp is not a
|
|
345
|
+
// reading of the frame timeline - handing apps a zero tick gave every
|
|
346
|
+
// reloaded instance one enormous dt on the next real frame
|
|
347
|
+
// (okf/done/onframe-tick-reset-on-reload.md). The callbacks stay
|
|
348
|
+
// registered and run on the first real render event, before the first
|
|
349
|
+
// paint with their writes applied.
|
|
350
|
+
function runFrame(t: number, frame: number, bootstrap = false) {
|
|
351
|
+
if (!bootstrap && animationFrames.size > 0) {
|
|
268
352
|
let frames = animationFrames
|
|
269
353
|
animationFrames = new Map()
|
|
270
354
|
for (let fn of frames.values()) fn(t, frame, refreshRate)
|
|
@@ -368,6 +452,17 @@ export function attachWindow(nodeId: number) {
|
|
|
368
452
|
bubble(raw, "onWheel")
|
|
369
453
|
})
|
|
370
454
|
|
|
455
|
+
// A native transition finished (runtime-side animation; see the
|
|
456
|
+
// `transition` prop). Target-only delivery, no bubbling: the element
|
|
457
|
+
// that declared the transition is the one interested in its end.
|
|
458
|
+
unsubTransitionEnd = on("transitionEnd", (raw: { target: number; property: string }) => {
|
|
459
|
+
try {
|
|
460
|
+
getEventHandler(raw.target, "onTransitionEnd")?.({ property: raw.property })
|
|
461
|
+
} catch (err) {
|
|
462
|
+
console.error("Error in onTransitionEnd handler:", err)
|
|
463
|
+
}
|
|
464
|
+
})
|
|
465
|
+
|
|
371
466
|
// Key events dispatch along the focused node's ancestor chain, leaf->root
|
|
372
467
|
// (the pointer bubbling contract), so a container hears keys from focused
|
|
373
468
|
// descendants and the window root hears everything: <window onKeyDown> is
|
|
@@ -376,13 +471,13 @@ export function attachWindow(nodeId: number) {
|
|
|
376
471
|
// at dispatch time from current focus (nothing to freeze: keyup follows
|
|
377
472
|
// focus, as in the DOM).
|
|
378
473
|
let dispatchKey = (raw: any, handler: string) => {
|
|
379
|
-
let target = focusedNode() ??
|
|
474
|
+
let target = focusedNode() ?? windowRootId
|
|
380
475
|
let stopped = false
|
|
381
476
|
let e = { ...raw, target, stopPropagation: () => (stopped = true) }
|
|
382
477
|
let path = getNodePath(target)
|
|
383
478
|
// A focused node detached this tick has no chain to the root; the
|
|
384
479
|
// window root must still hear the key.
|
|
385
|
-
if (path[path.length - 1] !==
|
|
480
|
+
if (path[path.length - 1] !== windowRootId) path.push(windowRootId)
|
|
386
481
|
for (let id of path) {
|
|
387
482
|
e.currentTarget = id
|
|
388
483
|
getEventHandler(id, handler)?.(e)
|
|
@@ -429,7 +524,7 @@ export function attachWindow(nodeId: number) {
|
|
|
429
524
|
// where flush() is illegal (not reentrant). Defer runFrame to a microtask
|
|
430
525
|
// so the first frame always runs after this callback returns.
|
|
431
526
|
unsubFirstResize = once("resize", () => {
|
|
432
|
-
queueMicrotask(() => runFrame(0, 0))
|
|
527
|
+
queueMicrotask(() => runFrame(0, 0, true))
|
|
433
528
|
})
|
|
434
529
|
})
|
|
435
530
|
|
|
@@ -442,6 +537,7 @@ export function attachWindow(nodeId: number) {
|
|
|
442
537
|
if (unsubEnter) unsubEnter()
|
|
443
538
|
if (unsubLeave) unsubLeave()
|
|
444
539
|
if (unsubWheel) unsubWheel()
|
|
540
|
+
if (unsubTransitionEnd) unsubTransitionEnd()
|
|
445
541
|
if (unsubKeyDown) unsubKeyDown()
|
|
446
542
|
if (unsubKeyUp) unsubKeyUp()
|
|
447
543
|
if (unsubBack) unsubBack()
|