nandi/jolt-nativepublic Fork 0
8e8cd51
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Keep asking for the place a list opens at

A conversation opened at its newest line most of the time, and at the top of
its backlog the rest of the time, and nothing about the room decided which.

iced runs a widget operation against the tree the last `view` built. The
commit a scroll area ARRIVES on is the one that builds it, so the ask that
goes out for where it should open — its end, or the offset the reader left it
at — can name a scrollable that does not exist yet, and is dropped where it
falls. Whether it lands is a question of which of the two happens first.

This is `reveal`'s problem at the other end: a row is measured by the layout
that draws it, so the frame a jump is asked on is a frame too early. `reveal`
answers it by asking again, and so does this. A list that has just mounted
re-asserts where it opened for a few frames, and stops the moment it reports
that it arrived — a mount settles in one or two, and what is left of the
budget after that would only be spent fighting a reader who opened a room and
scrolled straight away. Both asks name an absolute place, so one that already
landed lands on the same place again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-12T11:02:44-07:00 Browse files
8e8cd51 parent: 4f920af
modified crates/jolt-cosmic/src/lib.rs +101 -1
@@ -454,6 +454,37 @@ fn scroll_log() -> bool {
454454 /// deciding nothing happened.
455455 const REVEAL_TRIES: u8 = 20;
456456
457+/// How many frames a list that has just mounted keeps asking for the place it
458+/// opens at.
459+///
460+/// `REVEAL_TRIES`' reasoning at the other end of the same problem. iced runs a
461+/// widget operation against the tree the last `view` built, so the ask that
462+/// goes out on the commit a scroll area ARRIVES on can find no such widget to
463+/// act on — the scrollable it names is one this frame is only now building.
464+/// Whether it lands is then a question of which happens first, which is not a
465+/// question a room should open on: most of the time the conversation opened at
466+/// its newest line and sometimes it opened at the top of the backlog.
467+///
468+/// Four frames rather than `reveal`'s twenty, and it stops the moment the list
469+/// reports that it arrived. A mount settles in one or two; what is left of the
470+/// budget after that is time spent fighting a reader who opened a room and
471+/// immediately scrolled, and there is no reason to spend more of it than the
472+/// mount actually needs.
473+const SETTLE_TRIES: u8 = 4;
474+
475+/// Whether the list called `name` is already where a mount sent it — its end
476+/// for `None`, that offset for `Some`.
477+///
478+/// Read off the memo, which is to say off what the toolkit last reported, so
479+/// a list nobody has heard from yet has not arrived and is asked again.
480+fn settled(memo: Option<&ScrollMemo>, want: Option<f32>) -> bool {
481+ match (memo, want) {
482+ (None, _) => false,
483+ (Some(m), None) => m.at_end,
484+ (Some(m), Some(y)) => (m.offset_y - y).abs() <= AT_END_SLACK,
485+ }
486+}
487+
457488 /// The row of the scroll area called `name` that is asking to be shown, as the
458489 /// tree has it now.
459490 ///
@@ -564,6 +595,10 @@ enum Message {
564595 /// Show whichever row of this scroll area is asking to be shown, and how
565596 /// many more frames to keep trying for. See `reveal`.
566597 Reveal(String, u8),
598+ /// Put this scroll area where it opened at — its end for `None`, that
599+ /// offset for `Some` — again, and how many more frames to keep at it.
600+ /// See `SETTLE_TRIES`.
601+ Settle(String, Option<f32>, u8),
567602 PickImage,
568603 Picked(Option<PathBuf>),
569604 }
@@ -629,7 +664,8 @@ impl App {
629664 // A row asking to be shown is measured on the frame it appears,
630665 // so the ask stands until the layout has a place for it — see
631666 // `reveal`, which asks again rather than giving up.
632- match scroll_move(&ask, memo) {
667+ let moved = scroll_move(&ask, memo);
668+ match moved {
633669 ScrollMove::Reveal(row) => tasks.push(reveal(ask.name.clone(), row, memo.height)),
634670 ScrollMove::End => tasks.push(snap_to_end(&ask.name)),
635671 ScrollMove::Restore(y) => tasks.push(iced_scrollable::scroll_to(
@@ -638,6 +674,22 @@ impl App {
638674 )),
639675 ScrollMove::Stay => {}
640676 }
677+ // And keeps asking, while this is the commit the list arrived on:
678+ // there may be no widget to have heard the ask above. `reveal`
679+ // does its own asking again; the other two are asked for here.
680+ if ask.fresh {
681+ let want = match moved {
682+ ScrollMove::End => Some(None),
683+ ScrollMove::Restore(y) => Some(Some(y)),
684+ _ => None,
685+ };
686+ if let Some(want) = want {
687+ let name = ask.name.clone();
688+ tasks.push(Task::future(async move {
689+ cosmic::Action::App(Message::Settle(name, want, SETTLE_TRIES))
690+ }));
691+ }
692+ }
641693 }
642694 // A list that was never scrolled keeps no memo worth the space; one
643695 // that was keeps its place for when it comes back.
@@ -741,6 +793,28 @@ impl cosmic::Application for App {
741793 // again, and keep looking for a few frames: a room the reader has
742794 // only just been taken to has to be built before its lines have
743795 // anywhere to be.
796+ // Asked again until the list says it arrived, or the budget is
797+ // out. Idempotent either way: both asks name an absolute place,
798+ // so one that already landed lands on the same place again.
799+ Message::Settle(name, want, tries) => {
800+ if settled(self.scrolls.get(&name), want) || tries == 0 {
801+ return Task::none();
802+ }
803+ let again = {
804+ let name = name.clone();
805+ Task::future(async move {
806+ cosmic::Action::App(Message::Settle(name, want, tries - 1))
807+ })
808+ };
809+ let now = match want {
810+ None => snap_to_end(&name),
811+ Some(y) => iced_scrollable::scroll_to(
812+ scroll_id(&name),
813+ AbsoluteOffset { x: None, y: Some(y) },
814+ ),
815+ };
816+ return Task::batch([now, again]);
817+ }
744818 Message::Reveal(name, tries) => {
745819 let viewport = self.scrolls.get(&name).map_or(0.0, |memo| memo.height);
746820 let place = asking_row(&self.tree, &name)
@@ -1938,6 +2012,32 @@ mod tests {
19382012 assert_eq!(report(&mut m, false), Some("away"));
19392013 }
19402014
2015+ #[test]
2016+ fn a_mount_keeps_asking_until_the_list_says_it_arrived() {
2017+ // Nothing heard from yet: the ask above may have found no widget to
2018+ // act on, so it stands.
2019+ assert!(!settled(None, None));
2020+ assert!(!settled(None, Some(512.0)));
2021+
2022+ // Reported somewhere else: still asking.
2023+ let mut m = memo();
2024+ m.at_end = false;
2025+ m.offset_y = 0.0;
2026+ assert!(!settled(Some(&m), None));
2027+ assert!(!settled(Some(&m), Some(512.0)));
2028+
2029+ // Arrived, and the asking stops.
2030+ m.at_end = true;
2031+ assert!(settled(Some(&m), None));
2032+ m.offset_y = 512.0;
2033+ assert!(settled(Some(&m), Some(512.0)));
2034+ // Within the slack a fractional offset leaves.
2035+ m.offset_y = 511.0;
2036+ assert!(settled(Some(&m), Some(512.0)));
2037+ m.offset_y = 480.0;
2038+ assert!(!settled(Some(&m), Some(512.0)));
2039+ }
2040+
19412041 #[test]
19422042 fn a_jump_is_the_counter_moving() {
19432043 let mut t = Tree::default();
@@ -454,6 +454,37 @@ fn scroll_log() -> bool {
454 /// deciding nothing happened.454 /// deciding nothing happened.
455 const REVEAL_TRIES: u8 = 20;455 const REVEAL_TRIES: u8 = 20;
456 456
457+/// How many frames a list that has just mounted keeps asking for the place it
458+/// opens at.
459+///
460+/// `REVEAL_TRIES`' reasoning at the other end of the same problem. iced runs a
461+/// widget operation against the tree the last `view` built, so the ask that
462+/// goes out on the commit a scroll area ARRIVES on can find no such widget to
463+/// act on — the scrollable it names is one this frame is only now building.
464+/// Whether it lands is then a question of which happens first, which is not a
465+/// question a room should open on: most of the time the conversation opened at
466+/// its newest line and sometimes it opened at the top of the backlog.
467+///
468+/// Four frames rather than `reveal`'s twenty, and it stops the moment the list
469+/// reports that it arrived. A mount settles in one or two; what is left of the
470+/// budget after that is time spent fighting a reader who opened a room and
471+/// immediately scrolled, and there is no reason to spend more of it than the
472+/// mount actually needs.
473+const SETTLE_TRIES: u8 = 4;
474+
475+/// Whether the list called `name` is already where a mount sent it — its end
476+/// for `None`, that offset for `Some`.
477+///
478+/// Read off the memo, which is to say off what the toolkit last reported, so
479+/// a list nobody has heard from yet has not arrived and is asked again.
480+fn settled(memo: Option<&ScrollMemo>, want: Option<f32>) -> bool {
481+ match (memo, want) {
482+ (None, _) => false,
483+ (Some(m), None) => m.at_end,
484+ (Some(m), Some(y)) => (m.offset_y - y).abs() <= AT_END_SLACK,
485+ }
486+}
487+
457 /// The row of the scroll area called `name` that is asking to be shown, as the488 /// The row of the scroll area called `name` that is asking to be shown, as the
458 /// tree has it now.489 /// tree has it now.
459 ///490 ///
@@ -564,6 +595,10 @@ enum Message {
564 /// Show whichever row of this scroll area is asking to be shown, and how595 /// Show whichever row of this scroll area is asking to be shown, and how
565 /// many more frames to keep trying for. See `reveal`.596 /// many more frames to keep trying for. See `reveal`.
566 Reveal(String, u8),597 Reveal(String, u8),
598+ /// Put this scroll area where it opened at — its end for `None`, that
599+ /// offset for `Some` — again, and how many more frames to keep at it.
600+ /// See `SETTLE_TRIES`.
601+ Settle(String, Option<f32>, u8),
567 PickImage,602 PickImage,
568 Picked(Option<PathBuf>),603 Picked(Option<PathBuf>),
569 }604 }
@@ -629,7 +664,8 @@ impl App {
629 // A row asking to be shown is measured on the frame it appears,664 // 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 — see665 // so the ask stands until the layout has a place for it — see
631 // `reveal`, which asks again rather than giving up.666 // `reveal`, which asks again rather than giving up.
632- match scroll_move(&ask, memo) {667+ let moved = scroll_move(&ask, memo);
668+ match moved {
633 ScrollMove::Reveal(row) => tasks.push(reveal(ask.name.clone(), row, memo.height)),669 ScrollMove::Reveal(row) => tasks.push(reveal(ask.name.clone(), row, memo.height)),
634 ScrollMove::End => tasks.push(snap_to_end(&ask.name)),670 ScrollMove::End => tasks.push(snap_to_end(&ask.name)),
635 ScrollMove::Restore(y) => tasks.push(iced_scrollable::scroll_to(671 ScrollMove::Restore(y) => tasks.push(iced_scrollable::scroll_to(
@@ -638,6 +674,22 @@ impl App {
638 )),674 )),
639 ScrollMove::Stay => {}675 ScrollMove::Stay => {}
640 }676 }
677+ // And keeps asking, while this is the commit the list arrived on:
678+ // there may be no widget to have heard the ask above. `reveal`
679+ // does its own asking again; the other two are asked for here.
680+ if ask.fresh {
681+ let want = match moved {
682+ ScrollMove::End => Some(None),
683+ ScrollMove::Restore(y) => Some(Some(y)),
684+ _ => None,
685+ };
686+ if let Some(want) = want {
687+ let name = ask.name.clone();
688+ tasks.push(Task::future(async move {
689+ cosmic::Action::App(Message::Settle(name, want, SETTLE_TRIES))
690+ }));
691+ }
692+ }
641 }693 }
642 // A list that was never scrolled keeps no memo worth the space; one694 // A list that was never scrolled keeps no memo worth the space; one
643 // that was keeps its place for when it comes back.695 // that was keeps its place for when it comes back.
@@ -741,6 +793,28 @@ impl cosmic::Application for App {
741 // again, and keep looking for a few frames: a room the reader has793 // again, and keep looking for a few frames: a room the reader has
742 // only just been taken to has to be built before its lines have794 // only just been taken to has to be built before its lines have
743 // anywhere to be.795 // anywhere to be.
796+ // Asked again until the list says it arrived, or the budget is
797+ // out. Idempotent either way: both asks name an absolute place,
798+ // so one that already landed lands on the same place again.
799+ Message::Settle(name, want, tries) => {
800+ if settled(self.scrolls.get(&name), want) || tries == 0 {
801+ return Task::none();
802+ }
803+ let again = {
804+ let name = name.clone();
805+ Task::future(async move {
806+ cosmic::Action::App(Message::Settle(name, want, tries - 1))
807+ })
808+ };
809+ let now = match want {
810+ None => snap_to_end(&name),
811+ Some(y) => iced_scrollable::scroll_to(
812+ scroll_id(&name),
813+ AbsoluteOffset { x: None, y: Some(y) },
814+ ),
815+ };
816+ return Task::batch([now, again]);
817+ }
744 Message::Reveal(name, tries) => {818 Message::Reveal(name, tries) => {
745 let viewport = self.scrolls.get(&name).map_or(0.0, |memo| memo.height);819 let viewport = self.scrolls.get(&name).map_or(0.0, |memo| memo.height);
746 let place = asking_row(&self.tree, &name)820 let place = asking_row(&self.tree, &name)
@@ -1938,6 +2012,32 @@ mod tests {
1938 assert_eq!(report(&mut m, false), Some("away"));2012 assert_eq!(report(&mut m, false), Some("away"));
1939 }2013 }
1940 2014
2015+ #[test]
2016+ fn a_mount_keeps_asking_until_the_list_says_it_arrived() {
2017+ // Nothing heard from yet: the ask above may have found no widget to
2018+ // act on, so it stands.
2019+ assert!(!settled(None, None));
2020+ assert!(!settled(None, Some(512.0)));
2021+
2022+ // Reported somewhere else: still asking.
2023+ let mut m = memo();
2024+ m.at_end = false;
2025+ m.offset_y = 0.0;
2026+ assert!(!settled(Some(&m), None));
2027+ assert!(!settled(Some(&m), Some(512.0)));
2028+
2029+ // Arrived, and the asking stops.
2030+ m.at_end = true;
2031+ assert!(settled(Some(&m), None));
2032+ m.offset_y = 512.0;
2033+ assert!(settled(Some(&m), Some(512.0)));
2034+ // Within the slack a fractional offset leaves.
2035+ m.offset_y = 511.0;
2036+ assert!(settled(Some(&m), Some(512.0)));
2037+ m.offset_y = 480.0;
2038+ assert!(!settled(Some(&m), Some(512.0)));
2039+ }
2040+
1941 #[test]2041 #[test]
1942 fn a_jump_is_the_counter_moving() {2042 fn a_jump_is_the_counter_moving() {
1943 let mut t = Tree::default();2043 let mut t = Tree::default();