diff --git a/examples/virtual-list.rs b/examples/virtual-list.rs index 7482f84..23bdc33 100644 --- a/examples/virtual-list.rs +++ b/examples/virtual-list.rs @@ -1,9 +1,32 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use cushy::value::Dynamic; use cushy::widget::MakeWidget; +use cushy::widgets::slider::Slidable; use cushy::widgets::VirtualList; use cushy::Run; fn list() -> impl MakeWidget { - VirtualList::new(50, |index| format!("Item {}", index)).expand() + let count = Dynamic::new(50); + let list = VirtualList::new(&count, |index| { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("System Time after 1970") + .as_secs(); + format!("Item {index} - {timestamp}") + }); + let content_changed = list.content_watcher().clone(); + + "Count" + .and(count.slider_between(0, 10_000).expand_horizontally()) + .and( + "Refresh" + .into_button() + .on_click(move |_| content_changed.notify()), + ) + .into_columns() + .and(list.expand()) + .into_rows() } fn main() -> cushy::Result { diff --git a/guide/guide-examples/examples/virtual-list.rs b/guide/guide-examples/examples/virtual-list.rs index f32654a..9fb3d55 100644 --- a/guide/guide-examples/examples/virtual-list.rs +++ b/guide/guide-examples/examples/virtual-list.rs @@ -4,7 +4,7 @@ use cushy::Run; // ANCHOR: list fn list() -> impl MakeWidget { - VirtualList::new(50, |index| format!("Item {}", index)).expand() + VirtualList::new(50, |index| format!("Item {index}")).expand() } // ANCHOR_END: list diff --git a/src/value.rs b/src/value.rs index 2217808..3ba8eea 100644 --- a/src/value.rs +++ b/src/value.rs @@ -3952,21 +3952,27 @@ impl InvalidationBatch<'_> { pub struct Watcher(Dynamic); impl Watcher { + /// Notifies any observers of this watcher and invokes all associated + /// callbacks. + pub fn notify(&self) { + let mut counter = self.0.lock(); + *counter = counter.wrapping_add(1); + } + /// Ensures all callbacks attached to this watcher are invoked when `other` /// is changed. pub fn watch(&self, other: &impl Source) where T: Send + 'static, { - let counter = self.0.clone(); + let counter = self.clone(); self.0 .set_source(other.for_each_subsequent_generational(move |guard| { // We want to drop our guard before changing the counter to // ensure all callbacks associated with our counter are executed // without this type holding any source locks. drop(guard); - let mut counter = counter.lock(); - *counter = counter.wrapping_add(1); + counter.notify(); })); } @@ -3989,6 +3995,45 @@ impl Watcher { } } +impl Source for Watcher { + fn try_map_generational( + &self, + map: impl FnOnce(DynamicGuard<'_, usize, true>) -> R, + ) -> Result { + self.0.try_map_generational(map) + } + + fn for_each_subsequent_generational_try(&self, for_each: F) -> CallbackHandle + where + F: for<'a> FnMut(DynamicGuard<'_, usize, true>) -> Result<(), CallbackDisconnected> + + Send + + 'static, + { + self.0.for_each_subsequent_generational_try(for_each) + } + + fn for_each_generational_cloned_try(&self, for_each: F) -> CallbackHandle + where + F: FnMut(GenerationalValue) -> Result<(), CallbackDisconnected> + Send + 'static, + { + self.0.for_each_generational_cloned_try(for_each) + } +} + +impl crate::context::sealed::Trackable for Watcher { + fn inner_invalidate_when_changed(&self, handle: WindowHandle, id: WidgetId) { + self.0.inner_invalidate_when_changed(handle, id); + } + + fn inner_redraw_when_changed(&self, handle: WindowHandle) { + self.0.inner_redraw_when_changed(handle); + } + + fn inner_sync_when_changed(&self, handle: WindowHandle) { + self.0.inner_sync_when_changed(handle); + } +} + /// A value that has its read and updated states tracked. pub struct Tracked where diff --git a/src/widgets/virtual_list.rs b/src/widgets/virtual_list.rs index 0d29042..f228fca 100644 --- a/src/widgets/virtual_list.rs +++ b/src/widgets/virtual_list.rs @@ -4,15 +4,15 @@ use std::ops::Range; use cushy::context::LayoutContext; use cushy::ConstraintLimit; -use figures::UnscaledUnit; +use figures::{IntoUnsigned, UnscaledUnit}; use super::scroll::OwnedWidget; -use crate::context::{AsEventContext, EventContext}; +use crate::context::{AsEventContext, EventContext, Trackable}; use crate::figures::units::{Px, UPx}; -use crate::figures::{IntoSigned, Point, Rect, Round, ScreenScale, Size, Zero}; +use crate::figures::{IntoSigned, Point, Rect, Round, Size, Zero}; use crate::kludgine::app::winit::event::{MouseScrollDelta, TouchPhase}; use crate::kludgine::app::winit::window::CursorIcon; -use crate::value::{Destination, Dynamic, DynamicReader, IntoDynamic, IntoValue, Source}; +use crate::value::{Destination, Dynamic, DynamicReader, IntoDynamic, IntoValue, Source, Watcher}; use crate::widget::{ Callback, EventHandling, MakeWidget, MountedWidget, Widget, WidgetInstance, HANDLED, IGNORED, }; @@ -51,7 +51,10 @@ pub struct VirtualList { vertical_scroll: OwnedWidget, items: VecDeque, content_size: Dynamic>, - /// Maximum scroll value - `max_scroll.y` + `control_size.height` should be the height of the content. + contents: Watcher, + contents_generation: usize, + /// Maximum scroll value - `max_scroll.y` + `control_size.height` should be + /// the height of the content. pub max_scroll: DynamicReader>, /// Current scroll value. Changes to this dynamic will scroll the list /// programmatically. @@ -103,8 +106,13 @@ impl VirtualList { .map_each_cloned(|y| Point::new(UPx::ZERO, y)) .into_reader(); + let contents = Watcher::default(); + let contents_generation = contents.get(); + Self { make_row, + contents, + contents_generation, vertical_scroll: OwnedWidget::new(vertical), items: VecDeque::new(), control_size: Dynamic::new(Size::default()), @@ -118,6 +126,12 @@ impl VirtualList { } } + /// Returns a [`Watcher`] that when notified will force this list to reload + /// its contents, including the currently visible rows. + pub const fn content_watcher(&self) -> &Watcher { + &self.contents + } + /// Returns a reader for the maximum scroll value. /// /// This represents the maximum amount that the scroll can be moved by. @@ -160,16 +174,27 @@ impl VirtualList { .hide(context); } + fn clear(&mut self, context: &mut LayoutContext<'_, '_, '_, '_>) { + for item in self.items.drain(..) { + context.remove_child(&item.mounted); + } + } + fn layout_rows( &mut self, item_count: usize, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { + let generation = self.contents.get_tracking_redraw(context); + if generation != self.contents_generation { + self.contents_generation = generation; + self.clear(context); + } let mut item_size = self.calculate_item_size(available_space, context).ceil(); let content_height = item_size.height * u32::try_from(item_count).unwrap_or(u32::MAX); - let content_height = content_height.into_upx(context.gfx.scale()); + let content_height = content_height.into_unsigned(); let new_control_size = Size::new( available_space.width.fill_or_fit(item_size.width), @@ -201,6 +226,9 @@ impl VirtualList { ); let scroll = self.scroll.get_tracking_invalidate(context); + let max_scroll_y = content_height.saturating_sub(new_control_size.height); + let scroll = scroll.min(Point::new(UPx::MAX, max_scroll_y)); + let start_item = (scroll.y.floor() / item_size.height).floor().get() as usize; let end_item = ((scroll.y.ceil() + new_control_size.height) / item_size.height) .ceil() @@ -213,9 +241,7 @@ impl VirtualList { let last = self.items.back().map(|t| t.index); if self.items.is_empty() || first.unwrap() > end_item || last.unwrap() < start_item { - for item in self.items.drain(..) { - context.remove_child(&item.mounted); - } + self.clear(context); self.items.extend( (start_item..=end_item).map(|index| self.make_row.make_row(index, context)), ); @@ -318,6 +344,8 @@ impl Widget for VirtualList { } fn redraw(&mut self, context: &mut cushy::context::GraphicsContext<'_, '_, '_, '_>) { + self.item_count.invalidate_when_changed(context); + self.contents.invalidate_when_changed(context); for child in &mut self.items { context.for_other(&child.mounted).redraw(); }