Making all easing functions themable

This commit is contained in:
Jonathan Johnson 2024-08-27 08:32:51 -07:00
parent e773bc123a
commit 9a148b8765
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
8 changed files with 205 additions and 22 deletions

View file

@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<!-- markdownlint-disable no-duplicate-heading -->
## Unreleased
### Fixed
- `Collapse`, `OverlayLayer`, and `Progress` all honor the theme components
`EasingIn` and `EasingOut` rather than hard-coded easing functions.
### Added
- `ComponentProbe<T>` is a new widget that allows reading a
`ComponentDefinition` value from the theme at runtime through a
`Dynamic<T::ComponentType>`. For example, a `ComponentProbe<TextSize>` will
provide access to a `Dynamic<Dimension>`. 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

View file

@ -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<Self> {
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<Self> {
ComponentProbe::default_wrapping(self, child)
}
}
/// Describes whether a type should invalidate a widget.
pub trait RequireInvalidation {
/// Cushy tracks two different states:

View file

@ -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.

View file

@ -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;

View file

@ -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 => {}

View file

@ -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<Component>
where
Component: ComponentDefinition,
{
component: Component,
probed: Dynamic<Component::ComponentType>,
child: WidgetRef,
}
impl<Component> ComponentProbe<Component>
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<Component::ComponentType> {
&self.probed
}
}
impl<Component> WrapperWidget for ComponentProbe<Component>
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));
}
}

View file

@ -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<OverlayState>,
easing: Dynamic<EasingFunction>,
}
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();
}
}

View file

@ -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<ZeroToOne>,
end: &Dynamic<ZeroToOne>,
degree_offset: Option<&Dynamic<Angle>>,
ease_in: &Dynamic<EasingFunction>,
ease_out: &Dynamic<EasingFunction>,
) {
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()