@solidrt/core 0.0.40 → 0.0.42

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/svg.ts ADDED
@@ -0,0 +1,71 @@
1
+ // SVG documents as data: parseSvg turns a document string into a flat list of
2
+ // draws that map straight onto <d-path> elements. Vector currency is path
3
+ // data, the same way raster currency is a texture id - there is no document
4
+ // element that swallows the source; the app owns the parsed data and composes
5
+ // ordinary primitives from it.
6
+
7
+ import { parseSvg as fluxParseSvg } from "flux:svg"
8
+ import { parseColor, type Gradient } from "./color"
9
+
10
+ /**
11
+ * Tags an inline SVG source, returning it unchanged. Documents small enough to
12
+ * belong beside the code that uses them stay in the file; the tag is what
13
+ * makes them legible there, because editors highlight markup inside a template
14
+ * literal only when a known tag marks it (the name matters - `svg` is one the
15
+ * grammars look for). Raw semantics, like `glsl`.
16
+ */
17
+ export let svg = String.raw
18
+
19
+ /**
20
+ * One resolved draw in document coordinates. The keys match `PathProps`, so a
21
+ * draw spreads onto a `<d-path>` unchanged: `<d-path {...draw} />`. A source
22
+ * path with both a fill and a stroke yields two draws (fill first).
23
+ */
24
+ export type SvgDraw = {
25
+ d: string
26
+ color: string | Gradient
27
+ drawStyle: "fill" | "stroke"
28
+ fillRule?: "nonzero" | "evenodd"
29
+ strokeWidth?: number
30
+ strokeCap?: "butt" | "round" | "square"
31
+ strokeJoin?: "miter" | "round" | "bevel"
32
+ }
33
+
34
+ /** A parsed document: intrinsic size (viewBox/width-height) plus the flat draw list. */
35
+ export type SvgDocument = {
36
+ width: number
37
+ height: number
38
+ draws: SvgDraw[]
39
+ }
40
+
41
+ /**
42
+ * Parses an SVG document string (an imported `.svg` asset, an icon library's
43
+ * string export, or a template literal) into plain draw data: geometry
44
+ * flattened to absolute path data with every transform baked in, paints
45
+ * resolved to colors or gradients. Render it by wrapping the draws in a view
46
+ * that fits the document's coordinate space into its box:
47
+ *
48
+ * let doc = createMemo(() => parseSvg(src))
49
+ * <view repaintBoundary viewBox={[doc().width, doc().height]} width={48} height={48}>
50
+ * {doc().draws.map((draw) => <d-path {...draw} />)}
51
+ * </view>
52
+ *
53
+ * The plain repaintBoundary is the recommended default: the parsed subtree is
54
+ * static, so it never re-records alongside changing siblings. Each `<d-path>`
55
+ * hit-tests its exact outline; when the document should act as ONE hit target
56
+ * (the usual icon case), add `pointerEvents="all"` to the wrapper - the box
57
+ * then matches as a whole and the per-path outline tests are skipped.
58
+ *
59
+ * `opts.color` drives `currentColor` in the document (any CSS color string),
60
+ * which is how monochrome icon sets (Lucide, Feather, Heroicons, ...) get
61
+ * recolored; explicit fills/strokes still win. Parsing is synchronous and
62
+ * sandboxed (no network, file, or data-URI access) and throws on an invalid
63
+ * document. Parse once per document under a memo, not per instance.
64
+ *
65
+ * Unsupported and skipped: clipPath, masks, filters, patterns, embedded
66
+ * images, and SVG text.
67
+ */
68
+ export function parseSvg(src: string, opts?: { color?: string }): SvgDocument {
69
+ if (opts?.color != null) return fluxParseSvg(src, { color: parseColor(opts.color) })
70
+ return fluxParseSvg(src)
71
+ }
package/src/types.d.ts CHANGED
@@ -2,8 +2,12 @@
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"
6
+ import type { TextInputHints } from "flux:rendertree"
5
7
  import type { Element } from "solid-js"
