Added content refresh for virtual list

Also expanded interactive example to have a slider for item count and a
refresh button.
This commit is contained in:
Jonathan Johnson 2024-11-08 09:01:09 -08:00
parent 4bc3f5a884
commit 1bb5495f53
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
4 changed files with 110 additions and 14 deletions

View file

@ -1,9 +1,32 @@
use std::time::{SystemTime, UNIX_EPOCH};
use cushy::value::Dynamic;
use cushy::widget::MakeWidget; use cushy::widget::MakeWidget;
use cushy::widgets::slider::Slidable;
use cushy::widgets::VirtualList; use cushy::widgets::VirtualList;
use cushy::Run; use cushy::Run;
fn list() -> impl MakeWidget { 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 { fn main() -> cushy::Result {

View file

@ -4,7 +4,7 @@ use cushy::Run;
// ANCHOR: list // ANCHOR: list
fn list() -> impl MakeWidget { fn list() -> impl MakeWidget {
VirtualList::new(50, |index| format!("Item {}", index)).expand() VirtualList::new(50, |index| format!("Item {index}")).expand()
} }
// ANCHOR_END: list // ANCHOR_END: list

View file

@ -3952,21 +3952,27 @@ impl InvalidationBatch<'_> {
pub struct Watcher(Dynamic<usize>); pub struct Watcher(Dynamic<usize>);
impl Watcher { 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` /// Ensures all callbacks attached to this watcher are invoked when `other`
/// is changed. /// is changed.
pub fn watch<T>(&self, other: &impl Source<T>) pub fn watch<T>(&self, other: &impl Source<T>)
where where
T: Send + 'static, T: Send + 'static,
{ {
let counter = self.0.clone(); let counter = self.clone();
self.0 self.0
.set_source(other.for_each_subsequent_generational(move |guard| { .set_source(other.for_each_subsequent_generational(move |guard| {
// We want to drop our guard before changing the counter to // We want to drop our guard before changing the counter to
// ensure all callbacks associated with our counter are executed // ensure all callbacks associated with our counter are executed
// without this type holding any source locks. // without this type holding any source locks.
drop(guard); drop(guard);
let mut counter = counter.lock(); counter.notify();
*counter = counter.wrapping_add(1);
})); }));
} }
@ -3989,6 +3995,45 @@ impl Watcher {
} }
} }
impl Source<usize> for Watcher {
fn try_map_generational<R>(
&self,
map: impl FnOnce(DynamicGuard<'_, usize, true>) -> R,
) -> Result<R, DeadlockError> {
self.0.try_map_generational(map)
}
fn for_each_subsequent_generational_try<F>(&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<F>(&self, for_each: F) -> CallbackHandle
where
F: FnMut(GenerationalValue<usize>) -> 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. /// A value that has its read and updated states tracked.
pub struct Tracked<Source> pub struct Tracked<Source>
where where

View file

@ -4,15 +4,15 @@ use std::ops::Range;
use cushy::context::LayoutContext; use cushy::context::LayoutContext;
use cushy::ConstraintLimit; use cushy::ConstraintLimit;
use figures::UnscaledUnit; use figures::{IntoUnsigned, UnscaledUnit};
use super::scroll::OwnedWidget; use super::scroll::OwnedWidget;
use crate::context::{AsEventContext, EventContext}; use crate::context::{AsEventContext, EventContext, Trackable};
use crate::figures::units::{Px, UPx}; 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::event::{MouseScrollDelta, TouchPhase};
use crate::kludgine::app::winit::window::CursorIcon; 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::{ use crate::widget::{
Callback, EventHandling, MakeWidget, MountedWidget, Widget, WidgetInstance, HANDLED, IGNORED, Callback, EventHandling, MakeWidget, MountedWidget, Widget, WidgetInstance, HANDLED, IGNORED,
}; };
@ -51,7 +51,10 @@ pub struct VirtualList {
vertical_scroll: OwnedWidget<ScrollBar>, vertical_scroll: OwnedWidget<ScrollBar>,
items: VecDeque<VirtualListItem>, items: VecDeque<VirtualListItem>,
content_size: Dynamic<Size<UPx>>, content_size: Dynamic<Size<UPx>>,
/// 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<Point<UPx>>, pub max_scroll: DynamicReader<Point<UPx>>,
/// Current scroll value. Changes to this dynamic will scroll the list /// Current scroll value. Changes to this dynamic will scroll the list
/// programmatically. /// programmatically.
@ -103,8 +106,13 @@ impl VirtualList {
.map_each_cloned(|y| Point::new(UPx::ZERO, y)) .map_each_cloned(|y| Point::new(UPx::ZERO, y))
.into_reader(); .into_reader();
let contents = Watcher::default();
let contents_generation = contents.get();
Self { Self {
make_row, make_row,
contents,
contents_generation,
vertical_scroll: OwnedWidget::new(vertical), vertical_scroll: OwnedWidget::new(vertical),
items: VecDeque::new(), items: VecDeque::new(),
control_size: Dynamic::new(Size::default()), 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. /// Returns a reader for the maximum scroll value.
/// ///
/// This represents the maximum amount that the scroll can be moved by. /// This represents the maximum amount that the scroll can be moved by.
@ -160,16 +174,27 @@ impl VirtualList {
.hide(context); .hide(context);
} }
fn clear(&mut self, context: &mut LayoutContext<'_, '_, '_, '_>) {
for item in self.items.drain(..) {
context.remove_child(&item.mounted);
}
}
fn layout_rows( fn layout_rows(
&mut self, &mut self,
item_count: usize, item_count: usize,
available_space: Size<ConstraintLimit>, available_space: Size<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_>, context: &mut LayoutContext<'_, '_, '_, '_>,
) -> Size<UPx> { ) -> Size<UPx> {
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 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 = 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( let new_control_size = Size::new(
available_space.width.fill_or_fit(item_size.width), available_space.width.fill_or_fit(item_size.width),
@ -201,6 +226,9 @@ impl VirtualList {
); );
let scroll = self.scroll.get_tracking_invalidate(context); 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 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) let end_item = ((scroll.y.ceil() + new_control_size.height) / item_size.height)
.ceil() .ceil()
@ -213,9 +241,7 @@ impl VirtualList {
let last = self.items.back().map(|t| t.index); let last = self.items.back().map(|t| t.index);
if self.items.is_empty() || first.unwrap() > end_item || last.unwrap() < start_item { if self.items.is_empty() || first.unwrap() > end_item || last.unwrap() < start_item {
for item in self.items.drain(..) { self.clear(context);
context.remove_child(&item.mounted);
}
self.items.extend( self.items.extend(
(start_item..=end_item).map(|index| self.make_row.make_row(index, context)), (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<'_, '_, '_, '_>) { 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 { for child in &mut self.items {
context.for_other(&child.mounted).redraw(); context.for_other(&child.mounted).redraw();
} }