@glissade/scene 0.19.0 → 0.20.0-pre.3
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/dist/collapseReplacer.d.ts +40 -0
- package/dist/constructionProps.js +81 -0
- package/dist/describe.d.ts +26 -1
- package/dist/describe.js +104 -23
- package/dist/diagnostics.d.ts +159 -0
- package/dist/diagnostics.js +384 -0
- package/dist/displayList.d.ts +27 -0
- package/dist/grid.d.ts +56 -0
- package/dist/grid.js +83 -0
- package/dist/identity.d.ts +33 -0
- package/dist/identity.js +67 -0
- package/dist/index.d.ts +14 -302
- package/dist/index.js +19 -523
- package/dist/layout.d.ts +3 -95
- package/dist/layout.js +11 -196
- package/dist/layoutCtors.d.ts +99 -0
- package/dist/layoutCtors.js +210 -0
- package/dist/motion.d.ts +60 -0
- package/dist/motion.js +171 -0
- package/dist/nodes.d.ts +36 -0
- package/dist/nodes.js +62 -187
- package/dist/scene.d.ts +71 -0
- package/dist/scene.js +120 -0
- package/package.json +23 -3
package/dist/layout.d.ts
CHANGED
|
@@ -1,101 +1,9 @@
|
|
|
1
|
-
import { r as DisplayListBuilder } from "./displayList.js";
|
|
2
|
-
import { B as NodeProps, L as EvalContext, V as PropInit, W as TextMeasurer, i as Group, z as Node } from "./nodes.js";
|
|
3
1
|
import { a as LayoutEngineMissingError, i as LayoutEngine, l as setLayoutEngine, n as LayoutChildSpec, o as LayoutResult, r as LayoutContainerSpec, s as getLayoutEngine, t as LayoutBox } from "./layoutEngine.js";
|
|
4
|
-
import {
|
|
2
|
+
import { Column, Layout, LayoutProps, Row, RowProps, Stack, StackProps } from "./layoutCtors.js";
|
|
5
3
|
|
|
6
|
-
//#region src/
|
|
4
|
+
//#region src/layoutEngineYoga.d.ts
|
|
7
5
|
|
|
8
|
-
interface LayoutProps extends NodeProps {
|
|
9
|
-
children?: Node[];
|
|
10
|
-
/** 'auto': the axis sizes itself from content (Yoga content sizing). */
|
|
11
|
-
width?: PropInit<number> | 'auto';
|
|
12
|
-
height?: PropInit<number> | 'auto';
|
|
13
|
-
direction?: 'row' | 'column';
|
|
14
|
-
gap?: PropInit<number>;
|
|
15
|
-
padding?: PropInit<number>;
|
|
16
|
-
justify?: LayoutContainerSpec['justify'];
|
|
17
|
-
align?: LayoutContainerSpec['align'];
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Flexbox container (center-anchored like every node). Flowable children
|
|
21
|
-
* (intrinsicSize ≠ null) are placed by the engine anchor-aware via
|
|
22
|
-
* flowOffset(): shapes center into their box, Text aligns its baseline
|
|
23
|
-
* origin to the box edge — labels in a column share a true left edge.
|
|
24
|
-
* Non-flowable children (e.g. Groups) emit untouched at the layout origin,
|
|
25
|
-
* BEFORE the flow — their dominant use is backgrounds. Flowed paint order is
|
|
26
|
-
* zIndex-sorted; flow order is array order.
|
|
27
|
-
*/
|
|
28
|
-
declare class Layout extends Group {
|
|
29
|
-
#private;
|
|
30
|
-
/** CLI/host detection marker — avoids importing this entry just to instanceof. */
|
|
31
|
-
static readonly isLayoutNode = true;
|
|
32
|
-
readonly width: BindableSignal<number>;
|
|
33
|
-
readonly height: BindableSignal<number>;
|
|
34
|
-
readonly gap: BindableSignal<number>;
|
|
35
|
-
readonly padding: BindableSignal<number>;
|
|
36
|
-
readonly direction: 'row' | 'column';
|
|
37
|
-
readonly justify: LayoutContainerSpec['justify'];
|
|
38
|
-
readonly align: LayoutContainerSpec['align'];
|
|
39
|
-
/** Content-sized axes ('auto'): the size signal is ignored, Yoga computes it. */
|
|
40
|
-
readonly autoWidth: boolean;
|
|
41
|
-
readonly autoHeight: boolean;
|
|
42
|
-
constructor(props?: LayoutProps);
|
|
43
|
-
intrinsicSize(measurer: TextMeasurer): {
|
|
44
|
-
w: number;
|
|
45
|
-
h: number;
|
|
46
|
-
};
|
|
47
|
-
/**
|
|
48
|
-
* The resolved container size — content-driven on 'auto' axes. Pure pull:
|
|
49
|
-
* reads the same signals the flow reads, so a sibling bound to it
|
|
50
|
-
* (e.g. panelBg height = () => panel.computedSize().h) tracks every input.
|
|
51
|
-
* The measurer defaults to the scene-injected one (estimating pre-scene).
|
|
52
|
-
*/
|
|
53
|
-
computedSize(measurer?: TextMeasurer): {
|
|
54
|
-
w: number;
|
|
55
|
-
h: number;
|
|
56
|
-
};
|
|
57
|
-
protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Stack-ergonomic props — a more DISCOVERABLE surface over {@link LayoutProps}.
|
|
61
|
-
* Everything `Layout` accepts (id, position, opacity, width/height, gap,
|
|
62
|
-
* padding, justify, children, …) passes through unchanged; the only difference
|
|
63
|
-
* from constructing `Layout` directly is the defaulting `Stack()` applies.
|
|
64
|
-
*/
|
|
65
|
-
interface StackProps extends LayoutProps {}
|
|
66
|
-
/**
|
|
67
|
-
* Thin convenience factory over the Yoga-backed {@link Layout} node — NOT a new
|
|
68
|
-
* class and NOT new signals, so a `Stack` inherits Layout's memoized, pure,
|
|
69
|
-
* dependency-tracked resolve verbatim: `Stack(props)` and the equivalent
|
|
70
|
-
* hand-written `Layout({...})` produce identical child positions.
|
|
71
|
-
*
|
|
72
|
-
* Stack-ergonomic defaults (the ONLY divergence from `Layout`):
|
|
73
|
-
* - `direction` defaults to `'column'` (the common vertical stack; Layout's own
|
|
74
|
-
* default is `'row'`).
|
|
75
|
-
* - `align` defaults to `'start'` — a true left edge for a label column (the
|
|
76
|
-
* dogfooding use case). This DIVERGES from `Layout`'s `'center'` default.
|
|
77
|
-
*
|
|
78
|
-
* Every other prop passes straight through to `Layout`.
|
|
79
|
-
*/
|
|
80
|
-
declare function Stack(props?: StackProps): Layout;
|
|
81
|
-
/**
|
|
82
|
-
* `Row`/`Column` props — `Stack`'s surface MINUS `direction` (the alias pins it).
|
|
83
|
-
* Omitting `direction` from the type means an explicit one can't even be passed,
|
|
84
|
-
* so `Row({...})` is unambiguously a row and `Column({...})` a column.
|
|
85
|
-
*/
|
|
86
|
-
interface RowProps extends Omit<StackProps, 'direction'> {}
|
|
87
|
-
/**
|
|
88
|
-
* `Stack({ direction: 'row' })` read as a name — a horizontal stack. Inherits
|
|
89
|
-
* Stack's `align:'start'` default and Layout's pure memoized resolve; the only
|
|
90
|
-
* difference from `Stack` is the pinned `direction`.
|
|
91
|
-
*/
|
|
92
|
-
declare function Row(props?: RowProps): Layout;
|
|
93
|
-
/**
|
|
94
|
-
* `Stack({ direction: 'column' })` read as a name — a vertical stack (the same
|
|
95
|
-
* direction `Stack` already defaults to, made explicit at the call site).
|
|
96
|
-
*/
|
|
97
|
-
declare function Column(props?: RowProps): Layout;
|
|
98
6
|
/** Load Yoga (wasm) and register it as the active LayoutEngine. Idempotent. */
|
|
99
7
|
declare function loadYogaLayoutEngine(): Promise<LayoutEngine>;
|
|
100
8
|
//#endregion
|
|
101
|
-
export { Column, Layout, type LayoutBox, type LayoutChildSpec, type LayoutContainerSpec, type LayoutEngine, LayoutEngineMissingError, LayoutProps, type LayoutResult, Row, RowProps, Stack, StackProps, getLayoutEngine, loadYogaLayoutEngine, setLayoutEngine };
|
|
9
|
+
export { Column, Layout, type LayoutBox, type LayoutChildSpec, type LayoutContainerSpec, type LayoutEngine, LayoutEngineMissingError, type LayoutProps, type LayoutResult, Row, type RowProps, Stack, type StackProps, getLayoutEngine, loadYogaLayoutEngine, setLayoutEngine };
|
package/dist/layout.js
CHANGED
|
@@ -1,201 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
//#region src/layout.ts
|
|
1
|
+
import { i as setLayoutEngine, n as getLayoutEngine, t as LayoutEngineMissingError } from "./layoutEngine.js";
|
|
2
|
+
import { Column, Layout, Row, Stack } from "./layoutCtors.js";
|
|
3
|
+
//#region src/layoutEngineYoga.ts
|
|
5
4
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
5
|
+
* The Yoga LayoutEngine LOADER (DESIGN.md §3.2). Split off the node ctors
|
|
6
|
+
* (`layoutCtors.ts`) so the ctors can ride the single-file IIFE without dragging
|
|
7
|
+
* Yoga's wasm: this module's `import('yoga-layout/load')` is what statically
|
|
8
|
+
* inlines the wasm-base64 binding under esbuild's IIFE format (no code-splitting
|
|
9
|
+
* keeps the dynamic import async). `@glissade/scene/layout` re-exports it, so
|
|
10
|
+
* existing importers are unaffected; the IIFE re-exports it too but externalizes
|
|
11
|
+
* `yoga-layout/load`, keeping the dynamic import a RUNTIME fetch (yoga stays out
|
|
12
|
+
* of the bundle).
|
|
13
13
|
*/
|
|
14
|
-
/**
|
|
15
|
-
* Flexbox container (center-anchored like every node). Flowable children
|
|
16
|
-
* (intrinsicSize ≠ null) are placed by the engine anchor-aware via
|
|
17
|
-
* flowOffset(): shapes center into their box, Text aligns its baseline
|
|
18
|
-
* origin to the box edge — labels in a column share a true left edge.
|
|
19
|
-
* Non-flowable children (e.g. Groups) emit untouched at the layout origin,
|
|
20
|
-
* BEFORE the flow — their dominant use is backgrounds. Flowed paint order is
|
|
21
|
-
* zIndex-sorted; flow order is array order.
|
|
22
|
-
*/
|
|
23
|
-
var Layout = class extends Group {
|
|
24
|
-
/** CLI/host detection marker — avoids importing this entry just to instanceof. */
|
|
25
|
-
static isLayoutNode = true;
|
|
26
|
-
width;
|
|
27
|
-
height;
|
|
28
|
-
gap;
|
|
29
|
-
padding;
|
|
30
|
-
direction;
|
|
31
|
-
justify;
|
|
32
|
-
align;
|
|
33
|
-
/** Content-sized axes ('auto'): the size signal is ignored, Yoga computes it. */
|
|
34
|
-
autoWidth;
|
|
35
|
-
autoHeight;
|
|
36
|
-
/**
|
|
37
|
-
* Sanctioned memoization (§2.1), core-`computed()`-backed: a pure function of
|
|
38
|
-
* the PARTICIPATING signals. The compute reads exactly the container props
|
|
39
|
-
* and child intrinsic-size signals it consumes, so the signal graph records
|
|
40
|
-
* those as deps and re-invokes Yoga only when one of THEM changes — a sibling
|
|
41
|
-
* mutating a non-participating signal does not invalidate the layout. Pulls
|
|
42
|
-
* the scene-injected measurer (the same one draw() uses via ctx.measurer);
|
|
43
|
-
* a caller-supplied non-default measurer bypasses this cache (see #compute).
|
|
44
|
-
*/
|
|
45
|
-
#memo = computed(() => this.#computeUncached(this.measurerSource?.() ?? fallbackMeasurer()));
|
|
46
|
-
constructor(props = {}) {
|
|
47
|
-
super(props);
|
|
48
|
-
this.autoWidth = props.width === "auto";
|
|
49
|
-
this.autoHeight = props.height === "auto";
|
|
50
|
-
this.width = initProp(signal(0), this.autoWidth ? void 0 : props.width);
|
|
51
|
-
this.height = initProp(signal(0), this.autoHeight ? void 0 : props.height);
|
|
52
|
-
this.gap = initProp(signal(0), props.gap);
|
|
53
|
-
this.padding = initProp(signal(0), props.padding);
|
|
54
|
-
this.direction = props.direction ?? "row";
|
|
55
|
-
this.justify = props.justify ?? "start";
|
|
56
|
-
this.align = props.align ?? "center";
|
|
57
|
-
this.registerTarget("width", this.width, "number");
|
|
58
|
-
this.registerTarget("height", this.height, "number");
|
|
59
|
-
this.registerTarget("gap", this.gap, "number");
|
|
60
|
-
this.registerTarget("padding", this.padding, "number");
|
|
61
|
-
}
|
|
62
|
-
intrinsicSize(measurer) {
|
|
63
|
-
if (!this.autoWidth && !this.autoHeight) return {
|
|
64
|
-
w: this.width(),
|
|
65
|
-
h: this.height()
|
|
66
|
-
};
|
|
67
|
-
return this.#compute(measurer).size;
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* The resolved container size — content-driven on 'auto' axes. Pure pull:
|
|
71
|
-
* reads the same signals the flow reads, so a sibling bound to it
|
|
72
|
-
* (e.g. panelBg height = () => panel.computedSize().h) tracks every input.
|
|
73
|
-
* The measurer defaults to the scene-injected one (estimating pre-scene).
|
|
74
|
-
*/
|
|
75
|
-
computedSize(measurer) {
|
|
76
|
-
const m = measurer ?? this.measurerSource?.() ?? fallbackMeasurer();
|
|
77
|
-
return this.#compute(m).size;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Route through the #memo (the dependency-tracked computed) when `measurer`
|
|
81
|
-
* is the default the memo itself pulls; otherwise compute fresh & UNCACHED —
|
|
82
|
-
* a caller-supplied non-default measurer must never read (or poison) a cache
|
|
83
|
-
* keyed on the scene-singleton measurer (the `computedSize(customMeasurer)`
|
|
84
|
-
* escape hatch).
|
|
85
|
-
*/
|
|
86
|
-
#compute(measurer) {
|
|
87
|
-
return measurer === (this.measurerSource?.() ?? fallbackMeasurer()) ? this.#memo() : this.#computeUncached(measurer);
|
|
88
|
-
}
|
|
89
|
-
#computeUncached(measurer) {
|
|
90
|
-
const container = {
|
|
91
|
-
width: this.autoWidth ? "auto" : this.width(),
|
|
92
|
-
height: this.autoHeight ? "auto" : this.height(),
|
|
93
|
-
direction: this.direction,
|
|
94
|
-
gap: this.gap(),
|
|
95
|
-
padding: this.padding(),
|
|
96
|
-
justify: this.justify,
|
|
97
|
-
align: this.align
|
|
98
|
-
};
|
|
99
|
-
this.trackStructure();
|
|
100
|
-
const flowable = [];
|
|
101
|
-
const absolute = [];
|
|
102
|
-
this.children.forEach((child, index) => {
|
|
103
|
-
const size = child.intrinsicSize(measurer);
|
|
104
|
-
if (size) flowable.push({
|
|
105
|
-
node: child,
|
|
106
|
-
spec: {
|
|
107
|
-
width: size.w,
|
|
108
|
-
height: size.h
|
|
109
|
-
},
|
|
110
|
-
index
|
|
111
|
-
});
|
|
112
|
-
else absolute.push(child);
|
|
113
|
-
});
|
|
114
|
-
const result = requireLayoutEngine().compute(container, flowable.map((f) => f.spec));
|
|
115
|
-
return {
|
|
116
|
-
result,
|
|
117
|
-
size: {
|
|
118
|
-
w: this.autoWidth ? result.width : container.width,
|
|
119
|
-
h: this.autoHeight ? result.height : container.height
|
|
120
|
-
},
|
|
121
|
-
flowable,
|
|
122
|
-
absolute
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
draw(out, ctx) {
|
|
126
|
-
const { result, size, flowable, absolute } = this.#compute(ctx.measurer);
|
|
127
|
-
const boxes = result.boxes;
|
|
128
|
-
const order = [...flowable].sort((a, b) => a.node.zIndex() - b.node.zIndex() || a.index - b.index);
|
|
129
|
-
const ox = -size.w / 2;
|
|
130
|
-
const oy = -size.h / 2;
|
|
131
|
-
for (const child of absolute) child.emit(out, ctx);
|
|
132
|
-
for (const entry of order) {
|
|
133
|
-
const box = boxes[flowable.indexOf(entry)];
|
|
134
|
-
const off = entry.node.flowOffset(ctx.measurer);
|
|
135
|
-
out.push({ op: "save" });
|
|
136
|
-
out.push({
|
|
137
|
-
op: "transform",
|
|
138
|
-
m: [
|
|
139
|
-
1,
|
|
140
|
-
0,
|
|
141
|
-
0,
|
|
142
|
-
1,
|
|
143
|
-
ox + box.x - off.x,
|
|
144
|
-
oy + box.y - off.y
|
|
145
|
-
]
|
|
146
|
-
});
|
|
147
|
-
entry.node.emit(out, ctx);
|
|
148
|
-
out.push({ op: "restore" });
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
/**
|
|
153
|
-
* Thin convenience factory over the Yoga-backed {@link Layout} node — NOT a new
|
|
154
|
-
* class and NOT new signals, so a `Stack` inherits Layout's memoized, pure,
|
|
155
|
-
* dependency-tracked resolve verbatim: `Stack(props)` and the equivalent
|
|
156
|
-
* hand-written `Layout({...})` produce identical child positions.
|
|
157
|
-
*
|
|
158
|
-
* Stack-ergonomic defaults (the ONLY divergence from `Layout`):
|
|
159
|
-
* - `direction` defaults to `'column'` (the common vertical stack; Layout's own
|
|
160
|
-
* default is `'row'`).
|
|
161
|
-
* - `align` defaults to `'start'` — a true left edge for a label column (the
|
|
162
|
-
* dogfooding use case). This DIVERGES from `Layout`'s `'center'` default.
|
|
163
|
-
*
|
|
164
|
-
* Every other prop passes straight through to `Layout`.
|
|
165
|
-
*/
|
|
166
|
-
function Stack(props = {}) {
|
|
167
|
-
return new Layout({
|
|
168
|
-
...props,
|
|
169
|
-
direction: props.direction ?? "column",
|
|
170
|
-
align: props.align ?? "start"
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* `Stack({ direction: 'row' })` read as a name — a horizontal stack. Inherits
|
|
175
|
-
* Stack's `align:'start'` default and Layout's pure memoized resolve; the only
|
|
176
|
-
* difference from `Stack` is the pinned `direction`.
|
|
177
|
-
*/
|
|
178
|
-
function Row(props = {}) {
|
|
179
|
-
return Stack({
|
|
180
|
-
...props,
|
|
181
|
-
direction: "row"
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* `Stack({ direction: 'column' })` read as a name — a vertical stack (the same
|
|
186
|
-
* direction `Stack` already defaults to, made explicit at the call site).
|
|
187
|
-
*/
|
|
188
|
-
function Column(props = {}) {
|
|
189
|
-
return Stack({
|
|
190
|
-
...props,
|
|
191
|
-
direction: "column"
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
function initProp(sig, init) {
|
|
195
|
-
if (typeof init === "function") sig.bindSource(init);
|
|
196
|
-
else if (init !== void 0) sig.set(init);
|
|
197
|
-
return sig;
|
|
198
|
-
}
|
|
199
14
|
/** Load Yoga (wasm) and register it as the active LayoutEngine. Idempotent. */
|
|
200
15
|
async function loadYogaLayoutEngine() {
|
|
201
16
|
const { loadYoga, FlexDirection, Justify, Align, Gutter, Edge, Direction } = await import("yoga-layout/load");
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { r as DisplayListBuilder } from "./displayList.js";
|
|
2
|
+
import { B as NodeProps, L as EvalContext, V as PropInit, W as TextMeasurer, i as Group, z as Node } from "./nodes.js";
|
|
3
|
+
import { r as LayoutContainerSpec } from "./layoutEngine.js";
|
|
4
|
+
import { BindableSignal } from "@glissade/core";
|
|
5
|
+
|
|
6
|
+
//#region src/layoutCtors.d.ts
|
|
7
|
+
|
|
8
|
+
interface LayoutProps extends NodeProps {
|
|
9
|
+
children?: Node[];
|
|
10
|
+
/** 'auto': the axis sizes itself from content (Yoga content sizing). */
|
|
11
|
+
width?: PropInit<number> | 'auto';
|
|
12
|
+
height?: PropInit<number> | 'auto';
|
|
13
|
+
direction?: 'row' | 'column';
|
|
14
|
+
gap?: PropInit<number>;
|
|
15
|
+
padding?: PropInit<number>;
|
|
16
|
+
justify?: LayoutContainerSpec['justify'];
|
|
17
|
+
align?: LayoutContainerSpec['align'];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Flexbox container (center-anchored like every node). Flowable children
|
|
21
|
+
* (intrinsicSize ≠ null) are placed by the engine anchor-aware via
|
|
22
|
+
* flowOffset(): shapes center into their box, Text aligns its baseline
|
|
23
|
+
* origin to the box edge — labels in a column share a true left edge.
|
|
24
|
+
* Non-flowable children (e.g. Groups) emit untouched at the layout origin,
|
|
25
|
+
* BEFORE the flow — their dominant use is backgrounds. Flowed paint order is
|
|
26
|
+
* zIndex-sorted; flow order is array order.
|
|
27
|
+
*/
|
|
28
|
+
declare class Layout extends Group {
|
|
29
|
+
#private;
|
|
30
|
+
/** CLI/host detection marker — avoids importing this entry just to instanceof. */
|
|
31
|
+
static readonly isLayoutNode = true;
|
|
32
|
+
readonly width: BindableSignal<number>;
|
|
33
|
+
readonly height: BindableSignal<number>;
|
|
34
|
+
readonly gap: BindableSignal<number>;
|
|
35
|
+
readonly padding: BindableSignal<number>;
|
|
36
|
+
readonly direction: 'row' | 'column';
|
|
37
|
+
readonly justify: LayoutContainerSpec['justify'];
|
|
38
|
+
readonly align: LayoutContainerSpec['align'];
|
|
39
|
+
/** Content-sized axes ('auto'): the size signal is ignored, Yoga computes it. */
|
|
40
|
+
readonly autoWidth: boolean;
|
|
41
|
+
readonly autoHeight: boolean;
|
|
42
|
+
constructor(props?: LayoutProps);
|
|
43
|
+
intrinsicSize(measurer: TextMeasurer): {
|
|
44
|
+
w: number;
|
|
45
|
+
h: number;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* The resolved container size — content-driven on 'auto' axes. Pure pull:
|
|
49
|
+
* reads the same signals the flow reads, so a sibling bound to it
|
|
50
|
+
* (e.g. panelBg height = () => panel.computedSize().h) tracks every input.
|
|
51
|
+
* The measurer defaults to the scene-injected one (estimating pre-scene).
|
|
52
|
+
*/
|
|
53
|
+
computedSize(measurer?: TextMeasurer): {
|
|
54
|
+
w: number;
|
|
55
|
+
h: number;
|
|
56
|
+
};
|
|
57
|
+
protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Stack-ergonomic props — a more DISCOVERABLE surface over {@link LayoutProps}.
|
|
61
|
+
* Everything `Layout` accepts (id, position, opacity, width/height, gap,
|
|
62
|
+
* padding, justify, children, …) passes through unchanged; the only difference
|
|
63
|
+
* from constructing `Layout` directly is the defaulting `Stack()` applies.
|
|
64
|
+
*/
|
|
65
|
+
interface StackProps extends LayoutProps {}
|
|
66
|
+
/**
|
|
67
|
+
* Thin convenience factory over the Yoga-backed {@link Layout} node — NOT a new
|
|
68
|
+
* class and NOT new signals, so a `Stack` inherits Layout's memoized, pure,
|
|
69
|
+
* dependency-tracked resolve verbatim: `Stack(props)` and the equivalent
|
|
70
|
+
* hand-written `Layout({...})` produce identical child positions.
|
|
71
|
+
*
|
|
72
|
+
* Stack-ergonomic defaults (the ONLY divergence from `Layout`):
|
|
73
|
+
* - `direction` defaults to `'column'` (the common vertical stack; Layout's own
|
|
74
|
+
* default is `'row'`).
|
|
75
|
+
* - `align` defaults to `'start'` — a true left edge for a label column (the
|
|
76
|
+
* dogfooding use case). This DIVERGES from `Layout`'s `'center'` default.
|
|
77
|
+
*
|
|
78
|
+
* Every other prop passes straight through to `Layout`.
|
|
79
|
+
*/
|
|
80
|
+
declare function Stack(props?: StackProps): Layout;
|
|
81
|
+
/**
|
|
82
|
+
* `Row`/`Column` props — `Stack`'s surface MINUS `direction` (the alias pins it).
|
|
83
|
+
* Omitting `direction` from the type means an explicit one can't even be passed,
|
|
84
|
+
* so `Row({...})` is unambiguously a row and `Column({...})` a column.
|
|
85
|
+
*/
|
|
86
|
+
interface RowProps extends Omit<StackProps, 'direction'> {}
|
|
87
|
+
/**
|
|
88
|
+
* `Stack({ direction: 'row' })` read as a name — a horizontal stack. Inherits
|
|
89
|
+
* Stack's `align:'start'` default and Layout's pure memoized resolve; the only
|
|
90
|
+
* difference from `Stack` is the pinned `direction`.
|
|
91
|
+
*/
|
|
92
|
+
declare function Row(props?: RowProps): Layout;
|
|
93
|
+
/**
|
|
94
|
+
* `Stack({ direction: 'column' })` read as a name — a vertical stack (the same
|
|
95
|
+
* direction `Stack` already defaults to, made explicit at the call site).
|
|
96
|
+
*/
|
|
97
|
+
declare function Column(props?: RowProps): Layout;
|
|
98
|
+
//#endregion
|
|
99
|
+
export { Column, Layout, LayoutProps, Row, RowProps, Stack, StackProps };
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { r as Group, y as fallbackMeasurer } from "./nodes.js";
|
|
2
|
+
import { r as requireLayoutEngine } from "./layoutEngine.js";
|
|
3
|
+
import { computed, signal } from "@glissade/core";
|
|
4
|
+
//#region src/layoutCtors.ts
|
|
5
|
+
/**
|
|
6
|
+
* '@glissade/scene/layout-ctors' (DESIGN.md §3.2): the YOGA-FREE flexbox node
|
|
7
|
+
* constructors — `Layout`/`Stack`/`Row`/`Column`. These classes only touch the
|
|
8
|
+
* LayoutEngine SEAM (`requireLayoutEngine`/`setLayoutEngine`) at COMPUTE time;
|
|
9
|
+
* they never import Yoga at construction. Splitting them off the loader
|
|
10
|
+
* (`loadYogaLayoutEngine`, which statically reaches `yoga-layout/load`) lets the
|
|
11
|
+
* single-file `@glissade/browser` IIFE expose `Stack`/`Row`/`Column`/`Layout`
|
|
12
|
+
* WITHOUT inlining Yoga's wasm — esbuild's IIFE format can't keep the loader's
|
|
13
|
+
* `import('yoga-layout/load')` async, so it would balloon the bundle (~47→~99 kB
|
|
14
|
+
* gz). The ctors here carry no such import, so they ride the IIFE cleanly.
|
|
15
|
+
*
|
|
16
|
+
* CAVEAT (the no-build cost of the split): a ctor on the IIFE needs an engine
|
|
17
|
+
* registered before it computes. Call `await glissade.loadYogaLayoutEngine()`
|
|
18
|
+
* once before evaluating a scene with a Layout/Stack/Row/Column node, else the
|
|
19
|
+
* first compute throws a clear `LayoutEngineMissingError`.
|
|
20
|
+
*
|
|
21
|
+
* The `@glissade/scene/layout` entry re-exports everything here PLUS the loader,
|
|
22
|
+
* so existing `@glissade/scene/layout` importers are unaffected.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Flexbox container (center-anchored like every node). Flowable children
|
|
26
|
+
* (intrinsicSize ≠ null) are placed by the engine anchor-aware via
|
|
27
|
+
* flowOffset(): shapes center into their box, Text aligns its baseline
|
|
28
|
+
* origin to the box edge — labels in a column share a true left edge.
|
|
29
|
+
* Non-flowable children (e.g. Groups) emit untouched at the layout origin,
|
|
30
|
+
* BEFORE the flow — their dominant use is backgrounds. Flowed paint order is
|
|
31
|
+
* zIndex-sorted; flow order is array order.
|
|
32
|
+
*/
|
|
33
|
+
var Layout = class extends Group {
|
|
34
|
+
/** CLI/host detection marker — avoids importing this entry just to instanceof. */
|
|
35
|
+
static isLayoutNode = true;
|
|
36
|
+
width;
|
|
37
|
+
height;
|
|
38
|
+
gap;
|
|
39
|
+
padding;
|
|
40
|
+
direction;
|
|
41
|
+
justify;
|
|
42
|
+
align;
|
|
43
|
+
/** Content-sized axes ('auto'): the size signal is ignored, Yoga computes it. */
|
|
44
|
+
autoWidth;
|
|
45
|
+
autoHeight;
|
|
46
|
+
/**
|
|
47
|
+
* Sanctioned memoization (§2.1), core-`computed()`-backed: a pure function of
|
|
48
|
+
* the PARTICIPATING signals. The compute reads exactly the container props
|
|
49
|
+
* and child intrinsic-size signals it consumes, so the signal graph records
|
|
50
|
+
* those as deps and re-invokes Yoga only when one of THEM changes — a sibling
|
|
51
|
+
* mutating a non-participating signal does not invalidate the layout. Pulls
|
|
52
|
+
* the scene-injected measurer (the same one draw() uses via ctx.measurer);
|
|
53
|
+
* a caller-supplied non-default measurer bypasses this cache (see #compute).
|
|
54
|
+
*/
|
|
55
|
+
#memo = computed(() => this.#computeUncached(this.measurerSource?.() ?? fallbackMeasurer()));
|
|
56
|
+
constructor(props = {}) {
|
|
57
|
+
super(props);
|
|
58
|
+
this.autoWidth = props.width === "auto";
|
|
59
|
+
this.autoHeight = props.height === "auto";
|
|
60
|
+
this.width = initProp(signal(0), this.autoWidth ? void 0 : props.width);
|
|
61
|
+
this.height = initProp(signal(0), this.autoHeight ? void 0 : props.height);
|
|
62
|
+
this.gap = initProp(signal(0), props.gap);
|
|
63
|
+
this.padding = initProp(signal(0), props.padding);
|
|
64
|
+
this.direction = props.direction ?? "row";
|
|
65
|
+
this.justify = props.justify ?? "start";
|
|
66
|
+
this.align = props.align ?? "center";
|
|
67
|
+
this.registerTarget("width", this.width, "number");
|
|
68
|
+
this.registerTarget("height", this.height, "number");
|
|
69
|
+
this.registerTarget("gap", this.gap, "number");
|
|
70
|
+
this.registerTarget("padding", this.padding, "number");
|
|
71
|
+
}
|
|
72
|
+
intrinsicSize(measurer) {
|
|
73
|
+
if (!this.autoWidth && !this.autoHeight) return {
|
|
74
|
+
w: this.width(),
|
|
75
|
+
h: this.height()
|
|
76
|
+
};
|
|
77
|
+
return this.#compute(measurer).size;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The resolved container size — content-driven on 'auto' axes. Pure pull:
|
|
81
|
+
* reads the same signals the flow reads, so a sibling bound to it
|
|
82
|
+
* (e.g. panelBg height = () => panel.computedSize().h) tracks every input.
|
|
83
|
+
* The measurer defaults to the scene-injected one (estimating pre-scene).
|
|
84
|
+
*/
|
|
85
|
+
computedSize(measurer) {
|
|
86
|
+
const m = measurer ?? this.measurerSource?.() ?? fallbackMeasurer();
|
|
87
|
+
return this.#compute(m).size;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Route through the #memo (the dependency-tracked computed) when `measurer`
|
|
91
|
+
* is the default the memo itself pulls; otherwise compute fresh & UNCACHED —
|
|
92
|
+
* a caller-supplied non-default measurer must never read (or poison) a cache
|
|
93
|
+
* keyed on the scene-singleton measurer (the `computedSize(customMeasurer)`
|
|
94
|
+
* escape hatch).
|
|
95
|
+
*/
|
|
96
|
+
#compute(measurer) {
|
|
97
|
+
return measurer === (this.measurerSource?.() ?? fallbackMeasurer()) ? this.#memo() : this.#computeUncached(measurer);
|
|
98
|
+
}
|
|
99
|
+
#computeUncached(measurer) {
|
|
100
|
+
const container = {
|
|
101
|
+
width: this.autoWidth ? "auto" : this.width(),
|
|
102
|
+
height: this.autoHeight ? "auto" : this.height(),
|
|
103
|
+
direction: this.direction,
|
|
104
|
+
gap: this.gap(),
|
|
105
|
+
padding: this.padding(),
|
|
106
|
+
justify: this.justify,
|
|
107
|
+
align: this.align
|
|
108
|
+
};
|
|
109
|
+
this.trackStructure();
|
|
110
|
+
const flowable = [];
|
|
111
|
+
const absolute = [];
|
|
112
|
+
this.children.forEach((child, index) => {
|
|
113
|
+
const size = child.intrinsicSize(measurer);
|
|
114
|
+
if (size) flowable.push({
|
|
115
|
+
node: child,
|
|
116
|
+
spec: {
|
|
117
|
+
width: size.w,
|
|
118
|
+
height: size.h
|
|
119
|
+
},
|
|
120
|
+
index
|
|
121
|
+
});
|
|
122
|
+
else absolute.push(child);
|
|
123
|
+
});
|
|
124
|
+
const result = requireLayoutEngine().compute(container, flowable.map((f) => f.spec));
|
|
125
|
+
return {
|
|
126
|
+
result,
|
|
127
|
+
size: {
|
|
128
|
+
w: this.autoWidth ? result.width : container.width,
|
|
129
|
+
h: this.autoHeight ? result.height : container.height
|
|
130
|
+
},
|
|
131
|
+
flowable,
|
|
132
|
+
absolute
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
draw(out, ctx) {
|
|
136
|
+
const { result, size, flowable, absolute } = this.#compute(ctx.measurer);
|
|
137
|
+
const boxes = result.boxes;
|
|
138
|
+
const order = [...flowable].sort((a, b) => a.node.zIndex() - b.node.zIndex() || a.index - b.index);
|
|
139
|
+
const ox = -size.w / 2;
|
|
140
|
+
const oy = -size.h / 2;
|
|
141
|
+
for (const child of absolute) child.emit(out, ctx);
|
|
142
|
+
for (const entry of order) {
|
|
143
|
+
const box = boxes[flowable.indexOf(entry)];
|
|
144
|
+
const off = entry.node.flowOffset(ctx.measurer);
|
|
145
|
+
out.push({ op: "save" });
|
|
146
|
+
out.push({
|
|
147
|
+
op: "transform",
|
|
148
|
+
m: [
|
|
149
|
+
1,
|
|
150
|
+
0,
|
|
151
|
+
0,
|
|
152
|
+
1,
|
|
153
|
+
ox + box.x - off.x,
|
|
154
|
+
oy + box.y - off.y
|
|
155
|
+
]
|
|
156
|
+
});
|
|
157
|
+
entry.node.emit(out, ctx);
|
|
158
|
+
out.push({ op: "restore" });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Thin convenience factory over the Yoga-backed {@link Layout} node — NOT a new
|
|
164
|
+
* class and NOT new signals, so a `Stack` inherits Layout's memoized, pure,
|
|
165
|
+
* dependency-tracked resolve verbatim: `Stack(props)` and the equivalent
|
|
166
|
+
* hand-written `Layout({...})` produce identical child positions.
|
|
167
|
+
*
|
|
168
|
+
* Stack-ergonomic defaults (the ONLY divergence from `Layout`):
|
|
169
|
+
* - `direction` defaults to `'column'` (the common vertical stack; Layout's own
|
|
170
|
+
* default is `'row'`).
|
|
171
|
+
* - `align` defaults to `'start'` — a true left edge for a label column (the
|
|
172
|
+
* dogfooding use case). This DIVERGES from `Layout`'s `'center'` default.
|
|
173
|
+
*
|
|
174
|
+
* Every other prop passes straight through to `Layout`.
|
|
175
|
+
*/
|
|
176
|
+
function Stack(props = {}) {
|
|
177
|
+
return new Layout({
|
|
178
|
+
...props,
|
|
179
|
+
direction: props.direction ?? "column",
|
|
180
|
+
align: props.align ?? "start"
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* `Stack({ direction: 'row' })` read as a name — a horizontal stack. Inherits
|
|
185
|
+
* Stack's `align:'start'` default and Layout's pure memoized resolve; the only
|
|
186
|
+
* difference from `Stack` is the pinned `direction`.
|
|
187
|
+
*/
|
|
188
|
+
function Row(props = {}) {
|
|
189
|
+
return Stack({
|
|
190
|
+
...props,
|
|
191
|
+
direction: "row"
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* `Stack({ direction: 'column' })` read as a name — a vertical stack (the same
|
|
196
|
+
* direction `Stack` already defaults to, made explicit at the call site).
|
|
197
|
+
*/
|
|
198
|
+
function Column(props = {}) {
|
|
199
|
+
return Stack({
|
|
200
|
+
...props,
|
|
201
|
+
direction: "column"
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function initProp(sig, init) {
|
|
205
|
+
if (typeof init === "function") sig.bindSource(init);
|
|
206
|
+
else if (init !== void 0) sig.set(init);
|
|
207
|
+
return sig;
|
|
208
|
+
}
|
|
209
|
+
//#endregion
|
|
210
|
+
export { Column, Layout, Row, Stack };
|