mirror of
https://github.com/danbulant/cushy
synced 2026-06-18 14:01:10 +00:00
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:
parent
4bc3f5a884
commit
1bb5495f53
4 changed files with 110 additions and 14 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
51
src/value.rs
51
src/value.rs
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue