1pub mod windowing;
4
5use ankql::ast::{
6 ComparisonOperator, Expr, Literal, OrderByItem, OrderDirection, PathExpr, Predicate, Selection,
7};
8use ankurah::changes::ChangeSet;
9use ankurah::core::selection::filter::Filterable;
10use ankurah::core::value::Value;
11use ankurah::{model::View, Context, LiveQuery};
12use ankurah_proto::EntityId;
13use ankurah_signals::{Mut, Peek, Read, Subscribe};
14
15pub use ankql::ast::{OrderByItem as OrderBy, Predicate as Filter};
17pub use ankurah_proto::EntityId as Id;
18pub use ankurah_signals;
19
20#[derive(Clone, Debug)]
26pub struct VisibleSet<V> {
27 pub items: Vec<V>,
29 pub intersection: Option<Intersection>,
31 pub has_more_preceding: bool,
33 pub has_more_following: bool,
35 pub should_auto_scroll: bool,
37 pub error: Option<String>,
39}
40
41impl<V> Default for VisibleSet<V> {
42 fn default() -> Self {
43 Self {
44 items: Vec::new(),
45 intersection: None,
46 has_more_preceding: true,
47 has_more_following: false,
48 should_auto_scroll: true,
49 error: None,
50 }
51 }
52}
53
54#[derive(Clone, Debug)]
56pub struct Intersection {
57 pub entity_id: EntityId,
58 pub index: usize,
59 pub direction: LoadDirection,
60}
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub enum LoadDirection {
71 Backward,
73 Forward,
75}
76
77#[derive(Clone, Debug)]
79struct PendingSlide {
80 continuation: EntityId,
82 limit: usize,
84 direction: LoadDirection,
86 reversed_order: bool,
88}
89
90#[derive(Clone, Copy, Debug, PartialEq, Eq)]
92pub enum ScrollMode {
93 Live, Backward, Forward, }
97
98fn value_to_literal(value: &Value) -> Literal {
104 match value {
105 Value::I16(v) => Literal::I16(*v),
106 Value::I32(v) => Literal::I32(*v),
107 Value::I64(v) => Literal::I64(*v),
108 Value::F64(v) => Literal::F64(*v),
109 Value::Bool(v) => Literal::Bool(*v),
110 Value::String(v) => Literal::String(v.clone()),
111 _ => Literal::String(format!("{:?}", value)),
113 }
114}
115
116pub struct ScrollManager<V: View + Clone + Send + Sync + 'static> {
122 livequery: LiveQuery<V>,
123 predicate: Predicate,
124 display_order: Vec<OrderByItem>,
125 visible_set: Mut<VisibleSet<V>>,
126 mode: Mut<ScrollMode>,
127 pending: Mut<Option<PendingSlide>>,
129 minimum_row_height: u32,
130 buffer_factor: f64,
131 viewport_height: u32,
132 _subscription: ankurah_signals::SubscriptionGuard,
133}
134
135impl<V: View + Clone + Send + Sync + 'static> ScrollManager<V> {
136 pub fn new(
146 ctx: &Context,
147 predicate: impl TryInto<Predicate, Error = impl std::fmt::Debug>,
148 display_order: impl IntoOrderBy,
149 minimum_row_height: u32,
150 buffer_factor: f64,
151 viewport_height: u32,
152 ) -> Result<Self, ankurah::error::RetrievalError> {
153 let predicate = predicate.try_into().expect("Failed to parse predicate");
154 let display_order = display_order
155 .into_order_by()
156 .expect("Failed to parse order");
157 let buffer_factor = buffer_factor.max(2.0);
158
159 let screen_items = windowing::screen_items(viewport_height, minimum_row_height);
161 let threshold = buffer_factor / 2.0;
162 let limit = windowing::live_window_size(screen_items, threshold);
163
164 let selection = Selection {
166 predicate: predicate.clone(),
167 order_by: Some(display_order.clone()),
168 limit: Some(limit as u64),
169 };
170 let livequery: LiveQuery<V> = ctx.query(selection)?;
171
172 let visible_set: Mut<VisibleSet<V>> = Mut::new(VisibleSet::default());
174 let pending: Mut<Option<PendingSlide>> = Mut::new(None);
175 let mode: Mut<ScrollMode> = Mut::new(ScrollMode::Live);
176
177 let is_desc = display_order
179 .first()
180 .map(|o| o.direction == OrderDirection::Desc)
181 .unwrap_or(false);
182
183 let visible_set_clone = visible_set.clone();
185 let pending_clone = pending.clone();
186 let mode_clone = mode.clone();
187 let subscription = livequery.subscribe(move |changeset: ChangeSet<V>| {
188 let current = visible_set_clone.peek();
189 if current.items.is_empty() && !changeset.resultset.peek().is_empty() {
191 return;
192 }
193 let mut items: Vec<V> = changeset.resultset.peek();
194
195 let slide = pending_clone.peek();
197 pending_clone.set(None);
198
199 let used_reversed_order = slide.as_ref().map(|s| s.reversed_order).unwrap_or(false);
202 if is_desc && !used_reversed_order {
203 items.reverse();
204 }
205
206 let (has_more_preceding, has_more_following, intersection, error) = if let Some(ref slide) = slide {
208 let (has_more_preceding, has_more_following) = match slide.direction {
210 LoadDirection::Backward => {
211 let more_older = if items.len() > slide.limit {
212 items.remove(0); true
214 } else {
215 false
216 };
217 (more_older, true) }
219 LoadDirection::Forward => {
220 let more_newer = if items.len() > slide.limit {
221 items.pop(); true
223 } else {
224 mode_clone.set(ScrollMode::Live);
226 false
227 };
228 let more_older = current.has_more_preceding ||
230 current.items.first().map(|old| items.first().map(|new|
231 old.entity().id() != new.entity().id()
232 ).unwrap_or(false)).unwrap_or(false);
233 (more_older, more_newer)
234 }
235 };
236
237 let (intersection, error) = match items.iter().position(|item| item.entity().id() == slide.continuation) {
239 Some(index) => (
240 Some(Intersection {
241 entity_id: slide.continuation,
242 index,
243 direction: slide.direction,
244 }),
245 None
246 ),
247 None => {
248 if slide.direction == LoadDirection::Forward {
249 tracing::debug!("Forward slide: no overlap, jumping to live");
250 (None, None)
251 } else {
252 (None, Some(format!(
253 "Intersection failed: {} not found in result",
254 slide.continuation
255 )))
256 }
257 }
258 };
259
260 (has_more_preceding, has_more_following, intersection, error)
261 } else {
262 (current.has_more_preceding, current.has_more_following, None, None)
263 };
264
265 visible_set_clone.set(VisibleSet {
266 items,
267 intersection,
268 has_more_preceding,
269 has_more_following,
270 should_auto_scroll: mode_clone.peek() == ScrollMode::Live,
271 error,
272 });
273 });
274
275 Ok(Self {
276 livequery,
277 predicate,
278 display_order,
279 visible_set,
280 mode,
281 pending,
282 minimum_row_height,
283 buffer_factor,
284 viewport_height,
285 _subscription: subscription,
286 })
287 }
288
289 pub async fn start(&self) {
292 self.livequery.wait_initialized().await;
293
294 let mut items: Vec<V> = self.livequery.peek();
295
296 let is_desc = self
297 .display_order
298 .first()
299 .map(|o| o.direction == OrderDirection::Desc)
300 .unwrap_or(false);
301 if is_desc {
302 items.reverse();
303 }
304
305 let live_window = self.live_window_size();
306 let has_more_preceding = items.len() >= live_window;
307
308 self.visible_set.set(VisibleSet {
309 items,
310 intersection: None,
311 has_more_preceding,
312 has_more_following: false,
313 should_auto_scroll: true,
314 error: None,
315 });
316 }
317
318 fn threshold(&self) -> f64 {
320 self.buffer_factor / 2.0
321 }
322
323 fn screen_items(&self) -> usize {
324 windowing::screen_items(self.viewport_height, self.minimum_row_height)
325 }
326
327 fn live_window_size(&self) -> usize {
328 windowing::live_window_size(self.screen_items(), self.threshold())
329 }
330
331 pub fn visible_set(&self) -> Read<VisibleSet<V>> {
333 self.visible_set.read()
334 }
335
336 pub fn mode(&self) -> ScrollMode {
337 self.mode.peek()
338 }
339
340 pub fn current_selection(&self) -> String {
342 let (selection, _version) = self.livequery.selection().peek();
343 format!("{}", selection)
344 }
345
346 pub fn on_scroll(&self, first_visible: EntityId, last_visible: EntityId, scrolling_backward: bool) {
353 let current = self.visible_set.peek();
354 let screen = self.screen_items();
355
356 let first_idx = current.items.iter().position(|item| item.entity().id() == first_visible);
358 let last_idx = current.items.iter().position(|item| item.entity().id() == last_visible);
359
360 let (first_visible_index, last_visible_index) = match (first_idx, last_idx) {
361 (Some(f), Some(l)) => (f, l),
362 _ => return, };
364
365 let items_above = first_visible_index;
366 let items_below = current.items.len().saturating_sub(last_visible_index + 1);
367
368 tracing::debug!(
369 "on_scroll: first={}, last={}, items_above={}, items_below={}, screen={}, scrolling_backward={}, has_more_preceding={}",
370 first_visible_index, last_visible_index, items_above, items_below, screen, scrolling_backward, current.has_more_preceding
371 );
372
373 if scrolling_backward && items_above <= screen && current.has_more_preceding {
375 self.mode.set(ScrollMode::Backward);
376 self.slide_window(¤t, first_visible_index, last_visible_index, LoadDirection::Backward);
377 } else if !scrolling_backward && items_below <= screen && current.has_more_following {
378 self.mode.set(ScrollMode::Forward);
379 self.slide_window(¤t, first_visible_index, last_visible_index, LoadDirection::Forward);
380 }
381 }
382
383 fn slide_window(
388 &self,
389 current: &VisibleSet<V>,
390 oldest_visible_index: usize,
391 newest_visible_index: usize,
392 direction: LoadDirection,
393 ) {
394 let buffer = 2 * self.screen_items(); let max_index = current.items.len().saturating_sub(1);
396
397 let (cursor_index, intersection_index, operator, reversed_order) = match direction {
399 LoadDirection::Backward => (
400 (newest_visible_index + buffer).min(max_index),
401 newest_visible_index,
402 ComparisonOperator::LessThanOrEqual,
403 false,
404 ),
405 LoadDirection::Forward => (
406 if current.has_more_preceding {
408 oldest_visible_index.saturating_sub(buffer)
409 } else {
410 0
411 },
412 oldest_visible_index,
413 ComparisonOperator::GreaterThanOrEqual,
414 true,
415 ),
416 };
417
418 let limit = (cursor_index.max(newest_visible_index) - cursor_index.min(oldest_visible_index) + 1) + buffer;
420
421 tracing::debug!(
422 "slide_window({:?}): visible=[{},{}], cursor={}, limit={}",
423 direction, oldest_visible_index, newest_visible_index, cursor_index, limit
424 );
425
426 let continuation = current.items.get(intersection_index)
428 .map(|item| item.entity().id())
429 .expect("intersection item must exist");
430
431 self.pending.set(Some(PendingSlide {
432 continuation,
433 limit,
434 direction,
435 reversed_order,
436 }));
437
438 let predicate = self.build_cursor_predicate(current, cursor_index, operator);
440
441 let order_by = if reversed_order {
443 self.display_order.iter().map(|item| OrderByItem {
444 direction: match item.direction {
445 OrderDirection::Asc => OrderDirection::Desc,
446 OrderDirection::Desc => OrderDirection::Asc,
447 },
448 ..item.clone()
449 }).collect()
450 } else {
451 self.display_order.clone()
452 };
453
454 let selection = Selection {
455 predicate,
456 order_by: Some(order_by),
457 limit: Some((limit + 1) as u64), };
459
460 if let Err(e) = self.livequery.update_selection(selection) {
461 tracing::error!("Failed to update selection for {:?} slide: {}", direction, e);
462 }
463 }
464
465 fn build_cursor_predicate(
467 &self,
468 current: &VisibleSet<V>,
469 cursor_index: usize,
470 operator: ComparisonOperator,
471 ) -> Predicate {
472 let Some(cursor_item) = current.items.get(cursor_index) else {
473 return self.predicate.clone();
474 };
475 let Some(order_item) = self.display_order.first() else {
476 return self.predicate.clone();
477 };
478 let field_name = order_item.path.first();
479 let Some(cursor_value) = cursor_item.entity().value(field_name) else {
480 return self.predicate.clone();
481 };
482
483 let cursor_predicate = Predicate::Comparison {
484 left: Box::new(Expr::Path(PathExpr::simple(field_name))),
485 operator,
486 right: Box::new(Expr::Literal(value_to_literal(&cursor_value))),
487 };
488
489 Predicate::And(
490 Box::new(self.predicate.clone()),
491 Box::new(cursor_predicate),
492 )
493 }
494}
495
496pub fn parse_order_by(s: &str) -> Result<Vec<OrderByItem>, String> {
501 use ankql::parser::parse_selection;
502 let selection_str = format!("true ORDER BY {}", s);
503 let selection =
504 parse_selection(&selection_str).map_err(|e| format!("Failed to parse ORDER BY: {}", e))?;
505 selection
506 .order_by
507 .ok_or_else(|| "No ORDER BY parsed".to_string())
508}
509
510pub trait IntoOrderBy {
511 fn into_order_by(self) -> Result<Vec<OrderByItem>, String>;
512}
513
514impl IntoOrderBy for &str {
515 fn into_order_by(self) -> Result<Vec<OrderByItem>, String> {
516 parse_order_by(self)
517 }
518}
519
520impl IntoOrderBy for Vec<OrderByItem> {
521 fn into_order_by(self) -> Result<Vec<OrderByItem>, String> {
522 Ok(self)
523 }
524}
525
526pub use ankurah_virtual_scroll_derive::generate_scroll_manager;