ReadOnly<T>, Owned<T>, IntoSource<T>, more

Closes #98

This finishes my initial refactoring of the dynamic system to add
support for several dataflows including:

- Pure data sources that can be implemented using an `Owned<T>` at the
  root of a graph of `Dynamic<U>`/`DynamicReader<U>`s.
- Read-only data sinks. I thought this would be more useful across other
  widgets, but in general, Progress and Label seem like the only types
  that this applies to currently.
- The ability to mix/match Dynamic/DynamicReader in tuple-based
  for_each/map_each.
This commit is contained in:
Jonathan Johnson 2024-01-02 13:13:26 -08:00
parent 8b19c4c304
commit 83e44912ee
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
18 changed files with 641 additions and 155 deletions

View file

@ -22,6 +22,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
parameter. This type tracks whether the reference is accessed using
`DerefMut`, allowing `map_mut` to skip invoking change callbacks if only
`Deref` is used.
- `redraw_when_changed()`/`invalidate_when_changed()` from some types have been
moved to the `Trackable` trait. This was to ensure all trackable types provide
the same API.
- `Label` has been refactored to accept any `Display` type. As a result of this,
`Label::text` is now named `display` and `Label::new()` now accepts an
`IntoReadOnly<T>` instead of `IntoValue<String>`.
### Fixed
@ -44,6 +50,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
are waiting for value when the last `Dynamic` is dropped.
- Compatibility with Rust v1.70.0 has been restored, and continuous integration
testing the MSRV has been added.
- `Progress` now utilizes `IntoSource<Progress>` instead of
`IntoDynamic<Progress>`. In general, this should not cause any code breakages
unless the traits were being used in generics.
### Changed
@ -52,6 +61,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
the cache is invalidated.
- All `Dynamic` mapping functions now utilize weak references, and clean up as
necessary if a value is not able to be upgraded.
- `ForEach`/`MapEach`'s implementations for tuples are now defined using
`Source<T>` and `DynamicRead<T>`. This allows combinations of `Dynamic<T>`s
and `DynamicReader<T>`s to be used in for_each/map_each expressions.
### Added
@ -84,6 +96,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `Source<T>` and `Destination<T>` are new traits that contain the reactive data
model's API interface. `Dynamic<T>` implements both traits, and
`DynamicReader<T>` implements only `Source<T>`.
- `DynamicRead<T>` is a new trait that provides read-only access to a dynamic's
contents.
- `IntoReadOnly<T>` is a new trait that types can implement to convert into a
`ReadOnly<T>`.
- `IntoReader<T>` is a new trait that types can implement to convert into a
`DynamicReader<T>`.
- `ReadOnly<T>` is a type similar to `Value<T>` but instead of possibly being a
`Dynamic<T>`, `ReadOnly::Reader` contains a `DynamicReader<T>`. This type can
be used where widgets that receive a value but never mutate it.
- `Owned<T>` is a new type that can be used where no shared ownership is
necessary. This type uses a `RefCell` internally instead of an `Arc` +
`Mutex`. `Owned<T>` implements `Source<T>` and `Destination<T>`.
- `GenerationalValue<T>` now implements `Default` when `T` does.
- `Value<T>` now implements `From<Dynamic<T>>`.
[99]: https://github.com/khonsulabs/cushy/issues/99
[120]: https://github.com/khonsulabs/cushy/issues/120

View file

