@solidrt/core 0.0.51 → 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/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, flush } from "@solidjs/signals"
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
- /** Scroll by a delta (positive moves content up/left), clamped to range. */
24
- scrollBy(dx: number, dy: number): void
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(x: number, y: number): void
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
- scrollBy: (dx, dy) => {
138
+ range,
139
+ behavior,
140
+ scrollTo: (o) => {
108
141
  let cur = offset()
109
- set(cur.x + dx, cur.y + dy)
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 viewBox={[doc().width, doc().height]} width={48} height={48}>
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
- /** See {@link PositionProps}: detached-only, never affects layout. */
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?: Record<string, TextureId>
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: content drawn in that
584
- * coordinate space is uniformly scaled to fit and centered in the element's
585
- * box (SVG's default preserveAspectRatio, generalized). A pure fit
586
- * transform - it never sizes the element, so give the box its size with
587
- * layout props. Composed innermost: the transform props still operate in
588
- * box space, and pointer events on children arrive in design coordinates.
589
- * The overflow clip and scrollX/scrollY stay box properties too: the clip
590
- * rect is the layout box and scroll offsets are box pixels, regardless of
591
- * fit scale. The natural wrapper for parseSvg draws, or any d-* subtree
592
- * authored in fixed design units.
593
- */
594
- viewBox?: [number, number]
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?: Record<string, TextureId>
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
- // A line's geometry is numbers, not a path string: the segment primitive to
702
- // reach for when endpoints move (each endpoint is one property write; a path
703
- // animates by rebuilding its `d` string). Endpoints (x1/y1/x2/y2) exist on
704
- // the detached `d-line` only. A laid-out `<line>` is practically a rule -
705
- // give it a thin box (length x strokeWidth); in general it draws its layout
706
- // box's top-left-to-bottom-right diagonal. For arbitrary angles and
707
- // connectors use `d-line`; for polylines and curves, a path.
708
- export interface LineProps extends PaintProps, PointerProps {
709
- /** Dash pattern in local units: the drawn segment length. Both onLength and offLength must be set to dash; with either unset the line is solid. */
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
- /** Dash pattern in local units: the gap length. Both onLength and offLength must be set to dash; with either unset the line is solid. */
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
- export interface PathProps extends PaintProps, PointerProps {
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
  }
package/src/window.ts CHANGED
@@ -208,14 +208,49 @@ export function keyboardHeight(): number {
208
208
  return keyboardHeightAccessor()
209
209
  }
210
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
+
211
237
  /**
212
238
  * Fires after layout has been computed for the current frame but before paint.
213
- * Setting properties that affect layout from this callback will be picked up
214
- * by a re-layout pass before painting (one extra pass; cascades beyond that
215
- * paint stale).
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).
216
243
  */
217
244
  export function onLayout(fn: () => void) {
218
- let unsubscribe = on("postLayout", fn)
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
+ }
219
254
  onCleanup(unsubscribe)
220
255
  return unsubscribe
221
256
  }
@@ -274,10 +309,20 @@ export function onBack(fn: (e: BackEvent) => void) {
274
309
 
275
310
  // ------ Window ----------------
276
311
 
277
- export function attachWindow(nodeId: number) {
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
278
319
  // The root carries the ambient move-interest bit for global onPointerMove
279
320
  // subscribers (it is on every hit path); see core.setInterestRoot.
280
321
  setInterestRoot(nodeId)
322
+ }
323
+
324
+ export function attachWindow(nodeId: number) {
325
+ setWindowRoot(nodeId)
281
326
  let unsubscribe: () => void = null!
282
327
  let unsubDown: () => void = null!
283
328
  let unsubUp: () => void = null!
@@ -426,13 +471,13 @@ export function attachWindow(nodeId: number) {
426
471
  // at dispatch time from current focus (nothing to freeze: keyup follows
427
472
  // focus, as in the DOM).
428
473
  let dispatchKey = (raw: any, handler: string) => {
429
- let target = focusedNode() ?? nodeId
474
+ let target = focusedNode() ?? windowRootId
430
475
  let stopped = false
431
476
  let e = { ...raw, target, stopPropagation: () => (stopped = true) }
432
477
  let path = getNodePath(target)
433
478
  // A focused node detached this tick has no chain to the root; the
434
479
  // window root must still hear the key.
435
- if (path[path.length - 1] !== nodeId) path.push(nodeId)
480
+ if (path[path.length - 1] !== windowRootId) path.push(windowRootId)
436
481
  for (let id of path) {
437
482
  e.currentTarget = id
438
483
  getEventHandler(id, handler)?.(e)