@flighthq/text 0.1.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.
@@ -0,0 +1,29 @@
1
+ import { computeTextFormatFontString } from './textFormatFont';
2
+
3
+ describe('computeTextFormatFontString', () => {
4
+ it('returns a CSS font string from format properties', () => {
5
+ const font = computeTextFormatFontString({ size: 16, font: 'Arial', bold: false, italic: false });
6
+ expect(font).toBe('normal normal 16px Arial');
7
+ });
8
+
9
+ it('includes bold and italic when set', () => {
10
+ const font = computeTextFormatFontString({ size: 12, font: 'sans-serif', bold: true, italic: true });
11
+ expect(font).toBe('italic bold 12px sans-serif');
12
+ });
13
+
14
+ it('uses defaults when size and font are omitted', () => {
15
+ const font = computeTextFormatFontString({});
16
+ expect(font).toContain('12px');
17
+ expect(font).toContain('serif');
18
+ });
19
+
20
+ it('uses italic style when italic is true', () => {
21
+ const font = computeTextFormatFontString({ italic: true });
22
+ expect(font).toMatch(/^italic /);
23
+ });
24
+
25
+ it('uses bold weight when bold is true', () => {
26
+ const font = computeTextFormatFontString({ bold: true });
27
+ expect(font).toContain(' bold ');
28
+ });
29
+ });
@@ -0,0 +1,265 @@
1
+ import { createRectangle } from '@flighthq/geometry';
2
+ import { getNodeLocalBoundsRevision, getNodeLocalContentRevision } from '@flighthq/node';
3
+ import { setTextLayoutMeasureProvider } from '@flighthq/textlayout';
4
+ import type { Node, PartialNode, TextLabel } from '@flighthq/types';
5
+ import { TextLabelKind } from '@flighthq/types';
6
+
7
+ import {
8
+ appendTextLabelString,
9
+ computeTextLabelLocalBoundsRectangle,
10
+ createTextLabel,
11
+ createTextLabelData,
12
+ createTextLabelRuntime,
13
+ getTextLabelFormat,
14
+ getTextLabelRuntime,
15
+ getTextLabelString,
16
+ setTextLabelAutoSize,
17
+ setTextLabelFormat,
18
+ setTextLabelHeight,
19
+ setTextLabelString,
20
+ setTextLabelWidth,
21
+ } from './textLabel';
22
+
23
+ describe('appendTextLabelString', () => {
24
+ it('appends the value to the existing text', () => {
25
+ const text = createTextLabel({ data: { text: 'hello' } });
26
+ appendTextLabelString(text, ' world');
27
+ expect(text.data.text).toBe('hello world');
28
+ });
29
+
30
+ it('invalidates local content after append', () => {
31
+ const text = createTextLabel({ data: { text: 'hi' } });
32
+ const content = getNodeLocalContentRevision(text);
33
+ appendTextLabelString(text, '!');
34
+ expect(getNodeLocalContentRevision(text)).toBe(content + 1);
35
+ });
36
+
37
+ it('does not invalidate when value is empty', () => {
38
+ const text = createTextLabel({ data: { text: 'hi' } });
39
+ const content = getNodeLocalContentRevision(text);
40
+ appendTextLabelString(text, '');
41
+ expect(getNodeLocalContentRevision(text)).toBe(content);
42
+ });
43
+ });
44
+
45
+ describe('computeTextLabelLocalBoundsRectangle', () => {
46
+ afterEach(() => {
47
+ setTextLayoutMeasureProvider(null);
48
+ });
49
+
50
+ it('sets out dimensions from data width and height when autoSize is none', () => {
51
+ const text = createTextLabel({ data: { width: 200, height: 50 } });
52
+ const out = createRectangle();
53
+ computeTextLabelLocalBoundsRectangle(out, text as unknown as Node);
54
+ expect(out.x).toBe(0);
55
+ expect(out.width).toBe(200);
56
+ expect(out.height).toBe(50);
57
+ });
58
+
59
+ it('grows to the measured single run under autoSize left, anchored at the origin', () => {
60
+ setTextLayoutMeasureProvider((value) => value.length * 7);
61
+ const text = createTextLabel({ data: { autoSize: 'left', width: 200, height: 50 } });
62
+ setTextLabelString(text, 'hi');
63
+ const out = createRectangle();
64
+ computeTextLabelLocalBoundsRectangle(out, text as unknown as Node);
65
+ expect(out.x).toBe(0);
66
+ expect(out.width).toBeGreaterThan(0);
67
+ expect(out.width).toBeLessThan(200);
68
+ });
69
+
70
+ it('falls back to the fixed box under autoSize when no measure provider exists', () => {
71
+ const text = createTextLabel({ data: { autoSize: 'left', width: 200, height: 50 } });
72
+ setTextLabelString(text, 'hi');
73
+ const out = createRectangle();
74
+ computeTextLabelLocalBoundsRectangle(out, text as unknown as Node);
75
+ expect(out.width).toBe(200);
76
+ expect(out.height).toBe(50);
77
+ });
78
+ });
79
+
80
+ describe('createTextLabel', () => {
81
+ let text: TextLabel;
82
+
83
+ beforeEach(() => {
84
+ text = createTextLabel();
85
+ });
86
+
87
+ it('initializes default values', () => {
88
+ expect(text.data.text).toBe('');
89
+ expect(text.data.autoSize).toBe('none');
90
+ expect(text.data.width).toBe(100);
91
+ expect(text.data.height).toBe(100);
92
+ expect(text.data.textFormat).not.toBeNull();
93
+ expect(text.kind).toStrictEqual(TextLabelKind);
94
+ });
95
+
96
+ it('allows pre-defined values', () => {
97
+ const base: PartialNode<TextLabel> = {
98
+ data: {
99
+ autoSize: 'left',
100
+ text: 'foo',
101
+ width: 300,
102
+ height: 40,
103
+ },
104
+ };
105
+ const obj = createTextLabel(base);
106
+ expect(obj.data.autoSize).toStrictEqual(base.data!.autoSize);
107
+ expect(obj.data.text).toStrictEqual(base.data!.text);
108
+ expect(obj.data.width).toBe(300);
109
+ expect(obj.data.height).toBe(40);
110
+ });
111
+
112
+ it('returns a new object for better hidden-class performance', () => {
113
+ const base = {};
114
+ const obj = createTextLabel(base);
115
+ expect(obj).not.toStrictEqual(base);
116
+ });
117
+ });
118
+
119
+ describe('createTextLabelData', () => {
120
+ it('returns default values', () => {
121
+ const data = createTextLabelData();
122
+ expect(data.autoSize).toBe('none');
123
+ expect(data.height).toBe(100);
124
+ expect(data.text).toBe('');
125
+ expect(data.textFormat).not.toBeNull();
126
+ expect(data.width).toBe(100);
127
+ });
128
+
129
+ it('allows pre-defined values', () => {
130
+ const data = createTextLabelData({ autoSize: 'left', text: 'hello', width: 250, height: 30 });
131
+ expect(data.autoSize).toBe('left');
132
+ expect(data.text).toBe('hello');
133
+ expect(data.width).toBe(250);
134
+ expect(data.height).toBe(30);
135
+ });
136
+ });
137
+
138
+ describe('createTextLabelRuntime', () => {
139
+ it('returns a non-null runtime', () => {
140
+ const runtime = createTextLabelRuntime();
141
+ expect(runtime).not.toBeNull();
142
+ });
143
+
144
+ it('starts without attached layout runtime state', () => {
145
+ const runtime = createTextLabelRuntime();
146
+ expect(runtime.textLayout).toBeNull();
147
+ });
148
+
149
+ it('uses computeTextLabelLocalBoundsRectangle', () => {
150
+ const runtime = createTextLabelRuntime();
151
+ expect(runtime.computeLocalBoundsRectangle).toStrictEqual(computeTextLabelLocalBoundsRectangle);
152
+ });
153
+ });
154
+
155
+ describe('getTextLabelFormat', () => {
156
+ it('returns the textFormat field', () => {
157
+ const format = { size: 24, bold: true };
158
+ const text = createTextLabel({ data: { textFormat: format } });
159
+ expect(getTextLabelFormat(text)).toBe(text.data.textFormat);
160
+ });
161
+ });
162
+
163
+ describe('getTextLabelRuntime', () => {
164
+ it('returns the runtime for a TextLabel', () => {
165
+ const text = createTextLabel();
166
+ const runtime = getTextLabelRuntime(text);
167
+ expect(runtime).not.toBeNull();
168
+ });
169
+ });
170
+
171
+ describe('getTextLabelString', () => {
172
+ it('returns the text field', () => {
173
+ const text = createTextLabel({ data: { text: 'hello' } });
174
+ expect(getTextLabelString(text)).toBe('hello');
175
+ });
176
+ });
177
+
178
+ describe('setTextLabelAutoSize', () => {
179
+ it('sets autoSize, bumps content, and invalidates local bounds', () => {
180
+ const text = createTextLabel();
181
+ const content = getNodeLocalContentRevision(text);
182
+ const bounds = getNodeLocalBoundsRevision(text);
183
+ setTextLabelAutoSize(text, 'left');
184
+ expect(text.data.autoSize).toBe('left');
185
+ expect(getNodeLocalContentRevision(text)).toBe(content + 1);
186
+ expect(getNodeLocalBoundsRevision(text)).not.toBe(bounds);
187
+ });
188
+
189
+ it('does not bump content when the value is unchanged', () => {
190
+ const text = createTextLabel();
191
+ const content = getNodeLocalContentRevision(text);
192
+ setTextLabelAutoSize(text, 'none');
193
+ expect(getNodeLocalContentRevision(text)).toBe(content);
194
+ });
195
+ });
196
+
197
+ describe('setTextLabelFormat', () => {
198
+ it('sets the format and bumps content without touching bounds', () => {
199
+ const text = createTextLabel();
200
+ const content = getNodeLocalContentRevision(text);
201
+ const bounds = getNodeLocalBoundsRevision(text);
202
+ const format = { size: 24 };
203
+ setTextLabelFormat(text, format);
204
+ expect(text.data.textFormat).toBe(format);
205
+ expect(getNodeLocalContentRevision(text)).toBe(content + 1);
206
+ expect(getNodeLocalBoundsRevision(text)).toBe(bounds);
207
+ });
208
+ });
209
+
210
+ describe('setTextLabelHeight', () => {
211
+ it('sets height, bumps content, and invalidates local bounds', () => {
212
+ const text = createTextLabel();
213
+ const content = getNodeLocalContentRevision(text);
214
+ const bounds = getNodeLocalBoundsRevision(text);
215
+ setTextLabelHeight(text, 250);
216
+ expect(text.data.height).toBe(250);
217
+ expect(getNodeLocalContentRevision(text)).toBe(content + 1);
218
+ expect(getNodeLocalBoundsRevision(text)).not.toBe(bounds);
219
+ });
220
+
221
+ it('does not bump content when the value is unchanged', () => {
222
+ const text = createTextLabel();
223
+ const content = getNodeLocalContentRevision(text);
224
+ setTextLabelHeight(text, text.data.height);
225
+ expect(getNodeLocalContentRevision(text)).toBe(content);
226
+ });
227
+ });
228
+
229
+ describe('setTextLabelString', () => {
230
+ it('sets text and bumps content without touching bounds', () => {
231
+ const text = createTextLabel();
232
+ const content = getNodeLocalContentRevision(text);
233
+ const bounds = getNodeLocalBoundsRevision(text);
234
+ setTextLabelString(text, 'hello');
235
+ expect(text.data.text).toBe('hello');
236
+ expect(getNodeLocalContentRevision(text)).toBe(content + 1);
237
+ expect(getNodeLocalBoundsRevision(text)).toBe(bounds);
238
+ });
239
+
240
+ it('does not bump content when the value is unchanged', () => {
241
+ const text = createTextLabel({ data: { text: 'same' } });
242
+ const content = getNodeLocalContentRevision(text);
243
+ setTextLabelString(text, 'same');
244
+ expect(getNodeLocalContentRevision(text)).toBe(content);
245
+ });
246
+ });
247
+
248
+ describe('setTextLabelWidth', () => {
249
+ it('sets width, bumps content, and invalidates local bounds', () => {
250
+ const text = createTextLabel();
251
+ const content = getNodeLocalContentRevision(text);
252
+ const bounds = getNodeLocalBoundsRevision(text);
253
+ setTextLabelWidth(text, 300);
254
+ expect(text.data.width).toBe(300);
255
+ expect(getNodeLocalContentRevision(text)).toBe(content + 1);
256
+ expect(getNodeLocalBoundsRevision(text)).not.toBe(bounds);
257
+ });
258
+
259
+ it('does not bump content when the value is unchanged', () => {
260
+ const text = createTextLabel();
261
+ const content = getNodeLocalContentRevision(text);
262
+ setTextLabelWidth(text, text.data.width);
263
+ expect(getNodeLocalContentRevision(text)).toBe(content);
264
+ });
265
+ });
@@ -0,0 +1,78 @@
1
+ import { createTextMetrics, setTextLayoutMeasureProvider } from '@flighthq/textlayout';
2
+
3
+ import { createRichText, setRichTextString } from './richText';
4
+ import { createTextLabel, setTextLabelString } from './textLabel';
5
+ import { ensureTextLayout, getTextLayout, getTextLayoutMetrics } from './textLabelLayout';
6
+
7
+ // A fake fixed-advance measure (7px per char) — exercises the ensure path without a renderer.
8
+ const measure = (text: string) => text.length * 7;
9
+
10
+ afterEach(() => {
11
+ setTextLayoutMeasureProvider(null);
12
+ });
13
+
14
+ describe('ensureTextLayout', () => {
15
+ it('leaves the layout null when no measure provider is registered', () => {
16
+ const richText = createRichText();
17
+ setRichTextString(richText, 'hello');
18
+ ensureTextLayout(richText);
19
+ expect(getTextLayout(richText)).toBeNull();
20
+ });
21
+
22
+ it('computes a layout for a RichText once a provider is registered', () => {
23
+ setTextLayoutMeasureProvider(measure);
24
+ const richText = createRichText();
25
+ setRichTextString(richText, 'hello');
26
+ ensureTextLayout(richText);
27
+ expect(getTextLayout(richText)).not.toBeNull();
28
+ });
29
+
30
+ it('computes a layout for a TextLabel via the shared single-run path', () => {
31
+ setTextLayoutMeasureProvider(measure);
32
+ const label = createTextLabel();
33
+ setTextLabelString(label, 'hi');
34
+ ensureTextLayout(label);
35
+ expect(getTextLayout(label)).not.toBeNull();
36
+ });
37
+ });
38
+
39
+ describe('getTextLayout', () => {
40
+ it('ensures the layout and returns it', () => {
41
+ setTextLayoutMeasureProvider(measure);
42
+ const richText = createRichText();
43
+ setRichTextString(richText, 'hi');
44
+ expect(getTextLayout(richText)).not.toBeNull();
45
+ });
46
+ });
47
+
48
+ describe('getTextLayoutMetrics', () => {
49
+ it('measures the content size for a RichText', () => {
50
+ setTextLayoutMeasureProvider(measure);
51
+ const richText = createRichText();
52
+ setRichTextString(richText, 'hello');
53
+ const out = createTextMetrics();
54
+ getTextLayoutMetrics(out, richText);
55
+ expect(out.width).toBeGreaterThan(0);
56
+ expect(out.numLines).toBeGreaterThanOrEqual(1);
57
+ });
58
+
59
+ it('measures the content size for a TextLabel', () => {
60
+ setTextLayoutMeasureProvider(measure);
61
+ const label = createTextLabel();
62
+ setTextLabelString(label, 'hello');
63
+ const out = createTextMetrics();
64
+ getTextLayoutMetrics(out, label);
65
+ expect(out.width).toBeGreaterThan(0);
66
+ });
67
+
68
+ it('zeroes the metrics when no provider is registered', () => {
69
+ const richText = createRichText();
70
+ setRichTextString(richText, 'hello');
71
+ const out = createTextMetrics();
72
+ out.width = 99;
73
+ getTextLayoutMetrics(out, richText);
74
+ expect(out.width).toBe(0);
75
+ expect(out.height).toBe(0);
76
+ expect(out.numLines).toBe(0);
77
+ });
78
+ });