@ -1,4 +1,4 @@
use cushy::value::{Destination, Dynamic, Source};
use cushy::value::{Destination, Dynamic, IntoReader, Source};
use cushy::widget::MakeWidget;
use cushy::Run;
@ -6,11 +6,12 @@ use cushy::Run;
fn main() -> cushy::Result {
// Create a dynamic usize.
let count = Dynamic::new(0_isize);
// Create a dynamic that contains `count.to_string()`
let count_label = count.map_each(ToString::to_string);
// Create a new button whose text is our dynamic string.
count_label
// Create a new label displaying `count`
count
.clone()
.into_label()
// Use the label as the contents of a button
.into_button()
// Set the `on_click` callback to a closure that increments the counter.
.on_click(move |_| count.set(count.get() + 1))

View file

@ -1,15 +1,14 @@
use std::string::ToString;
use cushy::value::{Dynamic, Source};
use cushy::value::{Dynamic, IntoReader};
use cushy::widget::MakeWidget;
use cushy::Run;
use figures::units::Lp;
fn main() -> cushy::Result {
let counter = Dynamic::new(0i32);
let label = counter.map_each(ToString::to_string);
label
counter
.clone()
.into_label()
.width(Lp::points(100))
.and("+".into_button().on_click(counter.with_clone(|counter| {
move |_| {

View file

@ -1,5 +1,5 @@
use cushy::debug::DebugContext;
use cushy::value::{Destination, Dynamic, Source};
use cushy::value::{Destination, Dynamic, IntoReader};
use cushy::widget::MakeWidget;
use cushy::widgets::slider::Slidable;
use cushy::{Application, Open, PendingApp};
@ -25,7 +25,7 @@ fn main() -> cushy::Result {
info.observe("Open Windows", &window_count, |window_count| {
window_count
.map_each(ToString::to_string)
.into_label()
.and(open_window_button.clone())
.into_columns()
});

View file

@ -1,5 +1,5 @@
use cushy::animation::{LinearInterpolate, PercentBetween};
use cushy::value::{Destination, Dynamic, ForEach, Source};
use cushy::value::{Destination, Dynamic, ForEach, IntoReader, Source};
use cushy::widget::MakeWidget;
use cushy::widgets::checkbox::Checkable;
use cushy::widgets::input::InputValue;
@ -31,7 +31,6 @@ fn u8_slider() -> impl MakeWidget {
let max = Dynamic::new(u8::MAX);
let max_text = max.linked_string();
let value = Dynamic::new(128_u8);
let value_text = value.map_each(ToString::to_string);
"Min"
.and(min_text.into_input())
@ -39,8 +38,8 @@ fn u8_slider() -> impl MakeWidget {
.and(max_text.into_input())
.into_columns()
.centered()
.and(value.slider_between(min, max))
.and(value_text.centered())
.and(value.clone().slider_between(min, max))
.and(value.into_label().centered())
.into_rows()
}

View file

@ -969,12 +969,12 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
/// Ensures that this widget will be redrawn when `value` has been updated.
pub fn redraw_when_changed(&self, value: &impl Trackable) {
value.redraw_when_changed(self.handle());
value.inner_redraw_when_changed(self.handle());
}
/// Ensures that this widget will be redrawn when `value` has been updated.
pub fn invalidate_when_changed(&self, value: &impl Trackable) {
value.invalidate_when_changed(self.handle(), self.current_node.id());
value.inner_invalidate_when_changed(self.handle(), self.current_node.id());
}
/// Returns the last layout of this widget.
@ -1344,7 +1344,27 @@ impl Default for WidgetCacheKey {
}
/// A type that can be tracked to refresh or invalidate widgets.
pub trait Trackable: sealed::Trackable {}
pub trait Trackable: sealed::Trackable {
/// Marks the widget for redraw when this value is updated.
///
/// This function has no effect if the value is constant.
fn redraw_when_changed(&self, context: &WidgetContext<'_, '_>)
where
Self: Sized,
{
context.redraw_when_changed(self);
}
/// Marks the widget for redraw when this value is updated.
///
/// This function has no effect if the value is constant.
fn invalidate_when_changed(&self, context: &WidgetContext<'_, '_>)
where
Self: Sized,
{
context.invalidate_when_changed(self);
}
}
impl<T> Trackable for T where T: sealed::Trackable {}
@ -1359,8 +1379,8 @@ pub(crate) mod sealed {
use crate::window::WindowHandle;
pub trait Trackable {
fn redraw_when_changed(&self, handle: WindowHandle);
fn invalidate_when_changed(&self, handle: WindowHandle, id: WidgetId);
fn inner_redraw_when_changed(&self, handle: WindowHandle);
fn inner_invalidate_when_changed(&self, handle: WindowHandle, id: WidgetId);
}
#[derive(Default, Clone)]

View file

@ -20,7 +20,7 @@ pub use palette::OklabHue;
use palette::{IntoColor, Okhsl, Srgb};
use crate::animation::{EasingFunction, ZeroToOne};
use crate::context::WidgetContext;
use crate::context::{Trackable, WidgetContext};
use crate::names::Name;
use crate::utils::Lazy;
use crate::value::{Dynamic, IntoValue, Source, Value};

View file

@ -7,18 +7,32 @@ use kludgine::app::winit::event::Modifiers;
use kludgine::app::winit::keyboard::ModifiersState;
/// Invokes the provided macro with a pattern that can be matched using this
/// `macro_rules!` expression: `$($type:ident $field:tt $var:ident),+`, where `$type` is an
/// identifier to use for the generic parameter and `$field` is the field index
/// inside of the tuple.
/// `macro_rules!` expression: `$($type:ident $field:tt $var:ident),+`, where
/// `$type` is an identifier to use for the generic parameter and `$field` is
/// the field index inside of the tuple.
///
/// If `impl_all_tuples!(macro_name, 2)` is provided, an additional identifier
/// will be provided before `$field`.
macro_rules! impl_all_tuples {
($macro_name:ident) => {
impl_all_tuples!($macro_name, 1);
};
($macro_name:ident, 1) => {
$macro_name!(T0 0 t0);
$macro_name!(T0 0 t0, T1 1 t1);
$macro_name!(T0 0 t0, T1 1 t1, T2 2 t2);
$macro_name!(T0 0 t0, T1 1 t1, T2 2 t2, T3 3 t3);
$macro_name!(T0 0 t0, T1 1 t1, T2 2 t2, T3 3 t3, T4 4 t4);
$macro_name!(T0 0 t0, T1 1 t1, T2 2 t2, T3 3 t3, T4 4 t4, T5 5 t5);
}
};
($macro_name:ident, 2) => {
$macro_name!(T0 Y0 0 t0);
$macro_name!(T0 Y0 0 t0, T1 Y1 1 t1);
$macro_name!(T0 Y0 0 t0, T1 Y1 1 t1, T2 Y2 2 t2);
$macro_name!(T0 Y0 0 t0, T1 Y1 1 t1, T2 Y2 2 t2, T3 Y3 3 t3);
$macro_name!(T0 Y0 0 t0, T1 Y1 1 t1, T2 Y2 2 t2, T3 Y3 3 t3, T4 Y4 4 t4);
$macro_name!(T0 Y0 0 t0, T1 Y1 1 t1, T2 Y2 2 t2, T3 Y3 3 t3, T4 Y4 4 t4, T5 Y5 5 t5);
};
}
/// Invokes a function with a clone of `self`.

View file

@ -1,5 +1,6 @@
//! Types for storing and interacting with values in Widgets.
use std::cell::{Ref, RefCell, RefMut};
use std::collections::HashMap;
use std::fmt::{self, Debug, Display};
use std::future::Future;
@ -22,7 +23,7 @@ use crate::utils::{run_in_bg, IgnorePoison, WithClone};
use crate::widget::{
Children, MakeWidget, MakeWidgetWithTag, OnceCallback, WidgetId, WidgetInstance,
};
use crate::widgets::{Radio, Select, Space, Switcher};
use crate::widgets::{Label, Radio, Select, Space, Switcher};
use crate::window::WindowHandle;
/// A source of one or more `T` values.
@ -607,14 +608,14 @@ pub struct Mutable<'a, T> {
#[derive(Debug)]
enum Mutated<'a> {
Tracked(&'a mut bool),
External(&'a mut bool),
Ignored,
}
impl Mutated<'_> {
fn set(&mut self, mutated: bool) {
match self {
Self::Tracked(value) => **value = mutated,
Self::External(value) => **value = mutated,
Self::Ignored => {}
}
}
@ -643,7 +644,7 @@ impl<'a, T> Mutable<'a, T> {
*mutated = false;
Self {
value,
mutated: Mutated::Tracked(mutated),
mutated: Mutated::External(mutated),
}
}
}
@ -657,6 +658,201 @@ impl<'a, T> From<&'a mut T> for Mutable<'a, T> {
}
}
/// A unique, reactive value.
///
/// This type is useful for situations where a value is owned by exactly one
/// type but needs to have reactivity through [`Source`]/[`Destination`].
///
/// A [`Dynamic`] utilizes a [`Arc`] + [`Mutex`] to support updating its values
/// from multiple threads. This type utilizes a [`RefCell`], preventing it from
/// being shared between multiple threads.
#[derive(Default)]
pub struct Owned<T> {
wrapped: RefCell<GenerationalValue<T>>,
callbacks: Arc<OwnedCallbacks<T>>,
}
impl<T> Owned<T> {
/// Returns a new reactive value.
pub fn new(value: T) -> Self {
Self {
wrapped: RefCell::new(GenerationalValue {
value,
generation: Generation::default(),
}),
callbacks: Arc::default(),
}
}
/// Borrows the contents of this value with read-only access.
pub fn borrow(&self) -> OwnedRef<'_, T> {
OwnedRef(self.wrapped.borrow())
}
/// Borrows the contents of this value with exclusive access.
///
/// When the returned type is accessed through [`DerefMut`], all associated
/// reactive callbacks will be invoked upon dropping the returned
/// [`OwnedMut`].
pub fn borrow_mut(&mut self) -> OwnedMut<'_, T> {
OwnedMut {
borrowed: self.wrapped.borrow_mut(),
accessed_mut: false,
owned: self,
}
}
/// Returns the contained value.
pub fn into_inner(self) -> T {
self.wrapped.into_inner().value
}
}
impl<T> Source<T> for Owned<T> {
fn try_map_generational<R>(
&self,
map: impl FnOnce(&GenerationalValue<T>) -> R,
) -> Result<R, DeadlockError> {
Ok(map(&self.wrapped.borrow()))
}
fn for_each_generational_try<F>(&self, for_each: F) -> CallbackHandle
where
T: Send + 'static,
F: for<'a> FnMut(&'a GenerationalValue<T>) -> Result<(), CallbackDisconnected>
+ Send
+ 'static,
{
let mut callbacks = self.callbacks.active.lock().ignore_poison();
CallbackHandle(CallbackHandleInner::Single(CallbackHandleData {
id: Some(callbacks.push(Box::new(for_each))),
callbacks: self.callbacks.clone(),
}))
}
fn for_each_generational_cloned_try<F>(&self, mut for_each: F) -> CallbackHandle
where
T: Clone + Send + 'static,
F: FnMut(GenerationalValue<T>) -> Result<(), CallbackDisconnected> + Send + 'static,
{
self.for_each_generational_try(move |value| for_each(value.clone()))
}
}
impl<T> Destination<T> for Owned<T>
where
T: 'static,
{
fn try_map_mut<R>(&self, map: impl FnOnce(Mutable<'_, T>) -> R) -> Result<R, DeadlockError> {
let mut updated = false;
let result = map(Mutable::new(
&mut self.wrapped.borrow_mut().value,
&mut updated,
));
if updated {
self.callbacks.invoke(&self.wrapped.borrow());
}
Ok(result)
}
}
/// A read-only reference to the value in an [`Owned`].
pub struct OwnedRef<'a, T>(Ref<'a, GenerationalValue<T>>)
where
T: 'static;
impl<T> Deref for OwnedRef<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
/// An exclusive reference to the value contained in an [`Owned`].
///
/// This type tracks if the referenced value is accessed through [`DerefMut`].
/// If it is, reactive callbacks associated with the [`Owned`] value will be
/// invoked.
pub struct OwnedMut<'a, T>
where
T: 'static,
{
owned: &'a Owned<T>,
borrowed: RefMut<'a, GenerationalValue<T>>,
accessed_mut: bool,
}
impl<T> Deref for OwnedMut<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.borrowed.value
}
}
impl<T> DerefMut for OwnedMut<'_, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.accessed_mut = true;
&mut self.borrowed.value
}
}
impl<T> Drop for OwnedMut<'_, T>
where
T: 'static,
{
fn drop(&mut self) {
if self.accessed_mut {
self.owned.callbacks.invoke(&self.borrowed);
}
}
}
struct OwnedCallbacks<T> {
active: Mutex<Lots<Box<dyn OwnedCallbackFn<T>>>>,
}
impl<T> Default for OwnedCallbacks<T> {
fn default() -> Self {
Self {
active: Mutex::default(),
}
}
}
impl<T> OwnedCallbacks<T>
where
T: 'static,
{
pub fn invoke(&self, value: &GenerationalValue<T>) {
let mut callbacks = self.active.lock().ignore_poison();
callbacks.drain_filter(|callback| callback.updated(value).is_err());
}
}
impl<T> CallbackCollection for OwnedCallbacks<T>
where
T: 'static,
{
fn remove(&self, id: LotId) {
self.active.lock().ignore_poison().remove(id);
}
}
trait OwnedCallbackFn<T>: Send + 'static {
fn updated(&mut self, value: &GenerationalValue<T>) -> Result<(), CallbackDisconnected>;
}
impl<F, T> OwnedCallbackFn<T> for F
where
F: for<'a> FnMut(&'a GenerationalValue<T>) -> Result<(), CallbackDisconnected> + Send + 'static,
{
fn updated(&mut self, value: &GenerationalValue<T>) -> Result<(), CallbackDisconnected> {
self(value)
}
}
/// An instance of a value that provides APIs to observe and react to its
/// contents.
pub struct Dynamic<T>(Arc<DynamicData<T>>);
@ -665,19 +861,7 @@ impl<T> Dynamic<T> {
/// Creates a new instance wrapping `value`.
pub fn new(value: T) -> Self {
Self(Arc::new(DynamicData {
state: Mutex::new(State {
wrapped: GenerationalValue {
value,
generation: Generation::default(),
},
callbacks: Arc::default(),
windows: AHashSet::new(),
readers: 0,
wakers: Vec::new(),
widgets: AHashSet::new(),
on_disconnect: Some(Vec::new()),
source_callback: CallbackHandle::default(),
}),
state: Mutex::new(State::new(value)),
during_callback_state: Mutex::default(),
sync: Condvar::default(),
}))
@ -839,14 +1023,6 @@ impl<T> Dynamic<T> {
with_clone(self.clone())
}
pub(crate) fn redraw_when_changed(&self, window: WindowHandle) {
self.0.redraw_when_changed(window);
}
pub(crate) fn invalidate_when_changed(&self, window: WindowHandle, widget: WidgetId) {
self.0.invalidate_when_changed(window, widget);
}
/// Returns a new reference-based reader for this dynamic value.
///
/// # Panics
@ -884,6 +1060,10 @@ impl<T> Dynamic<T> {
/// thread.
#[must_use]
pub fn lock(&self) -> DynamicGuard<'_, T> {
self.lock_inner()
}
fn lock_inner<const READONLY: bool>(&self) -> DynamicGuard<'_, T, READONLY> {
DynamicGuard {
guard: self.0.state().expect("deadlocked"),
accessed_mut: false,
@ -1007,12 +1187,12 @@ impl MakeWidgetWithTag for Dynamic<Option<WidgetInstance>> {
}
impl<T> context::sealed::Trackable for Dynamic<T> {
fn redraw_when_changed(&self, handle: WindowHandle) {
self.redraw_when_changed(handle);
fn inner_redraw_when_changed(&self, handle: WindowHandle) {
self.0.redraw_when_changed(handle);
}
fn invalidate_when_changed(&self, handle: WindowHandle, id: WidgetId) {
self.invalidate_when_changed(handle, id);
fn inner_invalidate_when_changed(&self, handle: WindowHandle, id: WidgetId) {
self.0.invalidate_when_changed(handle, id);
}
}
@ -1240,6 +1420,10 @@ impl Display for DeadlockError {
}
}
trait CallbackCollection: Send + Sync + 'static {
fn remove(&self, id: LotId);
}
/// A handle to a callback installed on a [`Dynamic`]. When dropped, the
/// callback will be uninstalled.
///
@ -1262,7 +1446,7 @@ enum CallbackHandleInner {
struct CallbackHandleData {
id: Option<LotId>,
callbacks: Arc<ChangeCallbacksData>,
callbacks: Arc<dyn CallbackCollection>,
}
impl Debug for CallbackHandle {
@ -1328,8 +1512,7 @@ impl CallbackHandleData {
impl Drop for CallbackHandleData {
fn drop(&mut self) {
if let Some(id) = self.id {
let mut data = self.callbacks.callbacks.lock().ignore_poison();
data.callbacks.remove(id);
self.callbacks.remove(id);
}
}
}
@ -1399,6 +1582,22 @@ struct State<T> {
}
impl<T> State<T> {
fn new(value: T) -> Self {
Self {
wrapped: GenerationalValue {
value,
generation: Generation::default(),
},
callbacks: Arc::default(),
windows: AHashSet::new(),
readers: 0,
wakers: Vec::new(),
widgets: AHashSet::new(),
on_disconnect: Some(Vec::new()),
source_callback: CallbackHandle::default(),
}
}
fn note_changed(&mut self) -> ChangeCallbacks {
self.wrapped.generation = self.wrapped.generation.next();
@ -1481,6 +1680,13 @@ struct ChangeCallbacksData {
sync: Condvar,
}
impl CallbackCollection for ChangeCallbacksData {
fn remove(&self, id: LotId) {
let mut data = self.callbacks.lock().ignore_poison();
data.callbacks.remove(id);
}
}
struct CallbacksList {
callbacks: Lots<Box<dyn ValueCallback>>,
invoked_at: Instant,
@ -1569,7 +1775,7 @@ where
}
/// A value stored in a [`Dynamic`] with its [`Generation`].
#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Default, Clone, Debug, Eq, PartialEq)]
pub struct GenerationalValue<T> {
/// The stored value.
pub value: T,
@ -1858,11 +2064,11 @@ impl<T> DynamicReader<T> {
}
impl<T> context::sealed::Trackable for DynamicReader<T> {
fn redraw_when_changed(&self, handle: WindowHandle) {
fn inner_redraw_when_changed(&self, handle: WindowHandle) {
self.source.redraw_when_changed(handle);
}
fn invalidate_when_changed(&self, handle: WindowHandle, id: WidgetId) {
fn inner_invalidate_when_changed(&self, handle: WindowHandle, id: WidgetId) {
self.source.invalidate_when_changed(handle, id);
}
}
@ -1997,6 +2203,78 @@ impl Generation {
}
}
/// A type that can convert into a `ReadOnly<T>`.
pub trait IntoReadOnly<T> {
/// Returns `self` as a `ReadOnly`.
fn into_read_only(self) -> ReadOnly<T>;
}
impl<T> IntoReadOnly<T> for T {
fn into_read_only(self) -> ReadOnly<T> {
ReadOnly::Constant(self)
}
}
impl<T> IntoReadOnly<T> for ReadOnly<T> {
fn into_read_only(self) -> ReadOnly<T> {
self
}
}
impl<T> IntoReadOnly<T> for Value<T> {
fn into_read_only(self) -> ReadOnly<T> {
match self {
Value::Constant(value) => ReadOnly::Constant(value),
Value::Dynamic(dynamic) => ReadOnly::Reader(dynamic.into_reader()),
}
}
}
impl<T> IntoReadOnly<T> for Dynamic<T> {
fn into_read_only(self) -> ReadOnly<T> {
self.create_reader().into_read_only()
}
}
impl<T> IntoReadOnly<T> for DynamicReader<T> {
fn into_read_only(self) -> ReadOnly<T> {
ReadOnly::Reader(self)
}
}
impl<T> IntoReadOnly<T> for Owned<T> {
fn into_read_only(self) -> ReadOnly<T> {
ReadOnly::Constant(self.into_inner())
}
}
/// A type that can be converted into a [`DynamicReader<T>`].
pub trait IntoReader<T> {
/// Returns this value as a reader.
fn into_reader(self) -> DynamicReader<T>;
/// Returns `self` being `Display`ed in a [`Label`] widget.
fn into_label(self) -> Label<T>
where
Self: Sized,
T: Debug + Display + Send + 'static,
{
Label::new(self.into_reader())
}
}
impl<T> IntoReader<T> for Dynamic<T> {
fn into_reader(self) -> DynamicReader<T> {
self.into_reader()
}
}
impl<T> IntoReader<T> for DynamicReader<T> {
fn into_reader(self) -> DynamicReader<T> {
self
}
}
/// A type that can convert into a `Dynamic<T>`.
pub trait IntoDynamic<T> {
/// Returns `self` as a dynamic.
@ -2086,6 +2364,90 @@ impl GetWidget<usize> for Vec<WidgetInstance> {
impl<T, W> Switchable<T> for W where W: IntoDynamic<T> {}
/// A value that can only be read from.
pub enum ReadOnly<T> {
/// A value that will not ever change externally.
Constant(T),
/// A value that is read from a dynamic.
Reader(DynamicReader<T>),
}
impl<T> ReadOnly<T> {
/// Returns a clone of the currently stored value.
#[must_use]
pub fn get(&self) -> T
where
T: Clone,
{
match self {
Self::Constant(value) => value.clone(),
Self::Reader(value) => value.get(),
}
}
/// Returns the current generation of the data stored, if the contained
/// value is [`Dynamic`].
pub fn generation(&self) -> Option<Generation> {
match self {
Self::Constant(_) => None,
Self::Reader(value) => Some(value.generation()),
}
}
/// Maps the current contents to `map` and returns the result.
pub fn map<R>(&self, map: impl FnOnce(&T) -> R) -> R {
match self {
Self::Constant(value) => map(value),
Self::Reader(dynamic) => dynamic.map_ref(map),
}
}
/// Returns a new value that is updated using `U::from(T.clone())` each time
/// `self` is updated.
#[must_use]
pub fn map_each<R, F>(&self, mut map: F) -> ReadOnly<R>
where
T: Send + 'static,
F: for<'a> FnMut(&'a T) -> R + Send + 'static,
R: PartialEq + Send + 'static,
{
match self {
Self::Constant(value) => ReadOnly::Constant(map(value)),
Self::Reader(dynamic) => ReadOnly::Reader(dynamic.map_each(map).into_reader()),
}
}
}
impl<T> From<DynamicReader<T>> for ReadOnly<T> {
fn from(value: DynamicReader<T>) -> Self {
Self::Reader(value)
}
}
impl<T> From<Dynamic<T>> for ReadOnly<T> {
fn from(value: Dynamic<T>) -> Self {
Self::from(value.into_reader())
}
}
impl<T> From<Owned<T>> for ReadOnly<T> {
fn from(value: Owned<T>) -> Self {
Self::Constant(value.into_inner())
}
}
impl<T> Debug for ReadOnly<T>
where
T: Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Constant(arg0) => Debug::fmt(arg0, f),
Self::Reader(arg0) => Debug::fmt(arg0, f),
}
}
}
/// A value that may be either constant or dynamic.
pub enum Value<T> {
/// A value that will not ever change externally.
@ -2205,26 +2567,42 @@ impl<T> Value<T> {
Value::Dynamic(value) => Some(value.generation()),
}
}
}
/// Marks the widget for redraw when this value is updated.
///
/// This function has no effect if the value is constant.
pub fn redraw_when_changed(&self, context: &WidgetContext<'_, '_>) {
if let Value::Dynamic(dynamic) = self {
context.redraw_when_changed(dynamic);
impl<T> crate::context::sealed::Trackable for ReadOnly<T> {
fn inner_invalidate_when_changed(&self, handle: WindowHandle, id: WidgetId) {
if let ReadOnly::Reader(dynamic) = self {
dynamic.inner_invalidate_when_changed(handle, id);
}
}
/// Marks the widget for redraw when this value is updated.
///
/// This function has no effect if the value is constant.
pub fn invalidate_when_changed(&self, context: &WidgetContext<'_, '_>) {
if let Value::Dynamic(dynamic) = self {
context.invalidate_when_changed(dynamic);
fn inner_redraw_when_changed(&self, handle: WindowHandle) {
if let ReadOnly::Reader(dynamic) = self {
dynamic.inner_redraw_when_changed(handle);
}
}
}
impl<T> crate::context::sealed::Trackable for Value<T> {
fn inner_invalidate_when_changed(&self, handle: WindowHandle, id: WidgetId) {
if let Value::Dynamic(dynamic) = self {
dynamic.inner_invalidate_when_changed(handle, id);
}
}
fn inner_redraw_when_changed(&self, handle: WindowHandle) {
if let Value::Dynamic(dynamic) = self {
dynamic.inner_redraw_when_changed(handle);
}
}
}
impl<T> From<Dynamic<T>> for Value<T> {
fn from(value: Dynamic<T>) -> Self {
Self::Dynamic(value)
}
}
impl<T> IntoDynamic<T> for Value<T> {
fn into_dynamic(self) -> Dynamic<T> {
match self {
@ -2285,9 +2663,9 @@ impl<'a> IntoValue<String> for &'a str {
}
}
impl IntoValue<String> for Dynamic<&'static str> {
fn into_value(self) -> Value<String> {
self.map_each(ToString::to_string).into_value()
impl<'a> IntoReadOnly<String> for &'a str {
fn into_read_only(self) -> ReadOnly<String> {
ReadOnly::Constant(self.to_string())
}
}
@ -2327,10 +2705,13 @@ pub trait ForEach<T> {
}
macro_rules! impl_tuple_for_each {
($($type:ident $field:tt $var:ident),+) => {
impl<$($type,)+> ForEach<($($type,)+)> for ($(&Dynamic<$type>,)+)
($($type:ident $source:ident $field:tt $var:ident),+) => {
impl<$($type,$source,)+> ForEach<($($type,)+)> for ($(&$source,)+)
where
$($type: Send + 'static,)+
$(
$source: DynamicRead<$type> + Source<$type> + Clone + Send + 'static,
$type: Send + 'static,
)+
{
type Ref<'a> = ($(&'a $type,)+);
@ -2408,7 +2789,7 @@ macro_rules! impl_tuple_for_each {
) => {
$handles += $var.for_each((&$for_each, $(&$rvar,)+).with_clone(|(for_each, $($rvar,)+)| {
move |$var: &$type| {
$(let $rvar = $rvar.lock();)+
$(let $rvar = $rvar.read();)+
let mut for_each =
for_each.lock().ignore_poison();
(for_each)(($(&$avar,)+));
@ -2417,7 +2798,26 @@ macro_rules! impl_tuple_for_each {
};
}
impl_all_tuples!(impl_tuple_for_each);
/// Read access to a value stored in a [`Dynamic`].
pub trait DynamicRead<T> {
/// Returns a guard that provides exclusive, read-only access to the value
/// contained wihtin this dynamic.
fn read(&self) -> DynamicGuard<'_, T, true>;
}
impl<T> DynamicRead<T> for Dynamic<T> {
fn read(&self) -> DynamicGuard<'_, T, true> {
self.lock_inner()
}
}
impl<T> DynamicRead<T> for DynamicReader<T> {
fn read(&self) -> DynamicGuard<'_, T, true> {
self.lock()
}
}
impl_all_tuples!(impl_tuple_for_each, 2);
/// A type that can create a `Dynamic<U>` from a `T` passed into a mapping
/// function.
@ -2435,11 +2835,14 @@ pub trait MapEach<T, U> {
}
macro_rules! impl_tuple_map_each {
($($type:ident $field:tt $var:ident),+) => {
impl<U, $($type),+> MapEach<($($type,)+), U> for ($(&Dynamic<$type>,)+)
($($type:ident $source:ident $field:tt $var:ident),+) => {
impl<U, $($type,$source),+> MapEach<($($type,)+), U> for ($(&$source,)+)
where
U: PartialEq + Send + 'static,
$($type: Send + 'static),+
$(
$type: Send + 'static,
$source: DynamicRead<$type> + Source<$type> + Clone + Send + 'static,
)+
{
type Ref<'a> = ($(&'a $type,)+);
@ -2448,7 +2851,7 @@ macro_rules! impl_tuple_map_each {
F: for<'a> FnMut(Self::Ref<'a>) -> U + Send + 'static,
{
let dynamic = {
$(let $var = self.$field.lock();)+
$(let $var = self.$field.read();)+
Dynamic::new(map_each(($(&$var,)+)))
};
@ -2465,7 +2868,7 @@ macro_rules! impl_tuple_map_each {
};
}
impl_all_tuples!(impl_tuple_map_each);
impl_all_tuples!(impl_tuple_map_each, 2);
/// A type that can have a `for_each` operation applied to it.
pub trait ForEachCloned<T> {
@ -2476,10 +2879,13 @@ pub trait ForEachCloned<T> {
}
macro_rules! impl_tuple_for_each_cloned {
($($type:ident $field:tt $var:ident),+) => {
impl<$($type,)+> ForEachCloned<($($type,)+)> for ($(&Dynamic<$type>,)+)
($($type:ident $source:ident $field:tt $var:ident),+) => {
impl<$($type,$source,)+> ForEachCloned<($($type,)+)> for ($(&$source,)+)
where
$($type: Clone + Send + 'static,)+
$(
$type: Clone + Send + 'static,
$source: Source<$type> + Clone + Send + 'static,
)+
{
#[allow(unused_mut)]
@ -2566,7 +2972,7 @@ macro_rules! impl_tuple_for_each_cloned {
};
}
impl_all_tuples!(impl_tuple_for_each_cloned);
impl_all_tuples!(impl_tuple_for_each_cloned, 2);
/// A type that can create a `Dynamic<U>` from a `T` passed into a mapping
/// function.
@ -2579,11 +2985,14 @@ pub trait MapEachCloned<T, U> {
}
macro_rules! impl_tuple_map_each_cloned {
($($type:ident $field:tt $var:ident),+) => {
impl<U, $($type),+> MapEachCloned<($($type,)+), U> for ($(&Dynamic<$type>,)+)
($($type:ident $source:ident $field:tt $var:ident),+) => {
impl<U, $($type,$source),+> MapEachCloned<($($type,)+), U> for ($(&$source,)+)
where
U: PartialEq + Send + 'static,
$($type: Clone + Send + 'static),+
$(
$type: Clone + Send + 'static,
$source: Source<$type> + Clone + Send + 'static,
)+
{
fn map_each_cloned<F>(&self, mut map_each: F) -> Dynamic<U>
@ -2608,7 +3017,7 @@ macro_rules! impl_tuple_map_each_cloned {
};
}
impl_all_tuples!(impl_tuple_map_each_cloned);
impl_all_tuples!(impl_tuple_map_each_cloned, 2);
/// The status of validating data.
#[derive(Debug, Default, Clone, Eq, PartialEq)]

View file

@ -19,6 +19,7 @@ use kludgine::app::winit::window::CursorIcon;
use kludgine::Color;
use crate::app::{Application, Open, PendingApp, Run};
use crate::context::sealed::Trackable as _;
use crate::context::{
AsEventContext, EventContext, GraphicsContext, LayoutContext, ManageWidget, WidgetContext,
};
@ -149,8 +150,9 @@ use crate::ConstraintLimit;
/// layout functions, it needs to ensure that all depended upon [`Dynamic`]s are
/// tracked using one of the various
/// `*_tracking_redraw()`/`*_tracking_invalidate()` functions. For example,
/// [`Dynamic::get_tracking_redraw()`] and
/// [`Dynamic::get_tracking_invalidate()`].
/// [`Source::get_tracking_redraw()`](crate::value::Source::get_tracking_redraw)
/// and
/// [`Source::get_tracking_invalidate()`](crate::value::Source::get_tracking_invalidate).
///
/// # Hover State: Hit Testing
///
@ -1564,7 +1566,7 @@ impl WidgetInstance {
pub(crate) fn enabled(&self, context: &WindowHandle) -> bool {
if let Value::Dynamic(dynamic) = &self.data.enabled {
dynamic.redraw_when_changed(context.clone());
dynamic.inner_redraw_when_changed(context.clone());
}
self.data.enabled.get()
}

View file

@ -10,7 +10,7 @@ use figures::units::{Lp, UPx};
use figures::{Fraction, IntoSigned, IntoUnsigned, Point, Rect, Round, ScreenScale, Size};
use intentional::{Assert, Cast};
use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext};
use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, Trackable};
use crate::styles::components::IntrinsicPadding;
use crate::styles::Dimension;
use crate::value::{Generation, IntoValue, Value};

View file

@ -1,30 +1,37 @@
//! A read-only text widget.
use std::fmt::Write;
use figures::units::{Px, UPx};
use figures::{Point, Round, Size};
use kludgine::text::{MeasuredText, Text, TextOrigin};
use kludgine::{CanRenderTo, Color, DrawableExt};
use crate::context::{GraphicsContext, LayoutContext};
use crate::context::{GraphicsContext, LayoutContext, Trackable};
use crate::styles::components::TextColor;
use crate::value::{Dynamic, Generation, IntoValue, Value};
use crate::value::{Dynamic, Generation, IntoReadOnly, ReadOnly, Value};
use crate::widget::{Widget, WidgetInstance};
use crate::window::WindowLocal;
use crate::ConstraintLimit;
/// A read-only text widget.
#[derive(Debug)]
pub struct Label {
pub struct Label<T> {
/// The contents of the label.
pub text: Value<String>,
pub display: ReadOnly<T>,
displayed: String,
prepared_text: WindowLocal<(MeasuredText<Px>, Option<Generation>, Px, Color)>,
}
impl Label {
impl<T> Label<T>
where
T: std::fmt::Debug + std::fmt::Display + Send + 'static,
{
/// Returns a new label that displays `text`.
pub fn new(text: impl IntoValue<String>) -> Self {
pub fn new(text: impl IntoReadOnly<T>) -> Self {
Self {
text: text.into_value(),
display: text.into_read_only(),
displayed: String::new(),
prepared_text: WindowLocal::default(),
}
}
@ -35,7 +42,7 @@ impl Label {
color: Color,
width: Px,
) -> &MeasuredText<Px> {
let check_generation = self.text.generation();
let check_generation = self.display.generation();
match self.prepared_text.get(context) {
Some((prepared, prepared_generation, prepared_width, prepared_color))
if prepared.can_render_to(&context.gfx)
@ -44,10 +51,14 @@ impl Label {
&& *prepared_width == width => {}
_ => {
context.apply_current_font_settings();
let measured = self.text.map(|text| {
let measured = self.display.map(|text| {
self.displayed.clear();
if let Err(err) = write!(&mut self.displayed, "{text}") {
tracing::error!("Error invoking Display: {err}");
}
context
.gfx
.measure_text(Text::new(text, color).wrap_at(width))
.measure_text(Text::new(&self.displayed, color).wrap_at(width))
});
self.prepared_text
.set(context, (measured, check_generation, width, color));
@ -61,9 +72,12 @@ impl Label {
}
}
impl Widget for Label {
impl<T> Widget for Label<T>
where
T: std::fmt::Debug + std::fmt::Display + Send + 'static,
{
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
self.text.invalidate_when_changed(context);
self.display.invalidate_when_changed(context);
let size = context.gfx.region().size;
let center = Point::from(size) / 2;
@ -90,7 +104,7 @@ impl Widget for Label {
}
fn summarize(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fmt.debug_tuple("Label").field(&self.text).finish()
fmt.debug_tuple("Label").field(&self.display).finish()
}
fn unmounted(&mut self, context: &mut crate::context::EventContext<'_, '_>) {
@ -99,19 +113,20 @@ impl Widget for Label {
}
macro_rules! impl_make_widget {
($($type:ty),*) => {
($($type:ty => $kind:ty),*) => {
$(impl crate::widget::MakeWidgetWithTag for $type {
fn make_with_tag(self, id: crate::widget::WidgetTag) -> WidgetInstance {
Label::new(self).make_with_tag(id)
Label::<$kind>::new(self).make_with_tag(id)
}
})*
};
}
impl_make_widget!(
&'_ str,
String,
Value<String>,
Dynamic<String>,
Dynamic<&'static str>
&'_ str => String,
String => String,
Dynamic<String> => String,
Dynamic<&'static str> => &'static str,
Value<String> => String,
ReadOnly<String> => String
);

View file

@ -12,7 +12,7 @@ use intentional::Assert;
use crate::animation::easings::EaseOutQuadradic;
use crate::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn, ZeroToOne};
use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext};
use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, Trackable};
use crate::utils::IgnorePoison;
use crate::value::{Destination, Dynamic, DynamicGuard, IntoValue, Source, Value};
use crate::widget::{

View file

@ -12,7 +12,7 @@ use crate::animation::easings::{EaseInQuadradic, EaseOutQuadradic};
use crate::animation::{
AnimationHandle, AnimationTarget, IntoAnimate, PercentBetween, Spawn, ZeroToOne,
};
use crate::value::{Destination, Dynamic, IntoDynamic, IntoValue, MapEach, Source, Value};
use crate::value::{Destination, Dynamic, IntoReadOnly, IntoReader, MapEach, ReadOnly, Source};
use crate::widget::{MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance};
use crate::widgets::slider::{InactiveTrackColor, Slidable, TrackColor, TrackSize};
use crate::widgets::Data;
@ -20,7 +20,7 @@ use crate::widgets::Data;
/// A bar-shaped progress indicator.
#[derive(Debug)]
pub struct ProgressBar {
progress: Value<Progress>,
progress: ReadOnly<Progress>,
spinner: bool,
}
@ -29,16 +29,16 @@ impl ProgressBar {
#[must_use]
pub const fn indeterminant() -> Self {
Self {
progress: Value::Constant(Progress::Indeterminant),
progress: ReadOnly::Constant(Progress::Indeterminant),
spinner: false,
}
}
/// Returns a new progress bar that displays `progress`.
#[must_use]
pub fn new(progress: impl IntoDynamic<Progress>) -> Self {
pub fn new(progress: impl IntoReadOnly<Progress>) -> Self {
Self {
progress: Value::Dynamic(progress.into_dynamic()),
progress: progress.into_read_only(),
spinner: false,
}
}
@ -100,7 +100,7 @@ impl MakeWidgetWithTag for ProgressBar {
);
match self.progress {
Value::Dynamic(progress) => {
ReadOnly::Reader(progress) => {
let callback = progress.for_each(move |progress| {
update_progress_bar(
*progress,
@ -112,7 +112,9 @@ impl MakeWidgetWithTag for ProgressBar {
});
Data::new_wrapping((callback, progress), slider).make_widget()
}
Value::Constant(_) => Data::new_wrapping(indeterminant_animation, slider).make_widget(),
ReadOnly::Constant(_) => {
Data::new_wrapping(indeterminant_animation, slider).make_widget()
}
}
}
}
@ -193,32 +195,28 @@ fn update_progress_bar(
}
/// A value that can be used in a progress indicator.
pub trait Progressable<T>: IntoDynamic<T> + Sized
pub trait Progressable<T>: IntoReader<T> + Sized
where
T: ProgressValue + Send,
{
/// Returns a new progress bar that displays progress from `T::MIN` to
/// `T::MAX`.
fn progress_bar(self) -> ProgressBar {
ProgressBar::new(
self.into_dynamic()
.map_each(|value| value.to_progress(None)),
)
ProgressBar::new(self.into_reader().map_each(|value| value.to_progress(None)))
}
/// Returns a new progress bar that displays progress from `T::MIN` to
/// `max`. The maximum value can be either a `T` or an `Option<T>`. If
/// `None` is the maximum value, an indeterminant progress bar will be
/// displayed.
fn progress_bar_to(self, max: impl IntoValue<T::Value>) -> ProgressBar
fn progress_bar_to(self, max: impl IntoReadOnly<T::Value>) -> ProgressBar
where
T: Send,
T::Value: PartialEq + Ranged + Send + Clone,
{
let max = max.into_value();
let max = max.into_read_only();
match max {
Value::Constant(max) => self.progress_bar_between(<T::Value>::MIN..=max),
Value::Dynamic(max) => {
ReadOnly::Constant(max) => self.progress_bar_between(<T::Value>::MIN..=max),
ReadOnly::Reader(max) => {
self.progress_bar_between(max.map_each(|max| <T::Value>::MIN..=max.clone()))
}
}
@ -230,26 +228,28 @@ where
/// displayed.
fn progress_bar_between<Range>(self, range: Range) -> ProgressBar
where
T: Send,
T::Value: Send,
Range: IntoValue<RangeInclusive<T::Value>>,
Range: IntoReadOnly<RangeInclusive<T::Value>>,
{
let value = self.into_dynamic();
let range = range.into_value();
match range {
Value::Constant(range) => ProgressBar::new(
value.map_each(move |value| value.to_progress(Some(range.start()..=range.end()))),
),
Value::Dynamic(range) => {
ProgressBar::new((&range, &value).map_each(|(range, value)| {
value.to_progress(Some(range.start()..=range.end()))
}))
}
}
let value = self.into_reader();
let range = range.into_read_only();
ProgressBar::new(match range {
ReadOnly::Constant(range) => value
.map_each(move |value| value.to_progress(Some(range.start()..=range.end())))
.into_reader(),
ReadOnly::Reader(range) => (&range, &value)
.map_each(|(range, value)| value.to_progress(Some(range.start()..=range.end())))
.into_reader(),
})
}
}
impl<U> Progressable<U> for Dynamic<U> where U: ProgressValue + Send {}
impl<T, U> Progressable<U> for T
where
T: IntoReader<U> + Send,
U: ProgressValue + Send,
{
}
/// A value that can be used in a progress indicator.
pub trait ProgressValue: 'static {

View file

@ -3,7 +3,7 @@
use figures::units::UPx;
use figures::{IntoSigned, Rect, Round, ScreenScale, Size};
use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext};
use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, Trackable};
use crate::styles::components::IntrinsicPadding;
use crate::styles::FlexibleDimension;
use crate::value::{Generation, IntoValue, Value};

View file

@ -8,7 +8,7 @@ use kludgine::app::winit::window::CursorIcon;
use kludgine::tilemap;
use kludgine::tilemap::TileMapFocus;
use crate::context::{EventContext, GraphicsContext, LayoutContext};
use crate::context::{EventContext, GraphicsContext, LayoutContext, Trackable};
use crate::tick::Tick;
use crate::value::{Dynamic, IntoValue, Value};
use crate::widget::{EventHandling, Widget, HANDLED, IGNORED};

View file

@ -5,7 +5,7 @@ use figures::units::{Px, UPx};
use figures::{IntoSigned, IntoUnsigned, Point, Rect, Round, ScreenScale, Size, Zero};
use intentional::Cast;
use crate::context::{AsEventContext, GraphicsContext, LayoutContext};
use crate::context::{AsEventContext, GraphicsContext, LayoutContext, Trackable};
use crate::styles::components::{IntrinsicPadding, LayoutOrder};
use crate::styles::{FlexibleDimension, HorizontalOrder};
use crate::value::{IntoValue, Value};

View file

@ -31,7 +31,8 @@ use crate::animation::{LinearInterpolate, PercentBetween, ZeroToOne};
use crate::app::{Application, Cushy, Open, PendingApp, Run};
use crate::context::sealed::InvalidationStatus;
use crate::context::{
AsEventContext, EventContext, Exclusive, GraphicsContext, LayoutContext, WidgetContext,
AsEventContext, EventContext, Exclusive, GraphicsContext, LayoutContext, Trackable,
WidgetContext,
};
use crate::graphics::{FontState, Graphics};
use crate::styles::{Edges, FontFamilyList, ThemePair};