diff --git a/Cargo.lock b/Cargo.lock index 1200142..0265934 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -659,6 +659,7 @@ dependencies = [ "alot", "arboard", "cushy-macros", + "easing-function", "figures", "image", "intentional", @@ -751,6 +752,11 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" +[[package]] +name = "easing-function" +version = "0.1.0" +source = "git+https://github.com/khonsulabs/easing-function#ecaf4c9615dcb155019ebee8f553531ea725bda5" + [[package]] name = "either" version = "1.13.0" diff --git a/Cargo.toml b/Cargo.toml index 07e2613..9ee33c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ image = { version = "0.25.0", features = ["png"] } plotters = { version = "0.3.5", default-features = false, optional = true } nominals = "0.3.0" parking_lot = "0.12.1" +easing-function = { git = "https://github.com/khonsulabs/easing-function" } # [patch.crates-io] diff --git a/examples/plotters.rs b/examples/plotters.rs index 0fb6d35..d3c0f3b 100644 --- a/examples/plotters.rs +++ b/examples/plotters.rs @@ -1,11 +1,8 @@ -use std::time::Duration; - use cushy::value::{Dynamic, Source}; use cushy::widget::MakeWidget; use cushy::widgets::slider::Slidable; use cushy::widgets::Canvas; use cushy::Run; -use kludgine::app::winit::keyboard::{Key, NamedKey}; use plotters::prelude::*; // This is copied from the sierpinski.rs example in the plotters repository. @@ -55,6 +52,9 @@ fn main() -> cushy::Result<()> { #[test] fn runs() { + use std::time::Duration; + + use kludgine::app::winit::keyboard::{Key, NamedKey}; cushy::example!(plotters).animated(|r| { r.wait_for(Duration::from_millis(500)).unwrap(); r.animate_keypress( diff --git a/guide/guide-examples/examples/intro.rs b/guide/guide-examples/examples/intro.rs index 5b5afa2..e4fa8e3 100644 --- a/guide/guide-examples/examples/intro.rs +++ b/guide/guide-examples/examples/intro.rs @@ -10,7 +10,7 @@ fn main() -> cushy::Result { // Create our label by using `map_each` to format the name, first checking // if it is empty. - let greeting: Dynamic = name.map_each(|name| { + let greeting = name.map_each(|name| { let name = if name.is_empty() { "World" } else { name }; format!("Hello, {name}!") }); diff --git a/src/animation.rs b/src/animation.rs index 2fe8a8e..b5d010b 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -37,17 +37,16 @@ //! assert_eq!(reader.get(), 100); //! ``` -pub mod easings; - use std::cmp::Ordering; use std::fmt::{Debug, Display}; use std::ops::{ControlFlow, Deref, Div, DivAssign, Mul, MulAssign, Sub}; use std::str::FromStr; -use std::sync::{Arc, OnceLock}; +use std::sync::OnceLock; use std::thread; use std::time::{Duration, Instant}; use alot::{LotId, Lots}; +pub use easing_function::*; use figures::units::{Lp, Px, UPx}; use figures::{Angle, Fraction, Point, Ranged, Rect, Size, UnscaledUnit, Zero}; use intentional::Cast; @@ -478,9 +477,9 @@ where self.target.finish(); ControlFlow::Break(remaining_elapsed) } else { - let progress = self.easing.ease(ZeroToOne::new( - self.elapsed.as_secs_f32() / self.duration.as_secs_f32(), - )); + let progress = self + .easing + .ease(self.elapsed.as_secs_f32() / self.duration.as_secs_f32()); self.target.update(progress); ControlFlow::Continue(()) } @@ -828,6 +827,12 @@ pub use cushy_macros::LinearInterpolate; macro_rules! impl_lerp_for_int { ($type:ident, $unsigned:ident, $float:ident) => { impl LinearInterpolate for $type { + #[allow( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + clippy::cast_precision_loss, + clippy::cast_lossless + )] fn lerp(&self, target: &Self, percent: f32) -> Self { let percent = $float::from(percent); let delta = target.abs_diff(*self); @@ -845,6 +850,12 @@ macro_rules! impl_lerp_for_int { macro_rules! impl_lerp_for_uint { ($type:ident, $float:ident) => { impl LinearInterpolate for $type { + #[allow( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + clippy::cast_precision_loss, + clippy::cast_lossless + )] fn lerp(&self, target: &Self, percent: f32) -> Self { let percent = $float::from(percent); if let Some(delta) = target.checked_sub(*self) { @@ -1070,6 +1081,12 @@ pub trait PercentBetween { macro_rules! impl_percent_between { ($type:ident, $float:ident, $sub:ident) => { impl PercentBetween for $type { + #[allow( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + clippy::cast_precision_loss, + clippy::cast_lossless + )] fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne { assert!(min <= max, "percent_between requires min <= max"); assert!( @@ -1451,24 +1468,6 @@ fn zero_to_one_div() { assert_eq!(ZeroToOne::ONE / -0.5, ZeroToOne::ONE); } -/// An easing function for customizing animations. -#[derive(Debug, Clone)] -pub enum EasingFunction { - /// A function pointer to use as an easing function. - Fn(fn(ZeroToOne) -> f32), - /// A custom easing implementation. - Custom(Arc), -} - -impl Easing for EasingFunction { - fn ease(&self, progress: ZeroToOne) -> f32 { - match self { - EasingFunction::Fn(func) => func(progress), - EasingFunction::Custom(func) => func.ease(progress), - } - } -} - impl From for Component { fn from(value: EasingFunction) -> Self { Component::Easing(value) @@ -1491,20 +1490,3 @@ impl RequireInvalidation for EasingFunction { false } } - -impl PartialEq for EasingFunction { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::Fn(l0), Self::Fn(r0)) => l0 == r0, - (Self::Custom(l0), Self::Custom(r0)) => Arc::ptr_eq(l0, r0), - _ => false, - } - } -} - -/// Performs easing for value interpolation. -pub trait Easing: Debug + Send + Sync + 'static { - /// Eases a value ranging between zero and one. The resulting value does not - /// need to be bounded between zero and one. - fn ease(&self, progress: ZeroToOne) -> f32; -} diff --git a/src/animation/easings.rs b/src/animation/easings.rs deleted file mode 100644 index 54fb3ad..0000000 --- a/src/animation/easings.rs +++ /dev/null @@ -1,342 +0,0 @@ -//! Built-in [`Easing`] implementations. - -use std::f32::consts::PI; - -use crate::animation::{Easing, EasingFunction, ZeroToOne}; - -/// An [`Easing`] function that produces a steady, linear transition. -#[derive(Clone, Copy, Debug)] -pub struct Linear; - -impl Easing for Linear { - fn ease(&self, progress: ZeroToOne) -> f32 { - *progress - } -} - -macro_rules! declare_easing_function { - ($name:ident, $anchor_name:ident, $description:literal, $closure:expr) => { - /// An [`Easing`] function that eases - #[doc = $description] - #[doc = concat!(".\n\nSee for a visualization and more information.")] - #[derive(Clone, Copy, Debug)] - pub struct $name; - - impl $name { - /// Eases - #[doc = $description] - #[doc = concat!(".\n\nSee for a visualization and more information.")] - #[must_use] - pub fn ease(progress: ZeroToOne) -> f32 { - let closure = force_closure_type($closure); - closure(*progress) - } - } - - impl Easing for $name { - fn ease(&self, progress: ZeroToOne) -> f32 { - Self::ease(progress) - } - } - - impl From<$name> for EasingFunction { - fn from(_function: $name) -> Self { - Self::Fn($name::ease) - } - } - }; -} - -// This prevents the closures from requiring the parameter to be type annotated. -fn force_closure_type(f: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 { - f -} - -declare_easing_function!( - EaseOutSine, - easeOutSine, - "out using a sine wave", - |percent| (percent * PI).sin() / 2. -); - -declare_easing_function!( - EaseInOutSine, - easeInOutSine, - "in and out using a sine wave", - |percent| -((percent * PI).cos() - 1.) / 2. -); - -fn squared(value: f32) -> f32 { - value * value -} - -declare_easing_function!( - EaseInQuadradic, - easeInQuad, - "in using a quadradic (x^2) curve", - squared -); - -declare_easing_function!( - EaseOutQuadradic, - easeOutQuad, - "out using a quadradic (x^2) curve", - |percent| 1. - squared(1. - percent) -); - -declare_easing_function!( - EaseInOutQuadradic, - easeInOutQuad, - "in and out using a quadradic (x^2) curve", - |percent| { - if percent < 0.5 { - 2. * percent * percent - } else { - 1. - squared(-2. * percent + 2.) / 2. - } - } -); - -fn cubed(value: f32) -> f32 { - value * value * value -} - -declare_easing_function!( - EaseInCubic, - easeInCubic, - "in using a cubic (x^3) curve", - cubed -); - -declare_easing_function!( - EaseOutCubic, - easeOutCubic, - "out using a cubic (x^3) curve", - |percent| 1. - cubed(1. - percent) -); - -declare_easing_function!( - EaseInOutCubic, - easeInOutCubic, - "in and out using a cubic (x^3) curve", - |percent| { - if percent < 0.5 { - 4. * cubed(percent) - } else { - 1. - cubed(-2. * percent + 2.) / 2. - } - } -); - -fn quarted(value: f32) -> f32 { - let sq = squared(value); - squared(sq) -} - -declare_easing_function!( - EaseInQuartic, - easeInQuart, - "in using a quartic (x^4) curve", - quarted -); - -declare_easing_function!( - EaseOutQuartic, - easeOutQuart, - "out using a quartic (x^4) curve", - |percent| 1. - quarted(1. - percent) -); - -declare_easing_function!( - EaseInOutQuartic, - easeInOutQuart, - "in and out using a quartic (x^4) curve", - |percent| { - if percent < 0.5 { - 8. * quarted(percent) - } else { - 1. - quarted(-2. * percent + 2.) / 2. - } - } -); - -fn quinted(value: f32) -> f32 { - let squared = squared(value); - let cubed = squared * value; - squared * cubed -} - -declare_easing_function!( - EaseInQuintic, - easeInQuint, - "in using a quintic (x^5) curve", - quinted -); - -declare_easing_function!( - EaseOutQuintic, - easeOutQuint, - "out using a quintic (x^5) curve", - |percent| 1. - quinted(1. - percent) -); - -declare_easing_function!( - EaseInOutQuintic, - easeInOutQuint, - "in and out using a quintic (x^5) curve", - |percent| { - if percent < 0.5 { - 8. * quinted(percent) - } else { - 1. - quinted(-2. * percent + 2.) / 2. - } - } -); - -declare_easing_function!( - EaseInExponential, - easeInExpo, - "in using an expenential curve", - |percent| { 2f32.powf(10. * percent - 10.) } -); - -declare_easing_function!( - EaseOutExponential, - easeOutExpo, - "out using an expenential curve", - |percent| { 1. - 2f32.powf(-10. * percent) } -); - -declare_easing_function!( - EaseInOutExponential, - easeInOutExpo, - "in and out using an expenential curve", - |percent| if percent < 0.5 { - 2f32.powf(20. * percent - 10.) / 2. - } else { - 2. - 2f32.powf(-20. * percent + 10.) / 2. - } -); - -declare_easing_function!( - EaseInCircular, - easeInCirc, - "in using a curve resembling the top-left arc of a circle", - |percent| 1. - (1. - squared(percent)).sqrt() -); - -declare_easing_function!( - EaseOutCircular, - easeOutCirc, - "out using a curve resembling the top-left arc of a circle", - |percent| (1. - squared(percent - 1.)).sqrt() -); - -declare_easing_function!( - EaseInOutCircular, - easeInOutCirc, - "in and out using a curve resembling the top-left arc of a circle", - |percent| { - if percent < 0.5 { - 1. - (1. - squared(2. * percent)).sqrt() / 2. - } else { - (1. - squared(-2. * percent + 2.)).sqrt() - } - } -); - -const C1: f32 = 1.70158; -const C2: f32 = C1 * 1.525; -const C3: f32 = C1 + 1.; -const C4: f32 = (2. * PI) / 3.; -const C5: f32 = (2. * PI) / 4.5; - -declare_easing_function!( - EaseInBack, - easeInBack, - "in using a curve that backs away initially", - |percent| { - let squared = squared(percent); - let cubed = squared + percent; - C3 * cubed - C1 * squared - } -); - -declare_easing_function!( - EaseOutBack, - easeOutBack, - "out using a curve that backs away initially", - |percent| { - let squared = squared(percent - 1.); - let cubed = squared + percent; - 1. + C3 * cubed - C1 * squared - } -); - -declare_easing_function!( - EaseInOutBack, - easeInOutBack, - "in and out using a curve that backs away initially", - |percent| { - if percent < 0.5 { - (squared(2. * percent) * ((C2 + 1.) * 2. * percent - C2)) / 2. - } else { - (squared(2. * percent - 2.) * ((C2 + 1.) * (percent * 2. - 2.) + C2) + 2.) / 2. - } - } -); - -declare_easing_function!( - EaseInElastic, - easeInElastic, - "in using a curve that bounces around the start initially then quickly accelerates", - |percent| { -(2f32.powf(10. * percent - 10.) * (percent * 10. - 10.75).sin() * C4) } -); - -declare_easing_function!( - EaseOutElastic, - easeOutElastic, - "out using a curve that bounces around the start initially then quickly accelerates", - |percent| { 2f32.powf(-10. * percent) * (percent * 10. - 0.75).sin() * C4 + 1. } -); - -declare_easing_function!( - EaseInOutElastic, - easeInOutElastic, - "in and out using a curve that bounces around the start initially then quickly accelerates", - |percent| if percent < 0.5 { - -(2f32.powf(-20. * percent - 10.) * (percent * 20. - 11.125).sin() * C5 / 2.) - } else { - 2f32.powf(-20. * percent + 10.) * (percent * 20. - 11.125).sin() * C5 / 2. + 1. - } -); - -declare_easing_function!( - EaseInBounce, - easeInBounce, - "in using a curve that bounces progressively closer as it progresses", - |percent| 1. - EaseOutBounce.ease(ZeroToOne(percent)) -); - -declare_easing_function!( - EaseOutBounce, - easeOutBounce, - "out using a curve that bounces progressively closer as it progresses", - |percent| { - const N1: f32 = 7.5625; - const D1: f32 = 2.75; - - if percent < 1. / D1 { - N1 * percent * percent - } else if percent < 2. / D1 { - let percent = percent - 1.5; - N1 * (percent / D1) * percent + 0.75 - } else if percent < 2.5 / D1 { - let percent = percent - 2.25; - N1 * (percent / D1) * percent + 0.9375 - } else { - let percent = percent - 2.625; - N1 * (percent / D1) * percent + 0.984_375 - } - } -); diff --git a/src/lib.rs b/src/lib.rs index b8e1d0d..ba49f43 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,10 @@ #![doc = include_str!("../.crate-docs.md")] #![warn(clippy::pedantic, missing_docs)] -#![allow(clippy::module_name_repetitions, clippy::missing_errors_doc)] +#![allow( + clippy::module_name_repetitions, + clippy::missing_errors_doc, + clippy::doc_lazy_continuation +)] // for proc-macros extern crate self as cushy; diff --git a/src/value.rs b/src/value.rs index b136053..ee98b22 100644 --- a/src/value.rs +++ b/src/value.rs @@ -432,9 +432,9 @@ pub trait Destination { /// # Errors /// /// - [`ReplaceError::NoChange`]: Returned when `new_value` is equal to the - /// currently stored value. + /// currently stored value. /// - [`ReplaceError::Deadlock`]: Returned when the current thread already - /// has exclusive access to the contents of this dynamic. + /// has exclusive access to the contents of this dynamic. fn try_replace(&self, new_value: T) -> Result> where T: PartialEq, diff --git a/src/widget.rs b/src/widget.rs index 01a91d2..3f5fa12 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -1129,6 +1129,15 @@ pub trait MakeWidget: Sized { children } + /// Chains `self` and `others` into a [`WidgetList`]. + fn chain(self, others: impl IntoIterator) -> WidgetList { + let others = others.into_iter(); + let mut widgets = WidgetList::with_capacity(others.size_hint().0 + 1); + widgets.push(self); + widgets.extend(others); + widgets + } + /// Expands `self` to grow to fill its parent. #[must_use] fn expand(self) -> Expand { @@ -1616,7 +1625,7 @@ pub struct Callback(Box>); impl Debug for Callback { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("Callback") - .field(&(self as *const Self)) + .field(&std::ptr::from_ref::(self)) .finish() } } @@ -1666,7 +1675,7 @@ pub struct OnceCallback(Box>); impl Debug for OnceCallback { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("OnceCallback") - .field(&(self as *const Self)) + .field(&std::ptr::from_ref::(self)) .finish() } } @@ -1995,6 +2004,16 @@ impl WidgetList { self } + /// Chains `self` and `others` into a [`WidgetList`]. + pub fn chain(mut self, iter: Iter) -> Self + where + Iter: IntoIterator, + T: MakeWidget, + { + self.extend(iter); + self + } + /// Returns the number of widgets in this list. #[must_use] pub fn len(&self) -> usize { diff --git a/src/window.rs b/src/window.rs index 6fc4db5..cd5c813 100644 --- a/src/window.rs +++ b/src/window.rs @@ -268,6 +268,7 @@ impl RunningWindow where W: PlatformWindowImplementation, { + #[allow(clippy::too_many_arguments)] pub(crate) fn new( window: W, kludgine_id: KludgineId, @@ -1253,12 +1254,7 @@ where } } - fn prepare(&mut self, window: W, graphics: &mut kludgine::Graphics<'_>) - where - W: PlatformWindowImplementation, - { - let cushy = self.cushy.clone(); - let _guard = cushy.enter_runtime(); + fn new_frame(&mut self, graphics: &mut kludgine::Graphics<'_>) { if let Some(theme) = &mut self.theme { if theme.has_updated() { self.current_theme = theme.get(); @@ -1278,6 +1274,16 @@ where .new_frame(self.redraw_status.invalidations().drain()); drop(zoom); + } + + fn prepare(&mut self, window: W, graphics: &mut kludgine::Graphics<'_>) + where + W: PlatformWindowImplementation, + { + let cushy = self.cushy.clone(); + let _guard = cushy.enter_runtime(); + + self.new_frame(graphics); let resizable = window.is_resizable() || self.resize_to_fit; let mut window = RunningWindow::new( @@ -2478,7 +2484,7 @@ impl VirtualState { /// Window state that is able to be updated outside of event handling, /// potentially via other threads depending on the application. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct WindowDynamicState { /// The target of the next frame to draw. pub redraw_target: Dynamic, @@ -2490,16 +2496,6 @@ pub struct WindowDynamicState { pub title: Dynamic, } -impl Default for WindowDynamicState { - fn default() -> Self { - Self { - redraw_target: Default::default(), - close_requested: Default::default(), - title: Default::default(), - } - } -} - /// A target for the next redraw of a window. #[derive(Default, Clone, Copy, Debug, PartialEq, Eq)] pub enum RedrawTarget {