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::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 {

View file

@ -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

View file

@ -3952,21 +3952,27 @@ impl InvalidationBatch<'_> {
pub struct Watcher(Dynamic<usize>);
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<T>(&self, other: &impl Source<T>)
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<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.
pub struct Tracked<Source>
where

View file

@ -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<ScrollBar>,
items: VecDeque<VirtualListItem>,
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>>,
/// 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<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_>,
) -> 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 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();
}