Repository: slint-ui/slint (language: rust)

Issue:
Panics and strange behavior when using > 2 million rows in custom ListView model
Slint 1.2.2

When trying to add 10 millions rows to model, after scrolling down to ~5.6 million record I got crash

Looks that after 2/3 millions of items it creates empty space between items 

Scroll bar after 3.3 million record go back to ~2.8 million record, probably due adding space after items

Selection dissapears when selected row is not in screen - this happens even with 10 rows

Project to test - [czkawka_slint_gui.zip](https://github.com/slint-ui/slint/files/13039433/czkawka_slint_gui.zip)


Video with visible problems:

https://github.com/slint-ui/slint/assets/41945903/d146dc8a-68ec-46bc-8a63-8be2169b22c9



Panic
```
thread 'main' panicked at /home/rafal/Projekty/Rust/czkawka/target/debug/build/czkawka_slint-0d08bfe5f757d603/out/main_window.rs:4492:141:
called `Option::unwrap()` on a `None` value
stack backtrace:
   0: rust_begin_unwind
             at /rustc/cc66ad468955717ab92600c770da8c1601a4ff33/library/std/src/panicking.rs:595:5
   1: core::panicking::panic_fmt
             at /rustc/cc66ad468955717ab92600c770da8c1601a4ff33/library/core/src/panicking.rs:67:14
   2: core::panicking::panic
             at /rustc/cc66ad468955717ab92600c770da8c1601a4ff33/library/core/src/panicking.rs:117:5
   3: core::option::Option<T>::unwrap
             at /rustc/cc66ad468955717ab92600c770da8c1601a4ff33/library/core/src/option.rs:935:21
   4: czkawka_slint::slint_generatedMainWindow::InnerSelectableTableView_root_27::subtree_component
             at /home/rafal/Projekty/Rust/czkawka/target/debug/build/czkawka_slint-0d08bfe5f757d603/out/main_window.rs:4492:90
   5: czkawka_slint::slint_generatedMainWindow::InnerComponent_selectabletableview_8::subtree_component
             at /home/rafal/Projekty/Rust/czkawka/target/debug/build/czkawka_slint-0d08bfe5f757d603/out/main_window.rs:9533:22
   6: <czkawka_slint::slint_generatedMainWindow::InnerComponent_selectabletableview_8 as i_slint_core::component::Component_vtable_mod::Component>::get_subtree_component
             at /home/rafal/Projekty/Rust/czkawka/target/debug/build/czkawka_slint-0d08bfe5f757d603/out/main_window.rs:9799:14
   7: <czkawka_slint::slint_generatedMainWindow::InnerComponent_selectabletableview_8 as const_field_offset::PinnedDrop>::drop::VT::get_subtree_component
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/i-slint-core-1.2.2/component.rs:40:1
   8: i_slint_core::component::Component_vtable_mod::ComponentTO::get_subtree_component
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/i-slint-core-1.2.2/component.rs:40:1
   9: i_slint_core::item_tree::ItemRc::find_sibling
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/i-slint-core-1.2.2/item_tree.rs:280:21
  10: i_slint_core::accessibility::accessible_descendents::{{closure}}
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/i-slint-core-1.2.2/accessibility.rs:57:39
  11: <core::iter::sources::from_fn::FromFn<F> as core::iter::traits::iterator::Iterator>::next
             at /rustc/cc66ad468955717ab92600c770da8c1601a4ff33/library/core/src/iter/sources/from_fn.rs:69:9
  12: <core::iter::adapters::map::Map<I,F> as core::iter::traits::iterator::Iterator>::next
             at /rustc/cc66ad468955717ab92600c770da8c1601a4ff33/library/core/src/iter/adapters/map.rs:103:9
  13: alloc::vec::Vec<T,A>::extend_desugared
             at /rustc/cc66ad468955717ab92600c770da8c1601a4ff33/library/alloc/src/vec/mod.rs:2796:35
  14: <alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<T,I>>::spec_extend
             at /rustc/cc66ad468955717ab92600c770da8c1601a4ff33/library/alloc/src/vec/spec_extend.rs:17:9
  15: <alloc::vec::Vec<T> as alloc::vec::spec_from_iter_nested::SpecFromIterNested<T,I>>::from_iter
             at /rustc/cc66ad468955717ab92600c770da8c1601a4ff33/library/alloc/src/vec/spec_from_iter_nested.rs:43:9
  16: <alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter
             at /rustc/cc66ad468955717ab92600c770da8c1601a4ff33/library/alloc/src/vec/spec_from_iter.rs:33:9
  17: <alloc::vec::Vec<T> as core::iter::traits::collect::FromIterator<T>>::from_iter
             at /rustc/cc66ad468955717ab92600c770da8c1601a4ff33/library/alloc/src/vec/mod.rs:2696:9
  18: core::iter::traits::iterator::Iterator::collect
             at /rustc/cc66ad468955717ab92600c770da8c1601a4ff33/library/core/src/iter/traits/iterator.rs:2053:9
  19: i_slint_backend_winit::accesskit::AccessKitAdapter::build_node_for_item_recursively
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/i-slint-backend-winit-1.2.2/accesskit.rs:235:14
  20: i_slint_backend_winit::accesskit::AccessKitAdapter::build_new_tree::{{closure}}
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/i-slint-backend-winit-1.2.2/accesskit.rs:272:13
  21: i_slint_core::properties::CURRENT_BINDING::<impl i_slint_core::properties::CURRENT_BINDING>::set
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/scoped-tls-hkt-0.1.4/src/lib.rs:265:25
  22: i_slint_core::properties::PropertyTracker<DirtyHandler>::evaluate_as_dependency_root
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/i-slint-core-1.2.2/properties.rs:1295:17
  23: i_slint_backend_winit::accesskit::AccessKitAdapter::build_new_tree
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/i-slint-backend-winit-1.2.2/accesskit.rs:271:23
  24: i_slint_backend_winit::accesskit::AccessKitAdapter::register_component::{{closure}}::{{closure}}
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/i-slint-backend-winit-1.2.2/accesskit.rs:133:49
  25: accesskit_winit::platform_impl::platform::Adapter::update_if_active
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/accesskit_winit-0.14.4/src/platform_impl/unix.rs:45:28
  26: accesskit_winit::Adapter::update_if_active
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/accesskit_winit-0.14.4/src/lib.rs:245:9
  27: i_slint_backend_winit::accesskit::AccessKitAdapter::register_component::{{closure}}
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/i-slint-backend-winit-1.2.2/accesskit.rs:133:29
  28: core::ops::function::FnOnce::call_once{{vtable.shim}}
             at /rustc/cc66ad468955717ab92600c770da8c1601a4ff33/library/core/src/ops/function.rs:250:5
  29: <alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once
             at /rustc/cc66ad468955717ab92600c770da8c1601a4ff33/library/alloc/src/boxed.rs:2007:9
  30: i_slint_core::timers::TimerList::maybe_activate_timers::{{closure}}
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/i-slint-core-1.2.2/timers.rs:272:29
  31: std::thread::local::LocalKey<T>::try_with
             at /rustc/cc66ad468955717ab92600c770da8c1601a4ff33/library/std/src/thread/local.rs:270:16
  32: std::thread::local::LocalKey<T>::with
             at /rustc/cc66ad468955717ab92600c770da8c1601a4ff33/library/std/src/thread/local.rs:246:9
  33: i_slint_core::timers::TimerList::maybe_activate_timers
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/i-slint-core-1.2.2/timers.rs:230:24
  34: i_slint_backend_winit::event_loop::run::{{closure}}
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/i-slint-backend-winit-1.2.2/event_loop.rs:591:17
  35: i_slint_backend_winit::event_loop::run::{{closure}}::{{closure}}
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/i-slint-backend-winit-1.2.2/event_loop.rs:658:65
  36: i_slint_backend_winit::event_loop::CURRENT_WINDOW_TARGET::<impl i_slint_backend_winit::event_loop::CURRENT_WINDOW_TARGET>::set
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/scoped-tls-hkt-0.1.4/src/lib.rs:265:25
  37: i_slint_backend_winit::event_loop::run::{{closure}}
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/i-slint-backend-winit-1.2.2/event_loop.rs:658:17
  38: winit::platform_impl::platform::sticky_exit_callback
  39: winit::platform_impl::platform::x11::EventLoop<T>::run_return::single_iteration
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/winit-0.28.7/src/platform_impl/linux/x11/mod.rs:324:13
  40: winit::platform_impl::platform::x11::EventLoop<T>::run_return
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/winit-0.28.7/src/platform_impl/linux/x11/mod.rs:483:27
  41: winit::platform_impl::platform::EventLoop<T>::run_return
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/winit-0.28.7/src/platform_impl/linux/mod.rs:785:56
  42: <winit::event_loop::EventLoop<T> as winit::platform::run_return::EventLoopExtRunReturn>::run_return
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/winit-0.28.7/src/platform/run_return.rs:51:9
  43: i_slint_backend_winit::event_loop::run
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/i-slint-backend-winit-1.2.2/event_loop.rs:649:20
  44: <i_slint_backend_winit::Backend as i_slint_core::platform::Platform>::run_event_loop
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/i-slint-backend-winit-1.2.2/lib.rs:231:9
  45: slint::run_event_loop::{{closure}}
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/slint-1.2.2/lib.rs:227:49
  46: i_slint_core::with_platform::{{closure}}
  47: std::thread::local::LocalKey<T>::try_with
             at /rustc/cc66ad468955717ab92600c770da8c1601a4ff33/library/std/src/thread/local.rs:270:16
  48: std::thread::local::LocalKey<T>::with
             at /rustc/cc66ad468955717ab92600c770da8c1601a4ff33/library/std/src/thread/local.rs:246:9
  49: i_slint_core::with_platform
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/i-slint-core-1.2.2/lib.rs:99:33
  50: i_slint_backend_selector::with_platform
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/i-slint-backend-selector-1.2.2/lib.rs:98:5
  51: slint::run_event_loop
             at /home/rafal/.cargo/registry/src/index.crates.io-6f17d22bba15001f/slint-1.2.2/lib.rs:227:5
  52: <czkawka_slint::slint_generatedMainWindow::MainWindow as i_slint_core::api::ComponentHandle>::run
             at /home/rafal/Projekty/Rust/czkawka/target/debug/build/czkawka_slint-0d08bfe5f757d603/out/main_window.rs:10163:14
  53: czkawka_slint::main
             at ./src/main.rs:20:5
  54: core::ops::function::FnOnce::call_once
             at /rustc/cc66ad468955717ab92600c770da8c1601a4ff33/library/core/src/ops/function.rs:250:5
```


Discussion / hints:
Thanks for your bug report. I can reproduce the problem.

One problem is that we use f32 for the coordinate,  and that the content-y of the underlying ListView can become really big, so big that we get rounding error on the f32 computations when computing the location of the items within the viewport.


In particular the problem is there: 
https://github.com/slint-ui/slint/blob/6cd68e84a7be8a43baa060565f0bbe2a11b9101b/internal/core/model.rs#L1081

the size of the row is 42.0px.  So `y` in that expression, which is the position in pixel within the viewport, is going to be 42 * some_index, where some_index can be millions.  So we are way over 100M pixels
 The listview_layout function will position the element at `y` , and add 42px to it.  The problem is that in f32,  `y+42` is actually a few pixel off as that number might not be representable in a f32.
hence the wrong gaps.

I don't explain the panic yet. It tries to query the vector of instance with a completely wrong index, as it iterate over widgets in the accessibility code.
The panics happen because the view is relayouted between the range check and the call to get_subtree:

https://github.com/slint-ui/slint/blob/6cd68e84a7be8a43baa060565f0bbe2a11b9101b/internal/core/item_tree.rs#L429-L439

We query the range line 429,  and check the validity of index line 432,  but the `get_subtree` notice that the tree is dirty (not sure why) and redo the layout. And the item computation turns out to be something different because the floating point computation give different result, and we just query the components outside of the range.
(next_subtree_index is outside of the new range which is recomputed by get_subtree itself)
https://github.com/slint-ui/slint/pull/3709  is fixing the panic.
#3709 fixed one panic, but there is still one panic reported in https://github.com/slint-ui/slint/pull/3709#issuecomment-1771498412:

```
thread 'main' panicked at /home/rafal/Projekty/Rust/czkawka/target/debug/build/czkawka_slint-3baf4a5d90d5188c/out/main_window.rs:6791:159:
called `Option::unwrap()` on a `None` value
stack backtrace:
   4: czkawka_slint::slint_generatedMainWindow::InnerComponent_text_54::item_geometry
   5: <czkawka_slint::slint_generatedMainWindow::InnerComponent_text_54 as i_slint_core::item_tree::ItemTree_vtable_mod::ItemTree>::item_geometry
   6: <czkawka_slint::slint_generatedMainWindow::InnerComponent_text_54 as const_field_offset::PinnedDrop>::drop::VT::item_geometry
   7: i_slint_core::item_tree::ItemTree_vtable_mod::ItemTreeTO::item_geometry
   8: i_slint_core::item_tree::ItemRc::geometry
   9: i_slint_backend_winit::accesskit::AccessKitAdapter::build_node_without_children
  10: i_slint_backend_winit::accesskit::AccessKitAdapter::build_node_for_item_recursively::{{closure}}
  11: i_slint_core::properties::CURRENT_BINDING::<impl i_slint_core::properties::CURRENT_BINDING>::set
  12: i_slint_core::properties::PropertyTracker<DirtyHandler>::evaluate_as_dependency_root
  13: i_slint_core::properties::PropertyTracker<DirtyHandler>::evaluate
  14: i_slint_backend_winit::accesskit::AccessKitAdapter::build_node_for_item_recursively
  15: i_slint_backend_winit::accesskit::AccessKitAdapter::build_node_for_item_recursively::{{closure}}
```

Which is just another symptom of https://github.com/slint-ui/slint/issues/3464 as the accessibility iterate over items, but while iterating the listview relayout code is executed that produces different result and delete the parent element.

-----

We have a combination of several problem:

 1. The main issue is that we represent coordinate in f32,  and the maximum integer that can fit in a f32 is 16777216. But the coordinate of the viewport relative to the flickable can be much larger than that when having many millions entries. And therefore their position is not accurately representable.

 2. We do relayout the list view more often than needed.  Commit 4d0568873069b7ca7134a5e3f18c68ff3a1f54e9 actually removed the `listview_geometry_tracker.evaluate_if_dirty`  which would only relayout when the listview is dirty. So we relayout all the time when traversing the elements, which is a lot.  Fortunately, the relayout is fairly fast as there are never so many elements visible at the same time. But still.

 3. If you sum it up, we get the layout redone too often wand because of point 1. it result in different amount of element which result in item being deleted which cause a panic because of #3464 


Also the fact that accessibility is rebuilding the tree even if there is no accessibility backend which is a bug in accesskit.


What can we do?

First thought was to use f64 for coordinates on the desktop. We already have a switch in https://github.com/slint-ui/slint/blob/5bf2c7192b6264947e5e80e7e85f831f4ea3f852/internal/core/lib.rs#L82 for the MCUs, we could as well allow to switch to f64.  But it turns out that even doing so wouldn't help fully because the femtovg and skia renderer use f32 internally and we do translate the canvas when we get into items.  Instead of translating the canvas like this, we could maintain offsets in f64 within slint, but that's some extra refactoring.

Another possibility would be to not have the listview items as chilren of the viewport, but instead, have them as sibling of the viewport. And so the layouting code can just give them a y position relative to the ListView instead of relative to the viewport. I think this is possible, but require some refactoring in the runtime and compiler.



Couldn't this be fixed with what I hinted at in #5758 (some modulo-like calculation), instead of letting a number grow uncontrollably? Scroll deltas should always be whole physical pixels, right? Then you should actually be able to maintain the illusion of a continuous list, while a rendered-items block is regularly shifted during scrolling (its data together with its position), without getting graphical-rendering imperfections that suddenly show on content block shift. I'm not sure whether these mid-animation shifts would require extra support from the animation system.

Of course, there's still the issue of how things are communicated to the user. For my needs in the linked discussion, the component would communicate a `duration` (a 64-bit value) that the content block should start with; meaning, the backend would provide appropriate data that the component then renders. This would be my precise "actual position in the data". For cases with an overly long list with a scrollbar, you could also establish a callback that tells the user to adapt the window into the data that the backing model then would represent (using integer indices). This means, you'd have the actual model, and an integer pair of model start in the virtual data and virtual-model length.

CC: @ezschemi

Fix patch:
diff --git a/api/cpp/include/slint.h b/api/cpp/include/slint.h
index 6d0eaae3c62..84874206e89 100644
--- a/api/cpp/include/slint.h
+++ b/api/cpp/include/slint.h
@@ -1190,13 +1190,13 @@ class Repeater
     void ensure_updated_listview(const Parent *parent,
                                  const private_api::Property<float> *viewport_width,
                                  const private_api::Property<float> *viewport_height,
-                                 [[maybe_unused]] const private_api::Property<float> *viewport_y,
+                                 const private_api::Property<float> *viewport_y,
                                  float listview_width, [[maybe_unused]] float listview_height) const
     {
         // TODO: the rust code in model.rs try to only allocate as many items as visible items
         ensure_updated(parent);
 
-        float h = compute_layout_listview(viewport_width, listview_width);
+        float h = compute_layout_listview(viewport_width, listview_width, viewport_y->get());
         viewport_height->set(h);
     }
 
@@ -1234,9 +1234,9 @@ class Repeater
     }
 
     float compute_layout_listview(const private_api::Property<float> *viewport_width,
-                                  float listview_width) const
+                                  float listview_width, float viewport_y) const
     {
-        float offset = 0;
+        float offset = viewport_y;
         viewport_width->set(listview_width);
         if (!inner)
             return offset;
diff --git a/internal/compiler/passes/flickable.rs b/internal/compiler/passes/flickable.rs
index db3e2ac9888..99aaeca94cd 100644
--- a/internal/compiler/passes/flickable.rs
+++ b/internal/compiler/passes/flickable.rs
@@ -11,9 +11,10 @@
 //! binding reference to fake properties
 
 use crate::expression_tree::{BindingExpression, Expression, MinMaxOp, NamedReference};
-use crate::langtype::{ElementType, NativeClass};
+use crate::langtype::{ElementType, NativeClass, Type};
 use crate::object_tree::{Component, Element, ElementRc};
 use crate::typeregister::TypeRegister;
+use core::cell::RefCell;
 use smol_str::{format_smolstr, SmolStr};
 use std::rc::Rc;
 
@@ -43,6 +44,46 @@ pub fn handle_flickable(root_component: &Rc<Component>, tr: &TypeRegister) {
 
 fn create_viewport_element(flickable: &ElementRc, native_empty: &Rc<NativeClass>) {
     let children = std::mem::take(&mut flickable.borrow_mut().children);
+    let is_listview = children
+        .iter()
+        .any(|c| c.borrow().repeated.as_ref().is_some_and(|r| r.is_listview.is_some()));
+
+    if is_listview {
+        // Fox Listview, we don't bind the y property to the geometry because for large listview, we want to support coordinate with more precision than f32
+        // so the actual geometry is relative to the Flickable instead of the viewport
+        // We still assign a binding to the y property in case it is read by someone
+        for c in &children {
+            if c.borrow().repeated.is_none() {
+                // Normally should not happen, listview should only have one children, and it should be repeated
+                continue;
+            }
+            let ElementType::Component(base) = c.borrow().base_type.clone() else { continue };
+            let inner_elem = &base.root_element;
+            let new_y = crate::layout::create_new_prop(
+                inner_elem,
+                SmolStr::new_static("actual-y"),
+                Type::LogicalLength,
+            );
+            new_y.mark_as_set();
+            inner_elem.borrow_mut().bindings.insert(
+                "y".into(),
+                RefCell::new(
+                    Expression::BinaryExpression {
+                        lhs: Expression::PropertyReference(new_y.clone()).into(),
+                        rhs: Expression::PropertyReference(NamedReference::new(
+                            flickable,
+                            SmolStr::new_static("viewport-y"),
+                        ))
+                        .into(),
+                        op: '-',
+                    }
+                    .into(),
+                ),
+            );
+            inner_elem.borrow_mut().geometry_props.as_mut().unwrap().y = new_y;
+        }
+    }
+
     let viewport = Element::make_rc(Element {
         id: format_smolstr!("{}-viewport", flickable.borrow().id),
         base_type: ElementType::Native(native_empty.clone()),
@@ -53,8 +94,12 @@ fn create_viewport_element(flickable: &ElementRc, native_empty: &Rc<NativeClass>
     });
     let element_type = flickable.borrow().base_type.clone();
     for prop in element_type.as_builtin().properties.keys() {
+        // bind the viewport's property to the flickable property, such as:  `width <=> parent.viewport-width`
         if let Some(vp_prop) = prop.strip_prefix("viewport-") {
-            // bind the viewport's property to the flickable property, such as:  `width <=> parent.viewport-width`
+            if is_listview && matches!(vp_prop, "y" | "height") {
+                //don't bind viewport-y for ListView because the layout is handled by the runtime
+                continue;
+            }
             viewport.borrow_mut().bindings.insert(
                 vp_prop.into(),
                 BindingExpression::new_two_way(NamedReference::new(flickable, prop.clone())).into(),
diff --git a/internal/compiler/passes/repeater_component.rs b/internal/compiler/passes/repeater_component.rs
index aa20816438c..14eba5eadf8 100644
--- a/internal/compiler/passes/repeater_component.rs
+++ b/internal/compiler/passes/repeater_component.rs
@@ -85,8 +85,6 @@ fn create_repeater_components(component: &Rc<Component>) {
                     RefCell::new(Expression::PropertyReference(listview.listview_width).into()),
                 );
             }
-
-            NamedReference::new(&comp.root_element, SmolStr::new_static("y")).mark_as_set();
         }
 
         let weak = Rc::downgrade(&comp);
diff --git a/internal/core/model.rs b/internal/core/model.rs
index 5efb240521d..e477d7388d9 100644
--- a/internal/core/model.rs
+++ b/internal/core/model.rs
@@ -974,23 +974,24 @@ impl<C: RepeatedItemTree + 'static> Repeater<C> {
         viewport_width.set(listview_width);
         let model = self.model();
         let row_count = model.row_count();
+        let zero = LogicalLength::zero();
         if row_count == 0 {
             self.0.inner.borrow_mut().instances.clear();
-            viewport_height.set(LogicalLength::zero());
-            viewport_y.set(LogicalLength::zero());
+            viewport_height.set(zero);
+            viewport_y.set(zero);
 
             return;
         }
 
         let listview_height = listview_height.get();
-        let mut vp_y = viewport_y.get().min(LogicalLength::zero());
+        let mut vp_y = viewport_y.get().min(zero);
 
         // We need some sort of estimation of the element height
         let cached_item_height = self.data().inner.borrow_mut().cached_item_height;
-        let element_height = if cached_item_height > LogicalLength::zero() {
+        let element_height = if cached_item_height > zero {
             cached_item_height
         } else {
-            let total_height = Cell::new(LogicalLength::zero());
+            let total_height = Cell::new(zero);
             let count = Cell::new(0);
             let get_height_visitor = |x: &ItemTreeRc<C>| {
                 let height = x.as_pin_ref().item_geometry(0).height_length();
@@ -1042,12 +1043,12 @@ impl<C: RepeatedItemTree + 'static> Repeater<C> {
             // We are jumping more than 1.5 screens, consider this as a random seek.
             inner.instances.clear();
             inner.offset = ((-vp_y / element_height).get().floor() as usize).min(row_count - 1);
-            (inner.offset, -vp_y)
+            (inner.offset, zero)
         } else if vp_y < inner.previous_viewport_y {
             // we scrolled down, try to find out the new offset.
-            let mut it_y = first_item_y;
+            let mut it_y = first_item_y + vp_y;
             let mut new_offset = inner.offset;
-            debug_assert!(it_y <= -vp_y); // we scrolled down, the anchor should be hidden
+            debug_assert!(it_y <= zero); // we scrolled down, the anchor should be hidden
             for (i, c) in inner.instances.iter_mut().enumerate() {
                 if c.0 == RepeatedInstanceState::Dirty {
                     if c.1.is_none() {
@@ -1060,7 +1061,7 @@ impl<C: RepeatedItemTree + 'static> Repeater<C> {
                     c.0 = RepeatedInstanceState::Clean;
                 }
                 let h = c.1.as_ref().unwrap().as_pin_ref().item_geometry(0).height_length();
-                if it_y + h >= -vp_y || new_offset + 1 >= row_count {
+                if it_y + h >= zero || new_offset + 1 >= row_count {
                     break;
                 }
                 it_y += h;
@@ -1069,14 +1070,15 @@ impl<C: RepeatedItemTree + 'static> Repeater<C> {
             (new_offset, it_y)
         } else {
             // We scrolled up, we'll instantiate items before offset in the loop
-            (inner.offset, first_item_y)
+            (inner.offset, first_item_y + vp_y)
         };
 
+        let mut loop_count = 0;
         loop {
             // If there is a gap before the new_offset and the beginning of the visible viewport,
             // try to fill it with items. First look at items that are before new_offset in the
             // inner.instances, if any.
-            while new_offset > inner.offset && new_offset_y > -vp_y {
+            while new_offset > inner.offset && new_offset_y > zero {
                 new_offset -= 1;
                 new_offset_y -= inner.instances[new_offset - inner.offset]
                     .1
@@ -1088,7 +1090,7 @@ impl<C: RepeatedItemTree + 'static> Repeater<C> {
             }
             // If there is still a gap, fill it with new instances before
             let mut new_instances = Vec::new();
-            while new_offset > 0 && new_offset_y > -vp_y {
+            while new_offset > 0 && new_offset_y > zero {
                 new_offset -= 1;
                 let new_instance = init();
                 if let Some(data) = model.row_data(new_offset) {
@@ -1137,13 +1139,13 @@ impl<C: RepeatedItemTree + 'static> Repeater<C> {
                     x.as_pin_ref().listview_layout(&mut y, viewport_width);
                 }
                 idx += 1;
-                if y >= -vp_y + listview_height {
+                if y >= listview_height {
                     break;
                 }
             }
 
             // create more items until there is no more room.
-            while y < -vp_y + listview_height && idx < row_count {
+            while y < listview_height && idx < row_count {
                 let new_instance = init();
                 if let Some(data) = model.row_data(idx) {
                     new_instance.update(idx, data);
@@ -1153,10 +1155,11 @@ impl<C: RepeatedItemTree + 'static> Repeater<C> {
                 inner.instances.push((RepeatedInstanceState::Clean, Some(new_instance)));
                 idx += 1;
             }
-            if y < -vp_y + listview_height && vp_y < LogicalLength::zero() {
+            if y < listview_height && vp_y < zero && loop_count < 3 {
                 assert!(idx >= row_count);
                 // we reached the end of the model, and we still have room. scroll a bit up.
-                vp_y = listview_height - y;
+                vp_y += listview_height - y;
+                loop_count += 1;
                 continue;
             }
 
@@ -1187,7 +1190,7 @@ impl<C: RepeatedItemTree + 'static> Repeater<C> {
             inner.cached_item_height = (y - new_offset_y) / inner.instances.len() as Coord;
             inner.anchor_y = inner.cached_item_height * inner.offset as Coord;
             viewport_height.set(inner.cached_item_height * row_count as Coord);
-            let new_viewport_y = -inner.anchor_y + vp_y + new_offset_y;
+            let new_viewport_y = -inner.anchor_y + new_offset_y;
             viewport_y.set(new_viewport_y);
             inner.previous_viewport_y = new_viewport_y;
             break;


Test patch:
diff --git a/tests/cases/elements/listview-millions.slint b/tests/cases/elements/listview-millions.slint
new file mode 100644
index 00000000000..a5c3c4ec37e
--- /dev/null
+++ b/tests/cases/elements/listview-millions.slint
@@ -0,0 +1,138 @@
+// Copyright © SixtyFPS GmbH <info@slint.dev>
+// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
+
+// As of now, the C++ optimization doesn't optimize hidden item of Listview so this test would take forever
+//ignore:cpp
+
+import { ListView } from "std-widgets.slint";
+export component TestCase inherits Window {
+    preferred-width: 300px;
+    preferred-height: 300px;
+
+    in-out property <string> result;
+    callback clicked(int);
+    in-out property <length> viewport-y <=> lv.viewport-y;
+
+    lv := ListView {
+        for _[num] in 2130000000: Rectangle {
+            height: 20px;
+            border-width: 1px;
+            border-color: red;
+            Text { text: num; }
+            TouchArea {
+                clicked => { result = "|"+num; root.clicked(num) }
+            }
+        }
+    }
+}
+
+
+/*
+
+
+```rust
+let instance = TestCase::new().unwrap();
+let clicked = std::rc::Rc::new(std::cell::RefCell::new(Vec::<i32>::new()));
+let clicked2 = clicked.clone();
+instance.on_clicked(move |x| clicked2.borrow_mut().push(x) );
+slint_testing::send_mouse_click(&instance, 5., 250.);
+assert_eq!(clicked.borrow().as_slice(), &[12]);
+slint_testing::send_mouse_click(&instance, 5., 263.);
+slint_testing::send_mouse_click(&instance, 5., 239.);
+assert_eq!(clicked.borrow().as_slice(), &[12, 13, 11]);
+
+instance.set_viewport_y(-20. * 1000.);
+clicked.borrow_mut().clear();
+slint_testing::send_mouse_click(&instance, 5., 250.);
+assert_eq!(clicked.borrow().as_slice(), &[1012]);
+slint_testing::send_mouse_click(&instance, 5., 263.);
+slint_testing::send_mouse_click(&instance, 5., 239.);
+assert_eq!(clicked.borrow().as_slice(), &[1012, 1013, 1011]);
+
+instance.set_viewport_y(-20. * 100000.);
+clicked.borrow_mut().clear();
+slint_testing::send_mouse_click(&instance, 5., 250.);
+assert_eq!(clicked.borrow().as_slice(), &[100012]);
+slint_testing::send_mouse_click(&instance, 5., 263.);
+slint_testing::send_mouse_click(&instance, 5., 239.);
+assert_eq!(clicked.borrow().as_slice(), &[100012, 100013, 100011]);
+
+instance.set_viewport_y(-20. * 10000000.);
+clicked.borrow_mut().clear();
+slint_testing::send_mouse_click(&instance, 5., 250.);
+assert_eq!(clicked.borrow().as_slice(), &[10000012]);
+slint_testing::send_mouse_click(&instance, 5., 263.);
+slint_testing::send_mouse_click(&instance, 5., 239.);
+assert_eq!(clicked.borrow().as_slice(), &[10000012, 10000013, 10000011]);
+
+instance.set_viewport_y(-20. * 210000000.);
+clicked.borrow_mut().clear();
+slint_testing::send_mouse_click(&instance, 5., 250.);
+assert_eq!(clicked.borrow().as_slice(), &[210000012]);
+slint_testing::send_mouse_click(&instance, 5., 263.);
+slint_testing::send_mouse_click(&instance, 5., 239.);
+assert_eq!(clicked.borrow().as_slice(), &[210000012, 210000013, 210000011]);
+
+// go all the way to the end, it shouldn't crash or loop forever
+instance.set_viewport_y(-20. * (2130000000. - 5.));
+slint_testing::send_mouse_click(&instance, 5., 250.);
+```
+
+```cpp
+auto handle = TestCase::create();
+const TestCase &instance = *handle;
+auto clicked = std::make_shared<std::vector<int>>();
+instance.on_clicked([clicked](int x) { clicked->push_back(x); });
+slint_testing::send_mouse_click(&instance, 5., 250.);
+assert(*clicked == std::vector<int>{12});
+```
+
+```js
+var instance = new slint.TestCase();
+var clicked = new Array();
+instance.clicked = function(x) { clicked.push(x); };
+slintlib.private_api.send_mouse_click(instance, 5., 250.);
+assert.deepEqual(clicked, [12]);
+slintlib.private_api.send_mouse_click(instance, 5., 263.);
+slintlib.private_api.send_mouse_click(instance, 5., 239.);
+assert.deepEqual(clicked, [12, 13, 11]);
+
+instance.viewport_y=(-20. * 1000.);
+clicked.length = 0;
+slintlib.private_api.send_mouse_click(instance, 5., 250.);
+assert.deepEqual(clicked, [1012]);
+slintlib.private_api.send_mouse_click(instance, 5., 263.);
+slintlib.private_api.send_mouse_click(instance, 5., 239.);
+assert.deepEqual(clicked, [1012, 1013, 1011]);
+
+instance.viewport_y=(-20. * 100000.);
+clicked.length = 0;
+slintlib.private_api.send_mouse_click(instance, 5., 250.);
+assert.deepEqual(clicked, [100012]);
+slintlib.private_api.send_mouse_click(instance, 5., 263.);
+slintlib.private_api.send_mouse_click(instance, 5., 239.);
+assert.deepEqual(clicked, [100012, 100013, 100011]);
+
+instance.viewport_y=(-20. * 10000000.);
+clicked.length = 0;
+slintlib.private_api.send_mouse_click(instance, 5., 250.);
+assert.deepEqual(clicked, [10000012]);
+slintlib.private_api.send_mouse_click(instance, 5., 263.);
+slintlib.private_api.send_mouse_click(instance, 5., 239.);
+assert.deepEqual(clicked, [10000012, 10000013, 10000011]);
+
+instance.viewport_y=(-20. * 210000000.);
+clicked.length = 0;
+slintlib.private_api.send_mouse_click(instance, 5., 250.);
+assert.deepEqual(clicked, [210000012]);
+slintlib.private_api.send_mouse_click(instance, 5., 263.);
+slintlib.private_api.send_mouse_click(instance, 5., 239.);
+assert.deepEqual(clicked, [210000012, 210000013, 210000011]);
+
+// go all the way to the end, it shouldn't crash or loop forever
+instance.viewport_y=(-20. * (2130000000. - 5.));
+slintlib.private_api.send_mouse_click(instance, 5., 250.);
+```
+
+
+*/