Added InvalidationBatch

This commit is contained in:
Jonathan Johnson 2024-01-11 09:47:51 -08:00
parent 64ad120be6
commit 956e4109f9
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
3 changed files with 213 additions and 18 deletions

View file

@ -192,6 +192,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `RgbaPicker` picks `Color`
- `ComponentPicker` is a picker of various `ColorComponent` implementors. It has
constructors for each
- `InvalidationBatch` is a type that can batch invalidation requests being made
by a background task. This can be useful if the background task is updating a
variety of `Dynamic<T>`s, but wish to limit redrawing the interface until the
task has completed its updates.
This type does not prevent redraws from being performed due to the operating
system or other threads requeseting them.
[99]: https://github.com/khonsulabs/cushy/issues/99
[120]: https://github.com/khonsulabs/cushy/issues/120

View file

@ -0,0 +1,82 @@
//! This example shows how to use a [`InvalidationBatch`] in a background task
//! to synchronize when the user interface is updated/invalidated.
//!
//! This does not prevent the user interface from displaying the intermediate
//! state if it is redrawn for other reasons or by other threads. For example,
//! if the user resizes the window, the window will be redrawn during the
//! resize, and the current values of the dynamic values will be used.
use std::time::Duration;
use cushy::value::{Destination, Dynamic, InvalidationBatch, Source};
use cushy::widget::MakeWidget;
use cushy::widgets::grid::{GridDimension, GridWidgets};
use cushy::widgets::progress::Progressable;
use cushy::widgets::Grid;
use cushy::Run;
// This task will update both `progress_a` and `progress_b` at varying times,
// but the user interface will only be refreshed when the `InvalidationBatch` is
// dropped.
fn background_task(progress_a: Dynamic<u8>, progress_b: Dynamic<u8>) {
loop {
InvalidationBatch::batch(|_batch| {
// This set of operations has a net effect of incrementing
// progress_a, and adding 5 to progress_b. But the operations are
// are done by first adding double the amount, waiting for a bit,
// then subtracting to get back to the desired loop behavior.
//
// This is a convoluted way to simulate having a complex operation
// in a background thread that a user wishes to synchronize the user
// interface updates to. The specific operations here aren't
// important. The important part is that all invalidation related to
// the changes to these widgets is delayed until this batch is
// executed, which happens when dropped or by using the batch
// parameter to this function to invoke them when desired.
progress_a.set(progress_a.get().wrapping_add(1));
progress_b.set(progress_b.get().wrapping_add(2));
std::thread::sleep(Duration::from_millis(100));
progress_b.set(progress_b.get().wrapping_add(2));
std::thread::sleep(Duration::from_millis(100));
progress_a.set(progress_a.get().wrapping_add(1));
progress_b.set(progress_b.get().wrapping_add(1));
});
// We sleep for 300 additional milliseconds to make the average loop
// take half a second. The progress will only ever be refreshed by this
// thread when the `progress_a` has been incremented by 2, and
// `progress_b` has been incremented by 5.
std::thread::sleep(Duration::from_millis(300));
}
}
fn main() -> cushy::Result {
const EXPLANATION: &str = "This example uses a background task that updates these progress values in such a way that it only requests this window be redrawn when the first has been incremented by 2 and the second has been incremented by 5. For fun, try resizing the window to force the window to redraw and observing that the intermediate states can still be seen.";
let progress_a = Dynamic::new(0);
let progress_a_text = progress_a.map_each(ToString::to_string);
let progress_b = Dynamic::new(0);
let progress_b_text = progress_b.map_each(ToString::to_string);
std::thread::spawn({
let progress_a = progress_a.clone();
let progress_b = progress_b.clone();
move || background_task(progress_a, progress_b)
});
EXPLANATION
.and(
Grid::from_rows(
GridWidgets::new()
.and((progress_a.progress_bar(), progress_a_text))
.and((progress_b.progress_bar(), progress_b_text)),
)
.dimensions([
GridDimension::Fractional { weight: 1 },
GridDimension::FitContent,
]),
)
.into_rows()
.contain()
.pad()
.expand_horizontally()
.centered()
.run()
}

View file

