Skip to main content

tilo_app/framework/
chain_list.rs

1//! Reading back through a chain's parts ([`Chain`]): the list view of
2//! anything an app keeps as a chain, a chat's messages or a log.
3//!
4//! A device holds a chain's head, and whatever earlier parts someone read
5//! back to. A view of the chain opens the part before the oldest it holds
6//! when its reader comes to the top of what it shows, one part at a time:
7//! the next only once the last one opened is here. A part this device has
8//! none of is asked of the workspace ([`Chain::hold`]); until it is here, the
9//! view reserves the place its items will take and shows them arriving
10//! ([`ChainPager::asked`]). When the workspace's hosts sent everything they hold
11//! without it, or none is connected, the place says the earlier items are
12//! not available, with nothing spinning ([`ChainPager::unavailable`]), until
13//! it arrives after all. The view keeps the item the reader looks at where
14//! it is while earlier ones come in above it.
15//!
16//! One more reason to read back is the app's to give: **an item looked
17//! for** that no part held here has (a link to an old message):
18//! [`ChainPager::seek`] opens earlier parts in turn until one has it, or the
19//! chain's first part did not. What a view counts across parts it does not
20//! hold (a reader's unread messages) a part carries of the ones before it
21//! ([`super::Chained::carry`]), so nothing is held for it.
22//!
23//! [`ChainPager`] is the mechanism, for a view that paints its own rows;
24//! [`ChainList`] is the same as a [`ListModel`] for a [`VirtualList`].
25//! Nothing here polls: the chain's [`Chain::revision`] moves, and the
26//! store wakes the window ([`ui_core::record::RecordStore::wake_on_arrival`]),
27//! when a part's history arrives or the workspace says it has none of it, and
28//! opening a part waits for a pass where the app's records take writes
29//! ([`ChainPager::settle`]).
30//!
31//! [`VirtualList`]: ui_core::widget::virtual_list::VirtualList
32
33use std::collections::HashMap;
34use std::hash::Hash;
35use std::ops::Range;
36use std::rc::Rc;
37
38use tilo_resource::Resource;
39use ui_core::arriving::{Arrival, ArrivalClock, Source};
40use ui_core::widget::virtual_list::{ListModel, ListModelChange};
41
42use super::chain::{Chain, Chained, PartArrival};
43
44/// What a settle pass of a [`ChainPager`] did.
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub enum Paged {
47    /// Nothing to open, or not now.
48    Nothing,
49    /// Opened part `n`: held from this device, or on its way from the workspace.
50    Opened(u64),
51    /// A held part has the item [`ChainPager::seek`] looked for.
52    Found,
53    /// Every part is held and none has the item looked for.
54    NotFound,
55}
56
57/// A predicate over one part: whether it holds what is looked for.
58type Seek<P> = Box<dyn Fn(&<P as Resource>::State) -> bool>;
59
60/// Where a view stands reading back through a chain. See the
61/// module docs (`self`).
62pub struct ChainPager<P: Chained> {
63    chain: Chain<P>,
64    /// The reader came to the top of what is held.
65    wanted: bool,
66    seeking: Option<Seek<P>>,
67    /// When the part on its way was asked for, from the workspace.
68    asked: Option<ArrivalClock>,
69}
70
71impl<P: Chained> ChainPager<P> {
72    #[must_use]
73    pub fn new(chain: Chain<P>) -> Self {
74        Self {
75            chain,
76            wanted: false,
77            seeking: None,
78            asked: None,
79        }
80    }
81
82    #[must_use]
83    pub fn chain(&self) -> &Chain<P> {
84        &self.chain
85    }
86
87    /// The oldest part held or asked for; `None` while nothing was written
88    /// to the chain.
89    #[must_use]
90    pub fn oldest(&self) -> Option<u64> {
91        let held = self.chain.parts().first().map(|(index, _)| *index);
92        let asked = self.chain.waited().first().map(|(index, _)| *index);
93        held.into_iter().chain(asked).min()
94    }
95
96    /// Whether the chain has parts before the oldest held or asked for.
97    #[must_use]
98    pub fn has_earlier(&self) -> bool {
99        self.oldest().is_some_and(|oldest| oldest > 0)
100    }
101
102    /// Whether a part asked for is still on its way from the workspace.
103    #[must_use]
104    pub fn arriving(&self) -> bool {
105        self.waiting(PartArrival::Arriving)
106    }
107
108    /// Whether a part asked for is not available from the workspace's hosts: it
109    /// is not coming until that changes, and nothing before it is asked
110    /// for.
111    #[must_use]
112    pub fn unavailable(&self) -> bool {
113        self.waiting(PartArrival::Unavailable)
114    }
115
116    /// Whether an earlier part asked for stands at `arrival`: one before
117    /// the newest part, which, still arriving itself, holds back reading
118    /// back without being earlier.
119    fn waiting(&self, arrival: PartArrival) -> bool {
120        let waited = self.chain.waited();
121        let held = self.chain.parts().last().map(|(index, _)| *index);
122        let newest = held
123            .into_iter()
124            .chain(waited.last().map(|(index, _)| *index))
125            .max();
126        waited
127            .iter()
128            .any(|(index, waiting)| *waiting == arrival && Some(*index) != newest)
129    }
130
131    /// Whether the view shows a place for earlier items above what it
132    /// holds: there are earlier parts, or one asked for is not here.
133    #[must_use]
134    pub fn loading(&self) -> bool {
135        self.has_earlier() || !self.chain.waited().is_empty()
136    }
137
138    /// Whether reading back waits on the workspace: a part is on its way, or
139    /// the workspace has none of one.
140    fn held_up(&self) -> bool {
141        !self.chain.waited().is_empty()
142    }
143
144    /// When the part on its way was asked for, and from where: what a
145    /// view's placeholder for it steps through
146    /// ([`ui_core::widget::ArrivalFace`]). `None` while nothing is on its
147    /// way.
148    #[must_use]
149    pub fn asked(&self) -> Option<&ArrivalClock> {
150        self.asked.as_ref().filter(|_| self.arriving())
151    }
152
153    /// The reader came to the top of what is held: the next settle pass
154    /// opens the part before.
155    pub fn reached_top(&mut self) {
156        if self.has_earlier() && !self.held_up() {
157            self.wanted = true;
158        }
159    }
160
161    /// Look for the item `found` names in the parts held, and open earlier
162    /// ones in turn until one has it: a settle pass answers [`Paged::Found`]
163    /// or [`Paged::NotFound`].
164    pub fn seek(&mut self, found: impl Fn(&P::State) -> bool + 'static) {
165        self.seeking = Some(Box::new(found));
166    }
167
168    /// Whether an item is looked for.
169    #[must_use]
170    pub fn seeking(&self) -> bool {
171        self.seeking.is_some()
172    }
173
174    /// Whether a settle pass has something to do now: a view asks for one
175    /// (a frame) when this turns true.
176    #[must_use]
177    pub fn wants_settle(&self) -> bool {
178        if self.seeking.is_some() {
179            return !self.arriving();
180        }
181        self.has_earlier() && !self.held_up() && self.wanted
182    }
183
184    /// Open the part before the oldest held when the reader reached the top
185    /// or an item is looked for; one at a time, once the last one opened
186    /// arrived. Run where the app's records take writes (a settle pass);
187    /// anywhere else it waits.
188    pub fn settle(&mut self) -> Paged {
189        if !self.chain.takes_changes() {
190            return Paged::Nothing;
191        }
192        let parts = self.chain.parts();
193        if let Some(found) = &self.seeking
194            && parts.iter().any(|(_, part)| found(part))
195        {
196            self.seeking = None;
197            return Paged::Found;
198        }
199        if self.arriving() {
200            return Paged::Nothing;
201        }
202        let Some(oldest) = self.oldest() else {
203            return Paged::Nothing;
204        };
205        // Nothing before a part the workspace has none of is asked for.
206        if oldest == 0 || self.unavailable() {
207            self.wanted = false;
208            if self.seeking.take().is_some() {
209                return Paged::NotFound;
210            }
211            return Paged::Nothing;
212        }
213        if !(self.wanted || self.seeking.is_some()) {
214            return Paged::Nothing;
215        }
216        self.wanted = false;
217        let earlier = oldest.saturating_sub(1);
218        match self.chain.hold(earlier) {
219            Ok(()) => {
220                if self.chain.waiting(earlier).is_some() {
221                    self.asked = Some(ArrivalClock::new(ui_core::time::now(), Source::host("")));
222                }
223                Paged::Opened(earlier)
224            }
225            Err(error) => {
226                tracing::warn!(kind = P::KIND, part = earlier, %error, "earlier part not opened");
227                self.seeking = None;
228                Paged::Nothing
229            }
230        }
231    }
232}
233
234/// A row of a [`ChainList`]: the place of the items still to come from
235/// earlier parts, or an item.
236#[derive(Clone, Debug, PartialEq, Eq, Hash)]
237pub enum ChainRow<K> {
238    Earlier,
239    Item(K),
240}
241
242/// The items of the parts held, oldest first, each with its key.
243type Rows<P, K, T> = Box<dyn Fn(&[(u64, Rc<<P as Resource>::State>)]) -> Vec<(K, T)>>;
244
245/// A chain's items as a [`ListModel`]: every held part's items, oldest
246/// first, after a row standing for the earlier parts while there are any
247/// ([`ChainRow::Earlier`], pending while one is on its way, failed with
248/// why when the workspace's hosts have none of it). Showing that
249/// row opens the part before ([`ChainPager::reached_top`]); a
250/// [`VirtualList`](ui_core::widget::virtual_list::VirtualList) keeps the
251/// row the reader looks at in place as items come in above it.
252pub struct ChainList<P: Chained, K, T> {
253    pager: ChainPager<P>,
254    rows: Rows<P, K, T>,
255    items: Vec<(K, T)>,
256    index: HashMap<K, usize>,
257    earlier: bool,
258    /// Why the earlier parts' row shows no more, while the workspace's hosts
259    /// have none of the part before.
260    unavailable: Option<String>,
261    seen: Option<u64>,
262}
263
264impl<P: Chained, K: Clone + Eq + Hash + 'static, T> ChainList<P, K, T> {
265    /// The items `rows` reads from the parts `chain` holds.
266    #[must_use]
267    pub fn new(
268        chain: Chain<P>,
269        rows: impl Fn(&[(u64, Rc<P::State>)]) -> Vec<(K, T)> + 'static,
270    ) -> Self {
271        let mut list = Self {
272            pager: ChainPager::new(chain),
273            rows: Box::new(rows),
274            items: Vec::new(),
275            index: HashMap::new(),
276            earlier: false,
277            unavailable: None,
278            seen: None,
279        };
280        list.read();
281        list
282    }
283
284    #[must_use]
285    pub fn pager(&mut self) -> &mut ChainPager<P> {
286        &mut self.pager
287    }
288
289    /// The item of row `index`; `None` for the earlier parts' row.
290    #[must_use]
291    pub fn item(&self, index: usize) -> Option<&T> {
292        let index = index.checked_sub(usize::from(self.earlier))?;
293        self.items.get(index).map(|(_, item)| item)
294    }
295
296    /// Read the items again when the chain moved. Whether it did.
297    fn read(&mut self) -> bool {
298        let revision = self.pager.chain().revision();
299        if self.seen == Some(revision) {
300            return false;
301        }
302        self.seen = Some(revision);
303        self.items = (self.rows)(&self.pager.chain().parts());
304        self.index = self
305            .items
306            .iter()
307            .enumerate()
308            .map(|(at, (key, _))| (key.clone(), at))
309            .collect();
310        self.earlier = self.pager.loading();
311        self.unavailable = self
312            .pager
313            .unavailable()
314            .then(ui_core::widget::conversation::EarlierUnavailableFace::label);
315        true
316    }
317}
318
319impl<P: Chained, K: Clone + Eq + Hash + 'static, T> ListModel for ChainList<P, K, T> {
320    type Key = ChainRow<K>;
321
322    fn len(&self) -> usize {
323        self.items.len().saturating_add(usize::from(self.earlier))
324    }
325
326    fn key_at(&self, index: usize) -> Option<Self::Key> {
327        if self.earlier && index == 0 {
328            return Some(ChainRow::Earlier);
329        }
330        let index = index.checked_sub(usize::from(self.earlier))?;
331        self.items
332            .get(index)
333            .map(|(key, _)| ChainRow::Item(key.clone()))
334    }
335
336    fn index_of(&self, key: &Self::Key) -> Option<usize> {
337        match key {
338            ChainRow::Earlier => self.earlier.then_some(0),
339            ChainRow::Item(key) => self
340                .index
341                .get(key)
342                .map(|at| at.saturating_add(usize::from(self.earlier))),
343        }
344    }
345
346    fn row_state(&self, index: usize) -> Arrival<(), &str> {
347        if self.earlier && index == 0 {
348            if let Some(why) = &self.unavailable {
349                return Arrival::Failed(why);
350            }
351            let from = self
352                .pager
353                .asked()
354                .map_or_else(|| Source::host(""), |asked| asked.source().clone());
355            return Arrival::Pending { from };
356        }
357        Arrival::Ready(())
358    }
359
360    fn request_range(&mut self, range: Range<usize>) {
361        if self.earlier && range.start == 0 {
362            self.pager.reached_top();
363            // Here when the app's records take writes; otherwise at the
364            // next pass that reads the list.
365            self.pager.settle();
366        }
367    }
368
369    fn poll_changed(&mut self) -> bool {
370        if self.pager.wants_settle() {
371            self.pager.settle();
372        }
373        self.read()
374    }
375
376    fn poll_changes(&mut self) -> Vec<ListModelChange<Self::Key>> {
377        Vec::new()
378    }
379}