mirror of
https://github.com/danbulant/cushy
synced 2026-06-20 23:11:12 +00:00
Added InvalidationBatch
This commit is contained in:
parent
64ad120be6
commit
956e4109f9
3 changed files with 213 additions and 18 deletions
|
|
@ -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
|
||||
|
|
|
|||
82
examples/invalidation-guard.rs
Normal file
82
examples/invalidation-guard.rs
Normal 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()
|
||||
}
|
||||
142
src/value.rs
142
src/value.rs
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue