authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-25 22:04:46-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-27 04:23:49-07:00
log710848e64a97302d4f0df26f496f6d1d3de7e809
tree2200ad3af046d89fe50684062b3d8cf611685539
parent1bdd0ee601a3ec4c0ebe18ad7a462ae85df968c2
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

feat: list rows squash out and unfold in one motion

Leaving rows fold into the seam their gap closes on while kept rows slide, blending toward the list's colour so folded rows never double up; arriving and reordered rows unfold from their seam in the same motion, and no row's text crosses another's. Assisted-by: claude-opus-5.5 Assisted-by: claude-opus-5

4 files changed, 246 insertions(+), 178 deletions(-)

crates/ui/src/lib.rs+18-5
......@@ -248,6 +248,9 @@ pub struct Spec<'a> {
248248 pub anchor: Option<Anchor>,
249249 /// How transparent the box and its contents are, from 0 to 1.
250250 pub fade: f32,
251 /// The colour `fade` blends the box and its contents into instead, so boxes fading
252 /// over one another never add up.
253 pub fade_into: Option<[f32; 4]>,
251254}
252255
253256/// Input the host forwards; positions and wheel distances are logical pixels.
......@@ -329,6 +332,7 @@ struct Built {
329332 anchor: Option<Anchor>,
330333 /// The opacity of the box and its contents; a popup's rises as it opens.
331334 alpha: f32,
335 fade_into: Option<[f32; 4]>,
332336 /// Rectangles relative to the box, painted over its fill.
333337 marks: Vec<([f32; 4], [f32; 4])>,
334338 computed: [f32; 2],
......@@ -420,9 +424,15 @@ enum Display {
420424}
421425
422426impl Display {
423 /// Multiplies the item's opacity by `alpha`.
424 fn fade(&mut self, alpha: f32) {
425 let fade = |color: &mut [f32; 4]| color[3] *= alpha;
427 /// Multiplies the item's opacity by `alpha`, or blends it that far into `into`.
428 fn fade(&mut self, alpha: f32, into: Option<[f32; 4]>) {
429 let fade = |color: &mut [f32; 4]| match into {
430 Some(into) => {
431 let [red, green, blue, _] = mix(into, *color, alpha);
432 *color = [red, green, blue, color[3]];
433 }
434 None => color[3] *= alpha,
435 };
426436 match self {
427437 Display::Rect {
428438 fill,
......@@ -1098,10 +1108,12 @@ impl Ui {
10981108 if inner_clip != clip {
10991109 self.display.push(Display::Clip(clip));
11001110 }
1101 let alpha = self.nodes[index].alpha;
1111 let Built {
1112 alpha, fade_into, ..
1113 } = self.nodes[index];
11021114 if alpha < 1.0 {
11031115 for item in &mut self.display[start..] {
1104 item.fade(alpha);
1116 item.fade(alpha, fade_into);
11051117 }
11061118 }
11071119 }
......@@ -1280,6 +1292,7 @@ impl Built {
12801292 cursor: spec.cursor,
12811293 anchor: spec.anchor,
12821294 alpha: 1.0 - spec.fade,
1295 fade_into: spec.fade_into,
12831296 marks: Vec::new(),
12841297 computed: [0.0; 2],
12851298 relative: [0.0; 2],
crates/ui/src/list.rs+198-151
......@@ -1,6 +1,7 @@
11//! 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.
45
56use crate::{Axis, Event, Flags, HALF_LIFE, Id, Size, Spec, Ui, fill, mix, px, scrollbar};
67use std::collections::{HashMap, HashSet};
......@@ -8,7 +9,7 @@ use winit::keyboard::NamedKey;
89
910/// Room beside a scrolling list's rows for its scrollbar.
1011pub(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.
1213const DURATION: f32 = 0.15;
1314
1415/// Items a list shows, in order.
......@@ -41,8 +42,8 @@ pub struct List<'a, R> {
4142 pub hover_selects: bool,
4243}
4344
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.
4647pub struct Row {
4748 pub key: u64,
4849 pub index: Option<usize>,
......@@ -58,8 +59,6 @@ pub(crate) struct State {
5859 shown: Vec<Shown>,
5960 /// The selection last frame, so a new one scrolls into view.
6061 selected: Option<u64>,
61 /// How far the furthest sliding row was from its place last frame.
62 slack: f64,
6362}
6463
6564#[derive(Clone, Copy)]
......@@ -67,72 +66,58 @@ struct Shown {
6766 key: u64,
6867 /// Where its row lies, from the top of the view.
6968 place: f64,
69 /// None for a row folding away after its item left or moved.
7070 index: Option<usize>,
7171 motion: Option<Motion>,
72 /// Where it was drawn from the top of the view, and how strongly.
73 drawn: f64,
74 alpha: f32,
7275}
7376
74impl 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.
8577#[derive(Clone, Copy)]
8678struct Motion {
87 from: f64,
88 to: f64,
89 /// Its opacity as it set out.
90 alpha: f32,
91 style: Style,
9279 elapsed: f32,
80 way: Way,
9381}
9482
95#[derive(Clone, Copy, PartialEq)]
96enum 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)]
84enum Way {
85 /// Slides to its place from this offset.
86 Slide(f64),
87 Fold(Fold),
10688}
10789
108impl 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)]
94struct 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
105impl 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)
136121 }
137122}
138123
......@@ -201,7 +186,7 @@ pub fn list<R: Rows>(
201186 }
202187 }
203188 let mut clicked = None;
204 for shown in &state.shown {
189 for shown in state.shown.iter().filter(|shown| shown.index.is_some()) {
205190 let signal = ui.signal(id.child(shown.key));
206191 let Some(index) = rows.find(shown.key).filter(|index| rows.selectable(*index)) else {
207192 continue;
......@@ -263,25 +248,21 @@ pub fn list<R: Rows>(
263248 let scroll = state.scroll;
264249 let step = scroll - anchored;
265250
266 let reach = height / 6.0;
267251 let dt = ui.dt;
268252 let tick = |motion: Motion| {
269253 let elapsed = motion.elapsed + dt;
270254 (elapsed < DURATION).then_some(Motion { elapsed, ..motion })
271255 };
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
279258 .shown
280259 .iter()
281260 .filter(|shown| shown.index.is_some())
261 .copied()
282262 .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.
285266 let order: Vec<(u64, usize)> = listed
286267 .iter()
287268 .filter_map(|shown| Some((shown.key, rows.find(shown.key)?)))
......@@ -299,81 +280,91 @@ pub fn list<R: Rows>(
299280 }
300281 let kept: HashSet<u64> = std::iter::successors(tails.last().copied(), |at| previous[*at])
301282 .map(|at| order[at].0)
283 .filter(|key| before[key].alpha >= 1.0)
302284 .collect();
303 let arrive = |index: usize| {
285
286 let settled = |index: usize| {
304287 let key = rows.key(index);
305288 let was = top(index) - anchored;
306289 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)
314292 } 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 }
324298 });
325299 Shown {
326300 key,
327301 place: top(index) - scroll,
328302 index: Some(index),
329303 motion,
304 drawn: 0.0,
305 alpha: 1.0,
330306 }
331307 };
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);
334311 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 }
336316 };
337317 let in_view = row_at(scroll)..(row_at(scroll + view) + 1).min(len);
338318 let mut shown = Vec::new();
339319 let mut index = in_view.start;
340320 while index < in_view.end {
341 if !entering(index) {
342 shown.push(arrive(index));
321 if !arriving(index) {
322 shown.push(settled(index));
343323 index += 1;
344324 continue;
345325 }
346326 let end = (index..in_view.end)
347 .find(|index| !entering(*index))
327 .find(|index| !arriving(*index))
348328 .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 }
361352 }));
362353 index = end;
363354 }
364355 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));
371361 }
372362 }
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
374365 .shown
375366 .iter()
376 .filter(|shown| shown.index.is_none() && rows.find(shown.key).is_none())
367 .filter(|shown| shown.index.is_none())
377368 .filter_map(|shown| {
378369 Some(Shown {
379370 place: shown.place - step,
......@@ -382,40 +373,94 @@ pub fn list<R: Rows>(
382373 })
383374 })
384375 .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]) {
388387 continue;
389388 }
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,
398400 index: None,
399401 motion: tick(Motion {
400 from: 0.0,
401 to: middle - y,
402 alpha,
403 style: Style::Leave,
404402 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 }),
405410 }),
411 drawn: 0.0,
412 alpha: 0.0,
406413 }));
407414 }
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
410429 .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));
414458 ui.animating |= state.scroll != state.target
415 || !leaving.is_empty()
459 || !folding.is_empty()
416460 || shown.iter().any(|shown| shown.motion.is_some());
417461
418462 let theme = ui.theme.clone();
463 let background = spec.fill.unwrap_or(theme.base);
419464 ui.open_as(
420465 id,
421466 Spec {
......@@ -428,22 +473,25 @@ pub fn list<R: Rows>(
428473 Some(rect) if most > 0.0 => px(rect[2] - rect[0] - GUTTER),
429474 _ => fill(),
430475 };
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) {
432479 let flags = match shown.index {
433480 Some(index) if rows.selectable(index) => Flags::FLOAT | Flags::CLICKABLE,
434481 _ => Flags::FLOAT,
435482 };
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 };
447495 build(
448496 ui,
449497 Row {
......@@ -468,10 +516,9 @@ pub fn list<R: Rows>(
468516 state.target = state.scroll;
469517 }
470518 ui.close();
471 shown.extend(leaving);
519 shown.extend(folding);
472520 shown.sort_by(|a, b| a.place.total_cmp(&b.place));
473521 state.shown = shown;
474 state.slack = slack;
475522 ui.lists.insert(id, state);
476523 clicked
477524}
crates/ui/src/popup.rs+1
......@@ -195,6 +195,7 @@ fn choose(
195195 id.child("rows"),
196196 Spec {
197197 size: [fill(), px(view)],
198 fill: Some(theme.popup),
198199 ..Spec::default()
199200 },
200201 list,
crates/ui/src/tests.rs+29-22
......@@ -1027,45 +1027,52 @@ fn alpha(ui: &Ui, key: u64) -> Option<f32> {
10271027 .map(|node| node.alpha)
10281028}
10291029
1030fn folding(ui: &Ui, key: u64) -> Option<f32> {
1031 let id = list_id().child(("folding", key));
1032 ui.nodes
1033 .iter()
1034 .find(|node| node.id == id)
1035 .map(|node| node.alpha)
1036}
1037
10301038#[test]
1031fn rows_keeping_their_order_slide_and_the_rest_fade_through() {
1039fn rows_slide_while_the_rest_fold_away_and_unfold_at_once() {
10321040 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
10331041 let mut selected = None;
10341042 let rows = Keyed::new(0..20);
10351043 settle_list(&mut ui, &rows, &mut selected);
10361044 let rows = Keyed::new([5, 0, 1, 2, 3, 100, 6, 7, 8, 9, 10, 11]);
10371045 list_frame(&mut ui, &rows, &mut selected, &[]);
1038 let crossing = row_top(&ui, 5).unwrap();
1039 assert!(crossing < 5.0 * LIST_ROW && crossing > 4.5 * LIST_ROW);
1040 assert!(alpha(&ui, 5).unwrap() < 1.0, "fading as it sets off");
1041 assert_eq!(row_top(&ui, 0), Some(0.0), "sliding waits for rows leaving");
1042 assert!(alpha(&ui, 4).unwrap() < 1.0);
1043 assert_eq!(alpha(&ui, 100), Some(0.0), "entering waits too");
1044 for _ in 0..4 {
1045 list_frame(&mut ui, &rows, &mut selected, &[]);
1046 }
1047 assert_eq!(alpha(&ui, 5), Some(0.0), "unseen half-way");
1048 assert_eq!(alpha(&ui, 4), Some(0.0));
10491046 let sliding = row_top(&ui, 0).unwrap();
1050 assert!(sliding > 0.0 && sliding < LIST_ROW);
1051 assert_eq!(
1052 alpha(&ui, 100),
1053 Some(0.0),
1054 "entering waits for rows sliding"
1047 assert!(
1048 sliding > 0.0 && sliding < LIST_ROW,
1049 "sliding at once: {sliding}"
10551050 );
1056 for _ in 0..5 {
1051 for key in [4, 5] {
1052 let alpha = folding(&ui, key).unwrap();
1053 assert!(alpha > 0.0 && alpha < 1.0, "{key} folds where it was");
1054 }
1055 assert!(row_top(&ui, 5).unwrap() < 0.0, "out of the seam at the top");
1056 list_frame(&mut ui, &rows, &mut selected, &[]);
1057 list_frame(&mut ui, &rows, &mut selected, &[]);
1058 for key in [5, 100] {
1059 let alpha = alpha(&ui, key).unwrap();
1060 assert!(alpha > 0.0 && alpha < 1.0, "{key} unfolds where it goes");
1061 }
1062 for _ in 0..7 {
10571063 list_frame(&mut ui, &rows, &mut selected, &[]);
10581064 }
10591065 assert_eq!(row_top(&ui, 5), Some(0.0), "there in about 150 ms");
10601066 assert_eq!(row_top(&ui, 0), Some(LIST_ROW));
1067 assert_eq!(row_top(&ui, 100), Some(5.0 * LIST_ROW));
10611068 assert_eq!(alpha(&ui, 100), Some(1.0));
1062 assert_eq!(alpha(&ui, 4), None);
1063 list_frame(&mut ui, &rows, &mut selected, &[]);
1069 assert_eq!((folding(&ui, 4), folding(&ui, 5)), (None, None));
10641070 assert!(!ui.wants_frame());
10651071}
10661072
10671073/// Two rows whose text is drawn over each other: their labels, centred in each row, meet
1068/// within the view while both can be seen.
1074/// within the view while both can be seen and one is fully drawn. Rows folding together,
1075/// both blended into the background, may meet.
10691076fn text_overlaps(ui: &mut Ui) -> Option<String> {
10701077 let text = ui.measure("Ag")[1];
10711078 let list = ui.nodes.iter().position(|node| node.id == list_id())?;
......@@ -1086,7 +1093,7 @@ fn text_overlaps(ui: &mut Ui) -> Option<String> {
10861093 rows.iter().enumerate().find_map(|(at, a)| {
10871094 rows[at + 1..]
10881095 .iter()
1089 .find(|b| a.0 < b.1 && b.0 < a.1)
1096 .find(|b| a.0 < b.1 && b.0 < a.1 && a.2.max(b.2) >= 1.0)
10901097 .map(|b| format!("{a:?} over {b:?}"))
10911098 })
10921099}