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 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
- - `ChatRenderer` + `ListState`: virtualized chat rendering
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 ChatRenderer(ctx, {
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.unshift({
634
- id: nextMessageId++,
635
- kind: "message",
636
- sender: Math.random() < 0.5 ? "A" : "B",
637
- content: randomText(10 + Math.floor(200 * Math.random())),
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.push({
643
- id: nextMessageId++,
644
- kind: "message",
645
- sender: Math.random() < 0.5 ? "A" : "B",
646
- content: randomText(10 + Math.floor(200 * Math.random())),
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 JUMP_DURATION_PER_ITEM = 28;
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 _pruneReplacementAnimations(_window: VisibleWindow<unknown>): boolean;
680
- protected _hittestVisibleWindow(window: VisibleWindow<VirtualizedResolvedItem>, test: HitTest): boolean;
681
- protected _captureVisibleItemSnapshot(window: VisibleWindow<unknown>): void;
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 _resolveVisibleWindow(now: number): VisibleWindowResult<VirtualizedResolvedItem>;
693
- protected abstract _readAnchor(state: NormalizedListState): number;
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/chat.d.ts
701
- /**
702
- * Virtualized renderer anchored to the bottom, suitable for chat-style UIs.
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 anchored to the top, suitable for timeline-style UIs.
747
+ * Virtualized list renderer with configurable anchor semantics.
717
748
  */
718
- declare class TimelineRenderer<C extends CanvasRenderingContext2D, T extends {}> extends VirtualizedRenderer<C, T> {
719
- protected _resolveVisibleWindow(now: number): VisibleWindowResult<VirtualizedResolvedItem>;
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, ChatRenderer, ChildLayoutResult, Context, CrossAxisAlignment, DebugRenderer, DeleteListItemAnimationOptions, DynValue, Fixed, Flex, FlexContainerOptions, FlexItem, FlexItemOptions, FlexLayoutResult, Group, HitTest, InlineSpan, JumpToOptions, LayoutConstraints, LayoutRect, ListState, MainAxisAlignment, MainAxisSize, MultilineText, MultilineTextOptions, Node, PaddingBox, PhysicalTextAlign, Place, RenderFeedback, RendererOptions, ShrinkWrap, Text, TextAlign, TextEllipsisPosition, TextJustifyMode, TextJustifyOptions, TextOptions, TextOverflowMode, TextOverflowWrapMode, TextStyleOptions, TextWhiteSpaceMode, TextWordBreakMode, TimelineRenderer, UpdateListItemAnimationOptions, VirtualizedRenderer, Wrapper, memoRenderItem, memoRenderItemBy };
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