diff --git a/CHANGELOG.md b/CHANGELOG.md index 7adc7784b..c3832ce3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,10 @@ Please only add new entries below the [Unreleased](#unreleased---releasedate) he ## [@Unreleased] - @ReleaseDate +### Features + +- **core**: Added the smooth widgets for transitioning the layout position and size. (#645 @M-Adoo) + ## [0.4.0-alpha.14] - 2024-10-30 ### Features diff --git a/core/src/animation/animate_state.rs b/core/src/animation/animate_state.rs index 96c91926c..677ec44a6 100644 --- a/core/src/animation/animate_state.rs +++ b/core/src/animation/animate_state.rs @@ -3,7 +3,7 @@ use std::convert::Infallible; use rxrust::{observable::ObservableExt, ops::box_it::BoxOp, prelude::BoxIt}; use super::*; -use crate::state::{ModifyScope, StateWatcher, StateWriter}; +use crate::prelude::*; /// Trait to help animate update the state. pub trait AnimateStateSetter { @@ -19,6 +19,36 @@ pub trait AnimateStateSetter { /// Trait to help animate calc the lerp value. pub trait AnimateState: AnimateStateSetter { fn calc_lerp_value(&mut self, from: &Self::Value, to: &Self::Value, rate: f32) -> Self::Value; + + /// Use an animate to transition the state after it modified. + fn transition( + self, transition: impl Transition + 'static, ctx: &BuildCtx, + ) -> Stateful> + where + Self: Sized, + Self::Value: PartialEq, + { + let state = self.clone_setter(); + let animate = Animate::declarer() + .transition(Box::new(transition)) + .from(self.get()) + .state(self) + .finish(ctx); + + let c_animate = animate.clone_writer(); + let init_value = observable::of(state.get()); + state + .animate_state_modifies() + .map(move |_| state.get()) + .merge(init_value) + .distinct_until_changed() + .pairwise() + .subscribe(move |(old, _)| { + animate.write().from = old; + animate.run(); + }); + c_animate + } } /// A state with a lerp function as an animation state that use the `lerp_fn` diff --git a/core/src/animation/transition.rs b/core/src/animation/transition.rs index fd2b677d9..4d0461def 100644 --- a/core/src/animation/transition.rs +++ b/core/src/animation/transition.rs @@ -23,54 +23,6 @@ pub struct RepeatTransition { pub transition: T, } -/// Trait help to transition the state. -pub trait TransitionState: Sized + 'static { - /// Use an animate to transition the state after it modified. - fn transition( - self, transition: impl Transition + 'static, ctx: &BuildCtx, - ) -> Stateful> - where - Self: AnimateState, - { - let state = self.clone_setter(); - let animate = Animate::declarer() - .transition(Box::new(transition)) - .from(self.get()) - .state(self) - .finish(ctx); - - let c_animate = animate.clone_writer(); - let init_value = observable::of(state.get()); - state - .animate_state_modifies() - .map(move |_| state.get()) - .merge(init_value) - .pairwise() - .subscribe(move |(old, _)| { - animate.write().from = old; - animate.run(); - }); - c_animate - } -} - -impl TransitionState for S {} - -/// Transition the state with a lerp function. -pub trait TransitionWithFn: AnimateStateSetter + Sized { - fn transition_with( - self, transition: Box, lerp_fn: F, ctx: &BuildCtx, - ) -> Stateful>> - where - F: FnMut(&Self::Value, &Self::Value, f32) -> Self::Value + 'static, - { - let animate_state = LerpFnState::new(self, lerp_fn); - animate_state.transition(transition, ctx) - } -} - -impl TransitionWithFn for S where S: AnimateStateSetter {} - /// Transition is a trait to help you calc the rate of change over time. pub trait Transition { /// Calc the rate of change of the duration from animation start. diff --git a/core/src/builtin_widgets.rs b/core/src/builtin_widgets.rs index da2a3a9aa..2b7ce8d3b 100644 --- a/core/src/builtin_widgets.rs +++ b/core/src/builtin_widgets.rs @@ -71,6 +71,8 @@ mod constrained_box; pub use constrained_box::*; mod text_style; pub use text_style::*; +mod smooth_layout; +pub use smooth_layout::*; use crate::prelude::*; diff --git a/core/src/builtin_widgets/align.rs b/core/src/builtin_widgets/align.rs index 28fdae63b..1b37a91ea 100644 --- a/core/src/builtin_widgets/align.rs +++ b/core/src/builtin_widgets/align.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, wrap_render::WrapRender}; +use crate::{prelude::*, wrap_render::*}; /// A enum that describe how widget align to its box. #[derive(Default, Clone, Copy, PartialEq, Eq, Hash)] @@ -77,21 +77,8 @@ impl Declare for VAlignWidget { fn declarer() -> Self::Builder { FatObj::new(()) } } -impl<'c> ComposeChild<'c> for HAlignWidget { - type Child = Widget<'c>; - - fn compose_child(this: impl StateWriter, child: Self::Child) -> Widget<'c> { - WrapRender::combine_child(this, child) - } -} - -impl<'c> ComposeChild<'c> for VAlignWidget { - type Child = Widget<'c>; - - fn compose_child(this: impl StateWriter, child: Self::Child) -> Widget<'c> { - WrapRender::combine_child(this, child) - } -} +impl_compose_child_for_wrap_render!(HAlignWidget); +impl_compose_child_for_wrap_render!(VAlignWidget); impl WrapRender for HAlignWidget { fn perform_layout(&self, mut clamp: BoxClamp, host: &dyn Render, ctx: &mut LayoutCtx) -> Size { diff --git a/core/src/builtin_widgets/anchor.rs b/core/src/builtin_widgets/anchor.rs index 538faad17..f83067082 100644 --- a/core/src/builtin_widgets/anchor.rs +++ b/core/src/builtin_widgets/anchor.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, wrap_render::WrapRender}; +use crate::{prelude::*, wrap_render::*}; /// Specifies the horizontal position you want to anchor the widget. #[derive(Debug, Clone, Copy)] @@ -171,13 +171,7 @@ impl Declare for RelativeAnchor { fn declarer() -> Self::Builder { FatObj::new(()) } } -impl<'c> ComposeChild<'c> for RelativeAnchor { - type Child = Widget<'c>; - - fn compose_child(this: impl StateWriter, child: Self::Child) -> Widget<'c> { - WrapRender::combine_child(this, child) - } -} +impl_compose_child_for_wrap_render!(RelativeAnchor); impl WrapRender for RelativeAnchor { fn perform_layout(&self, clamp: BoxClamp, host: &dyn Render, ctx: &mut LayoutCtx) -> Size { diff --git a/core/src/builtin_widgets/box_decoration.rs b/core/src/builtin_widgets/box_decoration.rs index 97885f472..465ccc1b2 100644 --- a/core/src/builtin_widgets/box_decoration.rs +++ b/core/src/builtin_widgets/box_decoration.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, wrap_render::WrapRender}; +use crate::{prelude::*, wrap_render::*}; /// The BoxDecoration provides configuration options to draw the background and /// border of a box. @@ -36,19 +36,13 @@ pub struct BorderSide { pub width: f32, } -impl<'c> ComposeChild<'c> for BoxDecoration { - type Child = Widget<'c>; - - fn compose_child(this: impl StateWriter, child: Self::Child) -> Widget<'c> { - WrapRender::combine_child(this, child) - } -} - impl BorderSide { #[inline] pub fn new(width: f32, color: Brush) -> Self { Self { width, color } } } +impl_compose_child_for_wrap_render!(BoxDecoration); + impl WrapRender for BoxDecoration { #[inline] fn perform_layout(&self, clamp: BoxClamp, host: &dyn Render, ctx: &mut LayoutCtx) -> Size { diff --git a/core/src/builtin_widgets/constrained_box.rs b/core/src/builtin_widgets/constrained_box.rs index 6845358c7..321d2c3dc 100644 --- a/core/src/builtin_widgets/constrained_box.rs +++ b/core/src/builtin_widgets/constrained_box.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, wrap_render::WrapRender}; +use crate::{prelude::*, wrap_render::*}; /// a widget that imposes additional constraints clamp on its child. #[derive(Clone, Default)] @@ -12,13 +12,7 @@ impl Declare for ConstrainedBox { fn declarer() -> Self::Builder { FatObj::new(()) } } -impl<'c> ComposeChild<'c> for ConstrainedBox { - type Child = Widget<'c>; - - fn compose_child(this: impl StateWriter, child: Self::Child) -> Widget<'c> { - WrapRender::combine_child(this, child) - } -} +impl_compose_child_for_wrap_render!(ConstrainedBox); impl WrapRender for ConstrainedBox { fn perform_layout(&self, clamp: BoxClamp, host: &dyn Render, ctx: &mut LayoutCtx) -> Size { diff --git a/core/src/builtin_widgets/foreground.rs b/core/src/builtin_widgets/foreground.rs index 2c17090c8..1d843577a 100644 --- a/core/src/builtin_widgets/foreground.rs +++ b/core/src/builtin_widgets/foreground.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, wrap_render::WrapRender}; +use crate::{prelude::*, wrap_render::*}; /// A widget that sets the brush for foreground elements. It's can be inherited /// by its descendants. When meet a color of `background`, the foreground will @@ -15,13 +15,7 @@ impl Declare for Foreground { fn declarer() -> Self::Builder { FatObj::new(()) } } -impl<'c> ComposeChild<'c> for Foreground { - type Child = Widget<'c>; - - fn compose_child(this: impl StateWriter, child: Self::Child) -> Widget<'c> { - WrapRender::combine_child(this, child) - } -} +impl_compose_child_for_wrap_render!(Foreground); impl WrapRender for Foreground { fn perform_layout(&self, clamp: BoxClamp, host: &dyn Render, ctx: &mut LayoutCtx) -> Size { diff --git a/core/src/builtin_widgets/ignore_pointer.rs b/core/src/builtin_widgets/ignore_pointer.rs index e090b4639..501fee488 100644 --- a/core/src/builtin_widgets/ignore_pointer.rs +++ b/core/src/builtin_widgets/ignore_pointer.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, wrap_render::WrapRender}; +use crate::{prelude::*, wrap_render::*}; #[derive(Declare, Clone)] pub struct IgnorePointer { @@ -6,13 +6,7 @@ pub struct IgnorePointer { pub ignore: bool, } -impl<'c> ComposeChild<'c> for IgnorePointer { - type Child = Widget<'c>; - - fn compose_child(this: impl StateWriter, child: Self::Child) -> Widget<'c> { - WrapRender::combine_child(this, child) - } -} +impl_compose_child_for_wrap_render!(IgnorePointer); impl WrapRender for IgnorePointer { #[inline] diff --git a/core/src/builtin_widgets/opacity.rs b/core/src/builtin_widgets/opacity.rs index 1161715a8..b443e7f80 100644 --- a/core/src/builtin_widgets/opacity.rs +++ b/core/src/builtin_widgets/opacity.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, wrap_render::WrapRender}; +use crate::{prelude::*, wrap_render::*}; #[derive(Clone)] pub struct Opacity { @@ -16,13 +16,7 @@ impl Default for Opacity { fn default() -> Self { Self { opacity: 1.0 } } } -impl<'c> ComposeChild<'c> for Opacity { - type Child = Widget<'c>; - - fn compose_child(this: impl StateWriter, child: Self::Child) -> Widget<'c> { - WrapRender::combine_child(this, child) - } -} +impl_compose_child_for_wrap_render!(Opacity); impl WrapRender for Opacity { fn perform_layout(&self, clamp: BoxClamp, host: &dyn Render, ctx: &mut LayoutCtx) -> Size { diff --git a/core/src/builtin_widgets/padding.rs b/core/src/builtin_widgets/padding.rs index 1aa4f7d3d..2ca5150b2 100644 --- a/core/src/builtin_widgets/padding.rs +++ b/core/src/builtin_widgets/padding.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, wrap_render::WrapRender}; +use crate::{prelude::*, wrap_render::*}; /// A widget that insets its child by the given padding. #[derive(Default)] @@ -12,13 +12,7 @@ impl Declare for Padding { fn declarer() -> Self::Builder { FatObj::new(()) } } -impl<'c> ComposeChild<'c> for Padding { - type Child = Widget<'c>; - - fn compose_child(this: impl StateWriter, child: Self::Child) -> Widget<'c> { - WrapRender::combine_child(this, child) - } -} +impl_compose_child_for_wrap_render!(Padding); impl WrapRender for Padding { fn perform_layout(&self, clamp: BoxClamp, host: &dyn Render, ctx: &mut LayoutCtx) -> Size { diff --git a/core/src/builtin_widgets/painting_style.rs b/core/src/builtin_widgets/painting_style.rs index ce936194d..4e1c1e338 100644 --- a/core/src/builtin_widgets/painting_style.rs +++ b/core/src/builtin_widgets/painting_style.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, wrap_render::WrapRender}; +use crate::{prelude::*, wrap_render::*}; /// Explain the method for rendering shapes and paths, including filling or /// stroking them. @@ -24,13 +24,7 @@ impl Declare for PaintingStyleWidget { fn declarer() -> Self::Builder { FatObj::new(()) } } -impl<'c> ComposeChild<'c> for PaintingStyleWidget { - type Child = Widget<'c>; - - fn compose_child(this: impl StateWriter, child: Self::Child) -> Widget<'c> { - WrapRender::combine_child(this, child) - } -} +impl_compose_child_for_wrap_render!(PaintingStyleWidget); impl WrapRender for PaintingStyleWidget { fn perform_layout(&self, clamp: BoxClamp, host: &dyn Render, ctx: &mut LayoutCtx) -> Size { diff --git a/core/src/builtin_widgets/smooth_layout.rs b/core/src/builtin_widgets/smooth_layout.rs new file mode 100644 index 000000000..4c098f1f4 --- /dev/null +++ b/core/src/builtin_widgets/smooth_layout.rs @@ -0,0 +1,174 @@ +//! Widgets use animation to transition the layout position or size from the +//! previous layout state after each layout performed. +//! +//! While animation can work on any state of the render widget, the layout +//! information is publicly read-only data provided by the framework. Therefore, +//! providing animation for transitioning a widget's layout size and position +//! can be challenging. The `smooth_layout` module offers six widgets - +//! `SmoothX`, `SmoothY`, `SmoothWidth`, `SmoothHeight`, `SmoothPos`, and +//! `SmoothSize` - to assist in transitioning the layout information along the +//! x-axis, y-axis, width, height, both axes, and size. +//! +//! # Example +//! +//! ```rust +//! use ribir::prelude::*; +//! +//! let _smooth_move_to_center = fn_widget! { +//! +//! // Create a smooth widget that operates on both the x-axis and y-axis. +//! let smooth = SmoothPos::default(); +//! // Enable the transition +//! let _animate = smooth.transition( +//! EasingTransition { +//! easing: easing::LinearEasing, +//! duration: Duration::from_millis(1000), +//! }, +//! ctx!(), +//! ); +//! +//! // Apply the smooth widget to the desired widget. +//! @ $smooth { +//! @Void { +//! clamp: BoxClamp::fixed_size(Size::new(100., 100.)), +//! h_align: HAlign::Center, +//! v_align: VAlign::Center, +//! background: Color::RED, +//! } +//! } +//! }; +//! ``` +use crate::{prelude::*, wrap_render::*}; + +smooth_pos_widget_impl!(SmoothPos, Point); +smooth_pos_widget_impl!(SmoothY, f32, y); +smooth_pos_widget_impl!(SmoothX, f32, x); +smooth_size_widget_impl!(SmoothSize, Size); +smooth_size_widget_impl!(SmoothHeight, f32, height); +smooth_size_widget_impl!(SmoothWidth, f32, width); + +#[derive(Default)] +struct SmoothImpl { + running: bool, + value: T, +} + +impl Stateful> { + fn transition( + &self, transition: impl Transition + 'static, ctx: &BuildCtx, + ) -> Stateful> + where + T: Lerp, + { + let animate = part_writer!(&mut self.value).transition(transition, ctx); + let this = self.clone_writer(); + watch!($animate.is_running()) + .distinct_until_changed() + .subscribe(move |running| { + let mut w = this.write(); + w.running = running; + w.forget_modifies(); + }); + animate + } +} + +macro_rules! smooth_size_widget_impl { + ($name:ident, $size_ty:ty $(, $field:ident)?) => { + #[doc = "This widget enables smooth size transitions for its child after layout.\ + See the [module-level documentation](self) for more."] + #[derive(Default)] + pub struct $name(Stateful>); + + impl WrapRender for $name { + fn perform_layout(&self, mut clamp: BoxClamp, host: &dyn Render, ctx: &mut LayoutCtx) + -> Size + { + let SmoothImpl { running, value } = *self.0.read(); + if running { + clamp.min $(.$field)? = value; + clamp.max $(.$field)? = value; + } + let size = host.perform_layout(clamp, ctx); + let new_v = size $(.$field)?; + if !running && value != new_v { + self.0.write().value = new_v; + } + size + } + } + + impl<'c> ComposeChild<'c> for $name { + type Child = Widget<'c>; + fn compose_child(this: impl StateWriter, child: Self::Child) -> Widget<'c> { + fn_widget!{ + let modifies = this.read().0.raw_modifies(); + WrapRender::combine_child(this, child) + .on_build(move |id, ctx| id.dirty_subscribe(modifies, ctx)) + }.into_widget() + } + } + + impl $name { + #[doc = "Enable the transition with the provided argument and return the animation of the transition."] + pub fn transition(&self, transition: impl Transition + 'static, ctx: &BuildCtx) + -> Stateful< Animate> + { + self.0.transition(transition, ctx) + } + } + }; +} + +macro_rules! smooth_pos_widget_impl { + ($name:ident, $size_ty:ty $(, $field:ident)?) => { + #[doc = "This widget enables smooth position transitions for its child after layout.\ + See the [module-level documentation](self) for more."] + #[derive(Default)] + pub struct $name(Stateful>); + + impl WrapRender for $name { + fn perform_layout(&self, clamp: BoxClamp, host: &dyn Render, ctx: &mut LayoutCtx) -> Size { + let smooth = self.0.clone_writer(); + if !smooth.read().running { + let wid = ctx.widget_id(); + let wnd = ctx.window(); + let _ = AppCtx::spawn_local(async move { + let pos = wnd.map_to_global(Point::zero(), wid); + smooth.write().value = pos$(.$field)?; + }); + } + + host.perform_layout(clamp, ctx) + } + + fn paint(&self, host: &dyn Render, ctx: &mut PaintingCtx) { + let SmoothImpl { running, value } = *self.0.read(); + if running { + let pos = ctx.map_to_global(Point::zero()); + #[allow(unused_assignments)] + let mut expect = pos; + expect $(.$field)? = value; + + let offset = expect - pos; + ctx.painter().translate(offset.x, offset.y); + } + host.paint(ctx); + } + } + + impl_compose_child_for_wrap_render!($name); + + impl $name { + #[doc = "Enable the transition with the provided argument and return the animation of the transition."] + pub fn transition(&self, transition: impl Transition + 'static, ctx: &BuildCtx) + -> Stateful< Animate> + { + self.0.transition(transition, ctx) + } + } + }; +} + +use smooth_pos_widget_impl; +use smooth_size_widget_impl; diff --git a/core/src/builtin_widgets/transform_widget.rs b/core/src/builtin_widgets/transform_widget.rs index 34f8d2e7d..fa83d7d02 100644 --- a/core/src/builtin_widgets/transform_widget.rs +++ b/core/src/builtin_widgets/transform_widget.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, wrap_render::WrapRender}; +use crate::{prelude::*, wrap_render::*}; #[derive(Clone, Default)] pub struct TransformWidget { @@ -11,13 +11,7 @@ impl Declare for TransformWidget { fn declarer() -> Self::Builder { FatObj::new(()) } } -impl<'c> ComposeChild<'c> for TransformWidget { - type Child = Widget<'c>; - - fn compose_child(this: impl StateWriter, child: Self::Child) -> Widget<'c> { - WrapRender::combine_child(this, child) - } -} +impl_compose_child_for_wrap_render!(TransformWidget); impl WrapRender for TransformWidget { #[inline] diff --git a/core/src/builtin_widgets/visibility.rs b/core/src/builtin_widgets/visibility.rs index f4510666f..837c37af6 100644 --- a/core/src/builtin_widgets/visibility.rs +++ b/core/src/builtin_widgets/visibility.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, wrap_render::WrapRender}; +use crate::{prelude::*, wrap_render::*}; #[derive(Default)] pub struct Visibility { @@ -33,13 +33,7 @@ struct VisibilityRender { display: bool, } -impl<'c> ComposeChild<'c> for VisibilityRender { - type Child = Widget<'c>; - - fn compose_child(this: impl StateWriter, child: Self::Child) -> Widget<'c> { - WrapRender::combine_child(this, child) - } -} +impl_compose_child_for_wrap_render!(VisibilityRender); impl WrapRender for VisibilityRender { #[inline] diff --git a/core/src/state/stateful.rs b/core/src/state/stateful.rs index 26e8101c9..e8aae944c 100644 --- a/core/src/state/stateful.rs +++ b/core/src/state/stateful.rs @@ -207,6 +207,10 @@ impl std::fmt::Debug for Stateful { } } +impl Default for Stateful { + fn default() -> Self { Self::new(W::default()) } +} + #[cfg(test)] mod tests { use std::{cell::RefCell, rc::Rc}; diff --git a/core/src/wrap_render.rs b/core/src/wrap_render.rs index 6d261270b..2d6c3a50d 100644 --- a/core/src/wrap_render.rs +++ b/core/src/wrap_render.rs @@ -121,3 +121,16 @@ where self.read().get_transform(host) } } + +macro_rules! impl_compose_child_for_wrap_render { + ($name:ty) => { + impl<'c> ComposeChild<'c> for $name { + type Child = Widget<'c>; + fn compose_child(this: impl StateWriter, child: Self::Child) -> Widget<'c> { + WrapRender::combine_child(this, child) + } + } + }; +} + +pub(crate) use impl_compose_child_for_wrap_render;