Tell the client where a scroll area landed, not where it changed
A scroll area reported its place — "end" or "away" — whenever that answer differed from the one in the memo it keeps per `scroll-key`. The memo is not the client's belief, though, and the two part company wherever something other than the reader's hand moved the list. Two of those, and both end the same way: a "jump to present" button over a backlog that is already at its newest line, which hides itself when clicked and scrolls nowhere, because there is nowhere to scroll to. The jump branch marked `at_end` on the way past, before the snap it was asking for had happened. So the report that came back agreed with the memo and was never passed on — the same mistake the reveal branch above it carries a paragraph warning against. And a list that has just mounted says nothing, because the place it opens at is the place the memo guessed it would. Nothing CHANGED. But the client's belief is about the list this one replaced, and since frq gave every room its own `scroll-key` that is another conversation entirely: read back through yesterday in one channel, open another, and the button comes along. So the memo carries what the client was last told, apart from where the list is, and a report is worth making exactly when those differ. A list that has just mounted has been told nothing, whatever was said about the last one under its name. The decision either way is now a pair of functions over the ask and the memo — `scroll_move` and `report` — which is the whole of the thinking and none of the toolkit, so it can be read and tested without a window to put it in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4f920af parent: 6316bb1 modified
crates/jolt-cosmic/src/lib.rs +174 -32 | @@ -229,6 +229,20 @@ struct ScrollMemo { | ||
| 229 | 229 | /// Whether the reader is at the newest line. A `stick-to-bottom` list |
| 230 | 230 | /// follows what arrives only while this holds. |
| 231 | 231 | at_end: bool, |
| 232 | + /// What the client was last told about that, and `None` while it has been | |
| 233 | + /// told nothing. | |
| 234 | + /// | |
| 235 | + /// Held apart from `at_end` because the two answer different questions. | |
| 236 | + /// `at_end` is where this list is; `told` is what the client believes, | |
| 237 | + /// and a report is worth making exactly when they differ. Reporting on a | |
| 238 | + /// change in `at_end` alone loses two cases, and both of them end with a | |
| 239 | + /// "jump to present" button over a backlog that is already at its newest | |
| 240 | + /// line. A list mounting says nothing, because the place it opens at is | |
| 241 | + /// the place the memo guessed it would — but the client's belief is about | |
| 242 | + /// the list this one REPLACED, which in frq is another room entirely. And | |
| 243 | + /// a jump says nothing, because the branch that asked for it marked the | |
| 244 | + /// memo on the way past, so the report that came back agreed with it. | |
| 245 | + told: Option<bool>, | |
| 232 | 246 | offset_y: f32, |
| 233 | 247 | /// How tall the viewport was when the reader last moved it, which is what |
| 234 | 248 | /// a jump centres a row in. Zero until they have: a list nobody has |
| @@ -329,6 +343,71 @@ fn scroll_asks(before: &Tree, now: &Tree) -> Vec<ScrollAsk> { | ||
| 329 | 343 | asks |
| 330 | 344 | } |
| 331 | 345 | |
| 346 | +/// What a commit asks of one scroll area, decided. | |
| 347 | +#[derive(Debug, PartialEq)] | |
| 348 | +enum ScrollMove { | |
| 349 | + /// Show this row of it. | |
| 350 | + Reveal(i32), | |
| 351 | + /// Take it to its newest line. | |
| 352 | + End, | |
| 353 | + /// Put it back where the reader left it, in points. | |
| 354 | + Restore(f32), | |
| 355 | + /// Leave it alone. | |
| 356 | + Stay, | |
| 357 | +} | |
| 358 | + | |
| 359 | +/// What to do with one scroll area, and the memo brought up to date. | |
| 360 | +/// | |
| 361 | +/// Split out from `take_tree` because it is the whole of the thinking and none | |
| 362 | +/// of the toolkit: everything here is the ask beside what is remembered, so it | |
| 363 | +/// can be read — and tested — without a window to put it in. | |
| 364 | +fn scroll_move(ask: &ScrollAsk, memo: &mut ScrollMemo) -> ScrollMove { | |
| 365 | + // A list this tree did not have a moment ago is a list the client has | |
| 366 | + // heard nothing about, whatever it heard about the last one under this | |
| 367 | + // name. Forgetting what it was told is what makes the next report happen, | |
| 368 | + // so that what it believes is about the list it is looking at — in frq, | |
| 369 | + // the room it is in rather than the room it came from. | |
| 370 | + if ask.fresh { | |
| 371 | + memo.told = None; | |
| 372 | + } | |
| 373 | + let jumped = !ask.fresh && ask.tick.is_some() && ask.tick != ask.tick_before; | |
| 374 | + // A row is asking to be shown. Whether or not it can be shown yet, | |
| 375 | + // nothing else may move this list while it is asking: the branch below | |
| 376 | + // would otherwise take a reader who was at the newest line — which is | |
| 377 | + // most readers, most of the time — straight back to it, and a jump that | |
| 378 | + // ends at the bottom of the room reads as a jump that did nothing. | |
| 379 | + // | |
| 380 | + // Nothing below marks the memo, either. Where a list lands is `report`'s | |
| 381 | + // to hear from the toolkit and pass on; a memo that wrote the answer down | |
| 382 | + // here would agree with the report when it came and keep it from the | |
| 383 | + // client — the list moved, nobody was told, and the client went on | |
| 384 | + // believing whatever it believed before. Which is a "jump to present" | |
| 385 | + // button over a backlog that is already at its newest line. | |
| 386 | + if let Some(row) = ask.reveal { | |
| 387 | + ScrollMove::Reveal(row) | |
| 388 | + } else if jumped || (ask.stick && memo.at_end) { | |
| 389 | + ScrollMove::End | |
| 390 | + } else if ask.fresh { | |
| 391 | + ScrollMove::Restore(memo.offset_y) | |
| 392 | + } else { | |
| 393 | + ScrollMove::Stay | |
| 394 | + } | |
| 395 | +} | |
| 396 | + | |
| 397 | +/// What to tell the client now that this list is at `at_end`, if anything. | |
| 398 | +/// | |
| 399 | +/// Against what it was last told rather than against where the list was a | |
| 400 | +/// moment ago. The two are the same answer for a list the reader is moving by | |
| 401 | +/// hand, and they part company wherever something else moved it — see `told`. | |
| 402 | +fn report(memo: &mut ScrollMemo, at_end: bool) -> Option<&'static str> { | |
| 403 | + (memo.told != Some(at_end)).then(|| { | |
| 404 | + memo.told = Some(at_end); | |
| 405 | + // "end" or "away", the strings libvidya emits: frq's handler compares | |
| 406 | + // against "end". | |
| 407 | + if at_end { "end" } else { "away" } | |
| 408 | + }) | |
| 409 | +} | |
| 410 | + | |
| 332 | 411 | /// Where the rows of the scroll area called `name` were last laid out. |
| 333 | 412 | /// |
| 334 | 413 | /// By name and not by node, because a scroll area outlives the node ids of a |
| @@ -543,37 +622,21 @@ impl App { | ||
| 543 | 622 | .entry(ask.name.clone()) |
| 544 | 623 | .or_insert(ScrollMemo { |
| 545 | 624 | at_end: ask.stick, |
| 625 | + told: None, | |
| 546 | 626 | offset_y: 0.0, |
| 547 | 627 | height: 0.0, |
| 548 | 628 | }); |
| 549 | - let jumped = !ask.fresh && ask.tick.is_some() && ask.tick != ask.tick_before; | |
| 550 | - // A row asking to be shown, and a place written down for it by the | |
| 551 | - // last layout. Both, or there is nothing to do yet: the row is | |
| 552 | - // measured on the frame it appears, and the ask stands until it | |
| 553 | - // has been. | |
| 554 | - // A row is asking to be shown. Whether or not it can be shown yet, | |
| 555 | - // nothing else may move this list while it is asking: the branch | |
| 556 | - // below would otherwise take a reader who was at the newest line — | |
| 557 | - // which is most readers, most of the time — straight back to it, | |
| 558 | - // and a jump that ends at the bottom of the room reads as a jump | |
| 559 | - // that did nothing. | |
| 560 | - if let Some(row) = ask.reveal { | |
| 561 | - // The memo is not told where this lands. `scrolled` is what | |
| 562 | - // knows, and it tells the client only when the answer CHANGES | |
| 563 | - // — so a memo that marked itself away from the end here stole | |
| 564 | - // that change from it: the list moved, the client was never | |
| 565 | - // told, and it went on believing the reader was at the newest | |
| 566 | - // line. Which is a "jump to present" button that does not | |
| 567 | - // appear until the reader scrolls to the present by hand. | |
| 568 | - tasks.push(reveal(ask.name.clone(), row, memo.height)); | |
| 569 | - } else if jumped || (ask.stick && memo.at_end) { | |
| 570 | - memo.at_end = true; | |
| 571 | - tasks.push(snap_to_end(&ask.name)); | |
| 572 | - } else if ask.fresh { | |
| 573 | - tasks.push(iced_scrollable::scroll_to( | |
| 629 | + // A row asking to be shown is measured on the frame it appears, | |
| 630 | + // so the ask stands until the layout has a place for it — see | |
| 631 | + // `reveal`, which asks again rather than giving up. | |
| 632 | + match scroll_move(&ask, memo) { | |
| 633 | + ScrollMove::Reveal(row) => tasks.push(reveal(ask.name.clone(), row, memo.height)), | |
| 634 | + ScrollMove::End => tasks.push(snap_to_end(&ask.name)), | |
| 635 | + ScrollMove::Restore(y) => tasks.push(iced_scrollable::scroll_to( | |
| 574 | 636 | scroll_id(&ask.name), |
| 575 | - AbsoluteOffset { x: None, y: Some(memo.offset_y) }, | |
| 576 | - )); | |
| 637 | + AbsoluteOffset { x: None, y: Some(y) }, | |
| 638 | + )), | |
| 639 | + ScrollMove::Stay => {} | |
| 577 | 640 | } |
| 578 | 641 | } |
| 579 | 642 | // A list that was never scrolled keeps no memo worth the space; one |
| @@ -589,17 +652,14 @@ impl App { | ||
| 589 | 652 | let at_end = room - y <= AT_END_SLACK; |
| 590 | 653 | let memo = self.scrolls.entry(name).or_insert(ScrollMemo { |
| 591 | 654 | at_end, |
| 655 | + told: None, | |
| 592 | 656 | offset_y: y, |
| 593 | 657 | height: viewport.bounds().height, |
| 594 | 658 | }); |
| 595 | - let was = memo.at_end; | |
| 596 | 659 | memo.at_end = at_end; |
| 597 | 660 | memo.offset_y = y; |
| 598 | 661 | memo.height = viewport.bounds().height; |
| 599 | - // "end" or "away", the strings libvidya emits: frq's handler compares | |
| 600 | - // against "end". | |
| 601 | - if was != at_end { | |
| 602 | - let place = if at_end { "end" } else { "away" }; | |
| 662 | + if let Some(place) = report(memo, at_end) { | |
| 603 | 663 | post(node, "change", place.to_owned(), 0.0); |
| 604 | 664 | } |
| 605 | 665 | } |
| @@ -1796,6 +1856,88 @@ mod tests { | ||
| 1796 | 1856 | assert_eq!(scroll_asks(&t, &t)[0].reveal, None); |
| 1797 | 1857 | } |
| 1798 | 1858 | |
| 1859 | + /// An ask for the scroll area called `name`, with nothing going on. | |
| 1860 | + fn ask(name: &str) -> ScrollAsk { | |
| 1861 | + ScrollAsk { | |
| 1862 | + name: name.to_owned(), | |
| 1863 | + stick: true, | |
| 1864 | + tick: Some(0.0), | |
| 1865 | + tick_before: Some(0.0), | |
| 1866 | + fresh: false, | |
| 1867 | + reveal: None, | |
| 1868 | + } | |
| 1869 | + } | |
| 1870 | + | |
| 1871 | + fn memo() -> ScrollMemo { | |
| 1872 | + ScrollMemo { at_end: true, told: Some(true), offset_y: 0.0, height: 600.0 } | |
| 1873 | + } | |
| 1874 | + | |
| 1875 | + #[test] | |
| 1876 | + fn a_jump_asks_for_the_end_without_saying_it_arrived() { | |
| 1877 | + let mut m = memo(); | |
| 1878 | + m.at_end = false; | |
| 1879 | + m.told = Some(false); | |
| 1880 | + let mut a = ask("backlog"); | |
| 1881 | + a.tick = Some(1.0); | |
| 1882 | + assert_eq!(scroll_move(&a, &mut m), ScrollMove::End); | |
| 1883 | + // The memo still says what the last report said, so the report that | |
| 1884 | + // comes back from the toolkit is a change, and is passed on. A memo | |
| 1885 | + // that marked itself here would swallow it, and the client would go | |
| 1886 | + // on believing the reader was away from the newest line. | |
| 1887 | + assert!(!m.at_end); | |
| 1888 | + assert_eq!(report(&mut m, true), Some("end")); | |
| 1889 | + assert_eq!(report(&mut m, true), None); | |
| 1890 | + } | |
| 1891 | + | |
| 1892 | + #[test] | |
| 1893 | + fn a_list_that_has_just_mounted_says_where_it_is() { | |
| 1894 | + // The reader left this list away from the end, and the client was | |
| 1895 | + // told so. It comes back — another room under the same widget, or the | |
| 1896 | + // same room after the lightbox took the screen — and lands at the end | |
| 1897 | + // because it sticks there. Nothing about `at_end` CHANGED across | |
| 1898 | + // that, and the client still has to hear it: what it believes is | |
| 1899 | + // about the list this one replaced. | |
| 1900 | + let mut m = memo(); | |
| 1901 | + m.at_end = true; | |
| 1902 | + m.told = Some(false); | |
| 1903 | + let mut a = ask("backlog"); | |
| 1904 | + a.fresh = true; | |
| 1905 | + assert_eq!(scroll_move(&a, &mut m), ScrollMove::End); | |
| 1906 | + assert_eq!(report(&mut m, true), Some("end")); | |
| 1907 | + } | |
| 1908 | + | |
| 1909 | + #[test] | |
| 1910 | + fn a_list_nobody_moved_is_not_reported_twice() { | |
| 1911 | + let mut m = memo(); | |
| 1912 | + assert_eq!(report(&mut m, true), None); | |
| 1913 | + assert_eq!(report(&mut m, false), Some("away")); | |
| 1914 | + assert_eq!(report(&mut m, false), None); | |
| 1915 | + assert_eq!(report(&mut m, true), Some("end")); | |
| 1916 | + } | |
| 1917 | + | |
| 1918 | + #[test] | |
| 1919 | + fn a_row_asking_to_be_shown_beats_the_end() { | |
| 1920 | + let mut m = memo(); | |
| 1921 | + let mut a = ask("backlog"); | |
| 1922 | + a.reveal = Some(42); | |
| 1923 | + a.tick = Some(1.0); | |
| 1924 | + assert_eq!(scroll_move(&a, &mut m), ScrollMove::Reveal(42)); | |
| 1925 | + } | |
| 1926 | + | |
| 1927 | + #[test] | |
| 1928 | + fn a_list_coming_back_is_put_where_it_was_left() { | |
| 1929 | + let mut m = memo(); | |
| 1930 | + m.at_end = false; | |
| 1931 | + m.offset_y = 512.0; | |
| 1932 | + let mut a = ask("backlog"); | |
| 1933 | + a.fresh = true; | |
| 1934 | + assert_eq!(scroll_move(&a, &mut m), ScrollMove::Restore(512.0)); | |
| 1935 | + // And it is asked about again, since the client's belief is about | |
| 1936 | + // whatever was under this name before. | |
| 1937 | + assert_eq!(m.told, None); | |
| 1938 | + assert_eq!(report(&mut m, false), Some("away")); | |
| 1939 | + } | |
| 1940 | + | |
| 1799 | 1941 | #[test] |
| 1800 | 1942 | fn a_jump_is_the_counter_moving() { |
| 1801 | 1943 | let mut t = Tree::default(); |
| @@ -229,6 +229,20 @@ struct ScrollMemo { | |||
| 229 | /// Whether the reader is at the newest line. A `stick-to-bottom` list | 229 | /// Whether the reader is at the newest line. A `stick-to-bottom` list |
| 230 | /// follows what arrives only while this holds. | 230 | /// follows what arrives only while this holds. |
| 231 | at_end: bool, | 231 | at_end: bool, |
| 232 | + /// What the client was last told about that, and `None` while it has been | ||
| 233 | + /// told nothing. | ||
| 234 | + /// | ||
| 235 | + /// Held apart from `at_end` because the two answer different questions. | ||
| 236 | + /// `at_end` is where this list is; `told` is what the client believes, | ||
| 237 | + /// and a report is worth making exactly when they differ. Reporting on a | ||
| 238 | + /// change in `at_end` alone loses two cases, and both of them end with a | ||
| 239 | + /// "jump to present" button over a backlog that is already at its newest | ||
| 240 | + /// line. A list mounting says nothing, because the place it opens at is | ||
| 241 | + /// the place the memo guessed it would — but the client's belief is about | ||
| 242 | + /// the list this one REPLACED, which in frq is another room entirely. And | ||
| 243 | + /// a jump says nothing, because the branch that asked for it marked the | ||
| 244 | + /// memo on the way past, so the report that came back agreed with it. | ||
| 245 | + told: Option<bool>, | ||
| 232 | offset_y: f32, | 246 | offset_y: f32, |
| 233 | /// How tall the viewport was when the reader last moved it, which is what | 247 | /// How tall the viewport was when the reader last moved it, which is what |
| 234 | /// a jump centres a row in. Zero until they have: a list nobody has | 248 | /// a jump centres a row in. Zero until they have: a list nobody has |
| @@ -329,6 +343,71 @@ fn scroll_asks(before: &Tree, now: &Tree) -> Vec<ScrollAsk> { | |||
| 329 | asks | 343 | asks |
| 330 | } | 344 | } |
| 331 | 345 | ||
| 346 | +/// What a commit asks of one scroll area, decided. | ||
| 347 | +#[derive(Debug, PartialEq)] | ||
| 348 | +enum ScrollMove { | ||
| 349 | + /// Show this row of it. | ||
| 350 | + Reveal(i32), | ||
| 351 | + /// Take it to its newest line. | ||
| 352 | + End, | ||
| 353 | + /// Put it back where the reader left it, in points. | ||
| 354 | + Restore(f32), | ||
| 355 | + /// Leave it alone. | ||
| 356 | + Stay, | ||
| 357 | +} | ||
| 358 | + | ||
| 359 | +/// What to do with one scroll area, and the memo brought up to date. | ||
| 360 | +/// | ||
| 361 | +/// Split out from `take_tree` because it is the whole of the thinking and none | ||
| 362 | +/// of the toolkit: everything here is the ask beside what is remembered, so it | ||
| 363 | +/// can be read — and tested — without a window to put it in. | ||
| 364 | +fn scroll_move(ask: &ScrollAsk, memo: &mut ScrollMemo) -> ScrollMove { | ||
| 365 | + // A list this tree did not have a moment ago is a list the client has | ||
| 366 | + // heard nothing about, whatever it heard about the last one under this | ||
| 367 | + // name. Forgetting what it was told is what makes the next report happen, | ||
| 368 | + // so that what it believes is about the list it is looking at — in frq, | ||
| 369 | + // the room it is in rather than the room it came from. | ||
| 370 | + if ask.fresh { | ||
| 371 | + memo.told = None; | ||
| 372 | + } | ||
| 373 | + let jumped = !ask.fresh && ask.tick.is_some() && ask.tick != ask.tick_before; | ||
| 374 | + // A row is asking to be shown. Whether or not it can be shown yet, | ||
| 375 | + // nothing else may move this list while it is asking: the branch below | ||
| 376 | + // would otherwise take a reader who was at the newest line — which is | ||
| 377 | + // most readers, most of the time — straight back to it, and a jump that | ||
| 378 | + // ends at the bottom of the room reads as a jump that did nothing. | ||
| 379 | + // | ||
| 380 | + // Nothing below marks the memo, either. Where a list lands is `report`'s | ||
| 381 | + // to hear from the toolkit and pass on; a memo that wrote the answer down | ||
| 382 | + // here would agree with the report when it came and keep it from the | ||
| 383 | + // client — the list moved, nobody was told, and the client went on | ||
| 384 | + // believing whatever it believed before. Which is a "jump to present" | ||
| 385 | + // button over a backlog that is already at its newest line. | ||
| 386 | + if let Some(row) = ask.reveal { | ||
| 387 | + ScrollMove::Reveal(row) | ||
| 388 | + } else if jumped || (ask.stick && memo.at_end) { | ||
| 389 | + ScrollMove::End | ||
| 390 | + } else if ask.fresh { | ||
| 391 | + ScrollMove::Restore(memo.offset_y) | ||
| 392 | + } else { | ||
| 393 | + ScrollMove::Stay | ||
| 394 | + } | ||
| 395 | +} | ||
| 396 | + | ||
| 397 | +/// What to tell the client now that this list is at `at_end`, if anything. | ||
| 398 | +/// | ||
| 399 | +/// Against what it was last told rather than against where the list was a | ||
| 400 | +/// moment ago. The two are the same answer for a list the reader is moving by | ||
| 401 | +/// hand, and they part company wherever something else moved it — see `told`. | ||
| 402 | +fn report(memo: &mut ScrollMemo, at_end: bool) -> Option<&'static str> { | ||
| 403 | + (memo.told != Some(at_end)).then(|| { | ||
| 404 | + memo.told = Some(at_end); | ||
| 405 | + // "end" or "away", the strings libvidya emits: frq's handler compares | ||
| 406 | + // against "end". | ||
| 407 | + if at_end { "end" } else { "away" } | ||
| 408 | + }) | ||
| 409 | +} | ||
| 410 | + | ||
| 332 | /// Where the rows of the scroll area called `name` were last laid out. | 411 | /// Where the rows of the scroll area called `name` were last laid out. |
| 333 | /// | 412 | /// |
| 334 | /// By name and not by node, because a scroll area outlives the node ids of a | 413 | /// By name and not by node, because a scroll area outlives the node ids of a |
| @@ -543,37 +622,21 @@ impl App { | |||
| 543 | .entry(ask.name.clone()) | 622 | .entry(ask.name.clone()) |
| 544 | .or_insert(ScrollMemo { | 623 | .or_insert(ScrollMemo { |
| 545 | at_end: ask.stick, | 624 | at_end: ask.stick, |
| 625 | + told: None, | ||
| 546 | offset_y: 0.0, | 626 | offset_y: 0.0, |
| 547 | height: 0.0, | 627 | height: 0.0, |
| 548 | }); | 628 | }); |
| 549 | - let jumped = !ask.fresh && ask.tick.is_some() && ask.tick != ask.tick_before; | 629 | + // A row asking to be shown is measured on the frame it appears, |
| 550 | - // A row asking to be shown, and a place written down for it by the | 630 | + // so the ask stands until the layout has a place for it — see |
| 551 | - // last layout. Both, or there is nothing to do yet: the row is | 631 | + // `reveal`, which asks again rather than giving up. |
| 552 | - // measured on the frame it appears, and the ask stands until it | 632 | + match scroll_move(&ask, memo) { |
| 553 | - // has been. | 633 | + ScrollMove::Reveal(row) => tasks.push(reveal(ask.name.clone(), row, memo.height)), |
| 554 | - // A row is asking to be shown. Whether or not it can be shown yet, | 634 | + ScrollMove::End => tasks.push(snap_to_end(&ask.name)), |
| 555 | - // nothing else may move this list while it is asking: the branch | 635 | + ScrollMove::Restore(y) => tasks.push(iced_scrollable::scroll_to( |
| 556 | - // below would otherwise take a reader who was at the newest line — | ||
| 557 | - // which is most readers, most of the time — straight back to it, | ||
| 558 | - // and a jump that ends at the bottom of the room reads as a jump | ||
| 559 | - // that did nothing. | ||
| 560 | - if let Some(row) = ask.reveal { | ||
| 561 | - // The memo is not told where this lands. `scrolled` is what | ||
| 562 | - // knows, and it tells the client only when the answer CHANGES | ||
| 563 | - // — so a memo that marked itself away from the end here stole | ||
| 564 | - // that change from it: the list moved, the client was never | ||
| 565 | - // told, and it went on believing the reader was at the newest | ||
| 566 | - // line. Which is a "jump to present" button that does not | ||
| 567 | - // appear until the reader scrolls to the present by hand. | ||
| 568 | - tasks.push(reveal(ask.name.clone(), row, memo.height)); | ||
| 569 | - } else if jumped || (ask.stick && memo.at_end) { | ||
| 570 | - memo.at_end = true; | ||
| 571 | - tasks.push(snap_to_end(&ask.name)); | ||
| 572 | - } else if ask.fresh { | ||
| 573 | - tasks.push(iced_scrollable::scroll_to( | ||
| 574 | scroll_id(&ask.name), | 636 | scroll_id(&ask.name), |
| 575 | - AbsoluteOffset { x: None, y: Some(memo.offset_y) }, | 637 | + AbsoluteOffset { x: None, y: Some(y) }, |
| 576 | - )); | 638 | + )), |
| 639 | + ScrollMove::Stay => {} | ||
| 577 | } | 640 | } |
| 578 | } | 641 | } |
| 579 | // A list that was never scrolled keeps no memo worth the space; one | 642 | // A list that was never scrolled keeps no memo worth the space; one |
| @@ -589,17 +652,14 @@ impl App { | |||
| 589 | let at_end = room - y <= AT_END_SLACK; | 652 | let at_end = room - y <= AT_END_SLACK; |
| 590 | let memo = self.scrolls.entry(name).or_insert(ScrollMemo { | 653 | let memo = self.scrolls.entry(name).or_insert(ScrollMemo { |
| 591 | at_end, | 654 | at_end, |
| 655 | + told: None, | ||
| 592 | offset_y: y, | 656 | offset_y: y, |
| 593 | height: viewport.bounds().height, | 657 | height: viewport.bounds().height, |
| 594 | }); | 658 | }); |
| 595 | - let was = memo.at_end; | ||
| 596 | memo.at_end = at_end; | 659 | memo.at_end = at_end; |
| 597 | memo.offset_y = y; | 660 | memo.offset_y = y; |
| 598 | memo.height = viewport.bounds().height; | 661 | memo.height = viewport.bounds().height; |
| 599 | - // "end" or "away", the strings libvidya emits: frq's handler compares | 662 | + if let Some(place) = report(memo, at_end) { |
| 600 | - // against "end". | ||
| 601 | - if was != at_end { | ||
| 602 | - let place = if at_end { "end" } else { "away" }; | ||
| 603 | post(node, "change", place.to_owned(), 0.0); | 663 | post(node, "change", place.to_owned(), 0.0); |
| 604 | } | 664 | } |
| 605 | } | 665 | } |
| @@ -1796,6 +1856,88 @@ mod tests { | |||
| 1796 | assert_eq!(scroll_asks(&t, &t)[0].reveal, None); | 1856 | assert_eq!(scroll_asks(&t, &t)[0].reveal, None); |
| 1797 | } | 1857 | } |
| 1798 | 1858 | ||
| 1859 | + /// An ask for the scroll area called `name`, with nothing going on. | ||
| 1860 | + fn ask(name: &str) -> ScrollAsk { | ||
| 1861 | + ScrollAsk { | ||
| 1862 | + name: name.to_owned(), | ||
| 1863 | + stick: true, | ||
| 1864 | + tick: Some(0.0), | ||
| 1865 | + tick_before: Some(0.0), | ||
| 1866 | + fresh: false, | ||
| 1867 | + reveal: None, | ||
| 1868 | + } | ||
| 1869 | + } | ||
| 1870 | + | ||
| 1871 | + fn memo() -> ScrollMemo { | ||
| 1872 | + ScrollMemo { at_end: true, told: Some(true), offset_y: 0.0, height: 600.0 } | ||
| 1873 | + } | ||
| 1874 | + | ||
| 1875 | + #[test] | ||
| 1876 | + fn a_jump_asks_for_the_end_without_saying_it_arrived() { | ||
| 1877 | + let mut m = memo(); | ||
| 1878 | + m.at_end = false; | ||
| 1879 | + m.told = Some(false); | ||
| 1880 | + let mut a = ask("backlog"); | ||
| 1881 | + a.tick = Some(1.0); | ||
| 1882 | + assert_eq!(scroll_move(&a, &mut m), ScrollMove::End); | ||
| 1883 | + // The memo still says what the last report said, so the report that | ||
| 1884 | + // comes back from the toolkit is a change, and is passed on. A memo | ||
| 1885 | + // that marked itself here would swallow it, and the client would go | ||
| 1886 | + // on believing the reader was away from the newest line. | ||
| 1887 | + assert!(!m.at_end); | ||
| 1888 | + assert_eq!(report(&mut m, true), Some("end")); | ||
| 1889 | + assert_eq!(report(&mut m, true), None); | ||
| 1890 | + } | ||
| 1891 | + | ||
| 1892 | + #[test] | ||
| 1893 | + fn a_list_that_has_just_mounted_says_where_it_is() { | ||
| 1894 | + // The reader left this list away from the end, and the client was | ||
| 1895 | + // told so. It comes back — another room under the same widget, or the | ||
| 1896 | + // same room after the lightbox took the screen — and lands at the end | ||
| 1897 | + // because it sticks there. Nothing about `at_end` CHANGED across | ||
| 1898 | + // that, and the client still has to hear it: what it believes is | ||
| 1899 | + // about the list this one replaced. | ||
| 1900 | + let mut m = memo(); | ||
| 1901 | + m.at_end = true; | ||
| 1902 | + m.told = Some(false); | ||
| 1903 | + let mut a = ask("backlog"); | ||
| 1904 | + a.fresh = true; | ||
| 1905 | + assert_eq!(scroll_move(&a, &mut m), ScrollMove::End); | ||
| 1906 | + assert_eq!(report(&mut m, true), Some("end")); | ||
| 1907 | + } | ||
| 1908 | + | ||
| 1909 | + #[test] | ||
| 1910 | + fn a_list_nobody_moved_is_not_reported_twice() { | ||
| 1911 | + let mut m = memo(); | ||
| 1912 | + assert_eq!(report(&mut m, true), None); | ||
| 1913 | + assert_eq!(report(&mut m, false), Some("away")); | ||
| 1914 | + assert_eq!(report(&mut m, false), None); | ||
| 1915 | + assert_eq!(report(&mut m, true), Some("end")); | ||
| 1916 | + } | ||
| 1917 | + | ||
| 1918 | + #[test] | ||
| 1919 | + fn a_row_asking_to_be_shown_beats_the_end() { | ||
| 1920 | + let mut m = memo(); | ||
| 1921 | + let mut a = ask("backlog"); | ||
| 1922 | + a.reveal = Some(42); | ||
| 1923 | + a.tick = Some(1.0); | ||
| 1924 | + assert_eq!(scroll_move(&a, &mut m), ScrollMove::Reveal(42)); | ||
| 1925 | + } | ||
| 1926 | + | ||
| 1927 | + #[test] | ||
| 1928 | + fn a_list_coming_back_is_put_where_it_was_left() { | ||
| 1929 | + let mut m = memo(); | ||
| 1930 | + m.at_end = false; | ||
| 1931 | + m.offset_y = 512.0; | ||
| 1932 | + let mut a = ask("backlog"); | ||
| 1933 | + a.fresh = true; | ||
| 1934 | + assert_eq!(scroll_move(&a, &mut m), ScrollMove::Restore(512.0)); | ||
| 1935 | + // And it is asked about again, since the client's belief is about | ||
| 1936 | + // whatever was under this name before. | ||
| 1937 | + assert_eq!(m.told, None); | ||
| 1938 | + assert_eq!(report(&mut m, false), Some("away")); | ||
| 1939 | + } | ||
| 1940 | + | ||
| 1799 | #[test] | 1941 | #[test] |
| 1800 | fn a_jump_is_the_counter_moving() { | 1942 | fn a_jump_is_the_counter_moving() { |
| 1801 | let mut t = Tree::default(); | 1943 | let mut t = Tree::default(); |