diff --git a/CHANGELOG.md b/CHANGELOG.md index face9bf..5b3eb6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## Unreleased + +### Fixed + +- `Collapse`, `OverlayLayer`, and `Progress` all honor the theme components + `EasingIn` and `EasingOut` rather than hard-coded easing functions. + +### Added + +- `ComponentProbe` is a new widget that allows reading a + `ComponentDefinition` value from the theme at runtime through a + `Dynamic`. For example, a `ComponentProbe` will + provide access to a `Dynamic`. Previously this required creating a + custom widget to access the runtime theme information. + + `ContextFreeComponent::probe()` and `ContextFreeComponent::probe_wrapping()` + provide an easy interface for creating probes from components. + ## v0.4.0 (2024-08-20) ### Breaking Changes diff --git a/src/styles.rs b/src/styles.rs index 31dff60..1122682 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -25,7 +25,9 @@ use crate::context::{Trackable, WidgetContext}; use crate::names::Name; use crate::utils::Lazy; use crate::value::{Dynamic, IntoValue, Source, Value}; +use crate::widget::MakeWidget; use crate::widgets::input::CowString; +use crate::widgets::ComponentProbe; #[macro_use] pub mod components; @@ -1135,6 +1137,25 @@ pub trait ComponentDefinition: NamedComponent { fn default_value(&self, context: &WidgetContext<'_>) -> Self::ComponentType; } +/// A [`ComponentDefinition`] that can provide a default value without access to +/// a runtime context. +pub trait ContextFreeComponent: ComponentDefinition { + /// Returns the default value for this component. + fn default(&self) -> Self::ComponentType; + + /// Returns a new probe that provides access to the runtime value of this + /// component. + fn probe(self) -> ComponentProbe { + ComponentProbe::default_for(self) + } + + /// Returns a new probe wrapping `child` that provides access to the runtime + /// value of this component. + fn probe_wrapping(self, child: impl MakeWidget) -> ComponentProbe { + ComponentProbe::default_wrapping(self, child) + } +} + /// Describes whether a type should invalidate a widget. pub trait RequireInvalidation { /// Cushy tracks two different states: diff --git a/src/styles/components.rs b/src/styles/components.rs index bc2769c..5b4cb19 100644 --- a/src/styles/components.rs +++ b/src/styles/components.rs @@ -62,6 +62,8 @@ macro_rules! define_components { define_components!($type, $($($default)*)?); } + + define_components!(default $component $type, $($($default)*)?); }; )*)*}; @@ -90,6 +92,29 @@ macro_rules! define_components { ($type:ty, $($expr:tt)+) => { define_components!($type, |_context| $($expr)*); }; + (default $component:ident $type:ty, . $($path:tt)*) => { + + }; + (default $component:ident $type:ty, |$context:ident| $($expr:tt)*) => { + }; + (default $component:ident $type:ty, @$path:path) => { + }; + (default $component:ident $type:ty, contrasting!($bg:ident, $($fg:ident),+ $(,)?)) => { + }; + (default $component:ident $type:ty, ) => { + impl $crate::styles::ContextFreeComponent for $component { + fn default(&self) -> Self::ComponentType { + <$type>::default() + } + } + }; + (default $component:ident $type:ty, $($expr:tt)+) => { + impl $crate::styles::ContextFreeComponent for $component { + fn default(&self) -> Self::ComponentType { + $($expr)* + } + } + }; } define_components! { @@ -204,7 +229,7 @@ define_components! { LayoutOrder(VisualOrder, "visual_order", VisualOrder::left_to_right()) /// The set of controls to allow focusing via tab key and initial focus /// selection. - AutoFocusableControls(FocusableWidgets, "focus", FocusableWidgets::default()) + AutoFocusableControls(FocusableWidgets, "focus") /// A [`Color`] to be used as the background color of a widget. WidgetBackground(Color, "widget_backgrond_color", Color::CLEAR_WHITE) /// A [`Color`] to be used to accent a widget. diff --git a/src/widgets.rs b/src/widgets.rs index 70fb672..8c17522 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -6,6 +6,7 @@ mod canvas; pub mod checkbox; mod collapse; pub mod color; +mod component_probe; pub mod container; mod custom; mod data; @@ -40,6 +41,7 @@ pub use self::button::Button; pub use self::canvas::Canvas; pub use self::checkbox::Checkbox; pub use self::collapse::Collapse; +pub use self::component_probe::ComponentProbe; pub use self::container::Container; pub use self::custom::Custom; pub use self::data::Data; diff --git a/src/widgets/collapse.rs b/src/widgets/collapse.rs index 59b1672..878b5d3 100644 --- a/src/widgets/collapse.rs +++ b/src/widgets/collapse.rs @@ -3,9 +3,9 @@ use std::time::Duration; use figures::units::Px; use figures::{Size, Zero}; -use crate::animation::easings::{EaseInQuadradic, EaseOutQuadradic}; -use crate::animation::{AnimationHandle, AnimationTarget, EasingFunction, Spawn}; +use crate::animation::{AnimationHandle, AnimationTarget, Spawn}; use crate::context::LayoutContext; +use crate::styles::components::{EasingIn, EasingOut}; use crate::value::{Dynamic, IntoDynamic, Source}; use crate::widget::{MakeWidget, WidgetRef, WrappedLayout, WrapperWidget}; use crate::ConstraintLimit; @@ -53,9 +53,9 @@ impl Collapse { fn note_child_size(&mut self, size: Px, context: &mut LayoutContext<'_, '_, '_, '_>) { let (easing, target) = if self.collapse.get_tracking_invalidate(context) { - (EasingFunction::from(EaseOutQuadradic), Px::ZERO) + (context.get(&EasingOut), Px::ZERO) } else { - (EasingFunction::from(EaseInQuadradic), size) + (context.get(&EasingIn), size) }; match &self.collapse_animation { Some(state) if state.target == target => {} diff --git a/src/widgets/component_probe.rs b/src/widgets/component_probe.rs new file mode 100644 index 0000000..d8b9a25 --- /dev/null +++ b/src/widgets/component_probe.rs @@ -0,0 +1,96 @@ +use std::fmt::Debug; + +use super::Space; +use crate::styles::{ComponentDefinition, ContextFreeComponent}; +use crate::value::{Destination, Dynamic}; +use crate::widget::{MakeWidget, WidgetRef, WrapperWidget}; + +/// A widget that provides access to a [`ComponentDefinition`]'s value through a +/// [`Dynamic`]. +/// +/// This widget enables access to runtime values provided by the theme without +/// creating a custom widget. After creating a probe, [`value()`](Self::value) +/// can be used to observe and use the value. +/// +/// The theme information retrieved will be the effective theme at the location +/// the probe is inserted in the widget hierarchy. +#[derive(Debug)] +pub struct ComponentProbe +where + Component: ComponentDefinition, +{ + component: Component, + probed: Dynamic, + child: WidgetRef, +} + +impl ComponentProbe +where + Component: ComponentDefinition, +{ + /// Returns a new probe that provides access to the runtime value of + /// `Component`. + /// + /// The initial contents of the dynamic will be `initial_value`. + pub fn new(component: Component, initial_value: Component::ComponentType) -> Self { + Self::new_wrapping(component, initial_value, Space::clear()) + } + + /// Returns a new probe wrapping `child` that provides access to the runtime + /// value of this component. + /// + /// The initial contents of the dynamic will be `initial_value`. + pub fn new_wrapping( + component: Component, + initial_value: Component::ComponentType, + child: impl MakeWidget, + ) -> Self { + Self { + component, + probed: Dynamic::new(initial_value), + child: WidgetRef::new(child), + } + } + + /// Returns a new probe that provides access to the runtime value of + /// `Component`. + pub fn default_for(component: Component) -> Self + where + Component: ContextFreeComponent, + { + let default = component.default(); + Self::new(component, default) + } + + /// Returns a new probe wrapping `child` that provides access to the runtime + /// value of this component. + pub fn default_wrapping(component: Component, child: impl MakeWidget) -> Self + where + Component: ContextFreeComponent, + { + let default = component.default(); + Self::new_wrapping(component, default, child) + } + + /// Returns the dynamic that contains the component's current value. + /// + /// This dynamic's contents will be updated whenever this probe is + /// invalidated. + pub const fn value(&self) -> &Dynamic { + &self.probed + } +} + +impl WrapperWidget for ComponentProbe +where + Component: ComponentDefinition + Debug + Send + 'static, + Component::ComponentType: PartialEq + Debug + Send + 'static, +{ + fn child_mut(&mut self) -> &mut crate::widget::WidgetRef { + &mut self.child + } + + fn redraw_foreground(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_>) { + self.probed.set(context.get(&self.component)); + } +} diff --git a/src/widgets/layers.rs b/src/widgets/layers.rs index 3e809ee..ba37ba9 100644 --- a/src/widgets/layers.rs +++ b/src/widgets/layers.rs @@ -6,14 +6,15 @@ use std::time::Duration; use alot::{LotId, OrderedLots}; use cushy::widget::{RootBehavior, WidgetInstance}; +use easing_function::EasingFunction; use figures::units::{Lp, Px, UPx}; use figures::{IntoSigned, IntoUnsigned, Point, Rect, Size, Zero}; use intentional::Assert; use parking_lot::Mutex; -use crate::animation::easings::EaseOutQuadradic; use crate::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn, ZeroToOne}; use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, Trackable}; +use crate::styles::components::EasingIn; use crate::value::{Destination, Dynamic, DynamicGuard, IntoValue, Source, Value}; use crate::widget::{ Callback, MakeWidget, MountedChildren, MountedWidget, Widget, WidgetId, WidgetList, WidgetRef, @@ -139,6 +140,7 @@ impl Widget for Layers { #[derive(Debug, Clone, Default)] pub struct OverlayLayer { state: Dynamic, + easing: Dynamic, } impl OverlayLayer { @@ -189,6 +191,7 @@ impl OverlayLayer { impl Widget for OverlayLayer { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { + self.easing.set(context.get(&EasingIn)); let state = self.state.lock(); for child in &state.overlays { @@ -691,7 +694,7 @@ impl OverlayBuilder<'_> { .opacity .transition_to(ZeroToOne::ONE) .over(Duration::from_millis(250)) - .with_easing(EaseOutQuadradic) + .with_easing(self.overlay.easing.get()) .launch(); } } diff --git a/src/widgets/progress.rs b/src/widgets/progress.rs index a857d72..de5e4cb 100644 --- a/src/widgets/progress.rs +++ b/src/widgets/progress.rs @@ -3,15 +3,17 @@ use std::ops::RangeInclusive; use std::time::Duration; +use easing_function::EasingFunction; use figures::units::Px; use figures::{Angle, Point, Ranged, ScreenScale, Size, Zero}; use kludgine::shapes::{Path, StrokeOptions}; use kludgine::Color; -use crate::animation::easings::{EaseInQuadradic, EaseOutQuadradic}; use crate::animation::{ AnimationHandle, AnimationTarget, IntoAnimate, PercentBetween, Spawn, ZeroToOne, }; +use crate::styles::components::{EasingIn, EasingOut}; +use crate::styles::ContextFreeComponent; 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}; @@ -91,29 +93,41 @@ impl MakeWidgetWithTag for ProgressBar { ) }; + let ease_in_probe = EasingIn.probe_wrapping(slider); + let ease_in = ease_in_probe.value().clone(); + let ease_out_probe = EasingOut.probe_wrapping(ease_in_probe); + let ease_out = ease_out_probe.value().clone(); update_progress_bar( self.progress.get(), &mut indeterminant_animation, &start, &end, degree_offset.as_ref(), + &ease_in, + &ease_out, ); match self.progress { ReadOnly::Reader(progress) => { - let callback = progress.for_each(move |progress| { - update_progress_bar( - *progress, - &mut indeterminant_animation, - &start, - &end, - degree_offset.as_ref(), - ); + let callback = progress.for_each({ + let ease_in = ease_in.clone(); + let ease_out = ease_out.clone(); + move |progress| { + update_progress_bar( + *progress, + &mut indeterminant_animation, + &start, + &end, + degree_offset.as_ref(), + &ease_in, + &ease_out, + ); + } }); - Data::new_wrapping((callback, progress), slider).make_widget() + Data::new_wrapping((callback, progress), ease_out_probe).make_widget() } ReadOnly::Constant(_) => { - Data::new_wrapping(indeterminant_animation, slider).make_widget() + Data::new_wrapping(indeterminant_animation, ease_out_probe).make_widget() } } } @@ -131,10 +145,14 @@ fn update_progress_bar( start: &Dynamic, end: &Dynamic, degree_offset: Option<&Dynamic>, + ease_in: &Dynamic, + ease_out: &Dynamic, ) { match progress { Progress::Indeterminant => { if indeterminant_animation.is_none() { + let ease_in = ease_in.get(); + let ease_out = ease_out.get(); *indeterminant_animation = Some(IndeterminantAnimations { _primary: ( start @@ -145,25 +163,25 @@ fn update_progress_bar( start .transition_to(ZeroToOne::new(0.33)) .over(Duration::from_millis(500)) - .with_easing(EaseInQuadradic), + .with_easing(ease_in.clone()), ) .and_then( start .transition_to(ZeroToOne::new(1.0)) .over(Duration::from_millis(500)) - .with_easing(EaseOutQuadradic), + .with_easing(ease_out.clone()), ), end.transition_to(ZeroToOne::ZERO) .immediately() .and_then( end.transition_to(ZeroToOne::new(0.75)) .over(Duration::from_millis(500)) - .with_easing(EaseInQuadradic), + .with_easing(ease_in), ) .and_then( end.transition_to(ZeroToOne::ONE) .over(Duration::from_millis(250)) - .with_easing(EaseOutQuadradic), + .with_easing(ease_out.clone()), ), ) .cycle()