chat-layout 1.2.0-3 → 1.2.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 +24 -1
- package/example/chat.ts +53 -14
- package/index.d.mts +78 -40
- package/index.mjs +1223 -467
- package/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ The current v2-style APIs are:
|
|
|
9
9
|
- `Place`: place a single child at `start` / `center` / `end`
|
|
10
10
|
- `ShrinkWrap`: search the narrowest width that keeps the current height stable
|
|
11
11
|
- `MultilineText`: text layout with logical `align` or physical `physicalAlign`
|
|
12
|
-
- `
|
|
12
|
+
- `ListRenderer` + `ListState`: virtualized chat or timeline rendering
|
|
13
13
|
- `memoRenderItem` / `memoRenderItemBy`: item render memoization
|
|
14
14
|
|
|
15
15
|
## Quick example
|
|
@@ -53,6 +53,29 @@ return row;
|
|
|
53
53
|
|
|
54
54
|
See [example/chat.ts](./example/chat.ts) for a full chat example.
|
|
55
55
|
|
|
56
|
+
## List insert animation
|
|
57
|
+
|
|
58
|
+
`pushAll()` and `unshiftAll()` can opt into short-list insertion animations. They only animate when the previous rendered frame still had spare space below the last item; otherwise they fall back to the normal hard cut:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
list.pushAll([nextMessage], {
|
|
62
|
+
distance: 24, // duration defaults to 220ms when animation options are present
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
list.unshiftAll([olderMessage], {
|
|
66
|
+
duration: 220,
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
To make chat-style inserts automatically follow the latest visible edge, pass `followIfAtBoundary: true`. When the viewport was already pinned to that edge, the insert behaves like a conditional `jumpTo()` instead of combining with the enter animation:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
list.pushAll([nextMessage], {
|
|
74
|
+
followIfAtBoundary: true,
|
|
75
|
+
duration: 220,
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
56
79
|
## Layout notes
|
|
57
80
|
|
|
58
81
|
- `Flex` handles the main axis only. It shrink-wraps on the cross axis unless you opt into stretch behavior.
|
package/example/chat.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
-
ChatRenderer,
|
|
3
2
|
Flex,
|
|
4
3
|
FlexItem,
|
|
5
4
|
Fixed,
|
|
5
|
+
ListRenderer,
|
|
6
6
|
ListState,
|
|
7
7
|
MultilineText,
|
|
8
8
|
PaddingBox,
|
|
@@ -221,6 +221,7 @@ const richReplyPreview: InlineSpan<C>[] = [
|
|
|
221
221
|
|
|
222
222
|
let currentHover: ChatItem | undefined;
|
|
223
223
|
const REPLACE_ANIMATION_DURATION = 320;
|
|
224
|
+
const INSERT_ANIMATION_DURATION = 220;
|
|
224
225
|
|
|
225
226
|
function revokeMessage(item: MessageItem): RevokedItem {
|
|
226
227
|
return {
|
|
@@ -253,6 +254,13 @@ class ItemDetector extends Wrapper<C> {
|
|
|
253
254
|
list.update(this.item, nextItem, {
|
|
254
255
|
duration: REPLACE_ANIMATION_DURATION,
|
|
255
256
|
});
|
|
257
|
+
} else if (test.type === "auxclick") {
|
|
258
|
+
if (!list.items.includes(this.item)) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
list.delete(this.item, {
|
|
262
|
+
duration: REPLACE_ANIMATION_DURATION,
|
|
263
|
+
});
|
|
256
264
|
}
|
|
257
265
|
return true;
|
|
258
266
|
}
|
|
@@ -491,7 +499,9 @@ const list = new ListState<ChatItem>([
|
|
|
491
499
|
},
|
|
492
500
|
{ id: 9, kind: "message", sender: "B", content: randomText(5) },
|
|
493
501
|
]);
|
|
494
|
-
const renderer = new
|
|
502
|
+
const renderer = new ListRenderer(ctx, {
|
|
503
|
+
anchorMode: "bottom",
|
|
504
|
+
underflowAlign: "top",
|
|
495
505
|
renderItem,
|
|
496
506
|
list,
|
|
497
507
|
});
|
|
@@ -555,6 +565,19 @@ canvas.addEventListener("click", (e) => {
|
|
|
555
565
|
}
|
|
556
566
|
});
|
|
557
567
|
|
|
568
|
+
canvas.addEventListener("contextmenu", (e) => {
|
|
569
|
+
e.preventDefault();
|
|
570
|
+
const { top, left } = canvas.getBoundingClientRect();
|
|
571
|
+
const result = renderer.hittest({
|
|
572
|
+
x: e.clientX - left,
|
|
573
|
+
y: e.clientY - top,
|
|
574
|
+
type: "auxclick",
|
|
575
|
+
});
|
|
576
|
+
if (!result) {
|
|
577
|
+
currentHover = undefined;
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
|
|
558
581
|
function randomEnglishText(words: number): string {
|
|
559
582
|
const out: string[] = [];
|
|
560
583
|
for (let i = 0; i < words; i += 1) {
|
|
@@ -610,21 +633,37 @@ function randomText(words: number): string {
|
|
|
610
633
|
}
|
|
611
634
|
|
|
612
635
|
button("unshift", () => {
|
|
613
|
-
list.
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
636
|
+
list.unshiftAll(
|
|
637
|
+
[
|
|
638
|
+
{
|
|
639
|
+
id: nextMessageId++,
|
|
640
|
+
kind: "message",
|
|
641
|
+
sender: Math.random() < 0.5 ? "A" : "B",
|
|
642
|
+
content: randomText(10 + Math.floor(200 * Math.random())),
|
|
643
|
+
},
|
|
644
|
+
],
|
|
645
|
+
{
|
|
646
|
+
duration: INSERT_ANIMATION_DURATION,
|
|
647
|
+
followIfAtBoundary: true,
|
|
648
|
+
},
|
|
649
|
+
);
|
|
619
650
|
});
|
|
620
651
|
|
|
621
652
|
button("push", () => {
|
|
622
|
-
list.
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
653
|
+
list.pushAll(
|
|
654
|
+
[
|
|
655
|
+
{
|
|
656
|
+
id: nextMessageId++,
|
|
657
|
+
kind: "message",
|
|
658
|
+
sender: Math.random() < 0.5 ? "A" : "B",
|
|
659
|
+
content: randomText(10 + Math.floor(200 * Math.random())),
|
|
660
|
+
},
|
|
661
|
+
],
|
|
662
|
+
{
|
|
663
|
+
distance: 24,
|
|
664
|
+
followIfAtBoundary: true,
|
|
665
|
+
},
|
|
666
|
+
);
|
|
628
667
|
});
|
|
629
668
|
|
|
630
669
|
button("jump middle", () => {
|
package/index.d.mts
CHANGED
|
@@ -513,6 +513,20 @@ interface UpdateListItemAnimationOptions {
|
|
|
513
513
|
/** Animation duration in milliseconds. */
|
|
514
514
|
duration?: number;
|
|
515
515
|
}
|
|
516
|
+
interface DeleteListItemAnimationOptions {
|
|
517
|
+
/** Animation duration in milliseconds. */
|
|
518
|
+
duration?: number;
|
|
519
|
+
}
|
|
520
|
+
interface InsertListItemsAnimationOptions {
|
|
521
|
+
/** Animation duration in milliseconds. */
|
|
522
|
+
duration?: number;
|
|
523
|
+
/** Enter offset in pixels measured from the final resting position. */
|
|
524
|
+
distance?: number;
|
|
525
|
+
/** Auto-follow the insertion edge when the viewport was already pinned there. */
|
|
526
|
+
followIfAtBoundary?: boolean;
|
|
527
|
+
}
|
|
528
|
+
type PushListItemsAnimationOptions = InsertListItemsAnimationOptions;
|
|
529
|
+
type UnshiftListItemsAnimationOptions = InsertListItemsAnimationOptions;
|
|
516
530
|
declare class ListState<T extends {}> {
|
|
517
531
|
#private;
|
|
518
532
|
/** Pixel offset from the anchored item edge. */
|
|
@@ -530,15 +544,23 @@ declare class ListState<T extends {}> {
|
|
|
530
544
|
/** Prepends one or more items. */
|
|
531
545
|
unshift(...items: T[]): void;
|
|
532
546
|
/** Prepends an array of items. */
|
|
533
|
-
unshiftAll(items: T[]): void;
|
|
547
|
+
unshiftAll(items: T[], animation?: UnshiftListItemsAnimationOptions): void;
|
|
534
548
|
/** Appends one or more items. */
|
|
535
549
|
push(...items: T[]): void;
|
|
536
550
|
/** Appends an array of items. */
|
|
537
|
-
pushAll(items: T[]): void;
|
|
551
|
+
pushAll(items: T[], animation?: PushListItemsAnimationOptions): void;
|
|
538
552
|
/**
|
|
539
553
|
* Updates an existing item by object identity.
|
|
540
554
|
*/
|
|
541
555
|
update(targetItem: T, nextItem: T, animation?: UpdateListItemAnimationOptions): void;
|
|
556
|
+
/**
|
|
557
|
+
* Starts deleting an existing item by object identity.
|
|
558
|
+
*/
|
|
559
|
+
delete(item: T, animation?: DeleteListItemAnimationOptions): void;
|
|
560
|
+
/**
|
|
561
|
+
* Finalizes a pending delete by removing the item from the list.
|
|
562
|
+
*/
|
|
563
|
+
finalizeDelete(item: T): void;
|
|
542
564
|
/**
|
|
543
565
|
* Sets the current anchor item and pixel offset.
|
|
544
566
|
*/
|
|
@@ -570,7 +592,24 @@ declare function memoRenderItemBy<C extends CanvasRenderingContext2D, T, K>(keyO
|
|
|
570
592
|
resetKey: (key: K) => boolean;
|
|
571
593
|
};
|
|
572
594
|
//#endregion
|
|
595
|
+
//#region src/renderer/virtualized/base-types.d.ts
|
|
596
|
+
/** Per-item draw/hittest callbacks produced by the resolver. */
|
|
597
|
+
type VirtualizedResolvedItem = {
|
|
598
|
+
draw: (y: number) => boolean;
|
|
599
|
+
hittest: (test: HitTest, y: number) => boolean;
|
|
600
|
+
};
|
|
601
|
+
//#endregion
|
|
573
602
|
//#region src/renderer/virtualized/solver.d.ts
|
|
603
|
+
type ListAnchorMode = "top" | "bottom";
|
|
604
|
+
type ListUnderflowAlign = "top" | "bottom";
|
|
605
|
+
interface ListLayoutOptions {
|
|
606
|
+
anchorMode?: ListAnchorMode;
|
|
607
|
+
underflowAlign?: ListUnderflowAlign;
|
|
608
|
+
}
|
|
609
|
+
interface ResolvedListLayoutOptions {
|
|
610
|
+
anchorMode: ListAnchorMode;
|
|
611
|
+
underflowAlign: ListUnderflowAlign;
|
|
612
|
+
}
|
|
574
613
|
interface VisibleListState {
|
|
575
614
|
position?: number;
|
|
576
615
|
offset: number;
|
|
@@ -589,6 +628,11 @@ interface VisibleWindow<T> {
|
|
|
589
628
|
drawList: VisibleWindowEntry<T>[];
|
|
590
629
|
shift: number;
|
|
591
630
|
}
|
|
631
|
+
interface VisibleWindowResult<T> {
|
|
632
|
+
normalizedState: NormalizedListState;
|
|
633
|
+
resolutionPath: number[];
|
|
634
|
+
window: VisibleWindow<T>;
|
|
635
|
+
}
|
|
592
636
|
//#endregion
|
|
593
637
|
//#region src/renderer/virtualized/base.d.ts
|
|
594
638
|
/**
|
|
@@ -604,10 +648,6 @@ interface JumpToOptions {
|
|
|
604
648
|
/** Called after the jump completes or finishes animating. */
|
|
605
649
|
onComplete?: () => void;
|
|
606
650
|
}
|
|
607
|
-
type VirtualizedResolvedItem<C extends CanvasRenderingContext2D> = {
|
|
608
|
-
draw: (y: number) => boolean;
|
|
609
|
-
hittest: (test: HitTest, y: number) => boolean;
|
|
610
|
-
};
|
|
611
651
|
/**
|
|
612
652
|
* Shared base class for virtualized list renderers.
|
|
613
653
|
*/
|
|
@@ -618,7 +658,7 @@ declare abstract class VirtualizedRenderer<C extends CanvasRenderingContext2D, T
|
|
|
618
658
|
#private;
|
|
619
659
|
static readonly MIN_JUMP_DURATION = 160;
|
|
620
660
|
static readonly MAX_JUMP_DURATION = 420;
|
|
621
|
-
static readonly
|
|
661
|
+
static readonly JUMP_DURATION_PER_PIXEL = 0.7;
|
|
622
662
|
constructor(graphics: C, options: {
|
|
623
663
|
renderItem: (item: T) => Node<C>;
|
|
624
664
|
list: ListState<T>;
|
|
@@ -636,14 +676,15 @@ declare abstract class VirtualizedRenderer<C extends CanvasRenderingContext2D, T
|
|
|
636
676
|
/** Replaces the current item collection. */
|
|
637
677
|
set items(value: T[]);
|
|
638
678
|
/** Renders the current visible window. */
|
|
639
|
-
|
|
679
|
+
render(feedback?: RenderFeedback): boolean;
|
|
640
680
|
/** Hit-tests the current visible window. */
|
|
641
|
-
|
|
681
|
+
hittest(test: {
|
|
642
682
|
x: number;
|
|
643
683
|
y: number;
|
|
644
684
|
type: "click" | "auxclick" | "hover";
|
|
645
685
|
}): boolean;
|
|
646
686
|
protected _readListState(): VisibleListState;
|
|
687
|
+
protected _resolveVisibleWindow(now: number): VisibleWindowResult<VirtualizedResolvedItem>;
|
|
647
688
|
protected _commitListState(state: NormalizedListState): void;
|
|
648
689
|
/**
|
|
649
690
|
* Scrolls the viewport to the requested item index.
|
|
@@ -651,57 +692,54 @@ declare abstract class VirtualizedRenderer<C extends CanvasRenderingContext2D, T
|
|
|
651
692
|
jumpTo(index: number, options?: JumpToOptions): void;
|
|
652
693
|
protected _resetRenderFeedback(feedback?: RenderFeedback): void;
|
|
653
694
|
protected _accumulateRenderFeedback(feedback: RenderFeedback, idx: number, top: number, height: number): void;
|
|
654
|
-
protected _renderDrawList(list: VisibleWindow<VirtualizedResolvedItem
|
|
655
|
-
protected _renderVisibleWindow(window: VisibleWindow<VirtualizedResolvedItem
|
|
656
|
-
protected
|
|
657
|
-
|
|
695
|
+
protected _renderDrawList(list: VisibleWindow<VirtualizedResolvedItem>["drawList"], shift: number, feedback?: RenderFeedback): boolean;
|
|
696
|
+
protected _renderVisibleWindow(window: VisibleWindow<VirtualizedResolvedItem>, feedback?: RenderFeedback, extraShift?: number): boolean;
|
|
697
|
+
protected _readVisibleRange(top: number, height: number): {
|
|
698
|
+
top: number;
|
|
699
|
+
bottom: number;
|
|
700
|
+
} | undefined;
|
|
701
|
+
protected _pruneTransitionAnimations(_window: VisibleWindow<unknown>, now: number): boolean;
|
|
702
|
+
protected _hittestVisibleWindow(window: VisibleWindow<VirtualizedResolvedItem>, test: HitTest, extraShift?: number): boolean;
|
|
703
|
+
protected _captureVisibleItemSnapshot(solution: VisibleWindowResult<unknown>, extraShift?: number): void;
|
|
704
|
+
protected _prepareRender(now: number): boolean;
|
|
658
705
|
protected _finishRender(requestRedraw: boolean): boolean;
|
|
659
706
|
protected _clampItemIndex(index: number): number;
|
|
660
707
|
protected _getItemHeight(index: number): number;
|
|
708
|
+
protected _getItemHeightAt(index: number, now: number): number;
|
|
709
|
+
protected _readAnchorAt(now: number): number | undefined;
|
|
710
|
+
protected _restoreAnchor(anchor: number): void;
|
|
661
711
|
protected _resolveItem(item: T, _index: number, now: number): {
|
|
662
|
-
value: VirtualizedResolvedItem
|
|
712
|
+
value: VirtualizedResolvedItem;
|
|
663
713
|
height: number;
|
|
664
714
|
};
|
|
665
|
-
protected _getAnchorAtOffset(index: number, offset: number): number;
|
|
666
715
|
protected abstract _normalizeListState(state: VisibleListState): NormalizedListState;
|
|
667
|
-
protected abstract
|
|
716
|
+
protected abstract _getLayoutOptions(): ResolvedListLayoutOptions;
|
|
717
|
+
protected abstract _resolveVisibleWindowForState(state: VisibleListState, now: number): VisibleWindowResult<VirtualizedResolvedItem>;
|
|
718
|
+
protected abstract _readAnchor(state: NormalizedListState, readItemHeight: (index: number) => number): number;
|
|
668
719
|
protected abstract _applyAnchor(anchor: number): void;
|
|
669
720
|
protected abstract _getDefaultJumpBlock(): NonNullable<JumpToOptions["block"]>;
|
|
670
721
|
protected abstract _getTargetAnchor(index: number, block: NonNullable<JumpToOptions["block"]>): number;
|
|
671
|
-
protected abstract _getAnimatedLayerOffset(slotHeight: number, nodeHeight: number): number;
|
|
672
722
|
}
|
|
673
723
|
//#endregion
|
|
674
|
-
//#region src/renderer/virtualized/
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
declare class ChatRenderer<C extends CanvasRenderingContext2D, T extends {}> extends VirtualizedRenderer<C, T> {
|
|
679
|
-
#private;
|
|
680
|
-
protected _getDefaultJumpBlock(): NonNullable<JumpToOptions["block"]>;
|
|
681
|
-
protected _normalizeListState(state: VisibleListState): NormalizedListState;
|
|
682
|
-
protected _readAnchor(state: NormalizedListState): number;
|
|
683
|
-
protected _applyAnchor(anchor: number): void;
|
|
684
|
-
protected _getTargetAnchor(index: number, block: NonNullable<JumpToOptions["block"]>): number;
|
|
685
|
-
protected _getAnimatedLayerOffset(slotHeight: number, nodeHeight: number): number;
|
|
686
|
-
render(feedback?: RenderFeedback): boolean;
|
|
687
|
-
hittest(test: HitTest): boolean;
|
|
724
|
+
//#region src/renderer/virtualized/list.d.ts
|
|
725
|
+
interface ListRendererOptions<C extends CanvasRenderingContext2D, T extends {}> extends ListLayoutOptions {
|
|
726
|
+
renderItem: (item: T) => Node<C>;
|
|
727
|
+
list: ListState<T>;
|
|
688
728
|
}
|
|
689
|
-
//#endregion
|
|
690
|
-
//#region src/renderer/virtualized/timeline.d.ts
|
|
691
729
|
/**
|
|
692
|
-
* Virtualized renderer
|
|
730
|
+
* Virtualized list renderer with configurable anchor semantics.
|
|
693
731
|
*/
|
|
694
|
-
declare class
|
|
732
|
+
declare class ListRenderer<C extends CanvasRenderingContext2D, T extends {}> extends VirtualizedRenderer<C, T> {
|
|
695
733
|
#private;
|
|
734
|
+
constructor(graphics: C, options: ListRendererOptions<C, T>);
|
|
735
|
+
protected _getLayoutOptions(): ResolvedListLayoutOptions;
|
|
736
|
+
protected _resolveVisibleWindowForState(state: VisibleListState, now: number): VisibleWindowResult<VirtualizedResolvedItem>;
|
|
696
737
|
protected _getDefaultJumpBlock(): NonNullable<JumpToOptions["block"]>;
|
|
697
738
|
protected _normalizeListState(state: VisibleListState): NormalizedListState;
|
|
698
|
-
protected _readAnchor(state: NormalizedListState): number;
|
|
739
|
+
protected _readAnchor(state: NormalizedListState, readItemHeight: (index: number) => number): number;
|
|
699
740
|
protected _applyAnchor(anchor: number): void;
|
|
700
741
|
protected _getTargetAnchor(index: number, block: NonNullable<JumpToOptions["block"]>): number;
|
|
701
|
-
protected _getAnimatedLayerOffset(_slotHeight: number, _nodeHeight: number): number;
|
|
702
|
-
render(feedback?: RenderFeedback): boolean;
|
|
703
|
-
hittest(test: HitTest): boolean;
|
|
704
742
|
}
|
|
705
743
|
//#endregion
|
|
706
|
-
export { Axis, BaseRenderer, Box,
|
|
744
|
+
export { Axis, BaseRenderer, Box, ChildLayoutResult, Context, CrossAxisAlignment, DebugRenderer, DeleteListItemAnimationOptions, DynValue, Fixed, Flex, FlexContainerOptions, FlexItem, FlexItemOptions, FlexLayoutResult, Group, HitTest, InlineSpan, InsertListItemsAnimationOptions, JumpToOptions, LayoutConstraints, LayoutRect, ListAnchorMode, ListLayoutOptions, ListRenderer, ListRendererOptions, ListState, ListUnderflowAlign, MainAxisAlignment, MainAxisSize, MultilineText, MultilineTextOptions, Node, PaddingBox, PhysicalTextAlign, Place, PushListItemsAnimationOptions, RenderFeedback, RendererOptions, ShrinkWrap, Text, TextAlign, TextEllipsisPosition, TextJustifyMode, TextJustifyOptions, TextOptions, TextOverflowMode, TextOverflowWrapMode, TextStyleOptions, TextWhiteSpaceMode, TextWordBreakMode, UnshiftListItemsAnimationOptions, UpdateListItemAnimationOptions, VirtualizedRenderer, Wrapper, memoRenderItem, memoRenderItemBy };
|
|
707
745
|
//# sourceMappingURL=index.d.mts.map
|