chat-layout 1.2.0-4 → 1.2.0-6
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 +35 -14
- package/index.d.mts +64 -31
- package/index.mjs +1240 -482
- 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 {
|
|
@@ -498,7 +499,9 @@ const list = new ListState<ChatItem>([
|
|
|
498
499
|
},
|
|
499
500
|
{ id: 9, kind: "message", sender: "B", content: randomText(5) },
|
|
500
501
|
]);
|
|
501
|
-
const renderer = new
|
|
502
|
+
const renderer = new ListRenderer(ctx, {
|
|
503
|
+
anchorMode: "bottom",
|
|
504
|
+
underflowAlign: "top",
|
|
502
505
|
renderItem,
|
|
503
506
|
list,
|
|
504
507
|
});
|
|
@@ -510,6 +513,8 @@ function drawFrame(): void {
|
|
|
510
513
|
maxIdx: Number.NaN,
|
|
511
514
|
min: Number.NaN,
|
|
512
515
|
max: Number.NaN,
|
|
516
|
+
canAutoFollowTop: false,
|
|
517
|
+
canAutoFollowBottom: false,
|
|
513
518
|
};
|
|
514
519
|
renderer.render(feedback);
|
|
515
520
|
|
|
@@ -630,21 +635,37 @@ function randomText(words: number): string {
|
|
|
630
635
|
}
|
|
631
636
|
|
|
632
637
|
button("unshift", () => {
|
|
633
|
-
list.
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
638
|
+
list.unshiftAll(
|
|
639
|
+
[
|
|
640
|
+
{
|
|
641
|
+
id: nextMessageId++,
|
|
642
|
+
kind: "message",
|
|
643
|
+
sender: Math.random() < 0.5 ? "A" : "B",
|
|
644
|
+
content: randomText(10 + Math.floor(200 * Math.random())),
|
|
645
|
+
},
|
|
646
|
+
],
|
|
647
|
+
{
|
|
648
|
+
duration: INSERT_ANIMATION_DURATION,
|
|
649
|
+
autoFollow: true,
|
|
650
|
+
},
|
|
651
|
+
);
|
|
639
652
|
});
|
|
640
653
|
|
|
641
654
|
button("push", () => {
|
|
642
|
-
list.
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
655
|
+
list.pushAll(
|
|
656
|
+
[
|
|
657
|
+
{
|
|
658
|
+
id: nextMessageId++,
|
|
659
|
+
kind: "message",
|
|
660
|
+
sender: Math.random() < 0.5 ? "A" : "B",
|
|
661
|
+
content: randomText(10 + Math.floor(200 * Math.random())),
|
|
662
|
+
},
|
|
663
|
+
],
|
|
664
|
+
{
|
|
665
|
+
distance: 24,
|
|
666
|
+
autoFollow: true,
|
|
667
|
+
},
|
|
668
|
+
);
|
|
648
669
|
});
|
|
649
670
|
|
|
650
671
|
button("jump middle", () => {
|
package/index.d.mts
CHANGED
|
@@ -19,6 +19,10 @@ interface RenderFeedback {
|
|
|
19
19
|
min: number;
|
|
20
20
|
/** Largest visible continuous item position, expressed in item coordinates rather than pixels. */
|
|
21
21
|
max: number;
|
|
22
|
+
/** Whether the current viewport may auto-follow inserts at the visual top edge. */
|
|
23
|
+
canAutoFollowTop: boolean;
|
|
24
|
+
/** Whether the current viewport may auto-follow inserts at the visual bottom edge. */
|
|
25
|
+
canAutoFollowBottom: boolean;
|
|
22
26
|
}
|
|
23
27
|
/**
|
|
24
28
|
* The main axis direction used by flex containers.
|
|
@@ -517,6 +521,16 @@ interface DeleteListItemAnimationOptions {
|
|
|
517
521
|
/** Animation duration in milliseconds. */
|
|
518
522
|
duration?: number;
|
|
519
523
|
}
|
|
524
|
+
interface InsertListItemsAnimationOptions {
|
|
525
|
+
/** Animation duration in milliseconds. */
|
|
526
|
+
duration?: number;
|
|
527
|
+
/** Enter offset in pixels measured from the final resting position. */
|
|
528
|
+
distance?: number;
|
|
529
|
+
/** Auto-follow the insertion edge when the viewport was already pinned there. */
|
|
530
|
+
autoFollow?: boolean;
|
|
531
|
+
}
|
|
532
|
+
type PushListItemsAnimationOptions = InsertListItemsAnimationOptions;
|
|
533
|
+
type UnshiftListItemsAnimationOptions = InsertListItemsAnimationOptions;
|
|
520
534
|
declare class ListState<T extends {}> {
|
|
521
535
|
#private;
|
|
522
536
|
/** Pixel offset from the anchored item edge. */
|
|
@@ -534,11 +548,11 @@ declare class ListState<T extends {}> {
|
|
|
534
548
|
/** Prepends one or more items. */
|
|
535
549
|
unshift(...items: T[]): void;
|
|
536
550
|
/** Prepends an array of items. */
|
|
537
|
-
unshiftAll(items: T[]): void;
|
|
551
|
+
unshiftAll(items: T[], animation?: UnshiftListItemsAnimationOptions): void;
|
|
538
552
|
/** Appends one or more items. */
|
|
539
553
|
push(...items: T[]): void;
|
|
540
554
|
/** Appends an array of items. */
|
|
541
|
-
pushAll(items: T[]): void;
|
|
555
|
+
pushAll(items: T[], animation?: PushListItemsAnimationOptions): void;
|
|
542
556
|
/**
|
|
543
557
|
* Updates an existing item by object identity.
|
|
544
558
|
*/
|
|
@@ -583,6 +597,10 @@ declare function memoRenderItemBy<C extends CanvasRenderingContext2D, T, K>(keyO
|
|
|
583
597
|
};
|
|
584
598
|
//#endregion
|
|
585
599
|
//#region src/renderer/virtualized/base-types.d.ts
|
|
600
|
+
type AutoFollowCapabilities = {
|
|
601
|
+
top: boolean;
|
|
602
|
+
bottom: boolean;
|
|
603
|
+
};
|
|
586
604
|
/** Per-item draw/hittest callbacks produced by the resolver. */
|
|
587
605
|
type VirtualizedResolvedItem = {
|
|
588
606
|
draw: (y: number) => boolean;
|
|
@@ -590,6 +608,16 @@ type VirtualizedResolvedItem = {
|
|
|
590
608
|
};
|
|
591
609
|
//#endregion
|
|
592
610
|
//#region src/renderer/virtualized/solver.d.ts
|
|
611
|
+
type ListAnchorMode = "top" | "bottom";
|
|
612
|
+
type ListUnderflowAlign = "top" | "bottom";
|
|
613
|
+
interface ListLayoutOptions {
|
|
614
|
+
anchorMode?: ListAnchorMode;
|
|
615
|
+
underflowAlign?: ListUnderflowAlign;
|
|
616
|
+
}
|
|
617
|
+
interface ResolvedListLayoutOptions {
|
|
618
|
+
anchorMode: ListAnchorMode;
|
|
619
|
+
underflowAlign: ListUnderflowAlign;
|
|
620
|
+
}
|
|
593
621
|
interface VisibleListState {
|
|
594
622
|
position?: number;
|
|
595
623
|
offset: number;
|
|
@@ -610,6 +638,7 @@ interface VisibleWindow<T> {
|
|
|
610
638
|
}
|
|
611
639
|
interface VisibleWindowResult<T> {
|
|
612
640
|
normalizedState: NormalizedListState;
|
|
641
|
+
resolutionPath: number[];
|
|
613
642
|
window: VisibleWindow<T>;
|
|
614
643
|
}
|
|
615
644
|
//#endregion
|
|
@@ -637,7 +666,7 @@ declare abstract class VirtualizedRenderer<C extends CanvasRenderingContext2D, T
|
|
|
637
666
|
#private;
|
|
638
667
|
static readonly MIN_JUMP_DURATION = 160;
|
|
639
668
|
static readonly MAX_JUMP_DURATION = 420;
|
|
640
|
-
static readonly
|
|
669
|
+
static readonly JUMP_DURATION_PER_PIXEL = 0.7;
|
|
641
670
|
constructor(graphics: C, options: {
|
|
642
671
|
renderItem: (item: T) => Node<C>;
|
|
643
672
|
list: ListState<T>;
|
|
@@ -663,67 +692,71 @@ declare abstract class VirtualizedRenderer<C extends CanvasRenderingContext2D, T
|
|
|
663
692
|
type: "click" | "auxclick" | "hover";
|
|
664
693
|
}): boolean;
|
|
665
694
|
protected _readListState(): VisibleListState;
|
|
695
|
+
protected _resolveVisibleWindow(now: number): VisibleWindowResult<VirtualizedResolvedItem>;
|
|
666
696
|
protected _commitListState(state: NormalizedListState): void;
|
|
667
697
|
/**
|
|
668
698
|
* Scrolls the viewport to the requested item index.
|
|
669
699
|
*/
|
|
670
700
|
jumpTo(index: number, options?: JumpToOptions): void;
|
|
701
|
+
/**
|
|
702
|
+
* Scrolls the viewport to the visual top edge and arms top auto-follow immediately.
|
|
703
|
+
*/
|
|
704
|
+
jumpToTop(options?: JumpToOptions): void;
|
|
705
|
+
/**
|
|
706
|
+
* Scrolls the viewport to the visual bottom edge and arms bottom auto-follow immediately.
|
|
707
|
+
*/
|
|
708
|
+
jumpToBottom(options?: JumpToOptions): void;
|
|
671
709
|
protected _resetRenderFeedback(feedback?: RenderFeedback): void;
|
|
672
710
|
protected _accumulateRenderFeedback(feedback: RenderFeedback, idx: number, top: number, height: number): void;
|
|
673
711
|
protected _renderDrawList(list: VisibleWindow<VirtualizedResolvedItem>["drawList"], shift: number, feedback?: RenderFeedback): boolean;
|
|
674
|
-
protected _renderVisibleWindow(window: VisibleWindow<VirtualizedResolvedItem>, feedback?: RenderFeedback): boolean;
|
|
712
|
+
protected _renderVisibleWindow(window: VisibleWindow<VirtualizedResolvedItem>, feedback?: RenderFeedback, extraShift?: number): boolean;
|
|
713
|
+
protected _readAutoFollowCapabilities(window: VisibleWindow<VirtualizedResolvedItem>, extraShift?: number): AutoFollowCapabilities;
|
|
675
714
|
protected _readVisibleRange(top: number, height: number): {
|
|
676
715
|
top: number;
|
|
677
716
|
bottom: number;
|
|
678
717
|
} | undefined;
|
|
679
|
-
protected
|
|
680
|
-
protected _hittestVisibleWindow(window: VisibleWindow<VirtualizedResolvedItem>, test: HitTest): boolean;
|
|
681
|
-
protected _captureVisibleItemSnapshot(
|
|
718
|
+
protected _pruneTransitionAnimations(_window: VisibleWindow<unknown>, now: number): boolean;
|
|
719
|
+
protected _hittestVisibleWindow(window: VisibleWindow<VirtualizedResolvedItem>, test: HitTest, extraShift?: number): boolean;
|
|
720
|
+
protected _captureVisibleItemSnapshot(solution: VisibleWindowResult<unknown>, extraShift?: number): void;
|
|
682
721
|
protected _prepareRender(now: number): boolean;
|
|
683
722
|
protected _finishRender(requestRedraw: boolean): boolean;
|
|
684
723
|
protected _clampItemIndex(index: number): number;
|
|
685
724
|
protected _getItemHeight(index: number): number;
|
|
725
|
+
protected _getItemHeightAt(index: number, now: number): number;
|
|
726
|
+
protected _readAnchorAt(now: number): number | undefined;
|
|
727
|
+
protected _restoreAnchor(anchor: number): void;
|
|
686
728
|
protected _resolveItem(item: T, _index: number, now: number): {
|
|
687
729
|
value: VirtualizedResolvedItem;
|
|
688
730
|
height: number;
|
|
689
731
|
};
|
|
690
|
-
protected _getAnchorAtOffset(index: number, offset: number): number;
|
|
691
732
|
protected abstract _normalizeListState(state: VisibleListState): NormalizedListState;
|
|
692
|
-
protected abstract
|
|
693
|
-
protected abstract
|
|
733
|
+
protected abstract _getLayoutOptions(): ResolvedListLayoutOptions;
|
|
734
|
+
protected abstract _resolveVisibleWindowForState(state: VisibleListState, now: number): VisibleWindowResult<VirtualizedResolvedItem>;
|
|
735
|
+
protected abstract _readAnchor(state: NormalizedListState, readItemHeight: (index: number) => number): number;
|
|
694
736
|
protected abstract _applyAnchor(anchor: number): void;
|
|
695
737
|
protected abstract _getDefaultJumpBlock(): NonNullable<JumpToOptions["block"]>;
|
|
696
738
|
protected abstract _getTargetAnchor(index: number, block: NonNullable<JumpToOptions["block"]>): number;
|
|
697
|
-
protected abstract _getAnimatedLayerOffset(slotHeight: number, nodeHeight: number): number;
|
|
698
739
|
}
|
|
699
740
|
//#endregion
|
|
700
|
-
//#region src/renderer/virtualized/
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
declare class ChatRenderer<C extends CanvasRenderingContext2D, T extends {}> extends VirtualizedRenderer<C, T> {
|
|
705
|
-
protected _resolveVisibleWindow(now: number): VisibleWindowResult<VirtualizedResolvedItem>;
|
|
706
|
-
protected _getDefaultJumpBlock(): NonNullable<JumpToOptions["block"]>;
|
|
707
|
-
protected _normalizeListState(state: VisibleListState): NormalizedListState;
|
|
708
|
-
protected _readAnchor(state: NormalizedListState): number;
|
|
709
|
-
protected _applyAnchor(anchor: number): void;
|
|
710
|
-
protected _getTargetAnchor(index: number, block: NonNullable<JumpToOptions["block"]>): number;
|
|
711
|
-
protected _getAnimatedLayerOffset(slotHeight: number, nodeHeight: number): number;
|
|
741
|
+
//#region src/renderer/virtualized/list.d.ts
|
|
742
|
+
interface ListRendererOptions<C extends CanvasRenderingContext2D, T extends {}> extends ListLayoutOptions {
|
|
743
|
+
renderItem: (item: T) => Node<C>;
|
|
744
|
+
list: ListState<T>;
|
|
712
745
|
}
|
|
713
|
-
//#endregion
|
|
714
|
-
//#region src/renderer/virtualized/timeline.d.ts
|
|
715
746
|
/**
|
|
716
|
-
* Virtualized renderer
|
|
747
|
+
* Virtualized list renderer with configurable anchor semantics.
|
|
717
748
|
*/
|
|
718
|
-
declare class
|
|
719
|
-
|
|
749
|
+
declare class ListRenderer<C extends CanvasRenderingContext2D, T extends {}> extends VirtualizedRenderer<C, T> {
|
|
750
|
+
#private;
|
|
751
|
+
constructor(graphics: C, options: ListRendererOptions<C, T>);
|
|
752
|
+
protected _getLayoutOptions(): ResolvedListLayoutOptions;
|
|
753
|
+
protected _resolveVisibleWindowForState(state: VisibleListState, now: number): VisibleWindowResult<VirtualizedResolvedItem>;
|
|
720
754
|
protected _getDefaultJumpBlock(): NonNullable<JumpToOptions["block"]>;
|
|
721
755
|
protected _normalizeListState(state: VisibleListState): NormalizedListState;
|
|
722
|
-
protected _readAnchor(state: NormalizedListState): number;
|
|
756
|
+
protected _readAnchor(state: NormalizedListState, readItemHeight: (index: number) => number): number;
|
|
723
757
|
protected _applyAnchor(anchor: number): void;
|
|
724
758
|
protected _getTargetAnchor(index: number, block: NonNullable<JumpToOptions["block"]>): number;
|
|
725
|
-
protected _getAnimatedLayerOffset(_slotHeight: number, _nodeHeight: number): number;
|
|
726
759
|
}
|
|
727
760
|
//#endregion
|
|
728
|
-
export { Axis, BaseRenderer, Box,
|
|
761
|
+
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 };
|
|
729
762
|
//# sourceMappingURL=index.d.mts.map
|