use serde::{Deserialize, Serialize};
use std::{marker::PhantomData, rc::Rc};
mod graph_operations;
mod properties_table;
#[cfg(test)]
mod tests;
mod untyped_property;
use crate::{Duration, EasingCurve, Interpolatable, TransitionQueueEntry};
use self::properties_table::{PropertyType, PROPERTY_MILLIS, PROPERTY_TIME};
pub use properties_table::EffectDrainReport;
use properties_table::PROPERTY_TABLE;
pub use untyped_property::UntypedProperty;
mod private {
slotmap::new_key_type!(
pub struct PropertyId;
);
}
pub trait PropertyValue: Default + Clone + Interpolatable + 'static {}
impl<T: Default + Clone + Interpolatable + 'static> PropertyValue for T {}
impl<T: PropertyValue> Interpolatable for Property<T> {
fn interpolate(&self, other: &Self, t: f64) -> Self {
let cp_self = self.clone();
let cp_other = other.clone();
Property::computed(
move || cp_self.get().interpolate(&cp_other.get(), t),
&[self.untyped(), other.untyped()],
)
}
}
#[derive(Clone)]
pub struct Property<T> {
untyped: UntypedProperty,
_phantom: PhantomData<T>,
}
impl<T: PropertyValue + std::fmt::Debug> std::fmt::Debug for Property<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Property ({:?})", self.get())
}
}
impl<T: PropertyValue> Property<T> {
pub fn new(val: T) -> Self {
Self::new_optional_name(val, None)
}
pub fn new_from_untyped(untyped: UntypedProperty) -> Self {
Self {
untyped,
_phantom: PhantomData {},
}
}
pub fn computed(evaluator: impl Fn() -> T + 'static, dependents: &[UntypedProperty]) -> Self {
Self::computed_with_config(evaluator, dependents, None, None)
}
pub fn computed_with_cutoff(
evaluator: impl Fn() -> T + 'static,
dependents: &[UntypedProperty],
cutoff: impl Fn(&T, &T) -> bool + 'static,
) -> Self {
Self::computed_with_config(evaluator, dependents, None, Some(Rc::new(cutoff)))
}
pub fn new_with_name(val: T, name: &str) -> Self {
Self::new_optional_name(val, Some(name))
}
pub fn computed_with_name(
evaluator: impl Fn() -> T + 'static,
dependents: &[UntypedProperty],
name: &str,
) -> Self {
Self::computed_with_config(evaluator, dependents, Some(name), None)
}
pub fn computed_with_cutoff_and_name(
evaluator: impl Fn() -> T + 'static,
dependents: &[UntypedProperty],
cutoff: impl Fn(&T, &T) -> bool + 'static,
name: &str,
) -> Self {
Self::computed_with_config(evaluator, dependents, Some(name), Some(Rc::new(cutoff)))
}
fn new_optional_name(val: T, name: Option<&str>) -> Self {
Self {
untyped: UntypedProperty::new(val, Vec::with_capacity(0), PropertyType::Literal, name),
_phantom: PhantomData {},
}
}
fn computed_with_config(
evaluator: impl Fn() -> T + 'static,
dependents: &[UntypedProperty],
name: Option<&str>,
cutoff: Option<Rc<dyn Fn(&T, &T) -> bool>>,
) -> Self {
let inbound: Vec<_> = dependents.iter().map(|v| v.get_id()).collect();
let start_val = T::default();
let evaluator = Rc::new(evaluator);
Self {
untyped: UntypedProperty::new(
start_val,
inbound,
PropertyType::Computed {
evaluator,
cutoff: cutoff.map(|predicate| properties_table::Cutoff {
predicate,
initialized: false,
}),
},
name,
),
_phantom: PhantomData {},
}
}
pub fn ease_to<D: Into<Duration>>(&self, end_val: T, duration: D, curve: EasingCurve) {
self.ease_to_value(end_val, duration.into(), curve, true);
}
pub fn ease_to_later<D: Into<Duration>>(&self, end_val: T, duration: D, curve: EasingCurve) {
self.ease_to_value(end_val, duration.into(), curve, false);
}
fn ease_to_value(&self, end_val: T, duration: Duration, curve: EasingCurve, overwrite: bool) {
PROPERTY_TABLE.with(|t| {
t.transition(
self.untyped.id,
TransitionQueueEntry {
duration,
curve,
ending_value: end_val,
},
overwrite,
)
})
}
pub fn cancel_transitions(&self) {
PROPERTY_TABLE.with(|t| t.cancel_transitions::<T>(self.untyped.id));
}
pub fn get(&self) -> T {
PROPERTY_TABLE.with(|t| t.get_value(self.untyped.id))
}
pub fn set(&self, val: T) {
PROPERTY_TABLE.with(|t| t.set_value(self.untyped.id, val));
}
#[doc(hidden)]
pub fn invalidate(&self) {
PROPERTY_TABLE.with(|t| t.invalidate(self.untyped.id));
}
pub fn set_if_neq(&self, val: T) -> bool
where
T: PartialEq,
{
let should_set =
PROPERTY_TABLE.with(|t| t.read_value(self.untyped.id, |current: &T| current != &val));
if should_set {
self.set(val);
}
should_set
}
pub fn update(&self, f: impl FnOnce(&mut T)) {
let mut val = self.get();
f(&mut val);
self.set(val);
}
pub fn read<V>(&self, f: impl FnOnce(&T) -> V) -> V {
PROPERTY_TABLE.with(|t| t.read_value(self.untyped.id, f))
}
pub fn replace_with(&self, target: Property<T>) {
PROPERTY_TABLE.with(|t| {
t.replace_property_keep_outbound_connections::<T>(self.untyped.id, target.untyped.id)
})
}
pub fn untyped(&self) -> UntypedProperty {
self.untyped.clone()
}
}
impl<T: PropertyValue> Default for Property<T> {
fn default() -> Self {
Property::new(T::default())
}
}
impl<'de, T: PropertyValue + Deserialize<'de>> Deserialize<'de> for Property<T> {
fn deserialize<D>(deserializer: D) -> Result<Property<T>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = T::deserialize(deserializer)?;
Ok(Property::new(value))
}
}
impl<T: PropertyValue + Serialize> Serialize for Property<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.get().serialize(serializer)
}
}
pub fn property_table_total_properties_count() -> usize {
PROPERTY_TABLE.with(|t| t.total_properties_count())
}
#[doc(hidden)]
pub fn register_effect_property(prop: &Property<()>) {
PROPERTY_TABLE.with(|t| t.register_effect(prop.untyped.id));
}
#[doc(hidden)]
pub fn register_effect_property_with_name(prop: &Property<()>, debug_name: &str) {
PROPERTY_TABLE.with(|t| t.register_effect_with_name(prop.untyped.id, Some(debug_name)));
}
#[doc(hidden)]
pub fn drain_effects(max_iterations: usize) -> usize {
PROPERTY_TABLE.with(|t| t.drain_effects(max_iterations))
}
#[doc(hidden)]
pub fn drain_effects_with_report(max_iterations: usize) -> EffectDrainReport {
PROPERTY_TABLE.with(|t| t.drain_effects_with_report(max_iterations))
}
#[doc(hidden)]
pub fn property_outbound_debug_names(prop: &UntypedProperty) -> Vec<String> {
PROPERTY_TABLE.with(|t| t.outbound_debug_names(prop.id))
}
#[doc(hidden)]
pub fn property_has_direct_outbound(source: &UntypedProperty, outbound: &UntypedProperty) -> bool {
PROPERTY_TABLE.with(|t| t.has_direct_outbound(source.id, outbound.id))
}
pub fn register_time(prop: &Property<u64>) {
PROPERTY_TIME.with_borrow_mut(|time| *time = prop.clone());
}
pub fn register_millis(prop: &Property<u64>) {
PROPERTY_MILLIS.with_borrow_mut(|time| *time = prop.clone());
}