@fulate/ui 1.0.3 → 1.0.5

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/README.md CHANGED
@@ -163,6 +163,57 @@ const metrics = measureTextRun(ctx, text, root.skin.text, 14, 500);
163
163
  测量所需的文字状态写入 `ctx`,不会调用 `save()` 或 `restore()`;Canvas 生命周期以及业务特有的
164
164
  文字顺序、对齐和绘制仍由 Shape 自己拥有。
165
165
 
166
+ ### 组合型多行文本
167
+
168
+ 需要在一个非 `Text` Element 中放置可换行文字时,使用公开的 `TextBlock` 组合原语。它与 `Text`
169
+ 共用同一份 hard-break、grapheme、`wordWrap`、`maxLines` 和 `ellipsis` 排版实现:
170
+
171
+ ```ts
172
+ import {
173
+ TextBlock,
174
+ getTextBlockVisualBounds,
175
+ paintTextBlock,
176
+ resolveTextStyle
177
+ } from "@fulate/ui";
178
+
179
+ const style = resolveTextStyle(
180
+ {
181
+ fontSize: 14,
182
+ fontWeight: 500,
183
+ textAlign: "left",
184
+ textBaseline: "top",
185
+ verticalAlign: "top",
186
+ wordWrap: true
187
+ },
188
+ root.skin.text
189
+ );
190
+ const block = new TextBlock();
191
+ const snapshot = block.sync({
192
+ text,
193
+ style,
194
+ width: 120,
195
+ height: 80,
196
+ overflow: "visible",
197
+ wordWrap: true,
198
+ ellipsis: false
199
+ });
200
+
201
+ paintTextBlock(ctx, snapshot, style, 120, 80);
202
+ const visualBounds = getTextBlockVisualBounds(snapshot, style, 120, 80);
203
+ ```
204
+
205
+ `measure()` 只返回紧凑的聚合 metrics;最终宽高确定后调用一次 `sync()`,并把同一个 snapshot
206
+ 同时交给 `paintTextBlock()` 和 `getTextBlockVisualBounds()`。`paintTextBlock()` 不会替调用者
207
+ `save()`/`restore()`、安装 clip 或决定坐标变换;组合 owner 自己负责 Canvas 生命周期、裁剪和把
208
+ 视觉 bounds 接入自己的 transform/dirty-rect 计算。`overflow: "visible"` 不会自动把文字裁掉,
209
+ 也不会自动改变 owner 的 authored 宽高;使用 `overflow: "hidden"` 时,真正的 Canvas clip 仍由
210
+ 组合 owner 安装。
211
+
212
+ `TextBlock` 不属于任何 `Element`、`Root`、History 或 frame runtime。组合 owner 负责保存 block、
213
+ 在输入或 Skin 变化时调用 `invalidate()`,并在需要响应浏览器字体加载完成时订阅
214
+ `subscribeTextMeasurementChanges()`;返回的函数用于解除订阅。不要绕过 `@fulate/ui` 根入口导入
215
+ `TextBlock` 的内部实现,也不要为同一段文字另建一套换行或测量器。
216
+
166
217
  ## Editor capabilities
167
218
 
168
219
  UI 元素通过对象 identity token 直接提供 Selection、Transform、Snap、Point 与 Content capability;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,10 @@
1
- export { measureTextWidth, fitTextWithEllipsis } from "./text/layout";
1
+ export { measureTextWidth, fitTextWithEllipsis, subscribeTextMeasurementChanges } from "./text/layout";
2
+ export type { TextLayoutLine, TextLayoutMetrics, TextLayoutOptions, TextLayoutSnapshot } from "./text/layout";
2
3
  export { measureTextRun } from "./text/paint";
4
+ export { paintTextBlock } from "./text/paint";
3
5
  export type { TextLineBoxMetrics, TextRunMetrics } from "./text/paint";
6
+ export { TextBlock, getTextBlockVisualBounds } from "./text/block";
7
+ export type { TextBlockLayoutInput } from "./text/block";
4
8
  export { Rectangle, RectangleProperties, RectangleTransform } from "./rectangle";
5
9
  export type { RectangleOption, RectangleOptionPatch } from "./rectangle";
