Skip to content

packages/core/src/lib/components/text.ts

Read as Markdown

This is the source snapshot used to build these API details. View this revision on GitHub.

Back to reference

1 import { resolveTexturePaint } from '../textures/texture-protocol.js';
2 import { definePrimitive } from '../define-primitive.js';
3 import { materializeTextChildren } from '../element/children.js';
4 import { GLOBAL_STATE } from '../global-state.js';
5 import type {
6   PibblEventFocusOptions,
7   PibblEventHandlers,
8   PibblEventParticipationOptions,
9 } from '../events/types.js';
10 import {
11   resolveOptionalLength,
12   stripLayoutParticipation,
13 } from '../style/resolve-built-in.js';
14 import { registerOpaqueStyleProperties } from '../style/resolve-dispatch.js';
15 import type { ResolvedTextStyle, TextStyle } from '../style/types.js';
16 import {
17   CanvasMeasurementService,
18   MeasureResult,
19   RenderingContext2D,
20 } from '../types.js';
21 import type { Constraints } from '../layout/types.js';
22 import type { PibblRenderTransaction, PibblTextInput } from '../types.js';
23 import type { PibblPhysics2DPropExtension } from '../types.js';
24 import { resolveLength } from '../style/resolve-length.js';
25 import { wireCommonEvents } from '../util/wire-common-events.js';
26 import { pushClippingPath } from '../util/push-clipping-path.js';
27 import { resolveTextClipBounds, resolveTextLayout } from './text-layout.js';
28 import { withReceiverInputProvision } from '../signals/input-provision.js';
29 
30 /**
31  * Authored inputs for Text, including the declared data and presentation options.
32  *
33  * @see {@link PibblEventHandlers}
34  * @see {@link PibblEventParticipationOptions}
35  * @see {@link PibblPhysics2DPropExtension}
36  * @see {@link PibblTextInput}
37  * @see {@link Text}
38  */
39 export interface TextProps
40   extends
41     PibblEventHandlers,
42     PibblEventFocusOptions,
43     PibblEventParticipationOptions,
44     PibblPhysics2DPropExtension {
45   /** Descendant content or the callback that supplies it. See {@link PibblTextInput}. */
46   children?: PibblTextInput;
47 }
48 
49 const materializedTextTransactions = new WeakMap<
50   PibblRenderTransaction,
51   WeakMap<object, string>
52 >();
53 
54 function renderText(
55   props: TextProps,
56   style: Readonly<ResolvedTextStyle>,
57   ctx: RenderingContext2D,
58 ) {
59   const text = materializeTextContent(props);
60   const {
61     font = '13px sans-serif',
62     textBaseline = 'top',
63     textAlign = 'left',
64     fill,
65     stroke,
66     strokeWidth = 0,
67     opacity,
68     cursor,
69   } = style;
70 
71   if (opacity !== undefined) {
72     ctx.globalAlpha = ctx.globalAlpha * opacity;
73   }
74 
75   ctx.font = font;
76   ctx.textBaseline = textBaseline;
77   ctx.textAlign = textAlign;
78 
79   const layout = resolveTextLayout(
80     value => ctx.measureText(value),
81     text,
82     style,
83   );
84   const clipBounds =
85     layout.clipBounds ?? resolveTextClipBounds(style, ctx.canvas.height);
86 
87   let releaseClip: (() => void) | undefined;
88   if (clipBounds) {
89     const clipPath = new Path2D();
90     clipPath.rect(
91       clipBounds.x,
92       clipBounds.y,
93       clipBounds.width,
94       clipBounds.height,
95     );
96     releaseClip = pushClippingPath(ctx, clipPath);
97   }
98 
99   try {
100     const textBounds = props.pointerEvents === 'none' ? undefined : new Path2D();
101 
102     for (const line of layout.lines) {
103       if (stroke && strokeWidth) {
104         ctx.strokeStyle = resolveTexturePaint(stroke, ctx, layout.bounds ?? line.bounds);
105         ctx.lineWidth = strokeWidth;
106         ctx.strokeText(line.text, line.x, line.y, line.squishWidth);
107       }
108 
109       if (fill) {
110         ctx.fillStyle = resolveTexturePaint(fill, ctx, layout.bounds ?? line.bounds);
111         ctx.fillText(line.text, line.x, line.y, line.squishWidth);
112       }
113 
114       if (textBounds) {
115         textBounds.moveTo(line.bounds.x, line.bounds.y);
116         textBounds.lineTo(line.bounds.x + line.bounds.width, line.bounds.y);
117         textBounds.lineTo(
118           line.bounds.x + line.bounds.width,
119           line.bounds.y + line.bounds.height,
120         );
121         textBounds.lineTo(line.bounds.x, line.bounds.y + line.bounds.height);
122         textBounds.closePath();
123       }
124     }
125 
126     wireCommonEvents(
127       props,
128       textBounds,
129       true,
130       strokeWidth,
131       cursor,
132       layout.bounds,
133     );
134   } finally {
135     releaseClip?.();
136   }
137 }
138 
139 /**
140  * Draws Canvas text from textual children using the resolved font, wrapping, fitting, and paint
141  * style.
142  *
143  * @param props - Authored component inputs, supplied through JSX or createElement. See the linked
144  * props and style types.
145  * @throws When called directly; Pibbl mounts this component through JSX or createElement.
146  *
147  * @see {@link TextProps}
148  * @see {@link TextStyle}
149  */
150 export const Text = definePrimitive<
151   TextProps,
152   TextStyle,
153   TextStyle,
154   ResolvedTextStyle
155 >(renderText, {
156   childInput: 'text',
157   resolveStyle: (style, context) => ({
158     ...stripLayoutParticipation(style),
159     x: 0,
160     y: 0,
161     width: resolveOptionalLength(
162       style.width,
163       context.percentageBasis.width,
164       'width',
165       context,
166     ),
167     height: resolveOptionalLength(
168       style.height,
169       context.percentageBasis.height,
170       'height',
171       context,
172     ),
173   }) as ResolvedTextStyle,
174   measure: ({ props, style, constraints, service }) =>
175     measureTextStyle(
176       materializeTextContent(props),
177       style,
178       constraints,
179       service,
180     ),
181 });
182 registerOpaqueStyleProperties(Text, [
183   'font',
184   'fill',
185   'stroke',
186   'cursor',
187   'fit',
188   'textBaseline',
189   'textAlign',
190 ]);
191 
192 function measureTextStyle(
193   text: string,
194   style: Readonly<TextStyle>,
195   constraints: Readonly<Constraints>,
196   service: CanvasMeasurementService,
197 ): MeasureResult {
198   const context = {
199     component: 'Text',
200     algorithm: 'measurement',
201     constraints,
202   };
203   const specifiedWidth = style.width === undefined ? undefined :
204     resolveLength(style.width, constraints.maxWidth, 'width', context);
205   const specifiedHeight = style.height === undefined ? undefined :
206     resolveLength(style.height, constraints.maxHeight, 'height', context);
207   const layoutWidth = specifiedWidth ?? (
208     style.wrap && Number.isFinite(constraints.maxWidth) ?
209       constraints.maxWidth :
210       undefined
211   );
212   const font = style.font ?? '13px sans-serif';
213   const layout = resolveTextLayout(
214     value => service.measureText(value, font),
215     text,
216     {
217       ...style,
218       x: 0,
219       y: 0,
220       width: layoutWidth,
221       height: specifiedHeight,
222     },
223   );
224   return {
225     status: 'measured',
226     size: {
227       width: specifiedWidth ?? (style.wrap && layoutWidth !== undefined ?
228         layoutWidth :
229         layout.bounds?.width ?? 0),
230       height: specifiedHeight ?? (style.wrap ?
231         layout.lines.length * (style.lineHeight ?? 13) :
232         layout.bounds?.height ?? 0),
233     },
234   };
235 }
236 
237 function materializeTextContent(props: Readonly<TextProps>): string {
238   const transaction = GLOBAL_STATE.renderTransaction;
239   const receiver = GLOBAL_STATE.primitiveReceiverToken;
240   if (!transaction || !receiver) {
241     return materializeTextChildren(props.children, 'Text');
242   }
243 
244   let receivers = materializedTextTransactions.get(transaction);
245   if (!receivers) {
246     receivers = new WeakMap();
247     materializedTextTransactions.set(transaction, receivers);
248   }
249   const cached = receivers.get(receiver);
250   if (cached !== undefined) return cached;
251   const materialized = withReceiverInputProvision(
252     transaction,
253     receiver,
254     () => materializeTextChildren(props.children, 'Text'),
255   );
256   receivers.set(receiver, materialized);
257   return materialized;
258 }
259 

Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.