virtualizer_adapter/
anchor.rs

1use core::fmt;
2
3use crate::VirtualizerKey;
4
5/// A scroll anchor that can be used to preserve visual position across data changes.
6///
7/// Typical use cases:
8/// - chat/timeline "prepend" (load older messages above) without content jumping
9/// - any reorder/replace where you want the viewport to stay anchored to an item identity
10///
11/// The adapter is responsible for providing a stable `key` and a `key_to_index` lookup when
12/// applying the anchor.
13#[derive(Clone, PartialEq, Eq)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub struct ScrollAnchor<K> {
16    pub key: K,
17    /// The distance from the anchor item's start to the viewport's scroll offset.
18    pub offset_in_viewport: u64,
19}
20
21impl<K: fmt::Debug> fmt::Debug for ScrollAnchor<K> {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        f.debug_struct("ScrollAnchor")
24            .field("key", &self.key)
25            .field("offset_in_viewport", &self.offset_in_viewport)
26            .finish()
27    }
28}
29
30/// Captures an anchor for the first visible item (by key).
31///
32/// Returns `None` if the virtualizer is disabled or the visible range is empty.
33pub fn capture_first_visible_anchor<K: VirtualizerKey>(
34    v: &virtualizer::Virtualizer<K>,
35) -> Option<ScrollAnchor<K>> {
36    let visible = v.visible_range();
37    if visible.is_empty() {
38        return None;
39    }
40    let index = visible.start_index;
41    let start = v.item_start(index)?;
42    let key = v.key_for(index);
43    let offset_in_viewport = v.scroll_offset().saturating_sub(start);
44    Some(ScrollAnchor {
45        key,
46        offset_in_viewport,
47    })
48}
49
50/// Applies a previously captured anchor by adjusting the scroll offset.
51///
52/// The adapter must provide a `key_to_index` mapping for the *current* dataset.
53///
54/// Returns `true` when the anchor was successfully applied.
55pub fn apply_anchor<K: VirtualizerKey>(
56    v: &mut virtualizer::Virtualizer<K>,
57    anchor: &ScrollAnchor<K>,
58    mut key_to_index: impl FnMut(&K) -> Option<usize>,
59) -> bool {
60    let Some(index) = key_to_index(&anchor.key) else {
61        return false;
62    };
63    let Some(start) = v.item_start(index) else {
64        return false;
65    };
66    let target = start.saturating_add(anchor.offset_in_viewport);
67    v.set_scroll_offset_clamped(target);
68    true
69}