6
8
 
9
+ export type { TextInputHints }
10
+
7
11
  // The "srt:*" lattice runner modules are declared in ./runtime-modules.d.ts
8
12
  // (referenced above) - ambient `declare module` only reaches consumers from a
9
13
  // non-module declaration file, and this file is a module.
@@ -173,6 +177,9 @@ export interface TransformProps {
173
177
  // Perspective viewing distance in pixels (CSS `perspective`). Enables the 3D
174
178
  // depth for rotateY; larger values give a shallower effect.
175
179
  perspective?: number
180
+ // Subtree translation in pixels, composited post-layout (no re-record, no
181
+ // layout). Unlike the draw primitives' detached-only x/y, these exist on
182
+ // layout views too - the drag/thumb idiom animates them freely.
176
183
  x?: number
177
184
  y?: number
178
185
  originX?: OriginX
@@ -235,6 +242,10 @@ export interface WheelEvent extends PointerEvent {
235
242
  // layout-dependent value ("a", "!", "Enter", "ArrowLeft"); `code` is the
236
243
  // physical, layout-independent key position ("KeyA", "Digit1", "NumpadEnter").
237
244
  // Printable characters for text entry arrive via onTextInput, not here.
245
+ // Routing: keydown/keyup dispatch along the focused node's ancestor chain,
246
+ // leaf->root, always ending at the window root; with nothing focused they go
247
+ // to the window root alone. <window onKeyDown> is therefore the app-global
248
+ // shortcut point.
238
249
  export interface KeyEvent {
239
250
  key: string
240
251
  code: string
@@ -243,6 +254,16 @@ export interface KeyEvent {
243
254
  ctrlKey: boolean
244
255
  altKey: boolean
245
256
  metaKey: boolean
257
+ /** Node id whose handler is currently running (bubbling changes it per call). */
258
+ currentTarget: number
259
+ /** Node id the dispatch started at: the focused node, or the window root when nothing is focused. */
260
+ target: number
261
+ /**
262
+ * Stops the event from reaching ancestor handlers. A component that consumed
263
+ * the key calls this so enclosing handlers (and app-global shortcuts on the
264
+ * window) do not also act on it.
265
+ */
266
+ stopPropagation: () => void
246
267
  }
247
268
 
248
269
  export interface TextEvent {
@@ -261,14 +282,70 @@ export interface PointerProps {
261
282
  onKeyDown?: (event: KeyEvent) => void
262
283
  onKeyUp?: (event: KeyEvent) => void
263
284
  onTextInput?: (event: TextEvent) => void
285
+ /**
286
+ * IME behavior for this node's text-entry sessions (keyboard type,
287
+ * capitalization, autocorrect); read when a session starts. Without it the
288
+ * OS defaults apply - notably sentence auto-capitalization, which
289
+ * identifier fields and terminals want off:
290
+ * `textInputHints={{ capitalize: "none", autocorrect: false }}`.
291
+ */
292
+ textInputHints?: TextInputHints
293
+ /**
294
+ * Declares the element a candidate for focus navigation, enumerable via
295
+ * getFocusables(). Candidacy only - it changes no behavior by itself; focus
296
+ * still moves through setFocus.
297
+ */
298
+ focusable?: boolean
264
299
  pointerEvents?: "auto" | "none" | "all"
265
300
  }
266
301
 
267
- interface Position {
302
+ /**
303
+ * Detached-only geometry, in paint-space pixels. Never affects layout: these
304
+ * props exist only on the d-* forms, where there is no layout box and the
305
+ * element owns its geometry. The layout forms of the draw primitives derive
306
+ * their geometry from the layout box instead (size it with width/height).
307
+ */
308
+ export interface PositionProps {
309
+ /** Horizontal offset of the drawn geometry; defaults to 0. */
268
310
  x?: number
311
+ /** Vertical offset of the drawn geometry; defaults to 0. */
269
312
  y?: number
270
313
  }
271
314
 
315
+ /** See {@link PositionProps}: detached-only, never affects layout. */
316
+ export interface GeometryProps extends PositionProps {
317
+ /** Drawn width; defaults to the inherited box width. */
318
+ w?: number
319
+ /** Drawn height; defaults to the inherited box height. */
320
+ h?: number
321
+ }
322
+
323
+ /** See {@link PositionProps}: detached-only, never affects layout. */
324
+ export interface OvalGeometryProps extends PositionProps {
325
+ /** Bounding box width of the ellipse (not a radius); defaults to the inherited box. */
326
+ w?: number
327
+ /** Bounding box height of the ellipse (not a radius); defaults to the inherited box. */
328
+ h?: number
329
+ }
330
+
331
+ /** See {@link PositionProps}: detached-only, never affects layout. */
332
+ export interface TextGeometryProps extends PositionProps {
333
+ // Shaping (wrap) width. Detached text wraps at the inherited ancestor size
334
+ // by default; set w for an unwrapped natural line or an explicit wrap width.
335
+ w?: number
336
+ // Reported-bounds height only; paragraph height always falls out of the text.
337
+ h?: number
338
+ }
339
+
340
+ /** See {@link PositionProps}: detached-only, never affects layout. */
341
+ export interface LineGeometryProps {
342
+ /** Endpoints default to spanning the box: (0,0) to (box width, box height). */
343
+ x1?: number
344
+ y1?: number
345
+ x2?: number
346
+ y2?: number
347
+ }
348
+
272
349
  // Primitives
273
350
 
274
351
  export interface WindowProps extends LayoutProps, PointerProps {
@@ -298,7 +375,7 @@ export interface WindowProps extends LayoutProps, PointerProps {
298
375
  */
299
376
  export interface WindowShaderProps {
300
377
  /** Linked program handle from linkProgram. */
301
- program: number
378
+ program: ProgramId
302
379
  /**
303
380
  * Uniforms filled by name, paced to the next real repaint. A number drives
304
381
  * a scalar (`float`/`int`); a flat number array drives the declared GLSL
@@ -306,7 +383,7 @@ export interface WindowShaderProps {
306
383
  */
307
384
  params?: Record<string, number | number[]>
308
385
  /** Extra sampler2D inputs: uniform name to texture id. */
309
- textures?: Record<string, number>
386
+ textures?: Record<string, TextureId>
310
387
  /** Vertices drawn (attributeless triangles). Default 3, the covering triangle. */
311
388
  vertexCount?: number
312
389
  /**
@@ -320,9 +397,23 @@ export interface WindowShaderProps {
320
397
  previous?: boolean
321
398
  }
322
399
 
323
- export interface ViewProps extends LayoutProps, TransformProps, PointerProps {
400
+ // Everything a view offers besides layout: d-view uses this directly (a
401
+ // detached view has no taffy presence, so layout props would be rejected at
402
+ // runtime); the layout `view` adds LayoutProps below.
403
+ export interface ViewOwnProps extends TransformProps, PointerProps {
324
404
  children?: Children
325
405
  trace?: boolean
406
+ /**
407
+ * Design-space size `[w, h]` for the children: content drawn in that
408
+ * coordinate space is uniformly scaled to fit and centered in the element's
409
+ * box (SVG's default preserveAspectRatio, generalized). A pure fit
410
+ * transform - it never sizes the element, so give the box its size with
411
+ * layout props. Composed innermost: the transform props still operate in
412
+ * box space, and pointer events on children arrive in design coordinates.
413
+ * The natural wrapper for parseSvg draws, or any d-* subtree authored in
414
+ * fixed design units.
415
+ */
416
+ viewBox?: [number, number]
326
417
  /**
327
418
  * Corner radii for the clip applied when overflow is non-visible (hidden,
328
419
  * clip, scroll on both axes). A single number rounds all four corners; an
@@ -349,54 +440,38 @@ export interface ViewProps extends LayoutProps, TransformProps, PointerProps {
349
440
  repaintBoundary?: boolean | "snapshot" | "snapshot-no-aa"
350
441
  }
351
442
 
443
+ export interface ViewProps extends ViewOwnProps, LayoutProps {}
444
+
352
445
  // draw primitives
353
446
 
354
- export interface RectProps extends Position, PaintProps, PointerProps {
355
- w?: number
356
- h?: number
357
- // Corner radius. A single number applies to all four corners; an array is
358
- // [top-left, top-right, bottom-right, bottom-left] (CSS border-radius order).
447
+ // A stroked rect paints inside its box, like a CSS border: the stroke's outer
448
+ // edge sits on the box edge rather than straddling it, so nothing bleeds past
449
+ // the box for a clip to cut. `path` and `line` strokes stay centered on their
450
+ // geometry - there the geometry is the stroke, not a box.
451
+ export interface RectProps extends PaintProps, PointerProps {
452
+ // Corner radius, measured on the box (the stroke's outer edge). A single
453
+ // number applies to all four corners; an array is [top-left, top-right,
454
+ // bottom-right, bottom-left] (CSS border-radius order).
359
455
  radius?: number | [number, number, number, number]
360
456
  }
361
457
 
362
- export interface OvalProps extends Position, PaintProps, PointerProps {
363
- /** Bounding box width of the ellipse (not a radius); defaults to the layout box. */
364
- w?: number
365
- /** Bounding box height of the ellipse (not a radius); defaults to the layout box. */
366
- h?: number
367
- }
458
+ // Strokes paint inside the box, same as `RectProps`.
459
+ export interface OvalProps extends PaintProps, PointerProps {}
368
460
 
369
461
  export interface LineProps extends PaintProps, PointerProps {
370
- x1?: number
371
- y1?: number
372
- x2?: number
373
- y2?: number
374
462
  /** 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. */
375
463
  onLength?: number
376
464
  /** Dash pattern in local units: the gap length. Both onLength and offLength must be set to dash; with either unset the line is solid. */
377
465
  offLength?: number
378
466
  }
379
467
 
380
- export interface PathProps extends Position, PaintProps, PointerProps {
468
+ export interface PathProps extends PaintProps, PointerProps {
381
469
  d?: string
382
470
  fillRule?: "nonzero" | "evenodd"
383
471
  }
384
472
 
385
- export interface SvgProps extends Position, PointerProps {
386
- // A whole SVG document as a string (an imported asset, a fetched string, or a
387
- // template literal). Parsed and rendered as one unit; takes no JSX children.
388
- src?: string
389
- // Drives currentColor in the document. Explicit fills/strokes still win.
390
- color?: Color
391
- }
392
-
393
- export interface TextProps extends Position, PaintProps, PointerProps {
473
+ export interface TextProps extends PaintProps, PointerProps {
394
474
  children?: Children
395
- // Shaping (wrap) width. Detached text wraps at the inherited ancestor size
396
- // by default; set w for an unwrapped natural line or an explicit wrap width.
397
- w?: number
398
- // Reported-bounds height only; paragraph height always falls out of the text.
399
- h?: number
400
475
  fontFamily?: "sans" | "serif" | "mono" | (string & {})
401
476
  fontSize?: number
402
477
  /**
@@ -420,8 +495,8 @@ export interface TextProps extends Position, PaintProps, PointerProps {
420
495
  * Texture alpha is premultiplied, so additive modes need no manual
421
496
  * premultiplication.
422
497
  */
423
- export interface TextureProps extends Position, PaintProps, PointerProps {
424
- src?: number
498
+ export interface TextureProps extends PaintProps, PointerProps {
499
+ src?: TextureId
425
500
  /**
426
501
  * How the texture's pixels map to the element box (CSS object-fit).
427
502
  * "fill" (default) stretches; "cover" and "none" crop; "contain" and
@@ -430,8 +505,6 @@ export interface TextureProps extends Position, PaintProps, PointerProps {
430
505
  * bars and "cover" cropped edges still hit-test as part of the element.
431
506
  */
432
507
  fit?: "fill" | "cover" | "contain" | "none" | "scale-down"
433
- w?: number
434
- h?: number
435
508
  srcX?: number
436
509
  srcY?: number
437
510
  srcW?: number
package/src/window.ts CHANGED
@@ -3,8 +3,8 @@ import { requestFrame } 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"
6
- import { getEventHandler, getFocusedNodeId, setFocus } from "./core"
7
- import { scanForOrphans } from "./renderer"
6
+ import { getEventHandler, focusedNode, setFocus, activateTextInput, setInterestRoot } from "./core"
7
+ import { scanForOrphans, getNodePath } from "./renderer"
8
8
 
9
9
  /**
10
10
  * Leaves the current app, unconditionally: back to the launcher in a dev
@@ -228,7 +228,10 @@ export function onBack(fn: (e: BackEvent) => void) {
228
228
 
229
229
  // ------ Window ----------------
230
230
 
231
- export function attachWindow(_nodeId: number) {
231
+ export function attachWindow(nodeId: number) {
232
+ // The root carries the ambient move-interest bit for global onPointerMove
233
+ // subscribers (it is on every hit path); see core.setInterestRoot.
234
+ setInterestRoot(nodeId)
232
235
  let unsubscribe: () => void = null!
233
236
  let unsubDown: () => void = null!
234
237
  let unsubUp: () => void = null!
@@ -304,11 +307,16 @@ export function attachWindow(_nodeId: number) {
304
307
 
305
308
  unsubDown = on("pointerDown", (raw: RawPointer) => {
306
309
  bubble(raw, "onPointerDown")
307
- // Outside-tap blur. Read focus AFTER per-node handlers so a tap that
308
- // moves focus to a new node is not immediately blurred again.
309
- let focused = getFocusedNodeId()
310
+ // Read focus AFTER per-node handlers so a tap that moves focus to a new
311
+ // node is not immediately blurred again.
312
+ let focused = focusedNode()
310
313
  if (focused != null && !raw.targets.includes(focused)) {
314
+ // Outside-tap blur.
311
315
  setFocus(null)
316
+ } else if (focused != null) {
317
+ // A tap on the focused node is the interaction that lets a pending
318
+ // text session raise the on-screen keyboard.
319
+ activateTextInput()
312
320
  }
313
321
  })
314
322
 
@@ -332,19 +340,31 @@ export function attachWindow(_nodeId: number) {
332
340
  bubble(raw, "onWheel")
333
341
  })
334
342
 
335
- unsubKeyDown = on("keydown", (e: any) => {
336
- let id = getFocusedNodeId()
337
- if (id != null) {
338
- getEventHandler(id, "onKeyDown")?.(e)
343
+ // Key events dispatch along the focused node's ancestor chain, leaf->root
344
+ // (the pointer bubbling contract), so a container hears keys from focused
345
+ // descendants and the window root hears everything: <window onKeyDown> is
346
+ // the app-global shortcut point. With nothing focused the path is the
347
+ // window root alone - key events are never dropped. The path is resolved
348
+ // at dispatch time from current focus (nothing to freeze: keyup follows
349
+ // focus, as in the DOM).
350
+ let dispatchKey = (raw: any, handler: string) => {
351
+ let target = focusedNode() ?? nodeId
352
+ let stopped = false
353
+ let e = { ...raw, target, stopPropagation: () => (stopped = true) }
354
+ let path = getNodePath(target)
355
+ // A focused node detached this tick has no chain to the root; the
356
+ // window root must still hear the key.
357
+ if (path[path.length - 1] !== nodeId) path.push(nodeId)
358
+ for (let id of path) {
359
+ e.currentTarget = id
360
+ getEventHandler(id, handler)?.(e)
361
+ if (stopped) break
339
362
  }
340
- })
363
+ }
341
364
 
342
- unsubKeyUp = on("keyup", (e: any) => {
343
- let id = getFocusedNodeId()
344
- if (id != null) {
345
- getEventHandler(id, "onKeyUp")?.(e)
346
- }
347
- })
365
+ unsubKeyDown = on("keydown", (raw: any) => dispatchKey(raw, "onKeyDown"))
366
+
367
+ unsubKeyUp = on("keyup", (raw: any) => dispatchKey(raw, "onKeyUp"))
348
368
 
349
369
  unsubBack = on("back", () => {
350
370
  let prevented = false
@@ -361,7 +381,7 @@ export function attachWindow(_nodeId: number) {
361
381
  })
362
382
 
363
383
  unsubTextInput = on("textInput", (e: any) => {
364
- let id = getFocusedNodeId()
384
+ let id = focusedNode()
365
385
  if (id != null) {
366
386
  getEventHandler(id, "onTextInput")?.(e)
367
387
  }
@@ -386,6 +406,7 @@ export function attachWindow(_nodeId: number) {
386
406
  })
387
407
 
388
408
  onCleanup(() => {
409
+ setInterestRoot(null)
389
410
  if (unsubscribe) unsubscribe()
390
411
  if (unsubDown) unsubDown()
391
412
  if (unsubUp) unsubUp()
package/examples/svg.tsx DELETED
@@ -1,49 +0,0 @@
1
- // <svg> draws a whole SVG *document* passed as a STRING in the `src` prop. This
2
- // is not HTML: there are no per-element <rect>/<circle>/<path> JSX children to
3
- // nest. You hand it the SVG source text and usvg parses it (CSS, transforms,
4
- // defs/use, gradients, clips) into a flat path tree the element renders. So an
5
- // SVG is a value (a string you import, fetch, or inline), not markup you author
6
- // inline with JSX.
7
- //
8
- // width/height set the drawn box (percentages work too); a multi-color document
9
- // keeps each shape's own fill, while a monochrome icon using stroke/fill
10
- // "currentColor" is recolored by the host `color` prop. For per-shape authored
11
- // or animated vector art, compose <d-path> instead of this document layer.
12
- //
13
- // Being a vector, an <svg> is resolution-independent: it stays crisp at any
14
- // drawn size x displayScale(). Prefer it over a raster <texture> (image.tsx)
15
- // whenever the render size is fluid or the display DPI varies.
16
- //
17
- // This is how you use an existing icon library (Lucide, Heroicons, Feather,
18
- // Material, etc.): those ship SVG source, so import/inline the icon string and
19
- // hand it to `src`. The `currentColor` convention they follow means the `color`
20
- // prop recolors them, exactly as `currentColor` would in a browser.
21
- import { render } from "@solidrt/core"
22
-
23
- // Multi-color document: each shape carries its own fill, no host color needed.
24
- const HOUSE = `
25
- <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
26
- <rect x="20" y="45" width="60" height="45" fill="#457b9d"/>
27
- <path d="M10 50 L50 15 L90 50 Z" fill="#e63946"/>
28
- <rect x="42" y="62" width="16" height="28" fill="#f1faee"/>
29
- <circle cx="50" cy="35" r="6" fill="#ffd166"/>
30
- </svg>`
31
-
32
- // Monochrome icon (Lucide arrow-right) drawn with currentColor, recolored below.
33
- const ARROW = `
34
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
35
- stroke-linecap="round" stroke-linejoin="round">
36
- <path d="M5 12h14"/>
37
- <path d="M12 5l7 7-7 7"/>
38
- </svg>`
39
-
40
- function App() {
41
- return (
42
- <window justifyContent="center" alignItems="center" flexDirection="row" gap={32}>
43
- <svg width={120} height={120} src={HOUSE} />
44
- <svg width={120} height={120} src={ARROW} color="#4f8cff" />
45
- </window>
46
- )
47
- }
48
-
49
- render(() => <App />)