6
10
  export { Circle, CircleTransform } from "./circle";
@@ -9,8 +13,8 @@ export { Polygon, PolygonTransform } from "./polygon";
9
13
  export type { PolygonOption } from "./polygon";
10
14
  export { VectorPath, VectorPathProperties, VectorPathTransform } from "./vector-path";
11
15
  export type { VectorPathCommand, VectorPathGeometry, VectorPathOption } from "./vector-path";
12
- export { Text, TextProperties, TextTransform } from "./text";
13
- export type { TextOption } from "./text";
16
+ export { Text, TextProperties, TextTransform, resolveTextStyle } from "./text";
17
+ export type { TextOption, TextStyleConfig, TextStyleKey, ResolvedTextStyle } from "./text";
14
18
  export { Image, ImageProperties, ImageTransform } from "./image";
15
19
  export type { ImageOption, ImageResizeOption, ImageResizeMode } from "./image";
16
20
  export { Bitmap } from "./bitmap";
package/dist/index.js CHANGED
@@ -12,13 +12,21 @@ function invalidateFontMeasurements() {
12
12
  clearCache();
13
13
  for (const listener of measurementListeners) listener();
14
14
  }
15
+ /**
16
+ * Subscribe a non-Text owner to browser font measurement completion.
17
+ *
18
+ * TextBlock has no Root lifecycle of its own, so composite owners use this
19
+ * hook to invalidate their block and publish the resulting visual change.
20
+ */
15
21
  function subscribeTextMeasurementChanges(listener) {
16
22
  if (!observesFontLoading && document.fonts) {
17
23
  observesFontLoading = true;
18
24
  document.fonts.addEventListener("loadingdone", invalidateFontMeasurements);
19
25
  }
20
26
  measurementListeners.add(listener);
21
- return () => measurementListeners.delete(listener);
27
+ return () => {
28
+ measurementListeners.delete(listener);
29
+ };
22
30
  }