@ -1389,12 +1389,12 @@ impl<T> DynamicData<T> {
pub fn redraw_when_changed(&self, window: WindowHandle) {
let mut state = self.state().expect("deadlocked");
state.windows.insert(window);
state.invalidation.windows.insert(window);
}
pub fn invalidate_when_changed(&self, window: WindowHandle, widget: WidgetId) {
let mut state = self.state().expect("deadlocked");
state.widgets.insert((window, widget));
state.invalidation.widgets.insert((window, widget));
}
pub fn map_mut<R>(&self, map: impl FnOnce(Mutable<T>) -> R) -> Result<R, DeadlockError> {
@ -1653,13 +1653,47 @@ impl AddAssign for CallbackHandle {
}
}
#[derive(Default)]
struct InvalidationState {
windows: AHashSet<WindowHandle>,
widgets: AHashSet<(WindowHandle, WidgetId)>,
wakers: Vec<Waker>,
}
impl InvalidationState {
fn invoke(&mut self) {
for (window, widget) in self.widgets.drain() {
window.invalidate(widget);
}
for window in self.windows.drain() {
window.redraw();
}
for waker in self.wakers.drain(..) {
waker.wake();
}
}
fn extend(&mut self, other: &mut InvalidationState) {
self.widgets.extend(other.widgets.drain());
self.windows.extend(other.windows.drain());
for waker in other.wakers.drain(..) {
if !self
.wakers
.iter()
.any(|existing| existing.will_wake(&waker))
{
self.wakers.push(waker);
}
}
}
}
struct State<T> {
wrapped: GenerationalValue<T>,
source_callback: CallbackHandle,
callbacks: Arc<ChangeCallbacksData>,
windows: AHashSet<WindowHandle>,
widgets: AHashSet<(WindowHandle, WidgetId)>,
wakers: Vec<Waker>,
invalidation: InvalidationState,
on_disconnect: Option<Vec<OnceCallback>>,
readers: usize,
}
@ -1672,10 +1706,12 @@ impl<T> State<T> {
generation: Generation::default(),
},
callbacks: Arc::default(),
windows: AHashSet::new(),
invalidation: InvalidationState {
windows: AHashSet::new(),
wakers: Vec::new(),
widgets: AHashSet::new(),
},
readers: 0,
wakers: Vec::new(),
widgets: AHashSet::new(),
on_disconnect: Some(Vec::new()),
source_callback: CallbackHandle::default(),
}
@ -1684,14 +1720,8 @@ impl<T> State<T> {
fn note_changed(&mut self) -> ChangeCallbacks {
self.wrapped.generation = self.wrapped.generation.next();
for (window, widget) in self.widgets.drain() {
window.invalidate(widget);
}
for window in self.windows.drain() {
window.redraw();
}
for waker in self.wakers.drain(..) {
waker.wake();
if !InvalidationBatch::take_invalidations(&mut self.invalidation) {
self.invalidation.invoke();
}
ChangeCallbacks {
@ -1714,7 +1744,7 @@ impl<T> State<T> {
fn cleanup(&mut self) -> StateCleanup {
StateCleanup {
on_disconnect: self.on_disconnect.take(),
wakers: std::mem::take(&mut self.wakers),
wakers: std::mem::take(&mut self.invalidation.wakers),
}
}
}
@ -2208,7 +2238,7 @@ impl<'a, T> Future for BlockUntilUpdatedFuture<'a, T> {
return Poll::Ready(false);
}
state.wakers.push(cx.waker().clone());
state.invalidation.wakers.push(cx.waker().clone());
Poll::Pending
}
}
@ -3508,6 +3538,82 @@ where
}
}
/// A batch of invalidations across one or more windows.
///
/// This type helps background tasks synchronize when to invalidate or redraw a
/// widget. Without this type, if a tracked dynamic is changed, the window is
/// immediately sent a request to redraw itself. These requests are batched to
/// ensure efficiency, but if a background task is updating several dynamics
/// independent of one another, it may desire that those updates only trigger
/// one redraw per "step".
///
/// The closure invoked by [`InvalidationBatch::batch`] will gather all
/// invalidations into a single batch that can be executed by the handle
/// provided or automatically when the closure returns.
pub struct InvalidationBatch<'a>(&'a RefCell<InvalidationBatchGuard>);
#[derive(Default)]
struct InvalidationBatchGuard {
nesting: usize,
state: InvalidationState,
}
thread_local! {
static GUARD: RefCell<InvalidationBatchGuard> = RefCell::default();
}
impl InvalidationBatch<'_> {
/// Executes `batched` gathering all tracked invalidations into a shared
/// batch.
///
/// The closure accepts an `&InvalidationBatch<'_>` parameter which can be
/// used to [`invoke()`](Self::invoke) the batch on-demand while during
/// `batched`.
///
/// This function supports nested invocation. When nested, only the
/// outermost batch can manually invoke. When the outermost batch's callback
/// ends, any pending invalidations are invoked automatically.
pub fn batch(batched: impl FnOnce(&InvalidationBatch<'_>)) {
GUARD.with(|guard| {
let mut batch = guard.borrow_mut();
batch.nesting += 1;
drop(batch);
batched(&InvalidationBatch(guard));
let mut batch = guard.borrow_mut();
batch.nesting -= 1;
if batch.nesting == 0 {
batch.state.invoke();
}
});
}
/// Invokes all pending invalidations.
///
/// This function is a no-op if `self` is a nested batch. Only the first batch of each thread
pub fn invoke(&self) {
let mut batch = self.0.borrow_mut();
if batch.nesting == 1 {
batch.state.invoke();
}
}
#[must_use]
fn take_invalidations(state: &mut InvalidationState) -> bool {
GUARD.with(|guard| {
let mut batch = guard.borrow_mut();
if batch.nesting > 0 {
// A batch is active on this thread
batch.state.extend(state);
true
} else {
false
}
})
}
}
#[test]
fn map_cycle_is_finite() {
crate::initialize_tracing();