| ... | @@ -1,6 +1,7 @@ | ... | @@ -1,6 +1,7 @@ |
| 1 | //! A virtualized list of equal rows keyed by their items: only rows in view are built, | 1 | //! A virtualized list of equal rows keyed by their items: only rows in view are built, |
| 2 | //! rows move to where their items go without passing over one another, and the view holds | 2 | //! rows slide to where their items go, rows leaving fold into the seam their gap closes on |
| 3 | //! its place on the selection while items arrive and leave above it. | 3 | //! and rows arriving unfold out of it, and the view holds its place on the selection while |
| | 4 | //! items arrive and leave above it. |
| 4 | | 5 | |
| 5 | use crate::{Axis, Event, Flags, HALF_LIFE, Id, Size, Spec, Ui, fill, mix, px, scrollbar}; | 6 | use crate::{Axis, Event, Flags, HALF_LIFE, Id, Size, Spec, Ui, fill, mix, px, scrollbar}; |
| 6 | use std::collections::{HashMap, HashSet}; | 7 | use std::collections::{HashMap, HashSet}; |
| ... | @@ -8,7 +9,7 @@ use winit::keyboard::NamedKey; | ... | @@ -8,7 +9,7 @@ use winit::keyboard::NamedKey; |
| 8 | | 9 | |
| 9 | /// Room beside a scrolling list's rows for its scrollbar. | 10 | /// Room beside a scrolling list's rows for its scrollbar. |
| 10 | pub(crate) const GUTTER: f32 = 12.0; | 11 | pub(crate) const GUTTER: f32 = 12.0; |
| 11 | /// Seconds a row takes to reach its new place: half leaving where it was, half arriving. | 12 | /// Seconds rows take to reach their new places. |
| 12 | const DURATION: f32 = 0.15; | 13 | const DURATION: f32 = 0.15; |
| 13 | | 14 | |
| 14 | /// Items a list shows, in order. | 15 | /// Items a list shows, in order. |
| ... | @@ -41,8 +42,8 @@ pub struct List<'a, R> { | ... | @@ -41,8 +42,8 @@ pub struct List<'a, R> { |
| 41 | pub hover_selects: bool, | 42 | pub hover_selects: bool, |
| 42 | } | 43 | } |
| 43 | | 44 | |
| 44 | /// A row to build: its item's key, its index while listed or None as it fades out after | 45 | /// A row to build: its item's key, its index while listed or None as it folds away after |
| 45 | /// leaving, and whether it is selected. | 46 | /// its item left or moved, and whether it is selected. |
| 46 | pub struct Row { | 47 | pub struct Row { |
| 47 | pub key: u64, | 48 | pub key: u64, |
| 48 | pub index: Option<usize>, | 49 | pub index: Option<usize>, |
| ... | @@ -58,8 +59,6 @@ pub(crate) struct State { | ... | @@ -58,8 +59,6 @@ pub(crate) struct State { |
| 58 | shown: Vec<Shown>, | 59 | shown: Vec<Shown>, |
| 59 | /// The selection last frame, so a new one scrolls into view. | 60 | /// The selection last frame, so a new one scrolls into view. |
| 60 | selected: Option<u64>, | 61 | selected: Option<u64>, |
| 61 | /// How far the furthest sliding row was from its place last frame. | | |
| 62 | slack: f64, | | |
| 63 | } | 62 | } |
| 64 | | 63 | |
| 65 | #[derive(Clone, Copy)] | 64 | #[derive(Clone, Copy)] |
| ... | @@ -67,72 +66,58 @@ struct Shown { | ... | @@ -67,72 +66,58 @@ struct Shown { |
| 67 | key: u64, | 66 | key: u64, |
| 68 | /// Where its row lies, from the top of the view. | 67 | /// Where its row lies, from the top of the view. |
| 69 | place: f64, | 68 | place: f64, |
| | 69 | /// None for a row folding away after its item left or moved. |
| 70 | index: Option<usize>, | 70 | index: Option<usize>, |
| 71 | motion: Option<Motion>, | 71 | motion: Option<Motion>, |
| | 72 | /// Where it was drawn from the top of the view, and how strongly. |
| | 73 | drawn: f64, |
| | 74 | alpha: f32, |
| 72 | } | 75 | } |
| 73 | | 76 | |
| 74 | impl Shown { | | |
| 75 | /// Where the row is drawn from the top of the view, and its opacity. | | |
| 76 | fn drawn(&self, reach: f64, slack: f64) -> (f64, f32) { | | |
| 77 | self.motion.map_or((self.place, 1.0), |motion| { | | |
| 78 | let (offset, alpha) = motion.at(reach, slack); | | |
| 79 | (self.place + offset, alpha) | | |
| 80 | }) | | |
| 81 | } | | |
| 82 | } | | |
| 83 | | | |
| 84 | /// A row's way to its place after the items change, between offsets from the place. | | |
| 85 | #[derive(Clone, Copy)] | 77 | #[derive(Clone, Copy)] |
| 86 | struct Motion { | 78 | struct Motion { |
| 87 | from: f64, | | |
| 88 | to: f64, | | |
| 89 | /// Its opacity as it set out. | | |
| 90 | alpha: f32, | | |
| 91 | style: Style, | | |
| 92 | elapsed: f32, | 79 | elapsed: f32, |
| | 80 | way: Way, |
| 93 | } | 81 | } |
| 94 | | 82 | |
| 95 | #[derive(Clone, Copy, PartialEq)] | 83 | #[derive(Clone, Copy)] |
| 96 | enum Style { | 84 | enum Way { |
| 97 | /// A move keeping its order with the rows around: waits out those leaving, then slides. | 85 | /// Slides to its place from this offset. |
| 98 | Slide, | 86 | Slide(f64), |
| 99 | /// Fades out setting off towards the midpoint and in arriving from it, unseen where | 87 | Fold(Fold), |
| 100 | /// it passes other rows. | | |
| 101 | Cross, | | |
| 102 | /// Fades in arriving from the middle of the rows entering with it. | | |
| 103 | Enter, | | |
| 104 | /// Fades out setting off towards the middle of the rows leaving with it. | | |
| 105 | Leave, | | |
| 106 | } | 88 | } |
| 107 | | 89 | |
| 108 | impl Motion { | 90 | /// A row folding into the gap its run leaves between two rows, or unfolding out of the |
| 109 | /// Its offset and opacity; beyond `reach` of both ends it cannot be seen, so its text | 91 | /// gap its run opens, squeezed among the run and blending into the background as the gap |
| 110 | /// never meets a neighbour's, nor arriving while a row sliding is `slack` from its place. | 92 | /// narrows. |
| 111 | fn at(self, reach: f64, slack: f64) -> (f64, f32) { | 93 | #[derive(Clone, Copy)] |
| 112 | let t = (self.elapsed / DURATION).min(1.0); | 94 | struct Fold { |
| 113 | let [first, second] = [(2.0 * t).min(1.0), (2.0 * t - 1.0).max(0.0)]; | 95 | /// The gap's edges as it starts and ends, as offsets from the row's place. |
| 114 | let out = |x: f32| 1.0 - (1.0 - x).powi(3); | 96 | start: [f64; 2], |
| 115 | let inward = |x: f32| x.powi(3); | 97 | end: [f64; 2], |
| 116 | let leave = self.alpha * (1.0 - out(first)); | 98 | /// The row's position in its run, and the run's length. |
| 117 | let (travel, fade) = match self.style { | 99 | at: usize, |
| 118 | Style::Slide => (out(second), 1.0), | 100 | of: usize, |
| 119 | Style::Cross => ( | 101 | /// How strongly it was drawn as it set out. |
| 120 | 0.5 * inward(first) + 0.5 * out(second), | 102 | alpha: f32, |
| 121 | if t < 0.5 { leave } else { inward(second) }, | 103 | } |
| 122 | ), | 104 | |
| 123 | Style::Enter => (out(second), inward(second)), | 105 | impl Fold { |
| 124 | Style::Leave => (inward(first), leave), | 106 | /// Where the row is drawn and how strongly, `progress` of the way through, never over |
| 125 | }; | 107 | /// the `solid` rows drawn at these tops, in order. Its text keeps inside the gap, gone |
| 126 | let offset = self.from + (self.to - self.from) * f64::from(travel); | 108 | /// before the gap is too narrow to hold it clear of the rows either side. |
| 127 | let near = (offset - self.from).abs().min((offset - self.to).abs()); | 109 | fn at(&self, place: f64, progress: f64, height: f64, solid: &[f64]) -> (f64, f32) { |
| 128 | let seen = |distance: f64| (1.0 - distance / reach).clamp(0.0, 1.0) as f32; | 110 | let [top, bottom] = [0, 1] |
| 129 | let alpha = match self.style { | 111 | .map(|side| place + self.start[side] + (self.end[side] - self.start[side]) * progress); |
| 130 | Style::Slide => 1.0, | 112 | let below = solid.partition_point(|y| y + height / 2.0 <= (top + bottom) / 2.0); |
| 131 | Style::Enter => fade.min(seen(near)) * seen(slack), | 113 | let top = below |
| 132 | Style::Cross if t >= 0.5 => fade.min(seen(near)) * seen(slack), | 114 | .checked_sub(1) |
| 133 | _ => fade.min(seen(near)), | 115 | .map_or(top, |above| top.max(solid[above] + height)); |
| 134 | }; | 116 | let bottom = solid.get(below).map_or(bottom, |y| bottom.min(*y)); |
| 135 | (offset, alpha) | 117 | let share = (bottom - top).max(0.0) / self.of as f64; |
| | 118 | let y = top + (self.at as f64 + 0.5) * share - height / 2.0; |
| | 119 | let room = (2.0 * share / height - 1.0).clamp(0.0, 1.0) as f32; |
| | 120 | (y, room * self.alpha) |
| 136 | } | 121 | } |
| 137 | } | 122 | } |
| 138 | | 123 | |
| ... | @@ -201,7 +186,7 @@ pub fn list<R: Rows>( | ... | @@ -201,7 +186,7 @@ pub fn list<R: Rows>( |
| 201 | } | 186 | } |
| 202 | } | 187 | } |
| 203 | let mut clicked = None; | 188 | let mut clicked = None; |
| 204 | for shown in &state.shown { | 189 | for shown in state.shown.iter().filter(|shown| shown.index.is_some()) { |
| 205 | let signal = ui.signal(id.child(shown.key)); | 190 | let signal = ui.signal(id.child(shown.key)); |
| 206 | let Some(index) = rows.find(shown.key).filter(|index| rows.selectable(*index)) else { | 191 | let Some(index) = rows.find(shown.key).filter(|index| rows.selectable(*index)) else { |
| 207 | continue; | 192 | continue; |
| ... | @@ -263,25 +248,21 @@ pub fn list<R: Rows>( | ... | @@ -263,25 +248,21 @@ pub fn list<R: Rows>( |
| 263 | let scroll = state.scroll; | 248 | let scroll = state.scroll; |
| 264 | let step = scroll - anchored; | 249 | let step = scroll - anchored; |
| 265 | | 250 | |
| 266 | let reach = height / 6.0; | | |
| 267 | let dt = ui.dt; | 251 | let dt = ui.dt; |
| 268 | let tick = |motion: Motion| { | 252 | let tick = |motion: Motion| { |
| 269 | let elapsed = motion.elapsed + dt; | 253 | let elapsed = motion.elapsed + dt; |
| 270 | (elapsed < DURATION).then_some(Motion { elapsed, ..motion }) | 254 | (elapsed < DURATION).then_some(Motion { elapsed, ..motion }) |
| 271 | }; | 255 | }; |
| 272 | // Rows built last frame, drawn in the view as anchored. | 256 | let progress = |motion: &Motion| 1.0 - (1.0 - f64::from(motion.elapsed / DURATION)).powi(3); |
| 273 | let before: HashMap<u64, Shown> = state | 257 | let listed: Vec<Shown> = state |
| 274 | .shown | | |
| 275 | .iter() | | |
| 276 | .map(|shown| (shown.key, *shown)) | | |
| 277 | .collect(); | | |
| 278 | let listed: Vec<&Shown> = state | | |
| 279 | .shown | 258 | .shown |
| 280 | .iter() | 259 | .iter() |
| 281 | .filter(|shown| shown.index.is_some()) | 260 | .filter(|shown| shown.index.is_some()) |
| | 261 | .copied() |
| 282 | .collect(); | 262 | .collect(); |
| 283 | // The most rows staying listed that keep their order slide; the rest would pass over | 263 | let before: HashMap<u64, Shown> = listed.iter().map(|shown| (shown.key, *shown)).collect(); |
| 284 | // them, so cross unseen. | 264 | // The most rows that stay in their order slide; the rest would pass over them, so fold |
| | 265 | // away and unfold where they go, as do rows moving while partly folded. |
| 285 | let order: Vec<(u64, usize)> = listed | 266 | let order: Vec<(u64, usize)> = listed |
| 286 | .iter() | 267 | .iter() |
| 287 | .filter_map(|shown| Some((shown.key, rows.find(shown.key)?))) | 268 | .filter_map(|shown| Some((shown.key, rows.find(shown.key)?))) |
| ... | @@ -299,81 +280,91 @@ pub fn list<R: Rows>( | ... | @@ -299,81 +280,91 @@ pub fn list<R: Rows>( |
| 299 | } | 280 | } |
| 300 | let kept: HashSet<u64> = std::iter::successors(tails.last().copied(), |at| previous[*at]) | 281 | let kept: HashSet<u64> = std::iter::successors(tails.last().copied(), |at| previous[*at]) |
| 301 | .map(|at| order[at].0) | 282 | .map(|at| order[at].0) |
| | 283 | .filter(|key| before[key].alpha >= 1.0) |
| 302 | .collect(); | 284 | .collect(); |
| 303 | let arrive = |index: usize| { | 285 | |
| | 286 | let settled = |index: usize| { |
| 304 | let key = rows.key(index); | 287 | let key = rows.key(index); |
| 305 | let was = top(index) - anchored; | 288 | let was = top(index) - anchored; |
| 306 | let motion = before.get(&key).and_then(|old| { | 289 | let motion = before.get(&key).and_then(|old| { |
| 307 | if old.index.is_some() && (old.place - was).abs() < 0.5 { | 290 | if (old.place - was).abs() < 0.5 { |
| 308 | return old.motion.and_then(tick); | 291 | old.motion.and_then(tick) |
| 309 | } | | |
| 310 | let (drawn, alpha) = old.drawn(reach, state.slack); | | |
| 311 | let from = drawn - was; | | |
| 312 | let style = if alpha < 1.0 || !kept.contains(&key) { | | |
| 313 | Style::Cross | | |
| 314 | } else { | 292 | } else { |
| 315 | Style::Slide | 293 | tick(Motion { |
| 316 | }; | 294 | elapsed: 0.0, |
| 317 | tick(Motion { | 295 | way: Way::Slide(old.drawn - was), |
| 318 | from, | 296 | }) |
| 319 | to: 0.0, | 297 | } |
| 320 | alpha, | | |
| 321 | style, | | |
| 322 | elapsed: 0.0, | | |
| 323 | }) | | |
| 324 | }); | 298 | }); |
| 325 | Shown { | 299 | Shown { |
| 326 | key, | 300 | key, |
| 327 | place: top(index) - scroll, | 301 | place: top(index) - scroll, |
| 328 | index: Some(index), | 302 | index: Some(index), |
| 329 | motion, | 303 | motion, |
| | 304 | drawn: 0.0, |
| | 305 | alpha: 1.0, |
| 330 | } | 306 | } |
| 331 | }; | 307 | }; |
| 332 | // Rows newly where the view already was, rather than scrolled into it, enter. | 308 | // Rows arriving where the view already was, rather than scrolled into it, unfold. |
| 333 | let entering = |index: usize| { | 309 | let arriving = |index: usize| { |
| | 310 | let key = rows.key(index); |
| 334 | let was = top(index) - anchored; | 311 | let was = top(index) - anchored; |
| 335 | !fresh && !before.contains_key(&rows.key(index)) && was + height > 0.0 && was < view | 312 | match before.get(&key) { |
| | 313 | Some(old) => (old.place - was).abs() >= 0.5 && !kept.contains(&key), |
| | 314 | None => !fresh && was + height > 0.0 && was < view, |
| | 315 | } |
| 336 | }; | 316 | }; |
| 337 | let in_view = row_at(scroll)..(row_at(scroll + view) + 1).min(len); | 317 | let in_view = row_at(scroll)..(row_at(scroll + view) + 1).min(len); |
| 338 | let mut shown = Vec::new(); | 318 | let mut shown = Vec::new(); |
| 339 | let mut index = in_view.start; | 319 | let mut index = in_view.start; |
| 340 | while index < in_view.end { | 320 | while index < in_view.end { |
| 341 | if !entering(index) { | 321 | if !arriving(index) { |
| 342 | shown.push(arrive(index)); | 322 | shown.push(settled(index)); |
| 343 | index += 1; | 323 | index += 1; |
| 344 | continue; | 324 | continue; |
| 345 | } | 325 | } |
| 346 | let end = (index..in_view.end) | 326 | let end = (index..in_view.end) |
| 347 | .find(|index| !entering(*index)) | 327 | .find(|index| !arriving(*index)) |
| 348 | .unwrap_or(in_view.end); | 328 | .unwrap_or(in_view.end); |
| 349 | let middle = (top(index) + top(end - 1)) / 2.0; | 329 | let [above, below] = [index.checked_sub(1), (end < len).then_some(end)] |
| 350 | shown.extend((index..end).map(|index| Shown { | 330 | .map(|side| side.map(|side| rows.key(side))); |
| 351 | key: rows.key(index), | 331 | let [first, last] = [top(index), top(end - 1) + height].map(|y| y - anchored); |
| 352 | place: top(index) - scroll, | 332 | // The seam opens where the rows either side were drawn, or mid-run with neither. |
| 353 | index: Some(index), | 333 | let seam = above |
| 354 | motion: tick(Motion { | 334 | .and_then(|key| Some(before.get(&key)?.drawn + height)) |
| 355 | from: middle - top(index), | 335 | .or_else(|| below.and_then(|key| Some(before.get(&key)?.drawn))) |
| 356 | to: 0.0, | 336 | .unwrap_or((first + last) / 2.0); |
| 357 | alpha: 0.0, | 337 | shown.extend((index..end).map(|at| { |
| 358 | style: Style::Enter, | 338 | let was = top(at) - anchored; |
| 359 | elapsed: 0.0, | 339 | Shown { |
| 360 | }), | 340 | motion: tick(Motion { |
| | 341 | elapsed: 0.0, |
| | 342 | way: Way::Fold(Fold { |
| | 343 | start: [seam - was; 2], |
| | 344 | end: [first - was, last - was], |
| | 345 | at: at - index, |
| | 346 | of: end - index, |
| | 347 | alpha: 1.0, |
| | 348 | }), |
| | 349 | }), |
| | 350 | ..settled(at) |
| | 351 | } |
| 361 | })); | 352 | })); |
| 362 | index = end; | 353 | index = end; |
| 363 | } | 354 | } |
| 364 | for old in &listed { | 355 | for old in &listed { |
| 365 | if let Some(index) = rows.find(old.key).filter(|index| !in_view.contains(index)) { | 356 | if let Some(index) = rows |
| 366 | let moving = arrive(index); | 357 | .find(old.key) |
| 367 | let drawn = moving.drawn(reach, state.slack).0; | 358 | .filter(|index| !in_view.contains(index) && kept.contains(&old.key)) |
| 368 | if drawn + height > 0.0 && drawn < view { | 359 | { |
| 369 | shown.push(moving); | 360 | shown.push(settled(index)); |
| 370 | } | | |
| 371 | } | 361 | } |
| 372 | } | 362 | } |
| 373 | let mut leaving: Vec<Shown> = state | 363 | // Rows already folding away carry on; runs of rows gone or moving out of order start. |
| | 364 | let mut folding: Vec<Shown> = state |
| 374 | .shown | 365 | .shown |
| 375 | .iter() | 366 | .iter() |
| 376 | .filter(|shown| shown.index.is_none() && rows.find(shown.key).is_none()) | 367 | .filter(|shown| shown.index.is_none()) |
| 377 | .filter_map(|shown| { | 368 | .filter_map(|shown| { |
| 378 | Some(Shown { | 369 | Some(Shown { |
| 379 | place: shown.place - step, | 370 | place: shown.place - step, |
| ... | @@ -382,40 +373,94 @@ pub fn list<R: Rows>( | ... | @@ -382,40 +373,94 @@ pub fn list<R: Rows>( |
| 382 | }) | 373 | }) |
| 383 | }) | 374 | }) |
| 384 | .collect(); | 375 | .collect(); |
| 385 | let gone = |shown: &&Shown| rows.find(shown.key).is_none(); | 376 | let staying = |shown: &Shown| { |
| 386 | for run in listed.chunk_by(|a, b| gone(a) == gone(b)) { | 377 | kept.contains(&shown.key) |
| 387 | if !gone(&run[0]) { | 378 | || rows |
| | 379 | .find(shown.key) |
| | 380 | .is_some_and(|index| (top(index) - anchored - shown.place).abs() < 0.5) |
| | 381 | }; |
| | 382 | let mut at = 0; |
| | 383 | for run in listed.chunk_by(|a, b| staying(a) == staying(b)) { |
| | 384 | let start = at; |
| | 385 | at += run.len(); |
| | 386 | if staying(&run[0]) { |
| 388 | continue; | 387 | continue; |
| 389 | } | 388 | } |
| 390 | let drawn: Vec<_> = run | 389 | let above = start.checked_sub(1).map(|side| listed[side].key); |
| 391 | .iter() | 390 | let below = listed.get(at).map(|side| side.key); |
| 392 | .map(|shown| shown.drawn(reach, state.slack)) | 391 | let [first, last] = [run[0].drawn, run[run.len() - 1].drawn + height]; |
| 393 | .collect(); | 392 | // The seam the gap closes on: where the row below or above it now lies. |
| 394 | let middle = (drawn[0].0 + drawn[drawn.len() - 1].0) / 2.0; | 393 | let seam = below |
| 395 | leaving.extend(run.iter().zip(drawn).map(|(shown, (y, alpha))| Shown { | 394 | .and_then(|key| Some(top(rows.find(key)?) - anchored)) |
| 396 | key: shown.key, | 395 | .or_else(|| above.and_then(|key| Some(top(rows.find(key)?) + height - anchored))) |
| 397 | place: y - step, | 396 | .unwrap_or(first); |
| | 397 | folding.extend(run.iter().enumerate().map(|(at, old)| Shown { |
| | 398 | key: old.key, |
| | 399 | place: old.drawn - step, |
| 398 | index: None, | 400 | index: None, |
| 399 | motion: tick(Motion { | 401 | motion: tick(Motion { |
| 400 | from: 0.0, | | |
| 401 | to: middle - y, | | |
| 402 | alpha, | | |
| 403 | style: Style::Leave, | | |
| 404 | elapsed: 0.0, | 402 | elapsed: 0.0, |
| | 403 | way: Way::Fold(Fold { |
| | 404 | start: [first - old.drawn, last - old.drawn], |
| | 405 | end: [seam - old.drawn; 2], |
| | 406 | at, |
| | 407 | of: run.len(), |
| | 408 | alpha: old.alpha, |
| | 409 | }), |
| 405 | }), | 410 | }), |
| | 411 | drawn: 0.0, |
| | 412 | alpha: 0.0, |
| 406 | })); | 413 | })); |
| 407 | } | 414 | } |
| 408 | shown.sort_by(|a, b| a.place.total_cmp(&b.place)); | 415 | |
| 409 | let slack = shown | 416 | // Rows sliding or still are drawn first, as folds are bounded by them. |
| | 417 | for shown in &mut shown { |
| | 418 | if let Some(Motion { |
| | 419 | way: Way::Slide(from), |
| | 420 | .. |
| | 421 | }) = shown.motion |
| | 422 | { |
| | 423 | shown.drawn = shown.place + from * (1.0 - progress(&shown.motion.unwrap())); |
| | 424 | } else { |
| | 425 | shown.drawn = shown.place; |
| | 426 | } |
| | 427 | } |
| | 428 | let mut solid: Vec<f64> = shown |
| 410 | .iter() | 429 | .iter() |
| 411 | .filter_map(|shown| shown.motion.filter(|motion| motion.style == Style::Slide)) | 430 | .filter(|shown| { |
| 412 | .map(|motion| motion.at(reach, 0.0).0.abs()) | 431 | !matches!( |
| 413 | .fold(0.0, f64::max); | 432 | shown.motion, |
| | 433 | Some(Motion { |
| | 434 | way: Way::Fold(_), |
| | 435 | .. |
| | 436 | }) |
| | 437 | ) |
| | 438 | }) |
| | 439 | .map(|shown| shown.drawn) |
| | 440 | .collect(); |
| | 441 | solid.sort_by(f64::total_cmp); |
| | 442 | for shown in shown.iter_mut().chain(&mut folding) { |
| | 443 | if let Some( |
| | 444 | motion @ Motion { |
| | 445 | way: Way::Fold(fold), |
| | 446 | .. |
| | 447 | }, |
| | 448 | ) = shown.motion |
| | 449 | { |
| | 450 | (shown.drawn, shown.alpha) = fold.at(shown.place, progress(&motion), height, &solid); |
| | 451 | } |
| | 452 | } |
| | 453 | shown.retain(|shown| { |
| | 454 | shown.drawn + height > 0.0 && shown.drawn < view |
| | 455 | || shown.index.is_some_and(|index| in_view.contains(&index)) |
| | 456 | }); |
| | 457 | shown.sort_by(|a, b| a.place.total_cmp(&b.place)); |
| 414 | ui.animating |= state.scroll != state.target | 458 | ui.animating |= state.scroll != state.target |
| 415 | || !leaving.is_empty() | 459 | || !folding.is_empty() |
| 416 | || shown.iter().any(|shown| shown.motion.is_some()); | 460 | || shown.iter().any(|shown| shown.motion.is_some()); |
| 417 | | 461 | |
| 418 | let theme = ui.theme.clone(); | 462 | let theme = ui.theme.clone(); |
| | 463 | let background = spec.fill.unwrap_or(theme.base); |
| 419 | ui.open_as( | 464 | ui.open_as( |
| 420 | id, | 465 | id, |
| 421 | Spec { | 466 | Spec { |
| ... | @@ -428,22 +473,25 @@ pub fn list<R: Rows>( | ... | @@ -428,22 +473,25 @@ pub fn list<R: Rows>( |
| 428 | Some(rect) if most > 0.0 => px(rect[2] - rect[0] - GUTTER), | 473 | Some(rect) if most > 0.0 => px(rect[2] - rect[0] - GUTTER), |
| 429 | _ => fill(), | 474 | _ => fill(), |
| 430 | }; | 475 | }; |
| 431 | for shown in leaving.iter().chain(&shown) { | 476 | // Rows folding away paint beneath the rest, and blend into the list rather than fade, |
| | 477 | // so those squeezed together never add up. |
| | 478 | for shown in folding.iter().chain(&shown) { |
| 432 | let flags = match shown.index { | 479 | let flags = match shown.index { |
| 433 | Some(index) if rows.selectable(index) => Flags::FLOAT | Flags::CLICKABLE, | 480 | Some(index) if rows.selectable(index) => Flags::FLOAT | Flags::CLICKABLE, |
| 434 | _ => Flags::FLOAT, | 481 | _ => Flags::FLOAT, |
| 435 | }; | 482 | }; |
| 436 | let (y, alpha) = shown.drawn(reach, slack); | 483 | let row_spec = Spec { |
| 437 | ui.open( | 484 | flags, |
| 438 | shown.key, | 485 | size: [width, px(row)], |
| 439 | Spec { | 486 | position: [0.0, shown.drawn as f32], |
| 440 | flags, | 487 | fade: 1.0 - shown.alpha, |
| 441 | size: [width, px(row)], | 488 | fade_into: Some(background), |
| 442 | position: [0.0, y as f32], | 489 | ..Spec::default() |
| 443 | fade: 1.0 - alpha, | 490 | }; |
| 444 | ..Spec::default() | 491 | match shown.index { |
| 445 | }, | 492 | Some(_) => ui.open(shown.key, row_spec), |
| 446 | ); | 493 | None => ui.open(("folding", shown.key), row_spec), |
| | 494 | }; |
| 447 | build( | 495 | build( |
| 448 | ui, | 496 | ui, |
| 449 | Row { | 497 | Row { |
| ... | @@ -468,10 +516,9 @@ pub fn list<R: Rows>( | ... | @@ -468,10 +516,9 @@ pub fn list<R: Rows>( |
| 468 | state.target = state.scroll; | 516 | state.target = state.scroll; |
| 469 | } | 517 | } |
| 470 | ui.close(); | 518 | ui.close(); |
| 471 | shown.extend(leaving); | 519 | shown.extend(folding); |
| 472 | shown.sort_by(|a, b| a.place.total_cmp(&b.place)); | 520 | shown.sort_by(|a, b| a.place.total_cmp(&b.place)); |
| 473 | state.shown = shown; | 521 | state.shown = shown; |
| 474 | state.slack = slack; | | |
| 475 | ui.lists.insert(id, state); | 522 | ui.lists.insert(id, state); |
| 476 | clicked | 523 | clicked |
| 477 | } | 524 | } |