mirror of
https://github.com/danbulant/cushy
synced 2026-07-05 19:20:36 +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::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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
51
src/value.rs
51
src/value.rs
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue