Skip to main content

teksilo_widgets/grid_view/
sections.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Section grouping for `GridView`.
5//!
6//! A [`SectionProvider`] partitions the flat model into named sections; the
7//! grid renders a header above each section's tile band and (optionally)
8//! keeps the current section's header pinned to the top while scrolling.
9//! Sections compose with the uniform tile layout.
10
11use std::rc::Rc;
12
13use teksilo_data::ListModel;
14
15/// Partitions a flat model into display sections.
16pub trait SectionProvider: 'static {
17    /// Number of sections.
18    fn section_count(&self) -> usize;
19    /// Number of items in `section`.
20    fn items_in_section(&self, section: usize) -> usize;
21    /// Display title for `section`.
22    fn section_title(&self, section: usize) -> String;
23
24    /// Per-section item counts, in order. Used by the layout strategy.
25    fn section_counts(&self) -> Vec<usize> {
26        (0..self.section_count())
27            .map(|s| self.items_in_section(s))
28            .collect()
29    }
30}
31
32/// A [`SectionProvider`] built by partitioning consecutive equal-key runs of
33/// an (already ordered) model. The titles come from each run's key.
34pub struct GroupingSections {
35    /// `(title, count)` per section, captured at build time.
36    runs: Vec<(String, usize)>,
37}
38
39impl GroupingSections {
40    fn new(runs: Vec<(String, usize)>) -> Self {
41        Self { runs }
42    }
43}
44
45impl SectionProvider for GroupingSections {
46    fn section_count(&self) -> usize {
47        self.runs.len()
48    }
49    fn items_in_section(&self, section: usize) -> usize {
50        self.runs.get(section).map(|(_, c)| *c).unwrap_or(0)
51    }
52    fn section_title(&self, section: usize) -> String {
53        self.runs
54            .get(section)
55            .map(|(t, _)| t.clone())
56            .unwrap_or_default()
57    }
58}
59
60/// Build a [`SectionProvider`] by grouping consecutive items of `model` that
61/// share a `key_fn` value into one section, titled by the key. The model is
62/// not sorted — callers should pre-sort if they want fully-grouped sections.
63pub fn grouping_sections<T, K, F>(model: &ListModel<T>, key_fn: F) -> GroupingSections
64where
65    T: 'static,
66    K: ToString + PartialEq + 'static,
67    F: Fn(&T) -> K + 'static,
68{
69    let mut runs: Vec<(String, usize)> = Vec::new();
70    let mut last_key: Option<K> = None;
71    for i in 0..model.len() {
72        let key = model.with_item(i, &key_fn);
73        if let Some(key) = key {
74            let same = last_key.as_ref().map(|k| *k == key).unwrap_or(false);
75            if same {
76                if let Some(last) = runs.last_mut() {
77                    last.1 += 1;
78                }
79            } else {
80                runs.push((key.to_string(), 1));
81                last_key = Some(key);
82            }
83        }
84    }
85    GroupingSections::new(runs)
86}
87
88/// Internal handle bundling the bits the grid needs from a section provider:
89/// the cached counts closure and the title lookup.
90#[derive(Clone)]
91pub(crate) struct SectionData {
92    pub(crate) counts_fn: Rc<dyn Fn() -> Vec<usize>>,
93    pub(crate) title_fn: Rc<dyn Fn(usize) -> String>,
94}