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}