1//! A virtualized list of equal rows keyed by their items: only rows in view are built, and
2//! the view holds its place on the selection while items arrive and leave above it.
3
4use crate::{Axis, Event, Flags, HALF_LIFE, Id, Size, Spec, Ui, fill, mix, px, scrollbar};
5use winit::keyboard::NamedKey;
6
7/// Room beside a scrolling list's rows for its scrollbar.
8pub(crate) const GUTTER: f32 = 12.0;
9/// How far rows fade out towards an end of the list they are cut at.
10const FADE: f32 = 12.0;
11
12/// Items a list shows, in order.
13pub trait Rows {
14 /// How many items are listed.
15 fn count(&self) -> usize;
16 /// The key naming the item at `index`, the same in every frame the item is listed.
17 fn key(&self, index: usize) -> u64;
18 /// Where the item named `key` is listed now.
19 fn find(&self, key: u64) -> Option<usize>;
20 /// Whether the selection may rest on row `index`.
21 fn selectable(&self, _index: usize) -> bool {
22 true
23 }
24 /// Extra room above row `index`, summed over it and the rows before; `index` may be
25 /// the length.
26 fn space_before(&self, _index: usize) -> f32 {
27 0.0
28 }
29}
30
31/// How a list shows and moves through its rows.
32pub struct List<'a, R> {
33 pub rows: &'a R,
34 /// Every row's height.
35 pub row: f32,
36 /// Arrow, page, Home and End presses meant for the list this frame; others are ignored.
37 pub keys: &'a [NamedKey],
38 /// The selection follows the pointer, as in a menu.
39 pub hover_selects: bool,
40}
41
42/// A row to build: its item's key, its index, and whether it is selected.
43pub struct Row {
44 pub key: u64,
45 pub index: usize,
46 pub selected: bool,
47}
48
49/// A list's view across frames, in logical pixels.
50#[derive(Default)]
51pub(crate) struct State {
52 scroll: f64,
53 target: f64,
54 /// Keys of the rows built last frame and their tops from the top of the view.
55 shown: Vec<(u64, f64)>,
56 /// The selection last frame, so a new one scrolls into view.
57 selected: Option<u64>,
58}
59
60impl State {
61 /// The keys of the rows built last frame.
62 pub(crate) fn shown(&self) -> impl Iterator<Item = u64> + '_ {
63 self.shown.iter().map(|(key, _)| *key)
64 }
65}
66
67/// Builds `list` into a box of `spec`, calling `build` inside each row in view, and moves
68/// `selected` with the list's keys. Returns the index of the row clicked.
69pub fn list<R: Rows>(
70 ui: &mut Ui,
71 id: Id,
72 spec: Spec<'_>,
73 list: List<'_, R>,
74 selected: &mut Option<u64>,
75 mut build: impl FnMut(&mut Ui, Row),
76) -> Option<usize> {
77 let List {
78 rows,
79 row,
80 keys,
81 hover_selects,
82 } = list;
83 let height = f64::from(row);
84 let len = rows.count();
85 let top = |index: usize| index as f64 * height + f64::from(rows.space_before(index));
86 // The first row whose bottom lies below `y`.
87 let row_at = |y: f64| {
88 let [mut low, mut high] = [0, len];
89 while low < high {
90 let middle = (low + high) / 2;
91 if top(middle) + height <= y {
92 low = middle + 1;
93 } else {
94 high = middle;
95 }
96 }
97 low
98 };
99 let rect = ui.laid_out(id);
100 let view = f64::from(match spec.size[1].size {
101 Size::Pixels(pixels) => pixels,
102 _ => rect.map_or(0.0, |rect| rect[3] - rect[1]),
103 });
104 let most = (top(len) - view).max(0.0);
105 let fresh = !ui.lists.contains_key(&id);
106 let mut state = ui.lists.remove(&id).unwrap_or_default();
107
108 // Hold the view on the selection, or the first row showing, wherever it now lies.
109 let held = state.selected.and_then(|selected| {
110 state
111 .shown
112 .iter()
113 .find(|(key, place)| *key == selected && (0.0..view).contains(place))
114 });
115 // At the top, the view stays there to show what arrives.
116 let anchor = held
117 .into_iter()
118 .chain(
119 state
120 .shown
121 .iter()
122 .filter(|(_, place)| state.scroll > 0.0 && place + height > 0.0),
123 )
124 .find_map(|(key, place)| Some(top(rows.find(*key)?) - place));
125 if let Some(scroll) = anchor {
126 state.target += scroll - state.scroll;
127 state.scroll = scroll;
128 }
129 state.scroll = state.scroll.clamp(0.0, most);
130 state.target = state.target.clamp(0.0, most);
131 let anchored = state.scroll;
132
133 for event in ui.signal(id).events {
134 if let Event::Wheel(delta) = event {
135 state.scroll = (state.scroll - f64::from(delta[1])).clamp(0.0, most);
136 state.target = state.scroll;
137 }
138 }
139 let mut clicked = None;
140 for (key, _) in &state.shown {
141 let signal = ui.signal(id.child(*key));
142 let Some(index) = rows.find(*key).filter(|index| rows.selectable(*index)) else {
143 continue;
144 };
145 if hover_selects && signal.hovered && ui.moved {
146 *selected = Some(*key);
147 state.selected = *selected;
148 }
149 if signal.clicked {
150 *selected = Some(*key);
151 clicked = Some(index);
152 }
153 }
154 let forward = |from: usize| (from..len).find(|index| rows.selectable(*index));
155 let backward = |from: usize| {
156 (0..=from.min(len.saturating_sub(1)))
157 .rev()
158 .find(|index| rows.selectable(*index))
159 };
160 let page = ((view / height) as usize).max(1);
161 let mut current = selected.and_then(|key| rows.find(key));
162 for key in keys.iter().filter(|_| len > 0) {
163 let next = match (key, current) {
164 (NamedKey::ArrowDown, None) => forward(row_at(anchored)),
165 (NamedKey::ArrowDown, Some(at)) => forward(at + 1),
166 (NamedKey::ArrowUp, None) => backward(row_at(anchored + view).saturating_sub(1)),
167 (NamedKey::ArrowUp, Some(at)) => at.checked_sub(1).and_then(backward),
168 (NamedKey::PageDown, at) => {
169 let to = at.map_or(0, |at| (at + page).min(len - 1));
170 forward(to).or_else(|| backward(to))
171 }
172 (NamedKey::PageUp, at) => {
173 let to = at.map_or(0, |at| at.saturating_sub(page));
174 backward(to).or_else(|| forward(to))
175 }
176 (NamedKey::Home, _) => forward(0),
177 (NamedKey::End, _) => backward(len - 1),
178 _ => None,
179 };
180 if next.is_some() {
181 current = next;
182 *selected = current.map(|index| rows.key(index));
183 }
184 }
185 if *selected != state.selected {
186 if let Some(at) = selected.and_then(|key| rows.find(key)) {
187 // The selection settles clear of the fades; the list's own ends have none.
188 let [first, last] = [
189 top(at) - f64::from(FADE),
190 top(at) + height + f64::from(FADE),
191 ];
192 let lowest = (last - view).min(first);
193 state.target = state.target.clamp(lowest, first).clamp(0.0, most);
194 }
195 state.selected = *selected;
196 }
197
198 let rate = 1.0 - 0.5_f64.powf(f64::from(ui.dt / HALF_LIFE));
199 if fresh || (state.target - state.scroll).abs() < 0.25 {
200 state.scroll = state.target;
201 } else {
202 state.scroll += (state.target - state.scroll) * rate;
203 }
204 let scroll = state.scroll;
205 ui.animating |= scroll != state.target;
206
207 // Rows fill all but a gutter for the scrollbar, from their first frame: the padding
208 // narrows what they fill, and they float from the corner.
209 let gutter = if most > 0.0 { GUTTER / 2.0 } else { 0.0 };
210 let fade = |cut: bool| if cut { FADE } else { 0.0 };
211 ui.open_as(
212 id,
213 Spec {
214 flags: spec.flags | Flags::SCROLL | Flags::CLIP,
215 axis: Axis::Y,
216 pad: [spec.pad[0] + gutter, spec.pad[1]],
217 fade: [fade(scroll > 0.5), fade(scroll < most - 0.5)],
218 ..spec
219 },
220 );
221 let width = fill();
222 let in_view = row_at(scroll)..(row_at(scroll + view) + 1).min(len);
223 let mut shown = Vec::with_capacity(in_view.len());
224 for index in in_view {
225 let key = rows.key(index);
226 let place = top(index) - scroll;
227 let flags = if rows.selectable(index) {
228 Flags::FLOAT | Flags::CLICKABLE
229 } else {
230 Flags::FLOAT
231 };
232 ui.open(
233 key,
234 Spec {
235 flags,
236 size: [width, px(row)],
237 position: [0.0, place as f32],
238 ..Spec::default()
239 },
240 );
241 build(
242 ui,
243 Row {
244 key,
245 index,
246 selected: *selected == Some(key),
247 },
248 );
249 ui.close();
250 shown.push((key, place));
251 }
252 let thumb = mix(ui.theme.text_dim, ui.theme.chip, 0.5);
253 if let Some(offset) = scrollbar(
254 ui,
255 "bar",
256 Axis::Y,
257 scroll as f32,
258 [0.0, most as f32],
259 view as f32,
260 false,
261 thumb,
262 ) {
263 // Rows keep their places relative to the new offset, or next frame's hold on the
264 // first row showing would scroll straight back.
265 let step = f64::from(offset) - scroll;
266 for (_, place) in &mut shown {
267 *place -= step;
268 }
269 state.scroll = f64::from(offset);
270 state.target = state.scroll;
271 }
272 ui.close();
273 state.shown = shown;
274 ui.lists.insert(id, state);
275 clicked
276}