textura 1.0.0
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/LICENSE +21 -0
- package/README.md +153 -0
- package/dist/engine.d.ts +23 -0
- package/dist/engine.js +245 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/types.d.ts +79 -0
- package/dist/types.js +5 -0
- package/package.json +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pretext contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Textura
|
|
2
|
+
|
|
3
|
+
DOM-free layout engine for the web. Combines [Yoga](https://github.com/facebook/yoga) (flexbox) with [Pretext](https://github.com/chenglou/pretext) (text measurement) to compute complete UI geometry — positions, sizes, and text line breaks — without ever touching the DOM.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
The browser's layout engine is a black box that blocks the main thread. Every `getBoundingClientRect()` or `offsetHeight` call triggers synchronous layout reflow. When components independently measure text, each measurement triggers a reflow of the entire document.
|
|
8
|
+
|
|
9
|
+
Yoga solves box layout (flexbox) in pure JS/WASM, but punts on text — it requires you to supply a `MeasureFunction` callback externally. Pretext solves text measurement with canvas `measureText`, but doesn't do box layout. **Textura joins them**: declare a tree of flex containers and text nodes, get back exact pixel geometry for everything.
|
|
10
|
+
|
|
11
|
+
This unlocks:
|
|
12
|
+
- **Worker-thread UI layout** — compute layout off the main thread, send only coordinates for painting
|
|
13
|
+
- **Zero-estimation virtualization** — know exact heights for 100K items before mounting a single DOM node
|
|
14
|
+
- **Canvas/WebGL/SVG rendering** — full layout engine for non-DOM renderers
|
|
15
|
+
- **Server-side layout** — generate pixel positions server-side (once Pretext gets server canvas)
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm install textura
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { init, computeLayout } from 'textura'
|
|
27
|
+
|
|
28
|
+
// Initialize Yoga WASM (call once)
|
|
29
|
+
await init()
|
|
30
|
+
|
|
31
|
+
// Describe your UI as a tree
|
|
32
|
+
const result = computeLayout({
|
|
33
|
+
width: 400,
|
|
34
|
+
padding: 16,
|
|
35
|
+
flexDirection: 'column',
|
|
36
|
+
gap: 12,
|
|
37
|
+
children: [
|
|
38
|
+
{
|
|
39
|
+
text: 'Hello World',
|
|
40
|
+
font: '24px Inter',
|
|
41
|
+
lineHeight: 32,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
flexDirection: 'row',
|
|
45
|
+
gap: 8,
|
|
46
|
+
children: [
|
|
47
|
+
{ width: 80, height: 80 }, // avatar
|
|
48
|
+
{
|
|
49
|
+
text: 'This is a message that will wrap to multiple lines based on available width.',
|
|
50
|
+
font: '16px Inter',
|
|
51
|
+
lineHeight: 22,
|
|
52
|
+
flexGrow: 1,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// result is a tree of computed layouts:
|
|
60
|
+
// {
|
|
61
|
+
// x: 0, y: 0, width: 400, height: ...,
|
|
62
|
+
// children: [
|
|
63
|
+
// { x: 16, y: 16, width: 368, height: 32, text: 'Hello World', lineCount: 1 },
|
|
64
|
+
// { x: 16, y: 60, width: 368, height: ...,
|
|
65
|
+
// children: [
|
|
66
|
+
// { x: 0, y: 0, width: 80, height: 80 },
|
|
67
|
+
// { x: 88, y: 0, width: 280, height: ..., text: '...', lineCount: ... },
|
|
68
|
+
// ]
|
|
69
|
+
// },
|
|
70
|
+
// ]
|
|
71
|
+
// }
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## API
|
|
75
|
+
|
|
76
|
+
### `init(): Promise<void>`
|
|
77
|
+
|
|
78
|
+
Initialize the Yoga WASM runtime. Must be called once before `computeLayout`.
|
|
79
|
+
|
|
80
|
+
### `computeLayout(tree, options?): ComputedLayout`
|
|
81
|
+
|
|
82
|
+
Compute layout for a declarative UI tree. Returns positions, sizes, and text metadata for every node.
|
|
83
|
+
|
|
84
|
+
**Options:**
|
|
85
|
+
- `width?: number` — available width for the root
|
|
86
|
+
- `height?: number` — available height for the root
|
|
87
|
+
- `direction?: 'ltr' | 'rtl'` — text direction (default: `'ltr'`)
|
|
88
|
+
|
|
89
|
+
### Node types
|
|
90
|
+
|
|
91
|
+
**Box nodes** — flex containers with children:
|
|
92
|
+
```ts
|
|
93
|
+
{
|
|
94
|
+
flexDirection: 'row',
|
|
95
|
+
gap: 8,
|
|
96
|
+
padding: 16,
|
|
97
|
+
children: [...]
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Text nodes** — leaf nodes with measured text content:
|
|
102
|
+
```ts
|
|
103
|
+
{
|
|
104
|
+
text: 'Hello world',
|
|
105
|
+
font: '16px Inter', // canvas font shorthand
|
|
106
|
+
lineHeight: 22, // line height in px
|
|
107
|
+
whiteSpace: 'pre-wrap', // optional: preserve spaces/tabs/newlines
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Both node types accept all flexbox properties: `flexDirection`, `flexWrap`, `justifyContent`, `alignItems`, `alignSelf`, `alignContent`, `flexGrow`, `flexShrink`, `flexBasis`, `width`, `height`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `padding*`, `margin*`, `border*`, `gap`, `position`, `top/right/bottom/left`, `aspectRatio`, `overflow`, `display`.
|
|
112
|
+
|
|
113
|
+
### `ComputedLayout`
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
interface ComputedLayout {
|
|
117
|
+
x: number
|
|
118
|
+
y: number
|
|
119
|
+
width: number
|
|
120
|
+
height: number
|
|
121
|
+
children: ComputedLayout[]
|
|
122
|
+
text?: string // present on text nodes
|
|
123
|
+
lineCount?: number // present on text nodes
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### `clearCache(): void`
|
|
128
|
+
|
|
129
|
+
Clear Pretext's internal measurement caches.
|
|
130
|
+
|
|
131
|
+
### `destroy(): void`
|
|
132
|
+
|
|
133
|
+
Release Yoga resources. Mostly useful for tests.
|
|
134
|
+
|
|
135
|
+
## How it works
|
|
136
|
+
|
|
137
|
+
1. You describe a UI tree using plain objects with CSS-like flex properties
|
|
138
|
+
2. `computeLayout` builds a Yoga node tree from your description
|
|
139
|
+
3. For text nodes, it wires Pretext's `prepare()` + `layout()` into Yoga's `MeasureFunction` — when Yoga asks "how tall is this text at width X?", Pretext answers using canvas-based measurement with cached segment widths
|
|
140
|
+
4. Yoga runs its flexbox algorithm over the tree
|
|
141
|
+
5. The computed positions and sizes are read back into a plain object tree
|
|
142
|
+
|
|
143
|
+
The text measurement is the key piece: Pretext handles Unicode segmentation, CJK character breaking, Arabic/bidi text, emoji, soft hyphens, and browser-specific quirks — all via `Intl.Segmenter` and canvas `measureText`, with 7680/7680 accuracy across Chrome/Safari/Firefox.
|
|
144
|
+
|
|
145
|
+
## Limitations
|
|
146
|
+
|
|
147
|
+
- Requires a browser environment (or `OffscreenCanvas` in a worker) for text measurement
|
|
148
|
+
- Text nodes use the same CSS target as Pretext: `white-space: normal`, `word-break: normal`, `overflow-wrap: break-word`, `line-break: auto`
|
|
149
|
+
- Use named fonts (`Inter`, `Helvetica`) — `system-ui` can produce inaccurate measurements on macOS
|
|
150
|
+
|
|
151
|
+
## Credits
|
|
152
|
+
|
|
153
|
+
Built on [Yoga](https://github.com/facebook/yoga) by Meta and [Pretext](https://github.com/chenglou/pretext) by Cheng Lou.
|
package/dist/engine.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { clearCache } from '@chenglou/pretext';
|
|
2
|
+
import { type LayoutNode, type ComputedLayout } from './types.js';
|
|
3
|
+
/** Initialize the Yoga WASM runtime. Must be called once before computeLayout. */
|
|
4
|
+
export declare function init(): Promise<void>;
|
|
5
|
+
/** Release Yoga config. Mostly useful for tests. */
|
|
6
|
+
export declare function destroy(): void;
|
|
7
|
+
/** Clear Pretext's internal measurement caches. */
|
|
8
|
+
export { clearCache };
|
|
9
|
+
export interface ComputeOptions {
|
|
10
|
+
/** Available width for the root container. Default: unconstrained. */
|
|
11
|
+
width?: number;
|
|
12
|
+
/** Available height for the root container. Default: unconstrained. */
|
|
13
|
+
height?: number;
|
|
14
|
+
/** Text direction. Default: 'ltr'. */
|
|
15
|
+
direction?: 'ltr' | 'rtl';
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Compute the full layout geometry for a declarative UI tree.
|
|
19
|
+
*
|
|
20
|
+
* Builds a Yoga node tree, wires Pretext text measurement into leaf nodes,
|
|
21
|
+
* runs Yoga's flexbox algorithm, and returns the computed positions and sizes.
|
|
22
|
+
*/
|
|
23
|
+
export declare function computeLayout(tree: LayoutNode, options?: ComputeOptions): ComputedLayout;
|
package/dist/engine.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { loadYoga, FlexDirection, Align, Justify, Wrap, Edge, Gutter, MeasureMode, PositionType, Overflow, Display, Direction, } from 'yoga-layout/load';
|
|
2
|
+
import { prepare, layout, clearCache } from '@chenglou/pretext';
|
|
3
|
+
import { isTextNode, } from './types.js';
|
|
4
|
+
let yoga = null;
|
|
5
|
+
let config = null;
|
|
6
|
+
function getConfig() {
|
|
7
|
+
if (config === null)
|
|
8
|
+
throw new Error('textura: call init() first');
|
|
9
|
+
return config;
|
|
10
|
+
}
|
|
11
|
+
/** Initialize the Yoga WASM runtime. Must be called once before computeLayout. */
|
|
12
|
+
export async function init() {
|
|
13
|
+
if (yoga !== null)
|
|
14
|
+
return;
|
|
15
|
+
yoga = await loadYoga();
|
|
16
|
+
config = yoga.Config.create();
|
|
17
|
+
config.setUseWebDefaults(true);
|
|
18
|
+
}
|
|
19
|
+
/** Release Yoga config. Mostly useful for tests. */
|
|
20
|
+
export function destroy() {
|
|
21
|
+
if (config !== null) {
|
|
22
|
+
config.free();
|
|
23
|
+
config = null;
|
|
24
|
+
}
|
|
25
|
+
yoga = null;
|
|
26
|
+
}
|
|
27
|
+
/** Clear Pretext's internal measurement caches. */
|
|
28
|
+
export { clearCache };
|
|
29
|
+
// --- Flex property mapping ---
|
|
30
|
+
const FLEX_DIRECTION_MAP = {
|
|
31
|
+
row: FlexDirection.Row,
|
|
32
|
+
column: FlexDirection.Column,
|
|
33
|
+
'row-reverse': FlexDirection.RowReverse,
|
|
34
|
+
'column-reverse': FlexDirection.ColumnReverse,
|
|
35
|
+
};
|
|
36
|
+
const JUSTIFY_MAP = {
|
|
37
|
+
'flex-start': Justify.FlexStart,
|
|
38
|
+
center: Justify.Center,
|
|
39
|
+
'flex-end': Justify.FlexEnd,
|
|
40
|
+
'space-between': Justify.SpaceBetween,
|
|
41
|
+
'space-around': Justify.SpaceAround,
|
|
42
|
+
'space-evenly': Justify.SpaceEvenly,
|
|
43
|
+
};
|
|
44
|
+
const ALIGN_MAP = {
|
|
45
|
+
auto: Align.Auto,
|
|
46
|
+
'flex-start': Align.FlexStart,
|
|
47
|
+
center: Align.Center,
|
|
48
|
+
'flex-end': Align.FlexEnd,
|
|
49
|
+
stretch: Align.Stretch,
|
|
50
|
+
baseline: Align.Baseline,
|
|
51
|
+
'space-between': Align.SpaceBetween,
|
|
52
|
+
'space-around': Align.SpaceAround,
|
|
53
|
+
'space-evenly': Align.SpaceEvenly,
|
|
54
|
+
};
|
|
55
|
+
const WRAP_MAP = {
|
|
56
|
+
nowrap: Wrap.NoWrap,
|
|
57
|
+
wrap: Wrap.Wrap,
|
|
58
|
+
'wrap-reverse': Wrap.WrapReverse,
|
|
59
|
+
};
|
|
60
|
+
function applyFlexProps(node, props) {
|
|
61
|
+
if (props.flexDirection !== undefined)
|
|
62
|
+
node.setFlexDirection(FLEX_DIRECTION_MAP[props.flexDirection]);
|
|
63
|
+
if (props.flexWrap !== undefined)
|
|
64
|
+
node.setFlexWrap(WRAP_MAP[props.flexWrap]);
|
|
65
|
+
if (props.justifyContent !== undefined)
|
|
66
|
+
node.setJustifyContent(JUSTIFY_MAP[props.justifyContent]);
|
|
67
|
+
if (props.alignItems !== undefined)
|
|
68
|
+
node.setAlignItems(ALIGN_MAP[props.alignItems]);
|
|
69
|
+
if (props.alignSelf !== undefined)
|
|
70
|
+
node.setAlignSelf(ALIGN_MAP[props.alignSelf]);
|
|
71
|
+
if (props.alignContent !== undefined)
|
|
72
|
+
node.setAlignContent(ALIGN_MAP[props.alignContent]);
|
|
73
|
+
if (props.flexGrow !== undefined)
|
|
74
|
+
node.setFlexGrow(props.flexGrow);
|
|
75
|
+
if (props.flexShrink !== undefined)
|
|
76
|
+
node.setFlexShrink(props.flexShrink);
|
|
77
|
+
if (props.flexBasis !== undefined)
|
|
78
|
+
node.setFlexBasis(props.flexBasis);
|
|
79
|
+
// Dimensions
|
|
80
|
+
if (props.width !== undefined)
|
|
81
|
+
node.setWidth(props.width);
|
|
82
|
+
if (props.height !== undefined)
|
|
83
|
+
node.setHeight(props.height);
|
|
84
|
+
if (props.minWidth !== undefined)
|
|
85
|
+
node.setMinWidth(props.minWidth);
|
|
86
|
+
if (props.maxWidth !== undefined)
|
|
87
|
+
node.setMaxWidth(props.maxWidth);
|
|
88
|
+
if (props.minHeight !== undefined)
|
|
89
|
+
node.setMinHeight(props.minHeight);
|
|
90
|
+
if (props.maxHeight !== undefined)
|
|
91
|
+
node.setMaxHeight(props.maxHeight);
|
|
92
|
+
// Padding
|
|
93
|
+
if (props.padding !== undefined)
|
|
94
|
+
node.setPadding(Edge.All, props.padding);
|
|
95
|
+
if (props.paddingTop !== undefined)
|
|
96
|
+
node.setPadding(Edge.Top, props.paddingTop);
|
|
97
|
+
if (props.paddingRight !== undefined)
|
|
98
|
+
node.setPadding(Edge.Right, props.paddingRight);
|
|
99
|
+
if (props.paddingBottom !== undefined)
|
|
100
|
+
node.setPadding(Edge.Bottom, props.paddingBottom);
|
|
101
|
+
if (props.paddingLeft !== undefined)
|
|
102
|
+
node.setPadding(Edge.Left, props.paddingLeft);
|
|
103
|
+
if (props.paddingHorizontal !== undefined)
|
|
104
|
+
node.setPadding(Edge.Horizontal, props.paddingHorizontal);
|
|
105
|
+
if (props.paddingVertical !== undefined)
|
|
106
|
+
node.setPadding(Edge.Vertical, props.paddingVertical);
|
|
107
|
+
// Margin
|
|
108
|
+
if (props.margin !== undefined)
|
|
109
|
+
node.setMargin(Edge.All, props.margin);
|
|
110
|
+
if (props.marginTop !== undefined)
|
|
111
|
+
node.setMargin(Edge.Top, props.marginTop);
|
|
112
|
+
if (props.marginRight !== undefined)
|
|
113
|
+
node.setMargin(Edge.Right, props.marginRight);
|
|
114
|
+
if (props.marginBottom !== undefined)
|
|
115
|
+
node.setMargin(Edge.Bottom, props.marginBottom);
|
|
116
|
+
if (props.marginLeft !== undefined)
|
|
117
|
+
node.setMargin(Edge.Left, props.marginLeft);
|
|
118
|
+
if (props.marginHorizontal !== undefined)
|
|
119
|
+
node.setMargin(Edge.Horizontal, props.marginHorizontal);
|
|
120
|
+
if (props.marginVertical !== undefined)
|
|
121
|
+
node.setMargin(Edge.Vertical, props.marginVertical);
|
|
122
|
+
// Border
|
|
123
|
+
if (props.border !== undefined)
|
|
124
|
+
node.setBorder(Edge.All, props.border);
|
|
125
|
+
if (props.borderTop !== undefined)
|
|
126
|
+
node.setBorder(Edge.Top, props.borderTop);
|
|
127
|
+
if (props.borderRight !== undefined)
|
|
128
|
+
node.setBorder(Edge.Right, props.borderRight);
|
|
129
|
+
if (props.borderBottom !== undefined)
|
|
130
|
+
node.setBorder(Edge.Bottom, props.borderBottom);
|
|
131
|
+
if (props.borderLeft !== undefined)
|
|
132
|
+
node.setBorder(Edge.Left, props.borderLeft);
|
|
133
|
+
// Gap
|
|
134
|
+
if (props.gap !== undefined)
|
|
135
|
+
node.setGap(Gutter.All, props.gap);
|
|
136
|
+
if (props.rowGap !== undefined)
|
|
137
|
+
node.setGap(Gutter.Row, props.rowGap);
|
|
138
|
+
if (props.columnGap !== undefined)
|
|
139
|
+
node.setGap(Gutter.Column, props.columnGap);
|
|
140
|
+
// Position
|
|
141
|
+
if (props.position !== undefined)
|
|
142
|
+
node.setPositionType(props.position === 'absolute' ? PositionType.Absolute : PositionType.Relative);
|
|
143
|
+
if (props.top !== undefined)
|
|
144
|
+
node.setPosition(Edge.Top, props.top);
|
|
145
|
+
if (props.right !== undefined)
|
|
146
|
+
node.setPosition(Edge.Right, props.right);
|
|
147
|
+
if (props.bottom !== undefined)
|
|
148
|
+
node.setPosition(Edge.Bottom, props.bottom);
|
|
149
|
+
if (props.left !== undefined)
|
|
150
|
+
node.setPosition(Edge.Left, props.left);
|
|
151
|
+
// Other
|
|
152
|
+
if (props.aspectRatio !== undefined)
|
|
153
|
+
node.setAspectRatio(props.aspectRatio);
|
|
154
|
+
if (props.overflow !== undefined) {
|
|
155
|
+
const map = { visible: Overflow.Visible, hidden: Overflow.Hidden, scroll: Overflow.Scroll };
|
|
156
|
+
node.setOverflow(map[props.overflow]);
|
|
157
|
+
}
|
|
158
|
+
if (props.display !== undefined)
|
|
159
|
+
node.setDisplay(props.display === 'none' ? Display.None : Display.Flex);
|
|
160
|
+
}
|
|
161
|
+
/** Metadata attached to Yoga nodes so we can extract text info during readback. */
|
|
162
|
+
const nodeMeta = new WeakMap();
|
|
163
|
+
function buildNode(desc) {
|
|
164
|
+
if (yoga === null)
|
|
165
|
+
throw new Error('textura: call init() first');
|
|
166
|
+
const node = yoga.Node.create(getConfig());
|
|
167
|
+
applyFlexProps(node, desc);
|
|
168
|
+
if (isTextNode(desc)) {
|
|
169
|
+
const whiteSpace = desc.whiteSpace;
|
|
170
|
+
const font = desc.font;
|
|
171
|
+
const text = desc.text;
|
|
172
|
+
const lineHeight = desc.lineHeight;
|
|
173
|
+
const meta = { text, font, lineHeight, lastLineCount: 0 };
|
|
174
|
+
nodeMeta.set(node, meta);
|
|
175
|
+
node.setMeasureFunc((width, widthMode, _height, _heightMode) => {
|
|
176
|
+
const prepared = prepare(text, font, whiteSpace ? { whiteSpace } : undefined);
|
|
177
|
+
// Determine max width for line breaking
|
|
178
|
+
let maxWidth;
|
|
179
|
+
if (widthMode === MeasureMode.Exactly) {
|
|
180
|
+
maxWidth = width;
|
|
181
|
+
}
|
|
182
|
+
else if (widthMode === MeasureMode.AtMost) {
|
|
183
|
+
maxWidth = width;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
// Undefined — no constraint. Use a very large width (single line).
|
|
187
|
+
maxWidth = 1e7;
|
|
188
|
+
}
|
|
189
|
+
const result = layout(prepared, maxWidth, lineHeight);
|
|
190
|
+
meta.lastLineCount = result.lineCount;
|
|
191
|
+
// For AtMost/Undefined, the intrinsic width could be narrower.
|
|
192
|
+
// We report the constrained width for Exactly/AtMost, and the
|
|
193
|
+
// full container for Undefined (Yoga handles shrinking).
|
|
194
|
+
const reportedWidth = widthMode === MeasureMode.Undefined ? maxWidth : width;
|
|
195
|
+
return { width: reportedWidth, height: result.height };
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
const children = desc.children;
|
|
200
|
+
if (children) {
|
|
201
|
+
for (let i = 0; i < children.length; i++) {
|
|
202
|
+
node.insertChild(buildNode(children[i]), i);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return node;
|
|
207
|
+
}
|
|
208
|
+
// --- Layout readback ---
|
|
209
|
+
function readLayout(node) {
|
|
210
|
+
const computed = {
|
|
211
|
+
x: node.getComputedLeft(),
|
|
212
|
+
y: node.getComputedTop(),
|
|
213
|
+
width: node.getComputedWidth(),
|
|
214
|
+
height: node.getComputedHeight(),
|
|
215
|
+
children: [],
|
|
216
|
+
};
|
|
217
|
+
const meta = nodeMeta.get(node);
|
|
218
|
+
if (meta) {
|
|
219
|
+
computed.text = meta.text;
|
|
220
|
+
computed.lineCount = meta.lastLineCount;
|
|
221
|
+
}
|
|
222
|
+
const childCount = node.getChildCount();
|
|
223
|
+
for (let i = 0; i < childCount; i++) {
|
|
224
|
+
computed.children.push(readLayout(node.getChild(i)));
|
|
225
|
+
}
|
|
226
|
+
return computed;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Compute the full layout geometry for a declarative UI tree.
|
|
230
|
+
*
|
|
231
|
+
* Builds a Yoga node tree, wires Pretext text measurement into leaf nodes,
|
|
232
|
+
* runs Yoga's flexbox algorithm, and returns the computed positions and sizes.
|
|
233
|
+
*/
|
|
234
|
+
export function computeLayout(tree, options) {
|
|
235
|
+
if (yoga === null)
|
|
236
|
+
throw new Error('textura: call init() first');
|
|
237
|
+
const root = buildNode(tree);
|
|
238
|
+
const w = options?.width;
|
|
239
|
+
const h = options?.height;
|
|
240
|
+
const dir = options?.direction === 'rtl' ? Direction.RTL : Direction.LTR;
|
|
241
|
+
root.calculateLayout(w, h, dir);
|
|
242
|
+
const result = readLayout(root);
|
|
243
|
+
root.freeRecursive();
|
|
244
|
+
return result;
|
|
245
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** CSS-like flexbox properties shared by all container nodes. */
|
|
2
|
+
export interface FlexProps {
|
|
3
|
+
/** Default: 'column' */
|
|
4
|
+
flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse';
|
|
5
|
+
flexWrap?: 'nowrap' | 'wrap' | 'wrap-reverse';
|
|
6
|
+
justifyContent?: 'flex-start' | 'center' | 'flex-end' | 'space-between' | 'space-around' | 'space-evenly';
|
|
7
|
+
alignItems?: 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'baseline';
|
|
8
|
+
alignSelf?: 'auto' | 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'baseline';
|
|
9
|
+
alignContent?: 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'space-between' | 'space-around' | 'space-evenly';
|
|
10
|
+
flexGrow?: number;
|
|
11
|
+
flexShrink?: number;
|
|
12
|
+
flexBasis?: number | 'auto';
|
|
13
|
+
width?: number | 'auto';
|
|
14
|
+
height?: number | 'auto';
|
|
15
|
+
minWidth?: number;
|
|
16
|
+
maxWidth?: number;
|
|
17
|
+
minHeight?: number;
|
|
18
|
+
maxHeight?: number;
|
|
19
|
+
padding?: number;
|
|
20
|
+
paddingTop?: number;
|
|
21
|
+
paddingRight?: number;
|
|
22
|
+
paddingBottom?: number;
|
|
23
|
+
paddingLeft?: number;
|
|
24
|
+
paddingHorizontal?: number;
|
|
25
|
+
paddingVertical?: number;
|
|
26
|
+
margin?: number | 'auto';
|
|
27
|
+
marginTop?: number | 'auto';
|
|
28
|
+
marginRight?: number | 'auto';
|
|
29
|
+
marginBottom?: number | 'auto';
|
|
30
|
+
marginLeft?: number | 'auto';
|
|
31
|
+
marginHorizontal?: number | 'auto';
|
|
32
|
+
marginVertical?: number | 'auto';
|
|
33
|
+
border?: number;
|
|
34
|
+
borderTop?: number;
|
|
35
|
+
borderRight?: number;
|
|
36
|
+
borderBottom?: number;
|
|
37
|
+
borderLeft?: number;
|
|
38
|
+
gap?: number;
|
|
39
|
+
rowGap?: number;
|
|
40
|
+
columnGap?: number;
|
|
41
|
+
position?: 'relative' | 'absolute';
|
|
42
|
+
top?: number;
|
|
43
|
+
right?: number;
|
|
44
|
+
bottom?: number;
|
|
45
|
+
left?: number;
|
|
46
|
+
aspectRatio?: number;
|
|
47
|
+
overflow?: 'visible' | 'hidden' | 'scroll';
|
|
48
|
+
display?: 'flex' | 'none';
|
|
49
|
+
}
|
|
50
|
+
/** A text leaf node. Has text content, font, and lineHeight for measurement. */
|
|
51
|
+
export interface TextNode extends FlexProps {
|
|
52
|
+
text: string;
|
|
53
|
+
/** Canvas font shorthand, e.g. '16px Inter' */
|
|
54
|
+
font: string;
|
|
55
|
+
/** Line height in pixels */
|
|
56
|
+
lineHeight: number;
|
|
57
|
+
/** Pretext whiteSpace mode */
|
|
58
|
+
whiteSpace?: 'normal' | 'pre-wrap';
|
|
59
|
+
}
|
|
60
|
+
/** A container (box) node that can have children. */
|
|
61
|
+
export interface BoxNode extends FlexProps {
|
|
62
|
+
children?: LayoutNode[];
|
|
63
|
+
}
|
|
64
|
+
/** A node in the declarative layout tree. */
|
|
65
|
+
export type LayoutNode = TextNode | BoxNode;
|
|
66
|
+
/** Type guard: is this node a text leaf? */
|
|
67
|
+
export declare function isTextNode(node: LayoutNode): node is TextNode;
|
|
68
|
+
/** Computed layout for a single node in the tree. */
|
|
69
|
+
export interface ComputedLayout {
|
|
70
|
+
x: number;
|
|
71
|
+
y: number;
|
|
72
|
+
width: number;
|
|
73
|
+
height: number;
|
|
74
|
+
children: ComputedLayout[];
|
|
75
|
+
/** Present only on text nodes: the measured line count. */
|
|
76
|
+
lineCount?: number;
|
|
77
|
+
/** Present only on text nodes: the original text content. */
|
|
78
|
+
text?: string;
|
|
79
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "textura",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "DOM-free layout engine combining Yoga flexbox with Pretext text measurement",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/razroo/textura"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
26
|
+
"check": "tsc",
|
|
27
|
+
"test": "bun test"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@chenglou/pretext": "^0.0.3",
|
|
31
|
+
"yoga-layout": "^3.2.1"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/bun": "latest",
|
|
35
|
+
"typescript": "6.0.2"
|
|
36
|
+
}
|
|
37
|
+
}
|