chat-layout 1.2.0-0 → 1.2.0-1
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 +18 -5
- package/example/chat.ts +6 -1
- package/index.d.mts +19 -1
- package/index.mjs +107 -1
- package/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,13 +7,14 @@ The current v2-style APIs are:
|
|
|
7
7
|
- `Flex`: row/column layout
|
|
8
8
|
- `FlexItem`: explicit `grow` / `shrink` / `alignSelf`
|
|
9
9
|
- `Place`: place a single child at `start` / `center` / `end`
|
|
10
|
+
- `ShrinkWrap`: search the narrowest width that keeps the current height stable
|
|
10
11
|
- `MultilineText`: text layout with logical `align` or physical `physicalAlign`
|
|
11
12
|
- `ChatRenderer` + `ListState`: virtualized chat rendering
|
|
12
13
|
- `memoRenderItem` / `memoRenderItemBy`: item render memoization
|
|
13
14
|
|
|
14
15
|
## Quick example
|
|
15
16
|
|
|
16
|
-
Use `Flex` to build structure, `FlexItem` to control resize behavior, and `Place` to align the final bubble:
|
|
17
|
+
Use `Flex` to build structure, `FlexItem` to control resize behavior, `ShrinkWrap` to keep the bubble as narrow as possible without adding lines, and `Place` to align the final bubble:
|
|
17
18
|
|
|
18
19
|
```ts
|
|
19
20
|
const bubble = new RoundedBox(
|
|
@@ -26,17 +27,27 @@ const bubble = new RoundedBox(
|
|
|
26
27
|
{ top: 6, bottom: 6, left: 10, right: 10, radii: 8, fill: "#ccc" },
|
|
27
28
|
);
|
|
28
29
|
|
|
30
|
+
const body = new ShrinkWrap(
|
|
31
|
+
new Flex(
|
|
32
|
+
[senderLine, bubble],
|
|
33
|
+
{ direction: "column", gap: 4, alignItems: item.sender === "A" ? "end" : "start" },
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
|
|
29
37
|
const row = new Flex(
|
|
30
38
|
[
|
|
31
39
|
avatar,
|
|
32
|
-
new FlexItem(
|
|
40
|
+
new FlexItem(
|
|
41
|
+
new Place(body, {
|
|
42
|
+
align: item.sender === "A" ? "end" : "start",
|
|
43
|
+
}),
|
|
44
|
+
{ grow: 1, shrink: 1 },
|
|
45
|
+
),
|
|
33
46
|
],
|
|
34
47
|
{ direction: "row", gap: 4, reverse: item.sender === "A" },
|
|
35
48
|
);
|
|
36
49
|
|
|
37
|
-
return
|
|
38
|
-
align: item.sender === "A" ? "end" : "start",
|
|
39
|
-
});
|
|
50
|
+
return row;
|
|
40
51
|
```
|
|
41
52
|
|
|
42
53
|
See [example/chat.ts](./example/chat.ts) for a full chat example.
|
|
@@ -47,6 +58,7 @@ See [example/chat.ts](./example/chat.ts) for a full chat example.
|
|
|
47
58
|
- `maxWidth` / `maxHeight` limit measurement, but do not automatically make children fill the cross axis.
|
|
48
59
|
- Use `alignItems: "stretch"` or `alignSelf: "stretch"` when a child should fill the computed cross size.
|
|
49
60
|
- `Place` is the simplest way to align a single bubble left, center, or right.
|
|
61
|
+
- `ShrinkWrap` is useful when a bubble sits inside a growable slot but should still collapse to the narrowest width that preserves its current line count.
|
|
50
62
|
- `MultilineText.align` uses logical values: `start`, `center`, `end`.
|
|
51
63
|
- `MultilineText.physicalAlign` uses physical values: `left`, `center`, `right`.
|
|
52
64
|
- `Text` and `MultilineText` default to `whiteSpace: "normal"`, using the library's canvas-first collapsible whitespace behavior.
|
|
@@ -103,6 +115,7 @@ Notes:
|
|
|
103
115
|
- Shrink only applies when there is a finite main-axis constraint and total content size overflows it.
|
|
104
116
|
- Overflow is redistributed by `shrink * basis`; today `basis` is internal-only and always `"auto"`.
|
|
105
117
|
- Custom nodes can implement `measureMinContent()` for better shrink results.
|
|
118
|
+
- `ShrinkWrap` complements flex shrink: it keeps probing narrower `maxWidth` values until the child would become taller, then uses the last safe width as the final layout.
|
|
106
119
|
- Known limitation: column shrink with `MultilineText` does not clip drawing by itself.
|
|
107
120
|
|
|
108
121
|
## Migration notes
|
package/example/chat.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
MultilineText,
|
|
8
8
|
PaddingBox,
|
|
9
9
|
Place,
|
|
10
|
+
ShrinkWrap,
|
|
10
11
|
Text,
|
|
11
12
|
Wrapper,
|
|
12
13
|
memoRenderItem,
|
|
@@ -357,7 +358,11 @@ const renderItem = memoRenderItem((item: ChatItem): Node<C> => {
|
|
|
357
358
|
alignItems: item.sender === "A" ? "end" : "start",
|
|
358
359
|
});
|
|
359
360
|
|
|
360
|
-
const
|
|
361
|
+
const shrinkWrappedBody = new ShrinkWrap<C>(body, {
|
|
362
|
+
preferredMinWidth: 160,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const alignedBody = new Place<C>(shrinkWrappedBody, {
|
|
361
366
|
align: item.sender === "A" ? "end" : "start",
|
|
362
367
|
});
|
|
363
368
|
|
package/index.d.mts
CHANGED
|
@@ -364,6 +364,24 @@ declare class Place<C extends CanvasRenderingContext2D> extends Wrapper<C> {
|
|
|
364
364
|
hittest(ctx: Context<C>, test: HitTest): boolean;
|
|
365
365
|
}
|
|
366
366
|
//#endregion
|
|
367
|
+
//#region src/nodes/shrinkwrap.d.ts
|
|
368
|
+
interface ShrinkWrapOptions {
|
|
369
|
+
tolerance?: number;
|
|
370
|
+
preferredMinWidth?: number;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Shrinks a single child to the narrowest width that does not increase its reference height.
|
|
374
|
+
*/
|
|
375
|
+
declare class ShrinkWrap<C extends CanvasRenderingContext2D> extends Wrapper<C> {
|
|
376
|
+
#private;
|
|
377
|
+
readonly options: ShrinkWrapOptions;
|
|
378
|
+
constructor(inner: Node<C>, options?: ShrinkWrapOptions);
|
|
379
|
+
measure(ctx: Context<C>): Box;
|
|
380
|
+
measureMinContent(ctx: Context<C>): Box;
|
|
381
|
+
draw(ctx: Context<C>, x: number, y: number): boolean;
|
|
382
|
+
hittest(ctx: Context<C>, test: HitTest): boolean;
|
|
383
|
+
}
|
|
384
|
+
//#endregion
|
|
367
385
|
//#region src/nodes/text.d.ts
|
|
368
386
|
/**
|
|
369
387
|
* Draws wrapped text using the configured line height and alignment.
|
|
@@ -658,5 +676,5 @@ declare class TimelineRenderer<C extends CanvasRenderingContext2D, T extends {}>
|
|
|
658
676
|
hittest(test: HitTest): boolean;
|
|
659
677
|
}
|
|
660
678
|
//#endregion
|
|
661
|
-
export { Axis, BaseRenderer, Box, ChatRenderer, ChildLayoutResult, Context, CrossAxisAlignment, DebugRenderer, DynValue, Fixed, Flex, FlexContainerOptions, FlexItem, FlexItemOptions, FlexLayoutResult, Group, HitTest, InlineSpan, JumpToOptions, LayoutConstraints, LayoutRect, ListState, MainAxisAlignment, MainAxisSize, MultilineText, MultilineTextOptions, Node, PaddingBox, PhysicalTextAlign, Place, RenderFeedback, RendererOptions, ReplaceListItemAnimationOptions, Text, TextAlign, TextEllipsisPosition, TextOptions, TextOverflowMode, TextOverflowWrapMode, TextStyleOptions, TextWhiteSpaceMode, TextWordBreakMode, TimelineRenderer, VirtualizedRenderer, Wrapper, memoRenderItem, memoRenderItemBy };
|
|
679
|
+
export { Axis, BaseRenderer, Box, ChatRenderer, ChildLayoutResult, Context, CrossAxisAlignment, DebugRenderer, DynValue, Fixed, Flex, FlexContainerOptions, FlexItem, FlexItemOptions, FlexLayoutResult, Group, HitTest, InlineSpan, JumpToOptions, LayoutConstraints, LayoutRect, ListState, MainAxisAlignment, MainAxisSize, MultilineText, MultilineTextOptions, Node, PaddingBox, PhysicalTextAlign, Place, RenderFeedback, RendererOptions, ReplaceListItemAnimationOptions, ShrinkWrap, Text, TextAlign, TextEllipsisPosition, TextOptions, TextOverflowMode, TextOverflowWrapMode, TextStyleOptions, TextWhiteSpaceMode, TextWordBreakMode, TimelineRenderer, VirtualizedRenderer, Wrapper, memoRenderItem, memoRenderItemBy };
|
|
662
680
|
//# sourceMappingURL=index.d.mts.map
|
package/index.mjs
CHANGED
|
@@ -814,6 +814,112 @@ var Place = class extends Wrapper {
|
|
|
814
814
|
});
|
|
815
815
|
}
|
|
816
816
|
};
|
|
817
|
+
//#endregion
|
|
818
|
+
//#region src/nodes/shrinkwrap.ts
|
|
819
|
+
const DEFAULT_TOLERANCE = .5;
|
|
820
|
+
const HEIGHT_EPSILON = 1e-6;
|
|
821
|
+
function withMaxWidth(constraints, maxWidth) {
|
|
822
|
+
return {
|
|
823
|
+
...constraints,
|
|
824
|
+
maxWidth
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
function computeShrinkwrapWidth(measure, lowerBound, upperBound, referenceHeight, tolerance = DEFAULT_TOLERANCE) {
|
|
828
|
+
const minWidth = Math.min(lowerBound, upperBound);
|
|
829
|
+
const maxWidth = Math.max(lowerBound, upperBound);
|
|
830
|
+
const effectiveTolerance = Math.max(tolerance, HEIGHT_EPSILON);
|
|
831
|
+
const lowerBoundBox = measure(minWidth);
|
|
832
|
+
if (lowerBoundBox.height <= referenceHeight + HEIGHT_EPSILON) return {
|
|
833
|
+
maxWidth: minWidth,
|
|
834
|
+
box: lowerBoundBox
|
|
835
|
+
};
|
|
836
|
+
let lo = minWidth;
|
|
837
|
+
let hi = maxWidth;
|
|
838
|
+
let hiBox = measure(maxWidth);
|
|
839
|
+
while (hi - lo > effectiveTolerance) {
|
|
840
|
+
const probeWidth = (lo + hi) / 2;
|
|
841
|
+
const probeBox = measure(probeWidth);
|
|
842
|
+
if (probeBox.height <= referenceHeight + HEIGHT_EPSILON) {
|
|
843
|
+
hi = probeWidth;
|
|
844
|
+
hiBox = probeBox;
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
lo = probeWidth;
|
|
848
|
+
}
|
|
849
|
+
return {
|
|
850
|
+
maxWidth: hi,
|
|
851
|
+
box: hiBox
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Shrinks a single child to the narrowest width that does not increase its reference height.
|
|
856
|
+
*/
|
|
857
|
+
var ShrinkWrap = class extends Wrapper {
|
|
858
|
+
constructor(inner, options = {}) {
|
|
859
|
+
super(inner);
|
|
860
|
+
this.options = options;
|
|
861
|
+
}
|
|
862
|
+
measure(ctx) {
|
|
863
|
+
const constraints = ctx.constraints;
|
|
864
|
+
const availableWidth = constraints?.maxWidth;
|
|
865
|
+
if (availableWidth == null) {
|
|
866
|
+
const childConstraints = constraints == null ? void 0 : { ...constraints };
|
|
867
|
+
const childBox = ctx.measureNode(this.inner, childConstraints);
|
|
868
|
+
this.#writeLayout(ctx, childBox, childConstraints);
|
|
869
|
+
return childBox;
|
|
870
|
+
}
|
|
871
|
+
const boundedConstraints = constraints == null ? { maxWidth: availableWidth } : constraints;
|
|
872
|
+
const referenceConstraints = { ...boundedConstraints };
|
|
873
|
+
const referenceBox = ctx.measureNode(this.inner, referenceConstraints);
|
|
874
|
+
let lowerBound = measureNodeMinContent(ctx, this.inner, boundedConstraints).width;
|
|
875
|
+
const preferredMinWidth = this.options.preferredMinWidth == null ? void 0 : Math.max(0, this.options.preferredMinWidth);
|
|
876
|
+
if (preferredMinWidth != null && preferredMinWidth <= availableWidth) lowerBound = Math.max(lowerBound, preferredMinWidth);
|
|
877
|
+
if (boundedConstraints.minWidth != null) lowerBound = Math.max(lowerBound, boundedConstraints.minWidth);
|
|
878
|
+
if (lowerBound >= availableWidth) {
|
|
879
|
+
this.#writeLayout(ctx, referenceBox, referenceConstraints);
|
|
880
|
+
return referenceBox;
|
|
881
|
+
}
|
|
882
|
+
const finalConstraints = withMaxWidth(boundedConstraints, computeShrinkwrapWidth((maxWidth) => ctx.measureNode(this.inner, withMaxWidth(boundedConstraints, maxWidth)), lowerBound, availableWidth, referenceBox.height, this.options.tolerance ?? DEFAULT_TOLERANCE).maxWidth);
|
|
883
|
+
const finalBox = ctx.measureNode(this.inner, finalConstraints);
|
|
884
|
+
this.#writeLayout(ctx, finalBox, finalConstraints);
|
|
885
|
+
return finalBox;
|
|
886
|
+
}
|
|
887
|
+
measureMinContent(ctx) {
|
|
888
|
+
return measureNodeMinContent(ctx, this.inner);
|
|
889
|
+
}
|
|
890
|
+
draw(ctx, x, y) {
|
|
891
|
+
const layoutResult = readLayoutResult(this, ctx);
|
|
892
|
+
if (!layoutResult) return this.inner.draw(ctx, x, y);
|
|
893
|
+
const childResult = getSingleChildLayout(layoutResult);
|
|
894
|
+
if (!childResult) return false;
|
|
895
|
+
return childResult.node.draw(withConstraints(ctx, childResult.constraints), x + childResult.rect.x, y + childResult.rect.y);
|
|
896
|
+
}
|
|
897
|
+
hittest(ctx, test) {
|
|
898
|
+
const layoutResult = readLayoutResult(this, ctx);
|
|
899
|
+
if (!layoutResult) return false;
|
|
900
|
+
const hit = findChildAtPoint(layoutResult.children, test.x, test.y, "rect");
|
|
901
|
+
if (!hit) return false;
|
|
902
|
+
return hit.child.node.hittest(withConstraints(ctx, hit.child.constraints), {
|
|
903
|
+
...test,
|
|
904
|
+
x: hit.localX,
|
|
905
|
+
y: hit.localY
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
#writeLayout(ctx, childBox, childConstraints) {
|
|
909
|
+
const childRect = createRect(0, 0, childBox.width, childBox.height);
|
|
910
|
+
writeLayoutResult(this, ctx, {
|
|
911
|
+
containerBox: childRect,
|
|
912
|
+
contentBox: childRect,
|
|
913
|
+
children: [{
|
|
914
|
+
node: this.inner,
|
|
915
|
+
rect: childRect,
|
|
916
|
+
contentBox: childRect,
|
|
917
|
+
constraints: childConstraints
|
|
918
|
+
}],
|
|
919
|
+
constraints: ctx.constraints
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
};
|
|
817
923
|
Number.POSITIVE_INFINITY;
|
|
818
924
|
const MIN_CONTENT_WIDTH_EPSILON = .001;
|
|
819
925
|
let sharedGraphemeSegmenter;
|
|
@@ -4066,6 +4172,6 @@ var TimelineRenderer = class extends VirtualizedRenderer {
|
|
|
4066
4172
|
}
|
|
4067
4173
|
};
|
|
4068
4174
|
//#endregion
|
|
4069
|
-
export { BaseRenderer, ChatRenderer, DebugRenderer, Fixed, Flex, FlexItem, Group, ListState, MultilineText, PaddingBox, Place, Text, TimelineRenderer, VirtualizedRenderer, Wrapper, memoRenderItem, memoRenderItemBy };
|
|
4175
|
+
export { BaseRenderer, ChatRenderer, DebugRenderer, Fixed, Flex, FlexItem, Group, ListState, MultilineText, PaddingBox, Place, ShrinkWrap, Text, TimelineRenderer, VirtualizedRenderer, Wrapper, memoRenderItem, memoRenderItemBy };
|
|
4070
4176
|
|
|
4071
4177
|
//# sourceMappingURL=index.mjs.map
|