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