@solidrt/core 0.0.55 → 0.0.56
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 +51 -18
- package/agents/painting.md +29 -12
- package/agents/performance.md +14 -3
- package/docs/reference/detached.md +8 -5
- package/docs/reference/drawing.md +1 -1
- package/docs/reference/elements.md +3 -1
- package/docs/reference/gpu.md +12 -1
- package/docs/reference/layout.md +28 -0
- package/examples/detached-positioning.tsx +3 -1
- package/examples/view-design-size.tsx +5 -0
- package/package.json +6 -5
- package/src/core.ts +15 -2
- package/src/environment.ts +87 -69
- package/src/gamepad.ts +10 -8
- package/src/gpu.ts +122 -18
- package/src/index.ts +2 -2
- package/src/pan.ts +20 -5
- package/src/runtime-modules.d.ts +7 -7
- package/src/text-input.ts +43 -8
- package/src/transform.ts +52 -23
- package/src/types.d.ts +80 -13
- package/src/window.ts +54 -38
package/src/text-input.ts
CHANGED
|
@@ -214,6 +214,9 @@ export type EditorLine = {
|
|
|
214
214
|
/** The caret's box in content coordinates (before scroll). */
|
|
215
215
|
export type CaretRect = { x: number; y: number; height: number }
|
|
216
216
|
|
|
217
|
+
/** One line's slice of a selection highlight, in content coordinates (before scroll). */
|
|
218
|
+
export type SelectionRect = { x: number; y: number; width: number; height: number }
|
|
219
|
+
|
|
217
220
|
export type TextEditorLayout = {
|
|
218
221
|
lines(): EditorLine[]
|
|
219
222
|
caret(): CaretRect
|
|
@@ -221,6 +224,8 @@ export type TextEditorLayout = {
|
|
|
221
224
|
caretLine(): number
|
|
222
225
|
/** The caret position (grapheme boundary) on line `line` nearest to content x. */
|
|
223
226
|
offsetAtX(line: number, x: number): number
|
|
227
|
+
/** Highlight boxes for the anchor..focus range, one per touched line; empty when collapsed. */
|
|
228
|
+
selectionRects(anchor: number, focus: number): SelectionRect[]
|
|
224
229
|
/** Index of the line at content y (clamped to the first/last line). */
|
|
225
230
|
lineAtY(y: number): number
|
|
226
231
|
/** The caret position one grapheme left/right of `offset`; a break sequence is one step. For createTextBuffer's `step`. */
|
|
@@ -322,18 +327,22 @@ export function createTextEditorLayout(
|
|
|
322
327
|
}
|
|
323
328
|
let caretLine = createMemo(() => lineOf(input().caret))
|
|
324
329
|
|
|
325
|
-
// The
|
|
326
|
-
// a grapheme, e.g. from a controlled value, snaps back).
|
|
327
|
-
let
|
|
328
|
-
let offset = input().caret
|
|
329
|
-
let index = caretLine()
|
|
330
|
-
let line = lines()[index]!
|
|
330
|
+
// The x of the last stop at or before `offset` on line `index` (an offset
|
|
331
|
+
// inside a grapheme, e.g. from a controlled value, snaps back).
|
|
332
|
+
let xAt = (index: number, offset: number): number => {
|
|
331
333
|
let x = 0
|
|
332
334
|
for (let stop of lineStops(index)) {
|
|
333
335
|
if (stop.offset > offset) break
|
|
334
336
|
x = stop.x
|
|
335
337
|
}
|
|
336
|
-
return
|
|
338
|
+
return x
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let caret = createMemo((): CaretRect => {
|
|
342
|
+
let offset = input().caret
|
|
343
|
+
let index = caretLine()
|
|
344
|
+
let line = lines()[index]!
|
|
345
|
+
return { x: xAt(index, offset), y: line.y, height: line.height }
|
|
337
346
|
})
|
|
338
347
|
|
|
339
348
|
// Only positions that show on this line are candidates: a boundary offset
|
|
@@ -352,6 +361,32 @@ export function createTextEditorLayout(
|
|
|
352
361
|
return best
|
|
353
362
|
}
|
|
354
363
|
|
|
364
|
+
// One box per line the range touches, offsets snapped to stops like the
|
|
365
|
+
// caret. A line the selection continues past (every one but the last)
|
|
366
|
+
// highlights to its ink end plus a space width, so the selected break - a
|
|
367
|
+
// newline, or the wrap the range crosses - is visible, and a selected empty
|
|
368
|
+
// line shows as that sliver alone. The last line ends at the focus stop; a
|
|
369
|
+
// range ending on a wrap boundary owns a zero-width box there (the boundary
|
|
370
|
+
// offset displays on the next line) and drops it.
|
|
371
|
+
let selectionRects = (anchor: number, focus: number): SelectionRect[] => {
|
|
372
|
+
let start = Math.min(anchor, focus)
|
|
373
|
+
let end = Math.max(anchor, focus)
|
|
374
|
+
if (start >= end) return []
|
|
375
|
+
let ls = lines()
|
|
376
|
+
let first = lineOf(start)
|
|
377
|
+
let last = lineOf(end)
|
|
378
|
+
let breakWidth = measureText(" ", input().font).width
|
|
379
|
+
let out: SelectionRect[] = []
|
|
380
|
+
for (let i = first; i <= last; i++) {
|
|
381
|
+
let line = ls[i]!
|
|
382
|
+
let x = i === first ? xAt(i, start) : 0
|
|
383
|
+
let width = i === last ? xAt(i, end) - x : Math.max(line.width - x, 0) + breakWidth
|
|
384
|
+
if (width <= 0) continue
|
|
385
|
+
out.push({ x, y: line.y, width, height: line.height })
|
|
386
|
+
}
|
|
387
|
+
return out
|
|
388
|
+
}
|
|
389
|
+
|
|
355
390
|
let lineAtY = (y: number): number => {
|
|
356
391
|
let ls = lines()
|
|
357
392
|
let index = 0
|
|
@@ -399,7 +434,7 @@ export function createTextEditorLayout(
|
|
|
399
434
|
setScrollY(follow(scrollY(), c.y, c.height, vh, contentHeight))
|
|
400
435
|
})
|
|
401
436
|
|
|
402
|
-
return { lines, caret, caretLine, offsetAtX, lineAtY, step, scrollX, scrollY }
|
|
437
|
+
return { lines, caret, caretLine, offsetAtX, selectionRects, lineAtY, step, scrollX, scrollY }
|
|
403
438
|
}
|
|
404
439
|
|
|
405
440
|
// Wrap units wider than `width` (through their glued pieces) split into one
|
package/src/transform.ts
CHANGED
|
@@ -26,14 +26,18 @@ const ENGAGE = 0.6
|
|
|
26
26
|
const QUIET_SMOOTH = 0.19
|
|
27
27
|
|
|
28
28
|
export type TransformDelta = {
|
|
29
|
-
/** Focal-point movement since the previous frame,
|
|
29
|
+
/** Focal-point movement since the previous frame, in the handler node's
|
|
30
|
+
* PARENT frame (the frame its own x/y live in: window pixels under no
|
|
31
|
+
* scaling ancestor, design pixels under a designSize fit), so it applies
|
|
32
|
+
* 1:1 to the node or to content inside it. */
|
|
30
33
|
dx: number
|
|
31
34
|
dy: number
|
|
32
35
|
/** Multiplicative span change since the previous frame (1 = unchanged, >1 = fingers spreading). */
|
|
33
36
|
scale: number
|
|
34
37
|
/** Rotation of the pointer pair since the previous frame, radians (0 with a single pointer). */
|
|
35
38
|
rotation: number
|
|
36
|
-
/** Current focal point in
|
|
39
|
+
/** Current focal point in the handler node's LOCAL frame (its own
|
|
40
|
+
* pixels) - the zoom-about anchor for content inside it. */
|
|
37
41
|
x: number
|
|
38
42
|
y: number
|
|
39
43
|
/** How many pointers are down right now. Consumers that give one- and
|
|
@@ -106,12 +110,25 @@ export interface TransformOptions {
|
|
|
106
110
|
// path, so an active transform survives leaving the node or the window.
|
|
107
111
|
// cancel() is the external-cancel hook; it ends an active gesture without
|
|
108
112
|
// onTransformEnd. Options are read at event time.
|
|
113
|
+
//
|
|
114
|
+
// Frames: the slop, the span filter and its rate gates are finger-travel
|
|
115
|
+
// thresholds, so they are measured in window pixels (clientX/clientY);
|
|
116
|
+
// the focal movement (dx/dy) and the pair angle are measured in the node's
|
|
117
|
+
// parent frame (parentX/parentY), the frame the node's own coordinates
|
|
118
|
+
// live in, so a scaled ancestor (a designSize fit) is accounted for and
|
|
119
|
+
// the delta applies unchanged to the node or to content inside it; the
|
|
120
|
+
// focal point (x/y) is reported in the node's local frame (localX/localY),
|
|
121
|
+
// the anchor a zoom-about-the-fingers needs. Scale and rotation are ratios
|
|
122
|
+
// and angles, the same in every uniformly scaled frame. The event carries
|
|
123
|
+
// all three frames exactly, on and off the node.
|
|
109
124
|
export function createTransform(options: TransformOptions) {
|
|
125
|
+
type Tracked = { cx: number; cy: number; px: number; py: number; lx: number; ly: number }
|
|
126
|
+
let at = (e: PointerEvent): Tracked => ({ cx: e.clientX, cy: e.clientY, px: e.parentX, py: e.parentY, lx: e.localX, ly: e.localY })
|
|
110
127
|
// All tracked pointers, in down order (the first two define the rotation pair).
|
|
111
|
-
let pointers = new Map<number,
|
|
128
|
+
let pointers = new Map<number, Tracked>()
|
|
112
129
|
let active = false
|
|
113
130
|
// Slop baseline while armed; last delivered configuration while active.
|
|
114
|
-
let ref: {
|
|
131
|
+
let ref: { cx: number; cy: number; px: number; py: number; span: number; angle: number } | null = null
|
|
115
132
|
// The zoom gate and filter (see the zoom-noise note above): scale stays 1
|
|
116
133
|
// until the SMOOTHED span leaves spanBase by SLOP, then streams smoothed
|
|
117
134
|
// ratios for the rest of the gesture.
|
|
@@ -128,27 +145,41 @@ export function createTransform(options: TransformOptions) {
|
|
|
128
145
|
// emits nothing.
|
|
129
146
|
let rebase = false
|
|
130
147
|
|
|
148
|
+
// Centroids in all three frames, the span in window pixels (it feeds the
|
|
149
|
+
// finger-travel gates), the pair angle in the parent frame.
|
|
131
150
|
let measure = () => {
|
|
132
151
|
let n = pointers.size
|
|
133
|
-
let
|
|
134
|
-
let
|
|
152
|
+
let cx = 0
|
|
153
|
+
let cy = 0
|
|
154
|
+
let px = 0
|
|
155
|
+
let py = 0
|
|
156
|
+
let lx = 0
|
|
157
|
+
let ly = 0
|
|
135
158
|
for (let p of pointers.values()) {
|
|
136
|
-
|
|
137
|
-
|
|
159
|
+
cx += p.cx
|
|
160
|
+
cy += p.cy
|
|
161
|
+
px += p.px
|
|
162
|
+
py += p.py
|
|
163
|
+
lx += p.lx
|
|
164
|
+
ly += p.ly
|
|
138
165
|
}
|
|
139
|
-
|
|
140
|
-
|
|
166
|
+
cx /= n
|
|
167
|
+
cy /= n
|
|
168
|
+
px /= n
|
|
169
|
+
py /= n
|
|
170
|
+
lx /= n
|
|
171
|
+
ly /= n
|
|
141
172
|
let span = 0
|
|
142
|
-
for (let p of pointers.values()) span += Math.hypot(p.
|
|
173
|
+
for (let p of pointers.values()) span += Math.hypot(p.cx - cx, p.cy - cy)
|
|
143
174
|
span /= n
|
|
144
175
|
let angle = 0
|
|
145
176
|
if (n >= 2) {
|
|
146
177
|
let pair = pointers.values()
|
|
147
178
|
let a = pair.next().value!
|
|
148
179
|
let b = pair.next().value!
|
|
149
|
-
angle = Math.atan2(b.
|
|
180
|
+
angle = Math.atan2(b.py - a.py, b.px - a.px)
|
|
150
181
|
}
|
|
151
|
-
return {
|
|
182
|
+
return { cx, cy, px, py, lx, ly, span, angle }
|
|
152
183
|
}
|
|
153
184
|
|
|
154
185
|
let reset = () => {
|
|
@@ -202,10 +233,10 @@ export function createTransform(options: TransformOptions) {
|
|
|
202
233
|
if (rotation > Math.PI) rotation -= 2 * Math.PI
|
|
203
234
|
else if (rotation < -Math.PI) rotation += 2 * Math.PI
|
|
204
235
|
}
|
|
205
|
-
let dx = m.
|
|
206
|
-
let dy = m.
|
|
236
|
+
let dx = m.px - ref.px
|
|
237
|
+
let dy = m.py - ref.py
|
|
207
238
|
ref = m
|
|
208
|
-
options.onTransformMove?.({ dx, dy, scale, rotation, x: m.
|
|
239
|
+
options.onTransformMove?.({ dx, dy, scale, rotation, x: m.lx, y: m.ly, pointers: pointers.size })
|
|
209
240
|
}
|
|
210
241
|
|
|
211
242
|
// An unmount mid-gesture must not leave resolved claims behind (or a live
|
|
@@ -226,24 +257,22 @@ export function createTransform(options: TransformOptions) {
|
|
|
226
257
|
// A finger joining an established gesture belongs to it outright; if
|
|
227
258
|
// the arena refuses (resolved elsewhere) the finger stays out.
|
|
228
259
|
if (!arena.steal(e.pointerId, owner)) return
|
|
229
|
-
pointers.set(e.pointerId,
|
|
260
|
+
pointers.set(e.pointerId, at(e))
|
|
230
261
|
rebase = true
|
|
231
262
|
return
|
|
232
263
|
}
|
|
233
|
-
pointers.set(e.pointerId,
|
|
264
|
+
pointers.set(e.pointerId, at(e))
|
|
234
265
|
ref = measure()
|
|
235
266
|
},
|
|
236
267
|
onPointerMove: (e: PointerEvent) => {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
p.x = e.clientX
|
|
240
|
-
p.y = e.clientY
|
|
268
|
+
if (!pointers.has(e.pointerId) || !ref) return
|
|
269
|
+
pointers.set(e.pointerId, at(e))
|
|
241
270
|
if (!active) {
|
|
242
271
|
// Arming runs per event: the slop test tolerates a mixed-age
|
|
243
272
|
// measure (8px against <=1 frame of staleness), and the arena
|
|
244
273
|
// steal must happen synchronously inside the dispatch walk.
|
|
245
274
|
let m = measure()
|
|
246
|
-
let travel = Math.hypot(m.
|
|
275
|
+
let travel = Math.hypot(m.cx - ref.cx, m.cy - ref.cy)
|
|
247
276
|
if (travel < SLOP && Math.abs(m.span - ref.span) < SLOP) return
|
|
248
277
|
let taken: number[] = []
|
|
249
278
|
let refused = false
|
package/src/types.d.ts
CHANGED
|
@@ -175,6 +175,48 @@ export interface PaintProps {
|
|
|
175
175
|
strokeWidth?: number
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
/**
|
|
179
|
+
* A drop shadow behind a shape (CSS box-shadow field semantics, one shadow):
|
|
180
|
+
* the shape's outer geometry, offset by x/y, grown by `spread`, softened by
|
|
181
|
+
* `blur` (a CSS-style radius in logical px), painted in `color` under the
|
|
182
|
+
* shape. It casts from the shape's outer box whatever the draw style (strokes
|
|
183
|
+
* paint inside, so the box is the outer edge) - except on `path`, where the
|
|
184
|
+
* shadow mirrors the element's own fill/stroke silhouette and `spread` is
|
|
185
|
+
* rejected (an arbitrary path cannot be inflated exactly). `color` is
|
|
186
|
+
* required; there is no currentColor to inherit.
|
|
187
|
+
*/
|
|
188
|
+
export interface ShadowProps {
|
|
189
|
+
x?: number
|
|
190
|
+
y?: number
|
|
191
|
+
blur?: number
|
|
192
|
+
spread?: number
|
|
193
|
+
color: Color
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* A subtree filter (the CSS filter functions, object form): the view's
|
|
198
|
+
* children are composited together, run through the set color operations -
|
|
199
|
+
* fused into one color matrix, applied in the fixed order grayscale, sepia,
|
|
200
|
+
* saturate, hueRotate, brightness, contrast, invert - and then blurred.
|
|
201
|
+
* `blur` is a CSS-style radius in logical px; `hueRotate` is radians (this
|
|
202
|
+
* API's angle convention); the rest are the CSS amounts (1 = unchanged for
|
|
203
|
+
* saturate/brightness/contrast, 0 = unchanged for the others; grayscale,
|
|
204
|
+
* sepia and invert saturate at 1). There is no `opacity` key - the view's
|
|
205
|
+
* own `opacity` prop is the same compositing layer. Like opacity, a filter
|
|
206
|
+
* on a non-boundary view costs a save_layer; on a repaintBoundary view it
|
|
207
|
+
* rides the composite for free. Hit testing is unaffected.
|
|
208
|
+
*/
|
|
209
|
+
export interface FilterProps {
|
|
210
|
+
blur?: number
|
|
211
|
+
grayscale?: number
|
|
212
|
+
sepia?: number
|
|
213
|
+
saturate?: number
|
|
214
|
+
hueRotate?: number
|
|
215
|
+
brightness?: number
|
|
216
|
+
contrast?: number
|
|
217
|
+
invert?: number
|
|
218
|
+
}
|
|
219
|
+
|
|
178
220
|
/** A percentage value, from `pct(50)`. Resolves against the element box. */
|
|
179
221
|
export type Pct = { readonly __unit: "pct"; v: number }
|
|
180
222
|
|
|
@@ -607,11 +649,13 @@ export interface WindowShaderProps {
|
|
|
607
649
|
program: ProgramId
|
|
608
650
|
/**
|
|
609
651
|
* Uniforms filled by name, paced to the next real repaint. A number drives
|
|
610
|
-
* a scalar (`float`/`int`); a flat number array
|
|
611
|
-
* type: 2/3/4 for
|
|
652
|
+
* a scalar (`float`/`int`); a flat number array (or a Float32Array /
|
|
653
|
+
* Float64Array) drives the declared GLSL type: 2/3/4 for
|
|
654
|
+
* `vec2`/`vec3`/`vec4`, 16 (column-major) for `mat4`. An undefined or null
|
|
655
|
+
* entry is skipped.
|
|
612
656
|
*/
|
|
613
|
-
params?: Record<string, number | number[]>
|
|
614
|
-
/** Extra sampler2D inputs: uniform name to texture id, or `{ id, filter?, wrap? }` for a per-binding sampling override. */
|
|
657
|
+
params?: Record<string, number | number[] | Float32Array | Float64Array>
|
|
658
|
+
/** Extra sampler2D inputs: uniform name to texture id, or `{ id, filter?, wrap? }` for a per-binding sampling override. An undefined or null entry is skipped. */
|
|
615
659
|
textures?: TextureBindings
|
|
616
660
|
/** Vertices drawn (attributeless triangles). Default 3, the covering triangle. */
|
|
617
661
|
vertexCount?: number
|
|
@@ -671,7 +715,9 @@ export interface ViewOwnProps extends TransformProps, PointerProps {
|
|
|
671
715
|
* skipping rasterization entirely. Costs texture memory and re-rasterizes
|
|
672
716
|
* on layout-size or display-scale changes. Content painted outside the
|
|
673
717
|
* element's layout box is cropped, and ancestor scale animations smear the
|
|
674
|
-
* bitmap; best for screen-aligned, static, raster-expensive content.
|
|
718
|
+
* bitmap; best for screen-aligned, static, raster-expensive content. On a
|
|
719
|
+
* d-view the box is the inherited frame (the nearest laid-out ancestor's
|
|
720
|
+
* box, or the design size under a designSize view), same as a capture.
|
|
675
721
|
*
|
|
676
722
|
* "snapshot-no-aa" is "snapshot" rasterized without anti-aliasing: cheaper
|
|
677
723
|
* (no multisampled scratch, one render pass), but vector content - svg
|
|
@@ -701,6 +747,19 @@ export interface ViewOwnProps extends TransformProps, PointerProps {
|
|
|
701
747
|
* unrotated, opaque content.
|
|
702
748
|
*/
|
|
703
749
|
shader?: ViewShaderProps | null
|
|
750
|
+
filter?: FilterProps
|
|
751
|
+
/**
|
|
752
|
+
* Filters the pixels already painted BENEATH the view's box, in place,
|
|
753
|
+
* before the view's own content draws - frosted glass over live content
|
|
754
|
+
* (CSS `backdrop-filter`). Same keys and semantics as `filter`. The
|
|
755
|
+
* region is the layout box; give the view an overflow clip with
|
|
756
|
+
* `clipRadius` for rounded glass. Costs an offscreen capture of the
|
|
757
|
+
* pixels beneath at that point in the frame, so treat it as a deliberate
|
|
758
|
+
* panel, not a casual style. Inside a `repaintBoundary="snapshot"`
|
|
759
|
+
* subtree it sees only that boundary's own offscreen content, never what
|
|
760
|
+
* is behind the boundary (the same containment `blendMode` has there).
|
|
761
|
+
*/
|
|
762
|
+
backdropFilter?: FilterProps
|
|
704
763
|
}
|
|
705
764
|
|
|
706
765
|
/**
|
|
@@ -716,11 +775,13 @@ export interface ViewShaderProps {
|
|
|
716
775
|
program: ProgramId
|
|
717
776
|
/**
|
|
718
777
|
* Uniforms filled by name, paced to the next real repaint. A number drives
|
|
719
|
-
* a scalar (`float`/`int`); a flat number array
|
|
720
|
-
* type: 2/3/4 for
|
|
778
|
+
* a scalar (`float`/`int`); a flat number array (or a Float32Array /
|
|
779
|
+
* Float64Array) drives the declared GLSL type: 2/3/4 for
|
|
780
|
+
* `vec2`/`vec3`/`vec4`, 16 (column-major) for `mat4`. An undefined or null
|
|
781
|
+
* entry is skipped.
|
|
721
782
|
*/
|
|
722
|
-
params?: Record<string, number | number[]>
|
|
723
|
-
/** Extra sampler2D inputs: uniform name to texture id, or `{ id, filter?, wrap? }` for a per-binding sampling override. */
|
|
783
|
+
params?: Record<string, number | number[] | Float32Array | Float64Array>
|
|
784
|
+
/** Extra sampler2D inputs: uniform name to texture id, or `{ id, filter?, wrap? }` for a per-binding sampling override. An undefined or null entry is skipped. */
|
|
724
785
|
textures?: TextureBindings
|
|
725
786
|
/**
|
|
726
787
|
* Transparent margin in logical px on every side of the layout box, for
|
|
@@ -761,10 +822,13 @@ export interface RectProps extends PaintProps, PointerProps, DashProps {
|
|
|
761
822
|
// number applies to all four corners; an array is [top-left, top-right,
|
|
762
823
|
// bottom-right, bottom-left] (CSS border-radius order).
|
|
763
824
|
radius?: number | [number, number, number, number]
|
|
825
|
+
shadow?: ShadowProps
|
|
764
826
|
}
|
|
765
827
|
|
|
766
828
|
// Strokes paint inside the box, same as `RectProps`.
|
|
767
|
-
export interface OvalProps extends PaintProps, PointerProps, DashProps {
|
|
829
|
+
export interface OvalProps extends PaintProps, PointerProps, DashProps {
|
|
830
|
+
shadow?: ShadowProps
|
|
831
|
+
}
|
|
768
832
|
|
|
769
833
|
/**
|
|
770
834
|
* A stroke's dash pattern, on every stroked primitive (`rect`, `oval`,
|
|
@@ -847,6 +911,8 @@ export interface LineProps extends PaintProps, PointerProps, DashProps {
|
|
|
847
911
|
export interface PathProps extends PaintProps, PointerProps, DashProps {
|
|
848
912
|
d?: string
|
|
849
913
|
fillRule?: "nonzero" | "evenodd"
|
|
914
|
+
/** Shadows the drawn silhouette (fill and/or stroke); `spread` is rejected here. */
|
|
915
|
+
shadow?: ShadowProps
|
|
850
916
|
}
|
|
851
917
|
|
|
852
918
|
export type FontFamily = "sans" | "serif" | "mono" | (string & {})
|
|
@@ -957,7 +1023,8 @@ export interface TextureProps extends PaintProps, PointerProps {
|
|
|
957
1023
|
// throws, and set src before params: a write with no src to route to
|
|
958
1024
|
// throws). However often a signal writes, the target renders once per
|
|
959
1025
|
// frame at the raster flush. A number drives a scalar (`float`/`int`); a
|
|
960
|
-
// flat number array
|
|
961
|
-
// `vec2`/`vec3`/`vec4`, 16 (column-major) for `mat4`.
|
|
962
|
-
|
|
1026
|
+
// flat number array (or a Float32Array / Float64Array) drives the declared
|
|
1027
|
+
// GLSL type: 2/3/4 for `vec2`/`vec3`/`vec4`, 16 (column-major) for `mat4`.
|
|
1028
|
+
// An undefined or null entry is skipped.
|
|
1029
|
+
params?: Record<string, number | number[] | Float32Array | Float64Array>
|
|
963
1030
|
}
|
package/src/window.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createSignal, onCleanup, onSettled, flush } from "@solidjs/signals"
|
|
1
|
+
import { createSignal, getOwner, onCleanup, onSettled, runWithOwner, flush } from "@solidjs/signals"
|
|
2
2
|
import { requestFrame, setPointerLock } from "flux:rendertree"
|
|
3
3
|
import { renderFrame } from "srt:render"
|
|
4
4
|
import { on, once } from "srt:events"
|
|
@@ -7,8 +7,8 @@ import { getEventHandler, focusedNode, setFocus, activateTextInput, setInterestR
|
|
|
7
7
|
import { scanForOrphans, getNodePath } from "./renderer"
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Leaves the current app, unconditionally: back to the
|
|
11
|
-
* client, quitting when standalone or at the
|
|
10
|
+
* Leaves the current app, unconditionally: back to the player in a dev
|
|
11
|
+
* client, quitting when standalone or at the player itself (on Android the
|
|
12
12
|
* client backgrounds instead of dying). The default action of an unprevented
|
|
13
13
|
* `back` event; call it directly to exit programmatically, e.g. after
|
|
14
14
|
* intercepting back for an unsaved-changes dialog.
|
|
@@ -44,7 +44,10 @@ let refreshRate = 60
|
|
|
44
44
|
* control, but measure their delays against the wall clock, not this paced
|
|
45
45
|
* timeline. performance.now() is not on it either: it is real elapsed time
|
|
46
46
|
* for measuring work; Date.now() is calendar time.
|
|
47
|
-
* Returns a cleanup function; also auto-cleans within
|
|
47
|
+
* Returns a cleanup function; also auto-cleans within an owned scope. Outside
|
|
48
|
+
* one - an effect's apply phase, an event handler - the returned cleanup is
|
|
49
|
+
* the only handle, so return it from the apply (`return onFrame(...)`) to run
|
|
50
|
+
* a loop only while a condition holds.
|
|
48
51
|
*/
|
|
49
52
|
export function onFrame(fn: (tick: number, frame: number, rate: number) => void) {
|
|
50
53
|
let frameId: number = null!
|
|
@@ -77,7 +80,7 @@ export function onFrame(fn: (tick: number, frame: number, rate: number) => void)
|
|
|
77
80
|
cancelled = true
|
|
78
81
|
animationFrames.delete(frameId)
|
|
79
82
|
}
|
|
80
|
-
onCleanup(cleanup)
|
|
83
|
+
if (getOwner()) onCleanup(cleanup)
|
|
81
84
|
return cleanup
|
|
82
85
|
}
|
|
83
86
|
|
|
@@ -101,7 +104,7 @@ interface ResizeEvent {
|
|
|
101
104
|
|
|
102
105
|
export function onResize(fn: (data: ResizeEvent) => void) {
|
|
103
106
|
let unsubscribe = on("resize", fn)
|
|
104
|
-
onCleanup(unsubscribe)
|
|
107
|
+
if (getOwner()) onCleanup(unsubscribe)
|
|
105
108
|
return unsubscribe
|
|
106
109
|
}
|
|
107
110
|
|
|
@@ -110,9 +113,13 @@ export function onResize(fn: (data: ResizeEvent) => void) {
|
|
|
110
113
|
// Singleton accessors over the same events as onResize / onWindowFocus. There
|
|
111
114
|
// is one window, so these are bare accessors rather than a createX instance.
|
|
112
115
|
// Lazily subscribed on first read (resize is sticky, so the first read sees the
|
|
113
|
-
// current value); app-lifetime, so no onCleanup.
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
+
// current value); app-lifetime, so no onCleanup. Each is created under no
|
|
117
|
+
// owner (runWithOwner(null)): the first read is usually a component or a
|
|
118
|
+
// computation, and a sticky event replays its cached value synchronously
|
|
119
|
+
// inside on(), so creating the signal in the reader's scope would write it
|
|
120
|
+
// there too - a render-time write, which the guard rejects. Detached, the
|
|
121
|
+
// fact belongs to the app, not to whoever read it first (the reasoning
|
|
122
|
+
// in full is on the ensure* functions in environment.ts).
|
|
116
123
|
|
|
117
124
|
let sizeAccessor: (() => { width: number; height: number }) | undefined
|
|
118
125
|
let safeAreaAccessor: (() => SafeArea) | undefined
|
|
@@ -120,17 +127,19 @@ let displayScaleAccessor: (() => number) | undefined
|
|
|
120
127
|
|
|
121
128
|
function ensureResizeState() {
|
|
122
129
|
if (sizeAccessor) return
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
runWithOwner(null, () => {
|
|
131
|
+
let [size, setSize] = createSignal({ width: 0, height: 0 })
|
|
132
|
+
let [safe, setSafe] = createSignal<SafeArea>({ top: 0, left: 0, right: 0, bottom: 0 })
|
|
133
|
+
let [scale, setScale] = createSignal(1)
|
|
134
|
+
on("resize", (e: ResizeEvent) => {
|
|
135
|
+
setSize({ width: e.width, height: e.height })
|
|
136
|
+
setSafe(e.safeArea)
|
|
137
|
+
setScale(e.displayScale)
|
|
138
|
+
})
|
|
139
|
+
sizeAccessor = size
|
|
140
|
+
safeAreaAccessor = safe
|
|
141
|
+
displayScaleAccessor = scale
|
|
130
142
|
})
|
|
131
|
-
sizeAccessor = size
|
|
132
|
-
safeAreaAccessor = safe
|
|
133
|
-
displayScaleAccessor = scale
|
|
134
143
|
}
|
|
135
144
|
|
|
136
145
|
/** Current window size, as a reactive accessor. */
|
|
@@ -156,12 +165,14 @@ let focusedAccessor: (() => boolean) | undefined
|
|
|
156
165
|
/** Whether the window currently has focus, as a reactive accessor. */
|
|
157
166
|
export function windowFocused(): boolean {
|
|
158
167
|
if (!focusedAccessor) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
168
|
+
runWithOwner(null, () => {
|
|
169
|
+
let [focused, setFocused] = createSignal(true)
|
|
170
|
+
on("windowFocus", () => setFocused(true))
|
|
171
|
+
on("windowBlur", () => setFocused(false))
|
|
172
|
+
focusedAccessor = focused
|
|
173
|
+
})
|
|
163
174
|
}
|
|
164
|
-
return focusedAccessor()
|
|
175
|
+
return focusedAccessor!()
|
|
165
176
|
}
|
|
166
177
|
|
|
167
178
|
/**
|
|
@@ -183,12 +194,14 @@ let pointerLockedAccessor: (() => boolean) | undefined
|
|
|
183
194
|
/** Whether the pointer is currently locked (relative mouse mode), as a reactive accessor. */
|
|
184
195
|
export function pointerLocked(): boolean {
|
|
185
196
|
if (!pointerLockedAccessor) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
197
|
+
runWithOwner(null, () => {
|
|
198
|
+
let [locked, setLocked] = createSignal(false)
|
|
199
|
+
// Sticky event: a subscriber after the lock still observes the state.
|
|
200
|
+
on("pointerLock", ({ locked }: { locked: boolean }) => setLocked(locked))
|
|
201
|
+
pointerLockedAccessor = locked
|
|
202
|
+
})
|
|
190
203
|
}
|
|
191
|
-
return pointerLockedAccessor()
|
|
204
|
+
return pointerLockedAccessor!()
|
|
192
205
|
}
|
|
193
206
|
|
|
194
207
|
let keyboardHeightAccessor: (() => number) | undefined
|
|
@@ -201,11 +214,13 @@ let keyboardHeightAccessor: (() => number) | undefined
|
|
|
201
214
|
*/
|
|
202
215
|
export function keyboardHeight(): number {
|
|
203
216
|
if (!keyboardHeightAccessor) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
217
|
+
runWithOwner(null, () => {
|
|
218
|
+
let [height, setHeight] = createSignal(0)
|
|
219
|
+
on("keyboardVisibility", ({ height: h }: { height: number }) => setHeight(h ?? 0))
|
|
220
|
+
keyboardHeightAccessor = height
|
|
221
|
+
})
|
|
207
222
|
}
|
|
208
|
-
return keyboardHeightAccessor()
|
|
223
|
+
return keyboardHeightAccessor!()
|
|
209
224
|
}
|
|
210
225
|
|
|
211
226
|
// Post-layout handlers run in registration order from one bus subscription,
|
|
@@ -251,19 +266,19 @@ export function onLayout(fn: () => void) {
|
|
|
251
266
|
let i = layoutHandlers.indexOf(fn)
|
|
252
267
|
if (i >= 0) layoutHandlers.splice(i, 1)
|
|
253
268
|
}
|
|
254
|
-
onCleanup(unsubscribe)
|
|
269
|
+
if (getOwner()) onCleanup(unsubscribe)
|
|
255
270
|
return unsubscribe
|
|
256
271
|
}
|
|
257
272
|
|
|
258
273
|
export function onWindowFocus(fn: () => void) {
|
|
259
274
|
let unsubscribe = on("windowFocus", fn)
|
|
260
|
-
onCleanup(unsubscribe)
|
|
275
|
+
if (getOwner()) onCleanup(unsubscribe)
|
|
261
276
|
return unsubscribe
|
|
262
277
|
}
|
|
263
278
|
|
|
264
279
|
export function onWindowBlur(fn: () => void) {
|
|
265
280
|
let unsubscribe = on("windowBlur", fn)
|
|
266
|
-
onCleanup(unsubscribe)
|
|
281
|
+
if (getOwner()) onCleanup(unsubscribe)
|
|
267
282
|
return unsubscribe
|
|
268
283
|
}
|
|
269
284
|
|
|
@@ -295,7 +310,8 @@ let backHandlers: ((e: BackEvent) => void)[] = []
|
|
|
295
310
|
* handler that does not prevent must not act either - the event is still on its
|
|
296
311
|
* way to whoever will handle it.
|
|
297
312
|
*
|
|
298
|
-
* Returns a cleanup function; also auto-cleans within
|
|
313
|
+
* Returns a cleanup function; also auto-cleans within an owned scope (outside
|
|
314
|
+
* one the returned cleanup is the only handle).
|
|
299
315
|
*/
|
|
300
316
|
export function onBack(fn: (e: BackEvent) => void) {
|
|
301
317
|
backHandlers.push(fn)
|
|
@@ -303,7 +319,7 @@ export function onBack(fn: (e: BackEvent) => void) {
|
|
|
303
319
|
let i = backHandlers.lastIndexOf(fn)
|
|
304
320
|
if (i >= 0) backHandlers.splice(i, 1)
|
|
305
321
|
}
|
|
306
|
-
onCleanup(cleanup)
|
|
322
|
+
if (getOwner()) onCleanup(cleanup)
|
|
307
323
|
return cleanup
|
|
308
324
|
}
|
|
309
325
|
|