1#![allow(clippy::await_holding_refcell_ref)]
4
5use crate::components::renderer_fn::renderer_fn;
6use crate::loaded_rows::{LoadedRows, RowState};
7use crate::selection::Selection;
8use crate::table_row::TableRow;
9use crate::{
10 ChangeEvent, ColumnSort, DefaultErrorRowRenderer, DefaultLoadingRowRenderer,
11 DefaultRowPlaceholderRenderer, DefaultTableBodyRenderer, DefaultTableHeadRenderer,
12 DefaultTableHeadRowRenderer, DefaultTableRowRenderer, DisplayStrategy, EventHandler,
13 HeadDragHandler, ReloadController, RowReader, SelectionChangeEvent, SortingMode,
14 TableClassesProvider, TableDataProvider, TableHeadEvent,
15};
16use leptos::prelude::*;
17use leptos::tachys::view::any_view::AnyView;
18use leptos::task::spawn_local;
19use leptos_use::core::{IntoElementMaybeSignal, OptionLocalRwSignal};
20use leptos_use::{
21 UseElementSizeOptions, UseElementSizeReturn, UseScrollOptions, UseScrollReturn,
22 use_debounce_fn, use_element_size_with_options, use_scroll_with_options,
23};
24use std::cell::RefCell;
25use std::collections::{HashSet, VecDeque};
26use std::fmt::Debug;
27use std::marker::PhantomData;
28use std::ops::Range;
29use std::rc::Rc;
30use std::sync::Arc;
31
32const MAX_DISPLAY_ROW_COUNT: usize = 500;
33
34renderer_fn!(
35 RowRendererFn<Row, Column>(
36 class: Signal<String>,
37 row: RwSignal<Row>,
38 index: usize,
39 selected: Signal<bool>,
40 on_select: EventHandler<web_sys::MouseEvent>,
41 columns: RwSignal<Vec<Column>>
42 )
43 default DefaultTableRowRenderer
44 where
45 Row: TableRow<Column> + 'static,
46 Column: Copy + Send + Sync + 'static
47);
48
49renderer_fn!(
50 RowPlaceholderRendererFn(height: Signal<f64>)
51 default DefaultRowPlaceholderRenderer
52);
53
54renderer_fn!(WrapperRendererFn(view: AnyView, class: Signal<String>));
55
56pub type BodyRef = Arc<dyn Fn(web_sys::Element, ())>;
57
58renderer_fn!(TbodyRendererFn(view: AnyView, class: Signal<String>, body_ref: BodyRef));
59
60renderer_fn!(
61 ErrorRowRendererFn(err: String, index: usize, col_count: usize)
62 default DefaultErrorRowRenderer
63);
64
65renderer_fn!(
66 LoadingRowRendererFn(class: Signal<String>, get_cell_class: Callback<(usize,), String>, get_cell_inner_class: Callback<(usize,), String>, index: usize, col_count: usize)
67 default DefaultLoadingRowRenderer
68);
69
70#[component]
72pub fn TableContent<Row, Column, DataP, Err, ClsP, ScrollEl, ScrollM>(
73 rows: DataP,
76 scroll_container: ScrollEl,
78 #[prop(optional, into)]
81 on_change: EventHandler<ChangeEvent<Row>>,
82 #[prop(optional, into)]
90 selection: Selection,
91 #[prop(optional, into)]
94 on_selection_change: EventHandler<SelectionChangeEvent<Row>>,
95 #[prop(default = DefaultTableHeadRenderer.into(), into)]
98 thead_renderer: WrapperRendererFn,
99 #[prop(default = DefaultTableBodyRenderer.into(), into)]
102 tbody_renderer: TbodyRendererFn,
103 #[prop(default = DefaultTableHeadRowRenderer.into(), into)]
106 thead_row_renderer: WrapperRendererFn,
107 #[prop(optional, into)]
110 row_renderer: RowRendererFn<Row, Column>,
111 #[prop(optional, into)]
115 loading_row_renderer: LoadingRowRendererFn,
116 #[prop(optional, into)]
120 error_row_renderer: ErrorRowRendererFn,
121 #[prop(optional, into)]
125 row_placeholder_renderer: RowPlaceholderRendererFn,
126 #[prop(optional, into)]
128 row_class: Signal<String>,
129 #[prop(optional, into)]
131 thead_class: Signal<String>,
132 #[prop(optional, into)]
134 thead_row_class: Signal<String>,
135 #[prop(optional, into)]
137 tbody_class: Signal<String>,
138 #[prop(optional, into)]
140 loading_cell_class: Signal<String>,
141 #[prop(optional, into)]
143 loading_cell_inner_class: Signal<String>,
144 #[prop(default = RwSignal::new(VecDeque::new()), into)]
148 sorting: RwSignal<VecDeque<(Column, ColumnSort)>>,
149 #[prop(optional)]
153 sorting_mode: SortingMode,
154 #[prop(default = RwSignal::new(Row::columns().into()), into)]
157 columns: RwSignal<Vec<Column>>,
158 #[prop(optional, into)]
164 on_row_count: EventHandler<usize>,
165 #[prop(optional)]
171 drag_handler: HeadDragHandler<Column>,
172 #[prop(optional)]
177 reload_controller: ReloadController,
178 #[prop(optional)]
186 display_strategy: DisplayStrategy,
187 #[prop(optional)]
191 loading_row_display_limit: Option<usize>,
192 #[prop(optional)]
194 row_reader: RowReader<Row>,
195
196 #[prop(optional)] _marker: PhantomData<(Err, ScrollM)>,
197) -> impl IntoView
198where
199 Column: Eq + Ord + Copy + Clone + Send + Sync + 'static,
200 Row: TableRow<Column, ClassesProvider = ClsP> + Clone + Send + Sync + 'static,
201 DataP: TableDataProvider<Row, Column, Err> + 'static,
202 Err: Debug + 'static,
203 ClsP: TableClassesProvider + Send + Sync + Copy + 'static,
204 ScrollEl: IntoElementMaybeSignal<web_sys::Element, ScrollM> + 'static,
205 ScrollM: 'static,
206{
207 let on_change = StoredValue::new(on_change);
208 let rows = Rc::new(RefCell::new(rows));
209
210 let class_provider = ClsP::new();
211
212 let row_class = Signal::derive(move || row_class.get());
213 let loading_cell_inner_class = Signal::derive(move || loading_cell_inner_class.get());
214 let loading_cell_class = Signal::derive(move || loading_cell_class.get());
215 let thead_class = Signal::derive(move || class_provider.thead(&thead_class.get()));
216 let thead_row_class = Signal::derive(move || class_provider.thead_row(&thead_row_class.get()));
217 let tbody_class = Signal::derive(move || class_provider.tbody(&tbody_class.get()));
218
219 let loaded_rows = RwSignal::new(LoadedRows::<Row>::new());
220
221 let _ = row_reader
222 .get_loaded_rows
223 .replace(Box::new(move |index: usize| {
224 loaded_rows.read()[index].clone()
225 }));
226
227 let first_selected_index = RwSignal::new(None::<usize>);
228
229 let (row_count, set_row_count) = signal(None::<usize>);
230
231 let set_known_row_count = move |row_count: usize| {
232 set_row_count.set(Some(row_count));
233 loaded_rows.write().resize(row_count);
234 on_row_count.run(row_count);
235 display_strategy.set_row_count(row_count);
236 };
237
238 let load_row_count = {
239 let rows = Rc::clone(&rows);
240 let set_known_row_count = set_known_row_count.clone();
241
242 move || {
243 spawn_local({
244 let rows = Rc::clone(&rows);
245 let set_known_row_count = set_known_row_count.clone();
246
247 async move {
248 let row_count = rows.borrow().row_count().await;
250
251 if sorting.try_with_untracked(|_| {}).is_none() {
253 return;
254 }
255
256 if let Some(row_count) = row_count {
257 set_known_row_count(row_count);
258 }
259 }
260 })
261 }
262 };
263
264 let (reload_count, set_reload_count) = signal(0_usize);
265 let clear = {
266 let load_row_count = load_row_count.clone();
267
268 move |clear_row_count: bool| {
269 selection.clear();
270 first_selected_index.set(None);
271 LoadedRows::<Row>::clear(&mut loaded_rows.write());
272
273 if clear_row_count {
274 let reload = row_count.get_untracked().is_some();
275 set_row_count.set(None);
276 if reload {
277 load_row_count();
278 }
279 }
280
281 set_reload_count.set(reload_count.get_untracked().overflowing_add(1).0);
282 }
283 };
284
285 let on_head_click = move |event: TableHeadEvent<Column>| {
286 sorting_mode.update_sorting_from_event(&mut sorting.write(), event);
287 };
288
289 Effect::new({
290 let clear = clear.clone();
291 let rows = Rc::clone(&rows);
292
293 move || {
294 let sorting = sorting.read();
295 if let Ok(mut rows) = rows.try_borrow_mut() {
296 rows.set_sorting(&sorting);
297 clear(false);
298 };
299 }
300 });
301
302 Effect::new({
303 let rows = Rc::clone(&rows);
304
305 move || {
306 reload_controller.track();
308 rows.borrow().track();
309 clear(true);
310 }
311 });
312
313 let selected_indices = match selection {
314 Selection::None => Signal::stored(HashSet::new()),
315 Selection::Single(selected_index) => Signal::derive(move || {
316 selected_index
317 .get()
318 .map(|i| HashSet::from([i]))
319 .unwrap_or_default()
320 }),
321 Selection::Multiple(selected_indices) => selected_indices.into(),
322 };
323
324 let scroll_container = scroll_container.into_element_maybe_signal();
325
326 let UseScrollReturn { y, set_y, .. } = use_scroll_with_options(
327 scroll_container,
328 UseScrollOptions::default().throttle(100.0),
329 );
330
331 let UseElementSizeReturn { height, .. } = use_element_size_with_options(
332 scroll_container,
333 UseElementSizeOptions::default().box_(web_sys::ResizeObserverBoxOptions::ContentBox),
334 );
335
336 Effect::new(move || {
337 if let DisplayStrategy::Virtualization | DisplayStrategy::Pagination { .. } =
338 display_strategy
339 {
340 load_row_count();
341 }
342 });
343
344 let (average_row_height, set_average_row_height) = signal(20.0);
345
346 let first_visible_row_index = if let DisplayStrategy::Pagination {
347 controller,
348 row_count,
349 } = display_strategy
350 {
351 Memo::new(move |_| controller.current_page.get() * row_count)
352 } else {
353 Memo::new(move |_| (y.get() / average_row_height.get()).floor() as usize)
354 };
355 let visible_row_count = match display_strategy {
356 DisplayStrategy::Pagination { row_count, .. } => Signal::derive(move || row_count),
357
358 DisplayStrategy::Virtualization | DisplayStrategy::InfiniteScroll => {
359 Memo::new(move |_| ((height.get() / average_row_height.get()).ceil() as usize).max(20))
360 .into()
361 }
362 };
363
364 let (display_range, set_display_range) = signal(0..0);
365
366 let placeholder_height_before =
367 if matches!(display_strategy, DisplayStrategy::Pagination { .. }) {
368 Signal::derive(move || 0.0)
369 } else {
370 Memo::new(move |_| display_range.get().start as f64 * average_row_height.get()).into()
371 };
372
373 let placeholder_height_after = if matches!(display_strategy, DisplayStrategy::Pagination { .. })
374 {
375 Signal::derive(move || 0.0)
376 } else {
377 Memo::new(move |_| {
378 let row_count_after = if let Some(row_count) = row_count.get() {
379 (row_count.saturating_sub(display_range.get().end)) as f64
380 } else {
381 0.0
382 };
383
384 row_count_after * average_row_height.get()
385 })
386 .into()
387 };
388
389 let tbody_el = OptionLocalRwSignal::<web_sys::Element>::new();
390
391 let compute_average_row_height = use_debounce_fn(
392 move || {
393 compute_average_row_height_from_loaded(
394 tbody_el,
395 display_range,
396 y,
397 &set_y,
398 set_average_row_height,
399 placeholder_height_before,
400 loaded_rows,
401 );
402 },
403 50.0,
404 );
405
406 Effect::watch(
407 move || {
408 reload_count.track();
410
411 (
413 first_visible_row_index.get(),
414 visible_row_count.get(),
415 row_count.get(),
416 )
417 },
418 move |(first_visible, visible_count, row_count_opt), _, _| {
419 let visible_count = *visible_count;
420 let row_count_opt = *row_count_opt;
421 let first_visible = *first_visible;
422
423 let visible_count = visible_count.min(MAX_DISPLAY_ROW_COUNT);
424
425 if visible_count == 0 {
426 return;
427 }
428
429 let mut start = first_visible.saturating_sub(visible_count * 2);
430 let mut end = start + visible_count * 5;
431
432 if let Some(row_count) = row_count_opt {
433 end = end.min(row_count);
435
436 start = start.min(end); } else {
439 return;
440 }
441
442 if let Some(chunk_size) = DataP::CHUNK_SIZE {
443 start = (start / chunk_size) * chunk_size;
444 if Some(end) != row_count_opt {
446 end = end.div_ceil(chunk_size) * chunk_size; }
448 }
449
450 let range = start..end;
451
452 set_display_range.set(match display_strategy {
453 DisplayStrategy::Virtualization | DisplayStrategy::InfiniteScroll => range.clone(),
454 DisplayStrategy::Pagination { row_count, .. } => {
455 first_visible..(first_visible + row_count).min(end)
456 }
457 });
458
459 loaded_rows.update_untracked(|loaded_rows| {
460 if end > loaded_rows.len() {
461 loaded_rows.resize(end);
462 }
463 });
464
465 let missing_range =
466 loaded_rows.with_untracked(|loaded_rows| loaded_rows.missing_range(range.clone()));
467
468 if let Some(missing_range) = missing_range {
469 let missing_start = missing_range.start.min(missing_range.end);
471 let missing_end = missing_range.end; let missing_range = missing_start..missing_end;
474
475 if missing_range.is_empty() {
476 return;
478 }
479
480 loaded_rows.write().write_loading(missing_range.clone());
481
482 let loading_ranges =
483 compute_ranges_to_load::<DataP, Row, Column, Err>(missing_range);
484
485 for missing_range in loading_ranges {
487 let compute_average_row_height = compute_average_row_height.clone();
488 spawn_local({
489 let rows = Rc::clone(&rows);
490 let set_known_row_count = set_known_row_count.clone();
491
492 async move {
493 let Some(latest_reload_count) = reload_count.try_get_untracked() else {
494 return;
495 };
496
497 let result = rows
499 .borrow()
500 .get_rows(missing_range.clone())
501 .await
502 .map_err(|err| format!("{err:?}"));
503
504 if let Some(reload_count) = reload_count.try_get_untracked() {
505 if reload_count != latest_reload_count {
507 return;
508 }
509
510 if let Ok((_, loaded_range)) = &result
511 && loaded_range.end < missing_range.end
512 {
513 match row_count_opt {
514 Some(row_count) => {
516 if loaded_range.end < row_count {
517 set_known_row_count(loaded_range.end);
518 }
519 }
520 None => {
521 set_known_row_count(loaded_range.end);
522 }
523 }
524 }
525 loaded_rows.write().write_loaded(result, missing_range);
526 compute_average_row_height();
527 }
528 }
529 });
530 }
531 }
532 },
533 false,
534 );
535
536 let thead_content =
537 Row::render_head_row(sorting.into(), on_head_click, drag_handler, columns).into_any();
538
539 fn clamp_range(range: Range<usize>, len: usize) -> Range<usize> {
540 let start = range.start.min(len);
541 let end = range.end.min(len);
542
543 if start > end {
544 return end..start;
545 }
546 start..end
547 }
548
549 let tbody_content = {
550 let row_renderer = row_renderer.clone();
551 let loading_row_renderer = loading_row_renderer.clone();
552 let error_row_renderer = error_row_renderer.clone();
553 let on_selection_change = on_selection_change.clone();
554
555 view! {
556 {row_placeholder_renderer.run(placeholder_height_before)}
557
558 <For
559 each=move || {
560 let loaded_rows = loaded_rows.read();
561 let display_range = display_range.read();
562
563 let iter = loaded_rows[clamp_range(display_range.clone(), loaded_rows.len())]
564 .iter()
565 .cloned()
566 .enumerate()
567 .map(|(i, row)| (i + display_range.start, row));
568
569 if let Some(loading_row_display_limit) = loading_row_display_limit {
570 let mut loading_row_count = 0;
571 iter.filter(|(_, row)| {
572 if matches!(row, RowState::Loading | RowState::Placeholder) {
573 loading_row_count += 1;
574 loading_row_count <= loading_row_display_limit
575 } else {
576 true
577 }
578 })
579 .collect::<Vec<_>>()
580 } else {
581 iter.collect::<Vec<_>>()
582 }
583 }
584
585 key=|(idx, row)| {
586 match row {
587 RowState::Loaded(_) => idx.to_string(),
588 RowState::Error(_) => format!("error-{idx}"),
589 RowState::Loading | RowState::Placeholder => format!("loading-{idx}"),
590 }
591 }
592
593 children={
594 let row_renderer = row_renderer.clone();
595 let loading_row_renderer = loading_row_renderer.clone();
596 let error_row_renderer = error_row_renderer.clone();
597 let on_selection_change = on_selection_change.clone();
598 move |(i, row)| {
599 match row {
600 RowState::Loaded(row) => {
601 let selected_signal = Signal::derive(move || {
602 selected_indices.read().contains(&i)
603 });
604
605 let class_signal = Signal::derive(move || {
606 class_provider
607 .row(i, selected_signal.get(), row_class.read().as_str())
608 });
609
610 let on_select = {
611 let on_selection_change = on_selection_change.clone();
612
613 move |evt: web_sys::MouseEvent| {
614 update_selection(evt, selection, first_selected_index, i);
615
616 let selection_change_event = SelectionChangeEvent {
617 row: row.into(),
618 row_index: i,
619 selected: selected_signal.get_untracked(),
620 };
621
622 on_selection_change.run(selection_change_event);
623 }
624 };
625
626 Effect::watch(
627 move || { row.track() },
628 move |_, _, _| {
629 let on_change = on_change.get_value();
630
631 on_change
632 .run(ChangeEvent {
633 row_index: i,
634 changed_row: row.into(),
635 });
636 },
637 false,
638 );
639 row_renderer
640 .run(class_signal, row, i, selected_signal, on_select.into(), columns)
641 }
642 RowState::Error(err) => {
643 error_row_renderer.run(err, i, Row::COLUMN_COUNT)
644 }
645 RowState::Loading | RowState::Placeholder => {
646 loading_row_renderer
647 .run(
648 Signal::derive(move || {
649 class_provider.row(i, false, row_class.read().as_str())
650 }),
651 Callback::new(move |(col_index,): (usize,)| {
652 class_provider
653 .loading_cell(
654 i,
655 col_index,
656 loading_cell_class.read().as_str(),
657 )
658 }),
659 Callback::new(move |(col_index,): (usize,)| {
660 class_provider
661 .loading_cell_inner(
662 i,
663 col_index,
664 loading_cell_inner_class.read().as_str(),
665 )
666 }),
667 i,
668 Row::COLUMN_COUNT,
669 )
670 }
671 }
672 }
673 }
674 />
675
676 {row_placeholder_renderer.run(placeholder_height_after)}
677 }
678 .into_any()
679 };
680
681 let tbody_directive = Arc::new(move |el: web_sys::Element, _: ()| {
682 tbody_el.set(Some(el));
683 });
684
685 let tbody = tbody_renderer.run(tbody_content, tbody_class, tbody_directive);
686
687 view! {
688 {thead_renderer.run(thead_row_renderer.run(thead_content, thead_row_class), thead_class)}
689
690 {tbody}
691 }
692}
693
694fn compute_ranges_to_load<DataP, Row, Column, Err>(missing_range: Range<usize>) -> Vec<Range<usize>>
695where
696 DataP: TableDataProvider<Row, Column, Err> + 'static,
697 Err: Debug,
698{
699 let mut loading_ranges = vec![];
700 if let Some(chunk_size) = DataP::CHUNK_SIZE {
701 let start = missing_range.start / chunk_size * chunk_size;
702 let mut current_range = start..start + chunk_size;
703 while current_range.end <= missing_range.end {
704 loading_ranges.push(current_range.clone());
705 current_range = current_range.end..current_range.end + chunk_size;
706 }
707 if current_range.end > missing_range.end && current_range.start < missing_range.end {
709 loading_ranges.push(current_range);
710 }
711 } else {
712 loading_ranges.push(missing_range);
713 }
714 loading_ranges
715}
716
717fn compute_average_row_height_from_loaded<Row, Column, ClsP>(
718 tbody_ref: OptionLocalRwSignal<web_sys::Element>,
719 display_range: ReadSignal<Range<usize>>,
720 y: Signal<f64>,
721 set_y: &impl Fn(f64),
722 set_average_row_height: WriteSignal<f64>,
723 placeholder_height_before: Signal<f64>,
724 loaded_rows: RwSignal<LoadedRows<Row>>,
725) where
726 Row: TableRow<Column, ClassesProvider = ClsP> + Send + Sync + Clone + 'static,
727 Column: Copy + Send + Sync + 'static,
728{
729 if let Some(el) = tbody_ref.get_untracked() {
730 let el: &web_sys::Element = ⪙
731 let display_range = display_range.get_untracked();
732 if display_range.end > 0 {
733 let avg_row_height = loaded_rows.with_untracked(|loaded_rows| {
734 let mut loading_row_start_index = None;
735 let mut loading_row_end_index = None;
736
737 for i in display_range.clone() {
738 if matches!(loaded_rows[i], RowState::Loaded(_) | RowState::Loading) {
739 if loading_row_start_index.is_none() {
740 loading_row_start_index = Some(i);
741 }
742 loading_row_end_index = Some(i);
743 } else if loading_row_end_index.is_some() {
744 break;
745 }
746 }
747
748 if let (Some(loading_row_start_index), Some(loading_row_end_index)) =
749 (loading_row_start_index, loading_row_end_index)
750 {
751 if loading_row_end_index == loading_row_start_index {
752 return None;
753 }
754
755 let children = el.children();
756
757 let first_loading_row = children
759 .get_with_index((loading_row_start_index + 1 - display_range.start) as u32);
760 let last_loading_row = children
761 .get_with_index((loading_row_end_index + 1 - display_range.start) as u32);
762
763 if let (Some(first_loading_row), Some(last_loaded_row)) =
764 (first_loading_row, last_loading_row)
765 {
766 return Some(
767 (last_loaded_row.get_bounding_client_rect().top()
768 - first_loading_row.get_bounding_client_rect().top())
769 / (loading_row_end_index - loading_row_start_index) as f64,
770 );
771 }
772 }
773
774 None
775 });
776
777 if let Some(avg_row_height) = avg_row_height {
778 let prev_placeholder_height_before = placeholder_height_before.get_untracked();
779
780 set_average_row_height.set(avg_row_height);
781
782 let new_placeholder_height_before = placeholder_height_before.get_untracked();
783 set_y(
784 y.get_untracked() - prev_placeholder_height_before
785 + new_placeholder_height_before,
786 );
787 }
788 }
789 }
790}
791
792fn get_keyboard_modifiers(evt: &web_sys::MouseEvent) -> (bool, bool) {
793 let meta_pressed = evt.meta_key() || evt.ctrl_key();
794 let shift_pressed = evt.shift_key();
795 (meta_pressed, shift_pressed)
796}
797
798fn update_selection(
799 evt: web_sys::MouseEvent,
800 selection: Selection,
801 first_selected_index: RwSignal<Option<usize>>,
802 i: usize,
803) {
804 match selection {
805 Selection::None => {}
806 Selection::Single(selected_index) => {
807 if selected_index.get_untracked() == Some(i) {
808 selected_index.set(None);
809 } else {
810 selected_index.set(Some(i));
811 }
812 }
813 Selection::Multiple(selected_indices) => {
814 let mut indices = selected_indices.write();
815 let (meta_pressed, shift_pressed) = get_keyboard_modifiers(&evt);
816
817 if meta_pressed {
818 if indices.contains(&i) {
819 indices.remove(&i);
820 } else {
821 indices.insert(i);
822 }
823 match indices.len() {
824 0 => first_selected_index.set(None),
825 1 => {
826 first_selected_index.set(Some(i));
827 }
828 _ => {
829 }
831 }
832 } else if shift_pressed {
833 if let Some(first_selected_index) = first_selected_index.get() {
834 let min = first_selected_index.min(i);
835 let max = first_selected_index.max(i);
836 for i in min..=max {
837 indices.insert(i);
838 }
839 } else {
840 indices.insert(i);
841 first_selected_index.set(Some(i));
842 }
843 } else {
844 HashSet::clear(&mut *indices);
845 indices.insert(i);
846 first_selected_index.set(Some(i));
847 }
848 }
849 }
850}
851
852#[cfg(test)]
853mod tests {
854 use crate::{TableDataProvider, components::table_content::compute_ranges_to_load};
855 use std::ops::Range;
856
857 type Row = ();
858 type Column = ();
859 type Err = ();
860
861 struct MockProviderChunk10;
863 impl TableDataProvider<Row, Column, Err> for MockProviderChunk10 {
864 const CHUNK_SIZE: Option<usize> = Some(10);
865
866 async fn get_rows(&self, _: Range<usize>) -> Result<(Vec<Row>, Range<usize>), ()> {
867 unreachable!()
868 }
869 }
870
871 struct MockProviderNoChunk;
873 impl TableDataProvider<Row, Column, Err> for MockProviderNoChunk {
874 const CHUNK_SIZE: Option<usize> = None;
875
876 async fn get_rows(&self, _: Range<usize>) -> Result<(Vec<Row>, Range<usize>), ()> {
877 unreachable!()
878 }
879 }
880
881 #[test]
882 fn test_exact_chunk_alignment() {
883 let missing = 10..30;
884 let result = compute_ranges_to_load::<MockProviderChunk10, Row, Column, _>(missing);
885
886 assert_eq!(result, vec![10..20, 20..30]);
887 }
888
889 #[test]
890 fn test_missing_range_starts_mid_chunk() {
891 let missing = 5..25;
892 let result = compute_ranges_to_load::<MockProviderChunk10, Row, Column, _>(missing);
893
894 assert_eq!(result, vec![0..10, 10..20, 20..30]);
895 }
896
897 #[test]
898 fn test_missing_range_smaller_than_chunk() {
899 let missing = 12..15;
900 let result = compute_ranges_to_load::<MockProviderChunk10, Row, Column, _>(missing);
901
902 assert_eq!(result, vec![10..20]);
903 }
904
905 #[test]
906 fn test_chunk_size_none() {
907 let missing = 42..99;
908 let result = compute_ranges_to_load::<MockProviderNoChunk, Row, Column, _>(missing.clone());
909
910 assert_eq!(result, vec![missing]);
911 }
912}