23
31
  function normalizeText(text) {
24
32
  return text.replace(/\r\n/g, "\n").replace(/[\r\f]/g, "\n");
@@ -749,7 +757,7 @@ var TEXT_STYLE_KEYS = Object.keys(TEXT_STYLE_DEFAULTS);
749
757
  function buildFontString(style, fontSize = style.fontSize, fontWeight = style.fontWeight) {
750
758
  return `${style.fontStyle} ${fontWeight} ${fontSize}px ${style.fontFamily}`;
751
759
  }
752
- function resolveTextStyle(instance, defaults) {
760
+ function resolveTextStyle(instance, defaults = TEXT_STYLE_DEFAULTS) {
753
761
  const resolved = {};
754
762
  for (const key of TEXT_STYLE_KEYS) {
755
763
  const authored = instance[key];
@@ -841,7 +849,13 @@ function measureTextLineBoxMetrics(text, style) {
841
849
  height: lineHeight
842
850
  };
843
851
  }
844
- /** 绘制一份已排版文字块;Text 与内部组合共同使用。 */
852
+ /**
853
+ * Paint a previously synchronized text block.
854
+ *
855
+ * The painter deliberately does not call `save()`/`restore()` or install a
856
+ * clip. The owner chooses the transform and clipping policy, then uses the
857
+ * same snapshot for `getTextBlockVisualBounds`.
858
+ */
845
859
  function paintTextBlock(ctx, layout, style, width, height) {
846
860
  if (layout.lines.length === 0) return;
847
861
  const font = buildFontString(style);
@@ -952,17 +966,18 @@ function paintTextContent(self, ctx, offsetY = 0) {
952
966
  ctx.restore();
953
967
  }
954
968
  //#endregion
955
- //#region packages/ui/src/paint/circle.ts
956
- /** 在当前 Canvas path 中加入圆形;不创建场景或 Geometry 对象。 */
957
- function traceCirclePath(ctx, center, radius) {
958
- ctx.arc(center.x, center.y, radius, 0, Math.PI * 2);
959
- }
960
- //#endregion
961
969
  //#region packages/ui/src/text/block.ts
962
970
  function sameLayoutKey(left, right) {
963
971
  return left !== null && left.text === right.text && left.font === right.font && left.letterSpacing === right.letterSpacing && left.width === right.width && left.height === right.height && left.lineHeight === right.lineHeight && left.wordWrap === right.wordWrap && left.maxLines === right.maxLines && left.overflow === right.overflow && left.ellipsis === right.ellipsis;
964
972
  }
965
- /** Text 与内部组合共享的唯一准备和最终排版 owner。 */
973
+ /**
974
+ * The canonical layout owner for a non-Element text block.
975
+ *
976
+ * A block owns prepared text and the latest final snapshot only; it does not
977
+ * own an Element, Root, transform, clip, dirty rectangle, or History entry.
978
+ * Composite owners should call `measure()` for aggregate metrics, then call
979
+ * `sync()` with the final width/height before painting or computing bounds.
980
+ */
966
981
  var TextBlock = class {
967
982
  prepared = null;
968
983
  metrics = null;
@@ -1023,7 +1038,7 @@ var TextBlock = class {
1023
1038
  };
1024
1039
  }
1025
1040
  };
1026
- /** 返回与共享 glyph painter 一致的文字 visual bounds。 */
1041
+ /** Return visual bounds for the same snapshot consumed by `paintTextBlock`. */
1027
1042
  function getTextBlockVisualBounds(layout, style, width, height) {
1028
1043
  if (layout.lines.length === 0) return null;
1029
1044
  let left = Infinity;
@@ -1059,6 +1074,12 @@ function getTextBlockVisualBounds(layout, style, width, height) {
1059
1074
  };
1060
1075
  }
1061
1076
  //#endregion
1077
+ //#region packages/ui/src/paint/circle.ts
1078
+ /** 在当前 Canvas path 中加入圆形;不创建场景或 Geometry 对象。 */
1079
+ function traceCirclePath(ctx, center, radius) {
1080
+ ctx.arc(center.x, center.y, radius, 0, Math.PI * 2);
1081
+ }
1082
+ //#endregion
1062
1083
  //#region packages/ui/src/connection-port-decoration.ts
1063
1084
  var DOT_RADIUS = 4;
1064
1085
  var LABEL_GAP = 3;
@@ -5946,4 +5967,4 @@ function applyLineTreeTopologyDelta(topology, delta, direction) {
5946
5967
  };
5947
5968
  }
5948
5969
  //#endregion
5949
- export { BaseLine, Bitmap, Circle, CircleTransform, ContentEditingCapabilityToken, DEFAULT_LINE_TREE_BRANCH_STYLE, EMPTY_LINE_TREE_TOPOLOGY, EditorFrameFlush, EndpointRelationIndex, Group, GroupTransform, Image, ImageProperties, ImageTransform, Line, LineTransform, LineTree, LineTreeProperties, LineTreeTransform, PointEditingCapabilityToken, Polygon, PolygonTransform, Rectangle, RectangleProperties, RectangleTransform, RippleOverlay, ScrollView, ScrollViewTransform, SelectionBehaviorCapabilityToken, SelectionBehaviorMode, SelectionDiveMode, SelectionSnapMode, SnapSourceCapabilityToken, Text, TextProperties, TextTransform, TransformInteractionCapabilityToken, Triangle, TriangleTransform, VectorPath, VectorPathProperties, VectorPathTransform, WorkerSurfacePool, Workspace, WorkspaceProperties, addLineTreeBranchAtTail, applyLineTreeTopologyDelta, arrow, autoPlacement, computePosition, connectEndpoint, connectionPortAccepts, createFloatingPlatform, createLineTreeTopology, createLineTreeTopologyDelta, defineEditorCapability, detectOverflow, disconnectEndpoint, fitTextWithEllipsis, fitWorkspace, flip, getEditorCapability, getEditorFrameFlush, getEndpointRelationIndex, getLineTreeBindableEndpoints, getLineTreeBranch, getLineTreeEndpointRelationBindings, getLineTreeJunctionAnchorIds, getLineTreePoint, getLineTreePointReferenceCounts, hasConnectionPortCapacity, hide, insertLineTreePoint, isEditorCapabilityHost, isEndpointRelationSource, limitShift, measureTextRun, measureTextWidth, moveLineTreePoint, offset, remapLineTreeRelations, removeLineTreeBranch, removeLineTreePoint, resolveConnectEndpointChange, resolveConnectionPortChanges, resolveDisconnectEndpointChanges, scaleLineTreeTopology, shift, size, updateLineTreeBranchStyle, workspaceFocusInsetsKey };
5970
+ export { BaseLine, Bitmap, Circle, CircleTransform, ContentEditingCapabilityToken, DEFAULT_LINE_TREE_BRANCH_STYLE, EMPTY_LINE_TREE_TOPOLOGY, EditorFrameFlush, EndpointRelationIndex, Group, GroupTransform, Image, ImageProperties, ImageTransform, Line, LineTransform, LineTree, LineTreeProperties, LineTreeTransform, PointEditingCapabilityToken, Polygon, PolygonTransform, Rectangle, RectangleProperties, RectangleTransform, RippleOverlay, ScrollView, ScrollViewTransform, SelectionBehaviorCapabilityToken, SelectionBehaviorMode, SelectionDiveMode, SelectionSnapMode, SnapSourceCapabilityToken, Text, TextBlock, TextProperties, TextTransform, TransformInteractionCapabilityToken, Triangle, TriangleTransform, VectorPath, VectorPathProperties, VectorPathTransform, WorkerSurfacePool, Workspace, WorkspaceProperties, addLineTreeBranchAtTail, applyLineTreeTopologyDelta, arrow, autoPlacement, computePosition, connectEndpoint, connectionPortAccepts, createFloatingPlatform, createLineTreeTopology, createLineTreeTopologyDelta, defineEditorCapability, detectOverflow, disconnectEndpoint, fitTextWithEllipsis, fitWorkspace, flip, getEditorCapability, getEditorFrameFlush, getEndpointRelationIndex, getLineTreeBindableEndpoints, getLineTreeBranch, getLineTreeEndpointRelationBindings, getLineTreeJunctionAnchorIds, getLineTreePoint, getLineTreePointReferenceCounts, getTextBlockVisualBounds, hasConnectionPortCapacity, hide, insertLineTreePoint, isEditorCapabilityHost, isEndpointRelationSource, limitShift, measureTextRun, measureTextWidth, moveLineTreePoint, offset, paintTextBlock, remapLineTreeRelations, removeLineTreeBranch, removeLineTreePoint, resolveConnectEndpointChange, resolveConnectionPortChanges, resolveDisconnectEndpointChanges, resolveTextStyle, scaleLineTreeTopology, shift, size, subscribeTextMeasurementChanges, updateLineTreeBranchStyle, workspaceFocusInsetsKey };
@@ -1,9 +1,10 @@
1
1
  import type { Rect } from "@fulate/share";
2
2
  import { TextLayoutSnapshot, type TextLayoutMetrics } from "./layout";
3
3
  import { type ResolvedTextStyle } from "./style";
4
+ /** Input for one composite text layout owner. */
4
5
  export interface TextBlockLayoutInput {
5
6
  readonly text: string;
6
- readonly style: ResolvedTextStyle;
7
+ readonly style: Readonly<ResolvedTextStyle>;
7
8
  readonly width?: number;
8
9
  readonly height?: number;
9
10
  readonly overflow: "hidden" | "visible";
@@ -11,7 +12,14 @@ export interface TextBlockLayoutInput {
11
12
  readonly maxLines?: number;
12
13
  readonly ellipsis?: boolean;
13
14
  }
14
- /** Text 与内部组合共享的唯一准备和最终排版 owner。 */
15
+ /**
16
+ * The canonical layout owner for a non-Element text block.
17
+ *
18
+ * A block owns prepared text and the latest final snapshot only; it does not
19
+ * own an Element, Root, transform, clip, dirty rectangle, or History entry.
20
+ * Composite owners should call `measure()` for aggregate metrics, then call
21
+ * `sync()` with the final width/height before painting or computing bounds.
22
+ */
15
23
  export declare class TextBlock {
16
24
  private prepared;
17
25
  private metrics;
@@ -27,5 +35,5 @@ export declare class TextBlock {
27
35
  private getPrepared;
28
36
  private getLayoutKey;
29
37
  }
30
- /** 返回与共享 glyph painter 一致的文字 visual bounds。 */
31
- export declare function getTextBlockVisualBounds(layout: TextLayoutSnapshot, style: ResolvedTextStyle, width: number, height: number): Rect | null;
38
+ /** Return visual bounds for the same snapshot consumed by `paintTextBlock`. */
39
+ export declare function getTextBlockVisualBounds(layout: TextLayoutSnapshot, style: Readonly<ResolvedTextStyle>, width: number, height: number): Rect | null;
@@ -2,10 +2,10 @@ import { ChangeImpact, type ElementTransformClass, type GeometrySnapshot, type L
2
2
  import { Rectangle, RectangleProperties, RectangleTransform, type RectangleOption } from "../rectangle";
3
3
  import { type BackgroundColor, type PointView, type Rect } from "@fulate/share";
4
4
  import { type TextLineBoxMetrics } from "./paint";
5
- import { TEXT_STYLE_DEFAULTS, type TextStyleKey, type TextStyleConfig, type ResolvedTextStyle, buildFontString } from "./style";
5
+ import { TEXT_STYLE_DEFAULTS, type TextStyleKey, type TextStyleConfig, type ResolvedTextStyle, buildFontString, resolveTextStyle } from "./style";
6
6
  import { type ContentEditingConsumer, type ContentEditingSession, type EditorCapabilityToken } from "../editor/capability";
7
7
  export type { TextStyleConfig, TextStyleKey, ResolvedTextStyle };
8
- export { buildFontString, TEXT_STYLE_DEFAULTS };
8
+ export { buildFontString, resolveTextStyle, TEXT_STYLE_DEFAULTS };
9
9
  export { measureTextRun } from "./paint";
10
10
  export type { TextLineBoxMetrics, TextRunMetrics } from "./paint";
11
11
  /**
@@ -1,5 +1,11 @@
1
1
  import { type LayoutLine } from "@chenglou/pretext";
2
- export declare function subscribeTextMeasurementChanges(listener: () => void): () => boolean;
2
+ /**
3
+ * Subscribe a non-Text owner to browser font measurement completion.
4
+ *
5
+ * TextBlock has no Root lifecycle of its own, so composite owners use this
6
+ * hook to invalidate their block and publish the resulting visual change.
7
+ */
8
+ export declare function subscribeTextMeasurementChanges(listener: () => void): () => void;
3
9
  export interface TextLayoutLine {
4
10
  readonly text: string;
5
11
  readonly width: number;
@@ -54,6 +54,12 @@ export interface TextLineBoxMetrics {
54
54
  }
55
55
  /** Measure the line box produced by the existing Text painter. */
56
56
  export declare function measureTextLineBoxMetrics(text: string, style: ResolvedTextStyle): TextLineBoxMetrics;
57
- /** 绘制一份已排版文字块;Text 与内部组合共同使用。 */
58
- export declare function paintTextBlock(ctx: CanvasRenderingContext2D, layout: TextLayoutSnapshot, style: ResolvedTextStyle, width: number, height: number): void;
57
+ /**
58
+ * Paint a previously synchronized text block.
59
+ *
60
+ * The painter deliberately does not call `save()`/`restore()` or install a
61
+ * clip. The owner chooses the transform and clipping policy, then uses the
62
+ * same snapshot for `getTextBlockVisualBounds`.
63
+ */
64
+ export declare function paintTextBlock(ctx: CanvasRenderingContext2D, layout: TextLayoutSnapshot, style: Readonly<ResolvedTextStyle>, width: number, height: number): void;
59
65
  export declare function paintTextContent(self: Text, ctx: CanvasRenderingContext2D, offsetY?: number): void;
@@ -10,4 +10,4 @@ export declare function buildFontString(style: {
10
10
  fontSize?: number;
11
11
  fontFamily?: string;
12
12
  }, fontSize?: number, fontWeight?: string | number): string;
13
- export declare function resolveTextStyle(instance: Record<string, any>, defaults: Record<string, any>): ResolvedTextStyle;
13
+ export declare function resolveTextStyle(instance: Partial<TextStyleConfig>, defaults?: Readonly<Required<TextStyleConfig>>): ResolvedTextStyle;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fulate/ui",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",