From b0391a5bf42606661cb68c9bf0197fe7d8b18cd6 Mon Sep 17 00:00:00 2001 From: Adoo Date: Tue, 8 Aug 2023 18:46:24 +0800 Subject: [PATCH] =?UTF-8?q?refactor(core):=20=F0=9F=92=A1implement=20state?= =?UTF-8?q?=20share=20and=20split=20hierarchy.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- algo/src/cow_rc.rs | 9 +- core/src/animation.rs | 2 - core/src/animation/animate.rs | 287 +++++----- core/src/animation/lerp.rs | 27 + core/src/animation/property.rs | 222 -------- core/src/animation/transition.rs | 65 ++- core/src/builtin_widgets.rs | 22 + core/src/builtin_widgets/anchor.rs | 10 +- core/src/builtin_widgets/clip.rs | 4 +- core/src/builtin_widgets/cursor.rs | 72 +-- core/src/builtin_widgets/fitted_box.rs | 2 +- core/src/builtin_widgets/focus_scope.rs | 2 +- core/src/builtin_widgets/ignore_pointer.rs | 2 +- core/src/builtin_widgets/key.rs | 26 +- core/src/builtin_widgets/scrollable.rs | 83 +-- core/src/builtin_widgets/theme.rs | 8 +- core/src/builtin_widgets/theme/icon_theme.rs | 8 +- .../builtin_widgets/theme/transition_theme.rs | 7 - core/src/builtin_widgets/unconstrained_box.rs | 2 +- core/src/builtin_widgets/visibility.rs | 22 +- core/src/context/app_ctx.rs | 9 +- core/src/context/build_context.rs | 13 +- core/src/declare.rs | 12 - core/src/dynamic_widget.rs | 4 +- core/src/lib.rs | 6 +- core/src/pipe.rs | 9 +- core/src/state.rs | 281 ++++++++-- core/src/state/partial_state.rs | 176 +++++++ core/src/state/readonly.rs | 8 +- core/src/state/route_state.rs | 164 ++++++ core/src/state/stateful.rs | 489 ++++++++++-------- core/src/test_helper.rs | 21 +- core/src/widget.rs | 17 +- .../src/widget_children/compose_child_impl.rs | 5 +- core/src/widget_tree.rs | 5 +- core/src/widget_tree/widget_id.rs | 4 +- core/src/window.rs | 49 +- dev-helper/src/widget_test.rs | 10 +- examples/counter/src/counter.rs | 6 +- examples/greet/Cargo.toml | 21 - examples/greet/README.md | 9 - examples/greet/src/greet.rs | 68 --- examples/greet/src/main.rs | 8 - examples/todos/src/todos.rs | 232 ++++----- macros/src/declare_derive2.rs | 66 ++- macros/src/declare_obj.rs | 29 +- macros/src/fn_widget_macro.rs | 29 +- macros/src/lib.rs | 93 ++-- macros/src/pipe_macro.rs | 44 +- macros/src/rdl_macro.rs | 79 +-- macros/src/route_state_macro.rs | 72 +++ macros/src/symbol_process.rs | 281 ++++++++-- macros/src/watch_macro.rs | 28 +- macros/src/widget_macro/code_gen.rs | 2 +- macros/src/widget_macro/name_used_info.rs | 4 +- painter/src/style.rs | 5 + ribir/src/app.rs | 4 +- tests/Cargo.toml | 4 - tests/animations_syntax_test.rs | 93 ---- .../declare/animations_base_syntax_pass.rs | 63 --- tests/rdl_macro_test.rs | 129 +---- themes/material/src/lib.rs | 117 +++-- themes/material/src/ripple.rs | 144 +++--- widgets/src/checkbox.rs | 11 +- widgets/src/input.rs | 2 +- widgets/src/input/handle.rs | 10 +- widgets/src/input/text_selectable.rs | 10 +- widgets/src/layout/container.rs | 2 +- widgets/src/layout/expanded.rs | 2 +- widgets/src/path.rs | 2 +- widgets/src/text_field.rs | 321 +++++------- 71 files changed, 2253 insertions(+), 1901 deletions(-) delete mode 100644 core/src/animation/property.rs create mode 100644 core/src/state/partial_state.rs create mode 100644 core/src/state/route_state.rs delete mode 100644 examples/greet/Cargo.toml delete mode 100644 examples/greet/README.md delete mode 100644 examples/greet/src/greet.rs delete mode 100644 examples/greet/src/main.rs create mode 100644 macros/src/route_state_macro.rs delete mode 100644 tests/animations_syntax_test.rs delete mode 100644 tests/compile_msg/declare/animations_base_syntax_pass.rs diff --git a/algo/src/cow_rc.rs b/algo/src/cow_rc.rs index f68d81c8f..9f86e09e6 100644 --- a/algo/src/cow_rc.rs +++ b/algo/src/cow_rc.rs @@ -1,5 +1,5 @@ use std::{ - borrow::Borrow, + borrow::{Borrow, BorrowMut}, fmt::Debug, hash::Hash, ops::{Bound, Range}, @@ -88,6 +88,13 @@ where } } +impl std::ops::DerefMut for CowArc +where + B::Owned: Clone + std::borrow::BorrowMut, +{ + fn deref_mut(&mut self) -> &mut B { self.to_mut().borrow_mut() } +} + impl Clone for CowArc { #[inline] fn clone(&self) -> Self { diff --git a/core/src/animation.rs b/core/src/animation.rs index fdf4ed031..e6e7466f3 100644 --- a/core/src/animation.rs +++ b/core/src/animation.rs @@ -11,5 +11,3 @@ mod lerp; pub use lerp::Lerp; mod repeat; pub use repeat::*; -mod property; -pub use property::{LerpProp, Prop, Property}; diff --git a/core/src/animation/animate.rs b/core/src/animation/animate.rs index db9d1d93e..d274f0cf9 100644 --- a/core/src/animation/animate.rs +++ b/core/src/animation/animate.rs @@ -1,32 +1,47 @@ -use crate::{ - prelude::*, - ticker::{FrameMsg, FrameTicker}, -}; -use std::{ - cell::RefCell, - ops::DerefMut, - rc::Rc, - time::{Duration, Instant}, -}; - -use super::property::AnimateProperty; - -#[derive(Declare)] -pub struct Animate { +use crate::{prelude::*, ticker::FrameMsg, window::WindowId}; +use std::time::{Duration, Instant}; + +#[derive(Declare2)] +pub struct Animate +where + T: Roc + 'static, + S: AnimateState + 'static, +{ + #[declare(strict)] pub transition: T, - pub prop: P, - pub from: P::Value, + #[declare(strict)] + pub state: S, + pub from: ::V, #[declare(skip)] - running_info: Option>, - #[declare(skip, default = ctx.window().frame_ticker.clone())] - frame_ticker: FrameTicker, - #[declare(skip, default = ctx.window().animate_track())] - animate_track: AnimateTrack, - #[declare(skip, default = ctx.window().frame_scheduler())] - frame_scheduler: FuturesLocalScheduler, + running_info: Option::V>>, + #[declare(skip, default = ctx.window().id())] + window_id: WindowId, +} + +pub trait AnimateState { + type State: Share; + fn state(&mut self) -> &mut Self::State; + + fn calc_lerp_value( + &mut self, + from: &::V, + to: &::V, + rate: f32, + ) -> ::V; } -pub struct AnimateInfo { +/// A state with a lerp function as an animation state that use the `lerp_fn` +/// function to calc the linearly lerp value by rate, and not require the value +/// type of the state to implement the `Lerp` trait. +/// +/// User can use it if the value type of the state is not implement the `Lerp` +/// or override the lerp algorithm of the value type of state. +pub struct LerpFnState { + lerp_fn: F, + state: S, +} + +pub(crate) struct AnimateInfo { from: V, to: V, start_at: Instant, @@ -36,53 +51,73 @@ pub struct AnimateInfo { _tick_msg_guard: Option>>, } -impl<'a, T: Roc, P: AnimateProperty> StatefulRef<'a, Animate> +impl State> where - Animate: 'static, + T: Roc + 'static, + S: AnimateState + 'static, + ::V: Clone, { pub fn run(&mut self) { - let new_to = self.prop.get(); - // if animate is running, animate start from current value. - let Animate { prop, running_info, .. } = self.deref_mut(); - if let Some(AnimateInfo { from, to, last_progress, .. }) = running_info { - *from = prop.calc_lerp_value(from, to, last_progress.value()); + let mut animate_ref = self.state_ref(); + let this = &mut *animate_ref; + let wnd_id = this.window_id; + let new_to = this.state.state().state_ref().clone(); + + if let Some(AnimateInfo { from, to, last_progress, .. }) = &mut this.running_info { + *from = this.state.calc_lerp_value(from, to, last_progress.value()); *to = new_to; - } else { - let animate = self.clone_stateful(); - let ticker = self.frame_ticker.frame_tick_stream(); - let unsub = ticker.subscribe(move |msg| match msg { - FrameMsg::NewFrame(_) => {} - FrameMsg::LayoutReady(time) => { - let p = animate.shallow_ref().lerp(time); - if matches!(p, AnimateProgress::Finish) { - let scheduler = animate.silent_ref().frame_scheduler.clone(); - let animate = animate.clone(); - observable::of(()) - .delay(Duration::ZERO, scheduler) - .subscribe(move |_| { - animate.silent_ref().stop(); + } else if let Some(wnd) = AppCtx::get_window(wnd_id) { + drop(animate_ref); + + let animate = self.clone_state(); + let ticker = wnd.frame_ticker.frame_tick_stream(); + let unsub = ticker.subscribe(move |msg| { + match msg { + FrameMsg::NewFrame(_) => {} + FrameMsg::LayoutReady(time) => { + let p = animate.state_ref().shallow().rate_at_instant(time); + if matches!(p, AnimateProgress::Finish) { + let wnd = AppCtx::get_window(wnd_id).unwrap(); + let animate = animate.clone(); + observable::timer((), Duration::ZERO, wnd.frame_scheduler()).subscribe(move |_| { + animate.state_ref().silent().stop(); }); + } + } + // use silent_ref because the state of animate change, bu no need to effect the framework. + FrameMsg::Finish(_) => { + let animate = &mut *animate.state_ref(); + let info = animate.running_info.as_mut().unwrap(); + if !matches!(info.last_progress, AnimateProgress::Finish) { + **animate.state.state().state_ref().shallow() = info.to.clone() + } + info.already_lerp = false; } } - // use silent_ref because the state of animate change, bu no need to effect the framework. - FrameMsg::Finish(_) => animate.silent_ref().frame_finished(), }); let guard = BoxSubscription::new(unsub).unsubscribe_when_dropped(); - self.running_info = Some(AnimateInfo { - from: self.from.clone(), + let animate = &mut *self.state_ref(); + animate.running_info = Some(AnimateInfo { + from: animate.from.clone(), to: new_to, start_at: Instant::now(), last_progress: AnimateProgress::Dismissed, _tick_msg_guard: Some(guard), already_lerp: false, }); - self.animate_track.set_actived(true); + wnd.inc_running_animate(); } } } -impl Animate { - fn lerp(&mut self, now: Instant) -> AnimateProgress { +impl Animate +where + S: AnimateState + 'static, +{ + fn rate_at_instant(&mut self, now: Instant) -> AnimateProgress + where + ::V: Clone, + { let AnimateInfo { from, to, @@ -102,15 +137,15 @@ impl Animate { let elapsed = now - *start_at; let progress = self.transition.rate_of_change(elapsed); - let prop = &mut self.prop; match progress { AnimateProgress::Between(rate) => { + let value = self.state.calc_lerp_value(from, to, rate); + let state = &mut self.state.state().state_ref(); // the state may change during animate. - *to = prop.get(); - let value = prop.calc_lerp_value(from, to, rate); - prop.shallow_set(value); + *to = state.clone(); + **state.shallow() = value; } - AnimateProgress::Dismissed => prop.set(from.clone()), + AnimateProgress::Dismissed => **self.state.state().state_ref().shallow() = from.clone(), AnimateProgress::Finish => {} } @@ -120,90 +155,102 @@ impl Animate { progress } - fn frame_finished(&mut self) { - let info = self - .running_info - .as_mut() - .expect("This animation is not running."); - - if !matches!(info.last_progress, AnimateProgress::Finish) { - self.prop.set(info.to.clone()) - } - info.already_lerp = false; - } - pub fn stop(&mut self) { - self.animate_track.set_actived(false); - self.running_info.take(); + if self.is_running() { + if let Some(wnd) = AppCtx::get_window(self.window_id) { + wnd.dec_running_animate(); + self.running_info.take(); + } + } } #[inline] pub fn is_running(&self) -> bool { self.running_info.is_some() } } -pub struct AnimateTrack { - pub(crate) actived: bool, - pub(crate) actived_cnt: Rc>, -} - -impl Drop for AnimateTrack { +impl Drop for Animate +where + P: AnimateState + 'static, +{ fn drop(&mut self) { - if self.actived { - *self.actived_cnt.borrow_mut() -= 1; + if self.is_running() { + if let Some(wnd) = AppCtx::get_window(self.window_id).filter(|_| self.is_running()) { + wnd.dec_running_animate(); + } } - self.actived = false; } } -impl AnimateTrack { - fn set_actived(&mut self, actived: bool) { - if self.actived == actived { - return; - } - self.actived = actived; - match actived { - true => *self.actived_cnt.borrow_mut() += 1, - false => *self.actived_cnt.borrow_mut() -= 1, - }; +impl AnimateState for S +where + S: Share, + S::V: Lerp, +{ + type State = S; + + fn state(&mut self) -> &mut Self::State { self } + + fn calc_lerp_value( + &mut self, + from: &::V, + to: &::V, + rate: f32, + ) -> ::V { + from.lerp(to, rate) } } +impl AnimateState for LerpFnState +where + S: Share, + F: FnMut(&::V, &::V, f32) -> ::V, +{ + type State = S; + + fn state(&mut self) -> &mut Self::State { &mut self.state } + + fn calc_lerp_value( + &mut self, + from: &::V, + to: &::V, + rate: f32, + ) -> ::V { + (self.lerp_fn)(from, to, rate) + } +} + +impl LerpFnState +where + S: Share, + F: FnMut(&::V, &::V, f32) -> ::V, +{ + #[inline] + pub fn new(state: S, lerp_fn: F) -> Self { Self { state, lerp_fn } } +} + #[cfg(test)] mod tests { use super::*; - use crate::{ - animation::{easing, Prop}, - declare::Declare, - state::Stateful, - test_helper::TestWindow, - }; + use crate::{animation::easing, state::Stateful, test_helper::TestWindow}; #[test] fn fix_animate_circular_mut_borrow() { let _guard = unsafe { AppCtx::new_lock_scope() }; - let wnd = TestWindow::new(Void {}); - let mut tree = wnd.widget_tree.borrow_mut(); - let ctx = BuildCtx::new(None, &mut tree); - - let animate = Animate::declare_builder() - .transition( - Transition::declare_builder() - .easing(easing::LINEAR) - .duration(Duration::ZERO) - .build(&ctx), - ) - .prop(Prop::new( - Stateful::new(1.), - |v| *v, - |_: &mut f32, _: f32| {}, - )) - .from(0.) - .build(&ctx); - - let animate = Stateful::new(animate); - animate.state_ref().run(); - - wnd.frame_ticker.emit(FrameMsg::LayoutReady(Instant::now())); + let w = fn_widget! { + let mut animate = @Animate { + transition: @Transition { + easing: easing::LINEAR, + duration: Duration::ZERO, + }.unwrap(), + state: Stateful::new(1.), + from: 0., + }; + animate.run(); + @Void {} + }; + + let mut wnd = TestWindow::new(w); + wnd.draw_frame(); } } diff --git a/core/src/animation/lerp.rs b/core/src/animation/lerp.rs index fa3cb5b89..a2801829c 100644 --- a/core/src/animation/lerp.rs +++ b/core/src/animation/lerp.rs @@ -1,3 +1,5 @@ +use ribir_text::{Em, FontSize, Pixel, PIXELS_PER_EM}; + use crate::prelude::{ Angle, Box2D, Brush, Color, DevicePoint, DeviceRect, DeviceSize, DeviceVector, Point, Radius, Rect, Size, Transform, Vector, @@ -169,6 +171,31 @@ impl Lerp for Transform { } } +impl Lerp for Pixel { + #[inline] + fn lerp(&self, to: &Self, factor: f32) -> Self { + let v = self.0.lerp(&to.0, factor); + Pixel(v.into()) + } +} + +impl Lerp for Em { + #[inline] + fn lerp(&self, to: &Self, factor: f32) -> Self { + let v = self.value().lerp(&to.value(), factor); + Em::relative_to(v, FontSize::Pixel(PIXELS_PER_EM.into())) + } +} + +impl Lerp for FontSize { + fn lerp(&self, to: &Self, factor: f32) -> Self { + let from = self.into_pixel().value(); + let to = to.into_pixel().value(); + let v = from.lerp(&to, factor); + FontSize::Pixel(v.into()) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/core/src/animation/property.rs b/core/src/animation/property.rs deleted file mode 100644 index 928a92fcb..000000000 --- a/core/src/animation/property.rs +++ /dev/null @@ -1,222 +0,0 @@ -use crate::state::Stateful; -use std::convert::Infallible; - -use super::Lerp; -use rxrust::{ - observable, - ops::box_it::{BoxIt, BoxOp}, - prelude::ObservableExt, -}; - -/// Property is a value with can be accessed and watch its changes. -pub trait Property { - type Value: Clone; - fn get(&self) -> Self::Value; - fn set(&mut self, v: Self::Value); - fn shallow_set(&mut self, v: Self::Value); - fn modifies(&self) -> BoxOp<'static, (), Infallible>; -} - -pub trait AnimateProperty: Property { - fn calc_lerp_value(&self, from: &Self::Value, to: &Self::Value, rate: f32) -> Self::Value; -} - -pub struct Prop { - target: Stateful, - getter: G, - setter: S, -} - -#[derive(Clone)] -pub struct LerpProp { - prop: P, - /// function calc the linearly lerp value by rate, three arguments are - /// `from` `to` and `rate`, specify `lerp_fn` when the animate state not - /// implement `Lerp` trait or you want to specify a custom lerp function. - lerp_fn: F, -} - -impl Prop -where - G: Fn(&T) -> V, - S: FnMut(&mut T, V), - V: Clone, -{ - #[inline] - pub fn new(target: Stateful, getter: G, setter: S) -> Self { Self { target, getter, setter } } -} - -impl LerpProp -where - P: Property, - F: Fn(&P::Value, &P::Value, f32) -> P::Value, -{ - #[inline] - pub fn new(prop: P, lerp_fn: F) -> Self { Self { prop, lerp_fn } } -} - -impl Property for Prop -where - G: Fn(&T) -> V, - S: FnMut(&mut T, V), - V: Clone, -{ - type Value = V; - - #[inline] - fn get(&self) -> V { (self.getter)(&*self.target.state_ref()) } - - #[inline] - fn set(&mut self, v: V) { (self.setter)(&mut *self.target.state_ref(), v); } - - #[inline] - fn shallow_set(&mut self, v: Self::Value) { (self.setter)(&mut *self.target.shallow_ref(), v); } - - #[inline] - fn modifies(&self) -> BoxOp<'static, (), Infallible> { self.target.modifies().box_it() } -} - -impl AnimateProperty for Prop -where - Self: Property, - Self::Value: Lerp, -{ - #[inline] - - fn calc_lerp_value(&self, from: &Self::Value, to: &Self::Value, rate: f32) -> Self::Value { - from.lerp(to, rate) - } -} - -impl Prop -where - Self: Property, - G: Fn(&T) -> V + Clone + 'static, - V: PartialEq + Clone + 'static, - T: 'static, -{ - pub fn changes(&self) -> BoxOp<'static, V, Infallible> { - let target = self.target.clone(); - let getter = self.getter.clone(); - self - .modifies() - .map(move |_| getter(&*target.state_ref())) - .distinct_until_changed() - .box_it() - } -} - -impl Property for LerpProp -where - P: Property, -{ - type Value = P::Value; - - #[inline] - fn get(&self) -> Self::Value { self.prop.get() } - - #[inline] - fn set(&mut self, v: Self::Value) { self.prop.set(v) } - - #[inline] - fn shallow_set(&mut self, v: Self::Value) { self.prop.shallow_set(v) } - - #[inline] - fn modifies(&self) -> BoxOp<'static, (), Infallible> { self.prop.modifies() } -} - -impl AnimateProperty for LerpProp -where - P: Property, - F: Fn(&P::Value, &P::Value, f32) -> P::Value + Clone, -{ - #[inline] - fn calc_lerp_value(&self, from: &Self::Value, to: &Self::Value, rate: f32) -> Self::Value { - (self.lerp_fn)(from, to, rate) - } -} - -impl LerpProp, F> -where - Prop: Property, - V: PartialEq + Clone + 'static, - G: Fn(&T) -> V + Clone + 'static, - T: 'static, -{ - #[inline] - pub fn changes(&self) -> BoxOp<'static, V, Infallible> { self.prop.changes() } -} - -macro_rules! impl_tuple_property { - ($ty: ident, $idx: tt $(,$other_ty: ident, $other_idx: tt)*) => { - impl_tuple_property!({$ty, $idx} $($other_ty, $other_idx),*); - }; - ( - {$($ty: ident, $idx: tt),+} - $next_ty: ident, $next_idx: tt - $(,$other_ty: ident, $other_idx: tt)* - ) => { - impl_tuple_property!({$($ty, $idx),+}); - impl_tuple_property!( - {$($ty, $idx,)+ $next_ty, $next_idx } - $($other_ty, $other_idx),* - ); - }; - ({ $($ty: ident, $idx:tt),+}) => { - impl<$($ty),+> Property for ($($ty,)+) - where - $($ty: Property + 'static,)+ - $($ty::Value: 'static),+ - { - type Value = ($($ty::Value,)+); - - #[inline] - fn get(&self) -> Self::Value { - ($(self.$idx.get(),)+) - } - - #[inline] - fn set(&mut self, v: Self::Value) { - $(self.$idx.set(v.$idx);)+ - } - - #[inline] - fn shallow_set(&mut self, v: Self::Value) { - $(self.$idx.shallow_set(v.$idx);)+ - } - - #[inline] - fn modifies(&self) -> BoxOp<'static, (), Infallible> { - observable::from_iter([$(self.$idx.modifies()),+]) - .merge_all(usize::MAX) - .box_it() - } - } - - impl<$($ty),+> AnimateProperty for ($($ty,)+) - where - Self: Property, - $($ty: AnimateProperty),+ - { - fn calc_lerp_value(&self, from: &Self::Value, to: &Self::Value, rate: f32) -> Self::Value{ - ($(self.$idx.calc_lerp_value(&from.$idx, &to.$idx, rate),)+) - } - } - } -} - -impl_tuple_property! {T0, 0, T1, 1, T2, 2, T3, 3, T4, 4, T5, 5, T6, 6, T7, 7,T8, 8, T9, 9, - T10, 10, T11, 11, T12, 12, T13, 13, T14, 14, T15, 15, T16, 16, T17, 17,T18, 18, T19, 19, - T20, 20, T21, 21, T22, 22, T23, 23, T24, 24, T25, 25, T26, 26, T27, 27,T28, 28, T29, 29, - T30, 30, T31, 31 -} - -impl Clone for Prop { - fn clone(&self) -> Self { - Prop { - target: self.target.clone(), - getter: self.getter.clone(), - setter: self.setter.clone(), - } - } -} diff --git a/core/src/animation/transition.rs b/core/src/animation/transition.rs index 80a263fc9..150fbf643 100644 --- a/core/src/animation/transition.rs +++ b/core/src/animation/transition.rs @@ -2,17 +2,58 @@ use super::easing::Easing; use crate::prelude::{BuildCtx, *}; use std::{ops::Deref, rc::Rc, time::Duration}; -/// Transition describe how the state change form init to final smoothly. -#[derive(Declare, Clone, Debug, PartialEq)] -pub struct Transition { +/// Transition use rate to describe how the state change form init to final +/// smoothly. +#[derive(Declare2, Clone, Debug, PartialEq)] +pub struct Transition { #[declare(default, convert=strip_option)] pub delay: Option, pub duration: Duration, + #[declare(strict)] pub easing: E, #[declare(default, convert=strip_option)] pub repeat: Option, } +/// Trait help to transition the state. +pub trait TransitionState: Sized + 'static { + fn transition(mut self, transition: T, ctx: &BuildCtx) + where + Self: AnimateState, + ::V: Clone, + { + let mut c_state = self.state().clone_state(); + + let from = c_state.state_ref().clone(); + let mut animate: State> = Animate::declare2_builder() + .transition(transition) + .from(from) + .state(self) + .build(ctx); + + let mut c_animate = animate.clone_state(); + let h = c_state.modifies().subscribe(move |_| { + animate.state_ref().from = c_state.state_ref().clone(); + animate.run(); + }); + c_animate.own_data(h); + } + + /// Transition the state with a lerp function. + fn transition_with( + self, + transition: T, + lerp_fn: impl FnMut(&Self::V, &Self::V, f32) -> Self::V + 'static, + ctx: &BuildCtx, + ) where + Self: Share, + Self::V: Clone, + { + let animate_state = LerpFnState::new(self, lerp_fn); + animate_state.transition(transition, ctx) + } +} + /// Calc the rate of change over time. pub trait Roc { /// Calc the rate of change of the duration from animation start. @@ -46,11 +87,19 @@ impl Roc for Transition { } } -impl Roc for Stateful { - #[inline] - fn rate_of_change(&self, dur: Duration) -> AnimateProgress { - self.state_ref().rate_of_change(dur) - } +impl Roc for T +where + T::V: Roc, +{ + fn rate_of_change(&self, dur: Duration) -> AnimateProgress { self.as_ref().rate_of_change(dur) } +} + +impl TransitionState for S {} +impl TransitionState for LerpFnState +where + S: Share + 'static, + F: FnMut(&::V, &::V, f32) -> ::V + 'static, +{ } impl Roc for Box { diff --git a/core/src/builtin_widgets.rs b/core/src/builtin_widgets.rs index 8aeb158c0..e9906c456 100644 --- a/core/src/builtin_widgets.rs +++ b/core/src/builtin_widgets.rs @@ -83,6 +83,11 @@ macro_rules! impl_fat_obj { } } + pub fn unwrap(self) -> T { + $(assert!(self.[< $builtin_ty: snake:lower >].is_none());)* + self.host + } + $( pub fn [< with_ $builtin_ty: snake:lower >]( mut self, builtin: State<$builtin_ty> @@ -155,6 +160,23 @@ macro_rules! impl_fat_obj { } } } + + impl SingleWithChild for Option> + where + T: SingleWithChild, + C: Into, + as SingleWithChild>::Target: Into + { + type Target = Widget; + fn with_child(self, c: C, ctx: &BuildCtx) -> Self::Target { + + if let Some(this) = self { + this.with_child(c, ctx).into() + } else { + c.into() + } + } + } } }; } diff --git a/core/src/builtin_widgets/anchor.rs b/core/src/builtin_widgets/anchor.rs index 9a4862101..f9dab4973 100644 --- a/core/src/builtin_widgets/anchor.rs +++ b/core/src/builtin_widgets/anchor.rs @@ -124,12 +124,10 @@ impl PositionUnit { } } - pub fn lerp_fn(self_size: f32) -> impl Fn(&Self, &Self, f32) -> Self + Clone { - move |from, to, rate| { - let from = from.abs_value(self_size); - let to = to.abs_value(self_size); - PositionUnit::Pixel(from.lerp(&to, rate)) - } + pub fn lerp_fn(from: &Self, to: &Self, rate: f32, self_size: f32) -> PositionUnit { + let from = from.abs_value(self_size); + let to = to.abs_value(self_size); + PositionUnit::Pixel(from.lerp(&to, rate)) } } diff --git a/core/src/builtin_widgets/clip.rs b/core/src/builtin_widgets/clip.rs index d69884f02..fcf234b7e 100644 --- a/core/src/builtin_widgets/clip.rs +++ b/core/src/builtin_widgets/clip.rs @@ -7,7 +7,7 @@ pub enum ClipType { Path(Path), } -#[derive(SingleChild, Clone, Declare)] +#[derive(SingleChild, Clone, Declare, Declare2)] pub struct Clip { #[declare(default)] pub clip: ClipType, @@ -27,7 +27,7 @@ impl Render for Clip { fn paint(&self, ctx: &mut PaintingCtx) { let path = match &self.clip { ClipType::Auto => { - let rect = Rect::from_size( + let rect: lyon_geom::euclid::Rect = Rect::from_size( ctx .box_rect() .expect("impossible without size in painting stage") diff --git a/core/src/builtin_widgets/cursor.rs b/core/src/builtin_widgets/cursor.rs index 3dc4d37f4..17e4b6f42 100644 --- a/core/src/builtin_widgets/cursor.rs +++ b/core/src/builtin_widgets/cursor.rs @@ -1,36 +1,31 @@ use crate::prelude::*; -use std::{cell::Cell, rc::Rc}; use winit::window::CursorIcon; /// `Cursor` is an attribute to assign an `cursor` to a widget. -#[derive(Declare, Debug, Declare2)] +#[derive(Declare, Default, Debug, Declare2)] pub struct Cursor { - #[declare(convert=custom, builtin, default)] - pub cursor: Rc>, + #[declare(builtin, default)] + pub cursor: CursorIcon, } impl ComposeChild for Cursor { type Child = Widget; - fn compose_child(this: State, child: Self::Child) -> Widget { - widget! { - states { - save_cursor: Stateful::new(CursorIcon::Default), - this: this.into_readonly() - } - DynWidget { - dyns: child, + fn compose_child(mut this: State, child: Self::Child) -> Widget { + fn_widget! { + let mut save_cursor = Stateful::new(CursorIcon::Default); + @$child { on_pointer_enter: move |e: &mut PointerEvent| { if e.point_type == PointerType::Mouse && e.mouse_buttons() == MouseButtons::empty() { let wnd = e.window(); - *save_cursor = wnd.get_cursor(); - wnd.set_cursor(this.cursor.get()); + *$save_cursor = wnd.get_cursor(); + wnd.set_cursor($this.get_cursor()); } }, on_pointer_leave: move |e: &mut PointerEvent| { - e.window().set_cursor(*save_cursor); + e.window().set_cursor(*$save_cursor); } } } @@ -38,53 +33,8 @@ impl ComposeChild for Cursor { } } -pub trait IntoCursorIcon { - fn into_cursor_icon(self) -> Rc>; -} - -impl IntoCursorIcon for Rc> { - #[inline] - fn into_cursor_icon(self) -> Rc> { self } -} - -impl IntoCursorIcon for CursorIcon { - #[inline] - fn into_cursor_icon(self) -> Rc> { Rc::new(Cell::new(self)) } -} - -impl CursorDeclarer { - #[inline] - pub fn cursor(mut self, icon: C) -> Self { - self.cursor = Some(icon.into_cursor_icon()); - self - } -} - impl Cursor { - #[inline] - pub fn set_declare_cursor(&mut self, icon: C) { - self.cursor = icon.into_cursor_icon(); - } -} - -impl Cursor { - #[inline] - pub fn icon(&self) -> CursorIcon { self.cursor.get() } - - #[inline] - pub fn set_icon(&self, icon: CursorIcon) { self.cursor.set(icon) } - - #[inline] - pub fn new_icon(icon: CursorIcon) -> Rc> { Rc::new(Cell::new(icon)) } -} - -impl Default for Cursor { - #[inline] - fn default() -> Self { - Cursor { - cursor: Rc::new(Cell::new(CursorIcon::Default)), - } - } + fn get_cursor(&self) -> CursorIcon { self.cursor } } #[cfg(test)] diff --git a/core/src/builtin_widgets/fitted_box.rs b/core/src/builtin_widgets/fitted_box.rs index b9ac76232..1a6f871c4 100644 --- a/core/src/builtin_widgets/fitted_box.rs +++ b/core/src/builtin_widgets/fitted_box.rs @@ -111,7 +111,7 @@ mod tests { wnd.draw_frame(); assert_layout_result_by_path!(wnd, {path = [0], size == expect,} ); - assert_eq!(c_fit.shallow_ref().scale_cache.get(), expected_scale); + assert_eq!(c_fit.state_ref().scale_cache.get(), expected_scale); } } diff --git a/core/src/builtin_widgets/focus_scope.rs b/core/src/builtin_widgets/focus_scope.rs index 15168e757..a73360dc8 100644 --- a/core/src/builtin_widgets/focus_scope.rs +++ b/core/src/builtin_widgets/focus_scope.rs @@ -1,6 +1,6 @@ use crate::{events::focus_mgr::FocusType, impl_query_self_only, prelude::*}; -#[derive(Declare, Clone, Default)] +#[derive(Declare, Declare2, Clone, Default)] pub struct FocusScope { /// If true, the descendants can not be focused. /// Default value is false, then the hold FocusScope subtree can be focused diff --git a/core/src/builtin_widgets/ignore_pointer.rs b/core/src/builtin_widgets/ignore_pointer.rs index fcbc367e4..4fe23d940 100644 --- a/core/src/builtin_widgets/ignore_pointer.rs +++ b/core/src/builtin_widgets/ignore_pointer.rs @@ -1,6 +1,6 @@ use crate::{impl_query_self_only, prelude::*}; -#[derive(Declare, SingleChild, Clone)] +#[derive(Declare, Declare2, SingleChild, Clone)] pub struct IgnorePointer { #[declare(default = true)] pub ignore: bool, diff --git a/core/src/builtin_widgets/key.rs b/core/src/builtin_widgets/key.rs index f97023c28..5e520e40d 100644 --- a/core/src/builtin_widgets/key.rs +++ b/core/src/builtin_widgets/key.rs @@ -66,15 +66,15 @@ pub struct KeyChange(pub Option, pub Option); impl Default for KeyChange { fn default() -> Self { KeyChange(None, None) } } -#[derive(Declare)] -pub struct KeyWidget { +#[derive(Declare, Declare2)] +pub struct KeyWidget { #[declare(convert=into)] pub key: Key, - pub value: Option, - #[declare(default)] + pub value: Option, + #[declare(skip)] before_value: Option, - #[declare(default)] + #[declare(skip)] status: KeyStatus, } @@ -91,12 +91,7 @@ where V: Clone + PartialEq, Self: Any, { - fn key(&self) -> Key { - match self { - State::Stateless(this) => this.key.clone(), - State::Stateful(this) => this.state_ref().key.clone(), - } - } + fn key(&self) -> Key { self.as_ref().key.clone() } fn record_before_value(&self, key: &dyn AnyKey) { assert_eq!(self.key(), key.key()); @@ -107,12 +102,11 @@ where match self { // stateless key widget needn't record before value. State::Stateless(_) => {} - State::Stateful(this) => match key { - State::Stateless(key) => this.state_ref().record_before_value(key.value.clone()), - State::Stateful(key) => this + State::Stateful(this) => { + this .state_ref() - .record_before_value(key.state_ref().value.clone()), - }, + .record_before_value(key.as_ref().value.clone()); + } } } diff --git a/core/src/builtin_widgets/scrollable.rs b/core/src/builtin_widgets/scrollable.rs index b26d315f5..128e36018 100644 --- a/core/src/builtin_widgets/scrollable.rs +++ b/core/src/builtin_widgets/scrollable.rs @@ -29,45 +29,34 @@ pub struct ScrollableWidget { impl ComposeChild for ScrollableWidget { type Child = Widget; - fn compose_child(this: State, child: Self::Child) -> Widget { - widget! { - states { this: this.into_writable() } - Clip { - UnconstrainedBox { - id: view, - dir: match this.scrollable { - Scrollable::X => UnconstrainedDir::X, - Scrollable::Y => UnconstrainedDir::Y, - Scrollable::Both => UnconstrainedDir::Both, - }, - clamp_dim: ClampDim::MAX_SIZE, - on_wheel: move |e| this.validate_scroll(Point::new(e.delta_x, e.delta_y)), - DynWidget { - id: content, - dyns: child, - left_anchor: this.scroll_pos.x, - top_anchor: this.scroll_pos.y, - } - } - } + fn compose_child(mut this: State, child: Self::Child) -> Widget { + fn_widget! { + let mut view = @UnconstrainedBox { + dir: pipe!(match $this.get_scrollable() { + Scrollable::X => UnconstrainedDir::X, + Scrollable::Y => UnconstrainedDir::Y, + Scrollable::Both => UnconstrainedDir::Both, + }), + clamp_dim: ClampDim::MAX_SIZE, + }; + + let mut child = @ $child { + left_anchor: pipe!($this.get_scroll_pos().x), + top_anchor: pipe!($this.get_scroll_pos().y), + }; - finally { - let_watch!(content.layout_size()) - .distinct_until_changed() - .subscribe(move |v| { - this.content_size = v; - // content size update need to update scroll offset. - let scroll_pos = this.scroll_pos; - this.jump_to(scroll_pos); - }); - let_watch!(view.layout_size()) - .distinct_until_changed() - .subscribe(move |v| { - this.page = v; - // view size update need to update scroll offset. - let scroll_pos = this.scroll_pos; - this.jump_to(scroll_pos); - }); + watch!($child.layout_size()) + .distinct_until_changed() + .subscribe(move |v| $this.set_content_size(v)); + watch!($view.layout_size()) + .distinct_until_changed() + .subscribe(move |v| $this.set_page(v)); + + @Clip { + @ $view { + on_wheel: move |e| $this.validate_scroll(Point::new(e.delta_x, e.delta_y)), + @ { child } + } } } .into() @@ -108,6 +97,22 @@ impl ScrollableWidget { } self.jump_to(new); } + + pub fn set_content_size(&mut self, content_size: Size) { + self.content_size = content_size; + self.sync_pos() + } + + pub fn set_page(&mut self, page: Size) { + self.page = page; + self.sync_pos() + } + + fn get_scrollable(&self) -> Scrollable { self.scrollable } + + fn get_scroll_pos(&self) -> Point { self.scroll_pos } + + fn sync_pos(&mut self) { self.jump_to(self.scroll_pos) } } #[cfg(test)] @@ -140,7 +145,7 @@ mod tests { modifiers: ModifiersState::default(), }); - wnd.layout(); + wnd.draw_frame(); let pos = wnd.layout_info_by_path(&[0, 0, 0, 0]).unwrap().pos; assert_eq!(pos.y, expect_y); let pos = wnd.layout_info_by_path(&[0, 0, 0, 0, 0]).unwrap().pos; diff --git a/core/src/builtin_widgets/theme.rs b/core/src/builtin_widgets/theme.rs index 1247c49b3..5f0c42921 100644 --- a/core/src/builtin_widgets/theme.rs +++ b/core/src/builtin_widgets/theme.rs @@ -81,15 +81,11 @@ pub struct ThemeWidget { impl ComposeChild for ThemeWidget { type Child = Widget; #[inline] - fn compose_child(this: State, child: Self::Child) -> Widget { + fn compose_child(mut this: State, child: Self::Child) -> Widget { use crate::prelude::*; FnWidget::new(move |ctx| { let ctx = ctx.force_as_mut(); - - let theme = match this { - State::Stateless(t) => t.theme, - State::Stateful(s) => s.state_ref().theme.clone(), - }; + let theme = this.state_ref().theme.clone(); AppCtx::load_font_from_theme(&theme); ctx.push_theme(theme.clone()); diff --git a/core/src/builtin_widgets/theme/icon_theme.rs b/core/src/builtin_widgets/theme/icon_theme.rs index c59c29268..52b35e129 100644 --- a/core/src/builtin_widgets/theme/icon_theme.rs +++ b/core/src/builtin_widgets/theme/icon_theme.rs @@ -59,12 +59,8 @@ pub const CUSTOM_ICON_START: NamedSvg = NamedSvg::new(65536); pub struct NamedSvg(pub usize); impl Compose for NamedSvg { - fn compose(this: State) -> Widget { - widget! { - states { this: this.into_readonly() } - FnWidget::new(move |ctx: &BuildCtx| this.of_or_miss(ctx)) - } - .into() + fn compose(mut this: State) -> Widget { + fn_widget! { @ { $this.of_or_miss(ctx!()) }}.into() } } diff --git a/core/src/builtin_widgets/theme/transition_theme.rs b/core/src/builtin_widgets/theme/transition_theme.rs index 3c0e94452..b018ecfcc 100644 --- a/core/src/builtin_widgets/theme/transition_theme.rs +++ b/core/src/builtin_widgets/theme/transition_theme.rs @@ -47,7 +47,6 @@ pub mod transitions { EASE_IN, EASE_OUT, EASE_IN_OUT, - SMOOTH_SCROLL, THEME_EXTEND ); @@ -124,12 +123,6 @@ impl Default for TransitionTheme { duration: Duration::from_millis(250), easing: easing::EASE_IN_OUT, repeat: None, - }, - transitions::SMOOTH_SCROLL:Transition { - delay: None, - duration: Duration::from_millis(200), - easing: easing::EASE_IN, - repeat: None, } } diff --git a/core/src/builtin_widgets/unconstrained_box.rs b/core/src/builtin_widgets/unconstrained_box.rs index 8e58eeedd..d8b5ac9a8 100644 --- a/core/src/builtin_widgets/unconstrained_box.rs +++ b/core/src/builtin_widgets/unconstrained_box.rs @@ -1,6 +1,6 @@ use crate::prelude::*; -#[derive(Declare, SingleChild)] +#[derive(Declare, Declare2, SingleChild)] /// A widget that imposes no constraints on its child, allowing it to layout and /// display as its "natural" size. Its size is equal to its child then clamp by /// parent. diff --git a/core/src/builtin_widgets/visibility.rs b/core/src/builtin_widgets/visibility.rs index 709925370..59a0f16e5 100644 --- a/core/src/builtin_widgets/visibility.rs +++ b/core/src/builtin_widgets/visibility.rs @@ -8,15 +8,14 @@ pub struct Visibility { impl ComposeChild for Visibility { type Child = Widget; - fn compose_child(this: State, child: Self::Child) -> Widget { - widget! { - states { this: this.into_readonly(), } - FocusScope { - skip_descendants: !this.visible, - can_focus: this.visible, - VisibilityRender { - display: this.visible, - widget::from(child) + fn compose_child(mut this: State, child: Self::Child) -> Widget { + fn_widget! { + @FocusScope { + skip_descendants: pipe!(!$this.get_visible()), + can_focus: pipe!($this.get_visible()), + @VisibilityRender { + display: pipe!($this.get_visible()), + @ { child } } } } @@ -24,7 +23,7 @@ impl ComposeChild for Visibility { } } -#[derive(SingleChild, Declare, Clone)] +#[derive(SingleChild, Declare, Declare2, Clone)] struct VisibilityRender { display: bool, } @@ -59,4 +58,7 @@ impl_query_self_only!(VisibilityRender); impl Visibility { #[inline] pub fn new(visible: bool) -> Self { Self { visible } } + + #[inline] + fn get_visible(&self) -> bool { self.visible } } diff --git a/core/src/context/app_ctx.rs b/core/src/context/app_ctx.rs index bea2f9041..d9ce45693 100644 --- a/core/src/context/app_ctx.rs +++ b/core/src/context/app_ctx.rs @@ -44,6 +44,7 @@ pub struct AppCtx { typography_store: TypographyStore, clipboard: RefCell>, runtime_waker: Box, + scheduler: FuturesLocalScheduler, executor: RefCell, } @@ -102,7 +103,7 @@ impl AppCtx { /// Get the scheduler of the application. #[track_caller] - pub fn scheduler() -> FuturesLocalScheduler { Self::shared().executor.borrow_mut().spawner() } + pub fn scheduler() -> FuturesLocalScheduler { Self::shared().scheduler.clone() } /// Get the clipboard of the application. #[track_caller] @@ -231,6 +232,9 @@ impl AppCtx { let reorder = TextReorder::default(); let typography_store = TypographyStore::new(reorder.clone(), font_db.clone(), shaper.clone()); + let executor = LocalPool::new(); + let scheduler = executor.spawner(); + let ctx = AppCtx { font_db, app_theme, @@ -238,7 +242,8 @@ impl AppCtx { reorder, typography_store, clipboard: RefCell::new(Box::new(MockClipboard {})), - executor: <_>::default(), + executor: RefCell::new(executor), + scheduler, runtime_waker: Box::new(MockWaker), windows: RefCell::new(ahash::HashMap::default()), }; diff --git a/core/src/context/build_context.rs b/core/src/context/build_context.rs index ff2e57d2b..ed6a7ad44 100644 --- a/core/src/context/build_context.rs +++ b/core/src/context/build_context.rs @@ -32,6 +32,13 @@ impl<'a> BuildCtx<'a> { /// Create a handle of this `BuildCtx` which support `Clone`, `Copy` and /// convert back to this `BuildCtx`. This let you can store the `BuildCtx`. pub fn handle(&self) -> BuildCtxHandle { + println!( + "generate handle {:?}", + BuildCtxHandle { + wnd_id: self.tree.window().id(), + ctx_from: self.ctx_from, + } + ); BuildCtxHandle { wnd_id: self.tree.window().id(), ctx_from: self.ctx_from, @@ -93,7 +100,7 @@ impl<'a> BuildCtx<'a> { /// and fire mount events. pub(crate) fn on_widget_mounted(&self, id: WidgetId) { self.assert_get(id).query_all_type( - |notifier: &StateChangeNotifier| { + |notifier: &Modifier| { let state_changed = self.tree.dirty_set.clone(); notifier .raw_modifies() @@ -167,6 +174,10 @@ impl BuildCtxHandle { /// Acquires a reference to the `BuildCtx` in this handle, maybe not exist if /// the window is closed or widget is removed. pub fn with_ctx(self, f: impl FnOnce(&BuildCtx) -> R) -> Option { + let wnd = AppCtx::get_window(self.wnd_id); + if wnd.is_none() { + println!("no window"); + } AppCtx::get_window(self.wnd_id).map(|wnd| { let mut tree = wnd.widget_tree.borrow_mut(); let ctx = BuildCtx::new(self.ctx_from, &mut tree); diff --git a/core/src/declare.rs b/core/src/declare.rs index d36d77d6f..36f15a76f 100644 --- a/core/src/declare.rs +++ b/core/src/declare.rs @@ -67,23 +67,11 @@ impl> DeclareFrom for DeclareInit { fn declare_from(value: V) -> Self { Self::Value(value.into()) } } -impl> DeclareFrom> for DeclareInit> { - #[inline] - fn declare_from(value: V) -> Self { Self::Value(Some(value.into())) } -} - impl + 'static> DeclareFrom, Pipe<()>> for DeclareInit { #[inline] fn declare_from(value: Pipe) -> Self { Self::Pipe(value.map(U::from)) } } -impl + 'static> DeclareFrom, Option>> - for DeclareInit> -{ - #[inline] - fn declare_from(value: Pipe) -> Self { Self::Pipe(value.map(|v| Some(v.into()))) } -} - #[derive(Debug, PartialEq, Hash)] pub struct DeclareStripOption(O); diff --git a/core/src/dynamic_widget.rs b/core/src/dynamic_widget.rs index 8400c24bd..44cb07f19 100644 --- a/core/src/dynamic_widget.rs +++ b/core/src/dynamic_widget.rs @@ -172,7 +172,7 @@ impl DynRender { // Place the real old render in node. std::mem::swap(&mut tmp_render, ctx.id.assert_get_mut(&mut ctx.tree.arena)); - self.dyn_widgets.modify_notifier.reset(); + self.dyn_widgets.modifier.reset(); let wrap_render = |gen_info, tree: &mut WidgetTree| { let new_render = DynRender { @@ -260,7 +260,7 @@ impl DynRender { let arena = &mut tree.arena; wid.assert_get(arena).query_all_type( - |notifier: &StateChangeNotifier| { + |notifier: &Modifier| { let state_changed = tree.dirty_set.clone(); // abandon the old subscribe notifier.reset(); diff --git a/core/src/lib.rs b/core/src/lib.rs index b900d9138..d2c0a0362 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -37,7 +37,6 @@ pub mod prelude { pub use crate::dynamic_widget::*; #[doc(no_inline)] pub use crate::events::*; - pub use crate::path_state_splitter; #[doc(no_inline)] pub use crate::pipe::Pipe; #[doc(no_inline)] @@ -60,8 +59,9 @@ pub mod prelude { pub use ribir_geom::*; #[doc(no_inline)] pub use ribir_macros::{ - ctx, fn_widget, include_svg, pipe, rdl, set_build_ctx, watch, widget, Declare, Declare2, Lerp, - MultiChild, SingleChild, Template, _dollar_ಠ_ಠ, + ctx, fn_widget, include_svg, partial_state, pipe, rdl, route_state, set_build_ctx, watch, + widget, Declare, Declare2, Lerp, MultiChild, SingleChild, Template, _dollar_ಠ_ಠ, + ribir_expanded_ಠ_ಠ, }; #[doc(no_inline)] pub use ribir_painter::*; diff --git a/core/src/pipe.rs b/core/src/pipe.rs index 2aa9279b4..075de411e 100644 --- a/core/src/pipe.rs +++ b/core/src/pipe.rs @@ -68,7 +68,6 @@ impl Pipe { } impl> WidgetBuilder for Pipe { - #[inline] fn build(self, ctx: &crate::context::BuildCtx) -> WidgetId { let (v, modifies) = self.unzip(); let id = v.into().build(ctx); @@ -99,6 +98,14 @@ impl> WidgetBuilder for Pipe { } } +impl + 'static> WidgetBuilder for Pipe> { + fn build(self, ctx: &crate::context::BuildCtx) -> WidgetId { + self + .map(|w| w.map_or_else(|| Widget::from(Void), |w| w.into())) + .build(ctx) + } +} + impl Pipe { pub(crate) fn into_only_parent(self, ctx: &mut BuildCtx) -> WidgetId where diff --git a/core/src/state.rs b/core/src/state.rs index aab5fd52a..9f96e466b 100644 --- a/core/src/state.rs +++ b/core/src/state.rs @@ -1,18 +1,117 @@ +mod partial_state; mod readonly; +mod route_state; mod stateful; -use std::{convert::Infallible, mem::MaybeUninit, rc::Rc}; +use std::{any::Any, convert::Infallible, mem::MaybeUninit, ops::DerefMut, rc::Rc}; + +pub use partial_state::*; pub use readonly::*; -use rxrust::{ops::box_it::BoxOp, prelude::ObservableItem}; +pub use route_state::*; +use rxrust::{ops::box_it::BoxOp, prelude::ObservableItem, subject::Subject}; pub use stateful::*; use crate::{ context::BuildCtx, dynamic_widget::DynWidget, prelude::{BoxMultiParent, BoxedSingleParent, MultiChild, SingleChild}, - widget::{Compose, Render, RenderFul, WidgetBuilder, WidgetId}, + widget::{Compose, Render, WidgetBuilder, WidgetId}, }; +/// A trait that a state should need to implement. +pub trait Share: Sized { + /// The value type of this state. + type V; + /// The origin state of this state. + type Origin: Share; + /// The share type of this state, for example `Stateful` is the share type of + /// `State`. + type S: Share; + /// The immutable reference type of the state, just use to access the value + type Ref<'a>: std::ops::Deref + where + Self: 'a; + /// The a reference type that can track the modifies of the state. + type RefTrack<'a>: RefShare + where + Self: 'a; + + /// Return the reference of the state. + fn as_ref(&self) -> Self::Ref<'_>; + + /// Return the reference of the state. + fn state_ref(&'_ mut self) -> Self::RefTrack<'_>; + + /// Clone the state and return a new one. The returned state should be the + /// same as the original one, if the original one is not shareable should + /// convert to a shareable one. + fn clone_state(&mut self) -> Self::S; + + /// Create a partial state of the state by what the `R` route to. This return + /// state let you can directly access the part of data what the `R` route to. + /// + /// The return state is a subset of the original state. When modifies the + /// origin state the return state will be notified. But when modifies the + /// return state the origin state will not be notified. + #[inline] + fn partial_state( + &mut self, + router: R1, + router_mut: R2, + ) -> PartialState + where + R1: FnOnce(&Self::V) -> &Target + Copy, + R2: FnOnce(&mut Self::V) -> &mut Target + Copy, + { + PartialState::new(self.clone_state(), router, router_mut) + } + + /// Create a state that help you quick route to the part of the origin state + /// before you access it. The return state and the origin state are the same + /// state. So when one of them is modified, they will both be notified. + fn route_state( + &mut self, + router: R1, + router_mut: R2, + ) -> RouteState + where + R1: FnOnce(&Self::V) -> &Target + Copy, + R2: FnOnce(&mut Self::V) -> &mut Target + Copy, + { + RouteState::new(self.clone_state(), router, router_mut) + } + + /// Return the origin state of the state if this state is a partial state. + /// Otherwise return itself. + fn origin_state(&mut self) -> Self::Origin; + + /// Return a modifies `Rx` stream of the state, user can subscribe it to + /// response the state changes. + fn modifies(&mut self) -> BoxOp<'static, (), Infallible>; + + fn raw_modifies(&mut self) -> Subject<'static, ModifyScope, Infallible>; + + /// Store an anonymous data to the state, so we can keep the `data` live as + /// long as the state. + fn own_data(&mut self, data: impl Any); +} + +/// A trait that a reference of a share state should implement. +pub trait RefShare: DerefMut { + /// Convert this state reference to silent reference which notifies will be + /// ignored by the framework. + fn silent(&mut self) -> &mut Self; + /// Convert this state reference to a shallow reference. Modify across this + /// reference will notify framework only. That means the modifies on shallow + /// reference should only effect framework but not effect on data. eg. + /// temporary to modify the state and then modifies it back to trigger the + /// view update. Use it only if you know how a shallow reference works. + fn shallow(&mut self) -> &mut Self; + /// Forget all modifies record in this reference. So all the modifies before + /// this call will not be notified. + fn forget_modifies(&self) -> ModifyScope; +} + /// Enum to store both stateless and stateful object. pub enum State { Stateless(W), @@ -20,10 +119,53 @@ pub enum State { } pub enum StateRef<'a, W> { - Stateful(StatefulRef<'a, W>), + Stateful(TrackRef<'a, W>), Stateless(&'a mut W), } +pub enum Ref<'a, W> { + Stateful(stateful::Ref<'a, W>), + Stateless(&'a W), +} + +impl Share for State { + type V = T; + type Origin = Stateful; + type S = Stateful; + type RefTrack<'a> = StateRef<'a, T> where Self: 'a; + type Ref<'a> = Ref<'a, T> where Self:'a; + + fn as_ref(&self) -> Self::Ref<'_> { + match self { + State::Stateless(w) => Ref::Stateless(w), + State::Stateful(w) => Ref::Stateful(w.as_ref()), + } + } + + fn state_ref(&'_ mut self) -> StateRef<'_, T> { + match self { + State::Stateless(w) => StateRef::Stateless(w), + State::Stateful(w) => StateRef::Stateful(w.state_ref()), + } + } + + fn clone_state(&mut self) -> Stateful { self.as_stateful().clone() } + + #[inline] + fn modifies(&mut self) -> BoxOp<'static, (), Infallible> { self.as_stateful().modifies() } + + #[inline] + fn raw_modifies(&mut self) -> Subject<'static, ModifyScope, Infallible> { + self.as_stateful().raw_modifies() + } + + #[inline] + fn origin_state(&mut self) -> Self::Origin { self.clone_state() } + + #[inline] + fn own_data(&mut self, data: impl Any) { self.as_stateful().own_data(data); } +} + impl SingleChild for State {} impl MultiChild for State {} @@ -80,18 +222,9 @@ impl State { } } - pub fn clone_stateful(&mut self) -> Stateful { self.to_stateful().clone() } - - pub fn modifies(&mut self) -> BoxOp<'static, (), Infallible> { self.to_stateful().modifies() } - - pub fn clone(&mut self) -> State { - let stateful = self.to_stateful().clone(); - State::Stateful(stateful) - } - - pub fn stateful_ref(&mut self) -> StatefulRef { self.to_stateful().state_ref() } + pub fn clone_stateful(&mut self) -> Stateful { self.as_stateful().clone() } - pub fn to_stateful(&mut self) -> &mut Stateful { + fn as_stateful(&mut self) -> &mut Stateful { match self { State::Stateless(w) => { // convert the stateless value to stateful first. @@ -110,13 +243,6 @@ impl State { State::Stateful(w) => w, } } - - pub fn state_ref(&mut self) -> StateRef { - match self { - State::Stateless(w) => StateRef::Stateless(w), - State::Stateful(w) => StateRef::Stateful(w.state_ref()), - } - } } pub(crate) trait StateFrom { @@ -159,15 +285,6 @@ impl StateFrom>> for State { } } -impl<'a, T> StateRef<'a, T> { - pub fn forget_modifies(&self) { - match self { - StateRef::Stateless(_) => {} - StateRef::Stateful(w) => w.forget_modifies(), - } - } -} - impl From for State where Self: StateFrom, @@ -175,9 +292,48 @@ where fn from(value: T) -> Self { StateFrom::state_from(value) } } +impl<'a, W> RefShare for StateRef<'a, W> { + fn silent(&mut self) -> &mut Self { + if let StateRef::Stateful(w) = self { + w.silent(); + } + + self + } + + fn shallow(&mut self) -> &mut Self { + if let StateRef::Stateful(w) = self { + w.shallow(); + } + + self + } + + fn forget_modifies(&self) -> ModifyScope { + if let StateRef::Stateful(w) = self { + w.forget_modifies() + } else { + ModifyScope::empty() + } + } +} + +impl<'a, W> std::ops::Deref for Ref<'a, W> { + type Target = W; + + #[track_caller] + fn deref(&self) -> &Self::Target { + match self { + Ref::Stateless(r) => r, + Ref::Stateful(s) => s.deref(), + } + } +} + impl<'a, W> std::ops::Deref for StateRef<'a, W> { type Target = W; + #[track_caller] fn deref(&self) -> &Self::Target { match self { StateRef::Stateful(s) => s.deref(), @@ -187,6 +343,7 @@ impl<'a, W> std::ops::Deref for StateRef<'a, W> { } impl<'a, W> std::ops::DerefMut for StateRef<'a, W> { + #[track_caller] fn deref_mut(&mut self) -> &mut Self::Target { match self { StateRef::Stateful(s) => s.deref_mut(), @@ -197,10 +354,15 @@ impl<'a, W> std::ops::DerefMut for StateRef<'a, W> { #[cfg(test)] mod tests { + use std::cell::Cell; + use super::*; + use crate::{context::AppCtx, prelude::*, rest_test_env, timer::Timer}; #[test] fn fix_dyn_widget_to_state_circular_mut_borrow_panic() { + let _guard = unsafe { AppCtx::new_lock_scope() }; + let dyn_widget = Stateful::new(DynWidget { dyns: Some(1) }); let c_dyns = dyn_widget.clone(); let _: State = dyn_widget.into(); @@ -208,4 +370,61 @@ mod tests { c_dyns.state_ref().dyns = Some(2); } } + + struct Origin { + a: i32, + b: i32, + } + + #[test] + fn path_state_splitter_test() { + rest_test_env!(); + + let mut origin = State::Stateless(Origin { a: 0, b: 0 }); + let mut a = partial_state!($origin.a); + let mut b = route_state!($origin.b); + + let track_origin = Rc::new(Cell::new(0)); + let track_a = Rc::new(Cell::new(0)); + let track_b = Rc::new(Cell::new(0)); + + let c_origin = track_origin.clone(); + origin.modifies().subscribe(move |_| { + c_origin.set(c_origin.get() + 1); + }); + + let c_a = track_a.clone(); + a.modifies().subscribe(move |_| { + c_a.set(c_a.get() + 1); + }); + + let c_b = track_b.clone(); + b.modifies().subscribe(move |_| { + c_b.set(c_b.get() + 1); + }); + + origin.state_ref().a = 1; + Timer::wake_timeout_futures(); + AppCtx::run_until_stalled(); + + assert_eq!(track_origin.get(), 1); + assert_eq!(track_a.get(), 1); + assert_eq!(track_b.get(), 1); + + *a.state_ref() = 1; + Timer::wake_timeout_futures(); + AppCtx::run_until_stalled(); + + assert_eq!(track_origin.get(), 1); + assert_eq!(track_a.get(), 2); + assert_eq!(track_b.get(), 1); + + *b.state_ref() = 1; + Timer::wake_timeout_futures(); + AppCtx::run_until_stalled(); + + assert_eq!(track_origin.get(), 2); + assert_eq!(track_a.get(), 3); + assert_eq!(track_b.get(), 2); + } } diff --git a/core/src/state/partial_state.rs b/core/src/state/partial_state.rs new file mode 100644 index 000000000..6e18e5772 --- /dev/null +++ b/core/src/state/partial_state.rs @@ -0,0 +1,176 @@ +use super::{route_state, Modifier, ModifyScope, RefShare, Share}; +use rxrust::{ + prelude::{ObservableItem, Observer}, + subject::Subject, + subscription::Subscription, +}; +use std::{any::Any, rc::Rc}; + +/// A partial state is a state that is a subset of another state. This state +/// share this part data with the original state. But its modifier is +/// independent with the original state. When modify this state, its original +/// state will not be notified. But when modify the original state, this state +/// will be notified. + +// Keep the `S` as the first generic, so the user know the actual state type +// when ide hints. +pub struct PartialState +where + T: Share, + R1: FnOnce(&T::V) -> &S + Copy, + R2: FnOnce(&mut T::V) -> &mut S + Copy, +{ + origin_state: T, + router: R1, + router_mut: R2, + modifier: Modifier, + connect_guard: Rc>, +} + +/// The reference of `OrphanState`. + +// Keep the `S` as the first generic, so the user know the actual state type +// when ide hints. +pub struct PartialRef +where + O: RefShare, + R1: FnOnce(&O::Target) -> &S + Copy, + R2: FnOnce(&mut O::Target) -> &mut S + Copy, +{ + origin_ref: O, + router: R1, + router_mut: R2, + modifier: Modifier, +} + +impl Share for PartialState +where + T: Share, + R1: FnOnce(&T::V) -> &S + Copy, + R2: FnOnce(&mut T::V) -> &mut S + Copy, +{ + type V = S; + type Origin = T::S; + type S = PartialState; + type RefTrack<'a> = PartialRef, R1, R2> where Self: 'a; + type Ref<'a> = route_state:: Ref, R1> where Self: 'a; + + fn as_ref(&self) -> Self::Ref<'_> { + route_state::Ref::new(self.origin_state.as_ref(), self.router) + } + + fn clone_state(&mut self) -> Self::S { + PartialState { + origin_state: self.origin_state.clone_state(), + router: self.router, + router_mut: self.router_mut, + modifier: self.modifier.clone(), + connect_guard: self.connect_guard.clone(), + } + } + + fn modifies(&mut self) -> rxrust::ops::box_it::BoxOp<'static, (), std::convert::Infallible> { + self.modifier.modifies() + } + + fn raw_modifies(&mut self) -> Subject<'static, ModifyScope, std::convert::Infallible> { + self.modifier.raw_modifies() + } + + #[inline] + fn origin_state(&mut self) -> Self::Origin { self.origin_state.clone_state() } + + #[inline] + fn state_ref(&'_ mut self) -> Self::RefTrack<'_> { + PartialRef { + origin_ref: self.origin_state.state_ref(), + router: self.router, + router_mut: self.router_mut, + modifier: self.modifier.clone(), + } + } + + #[inline] + fn own_data(&mut self, data: impl std::any::Any) { self.origin_state.own_data(data); } +} + +impl PartialState +where + T: Share, + R1: FnOnce(&T::V) -> &S + Copy, + R2: FnOnce(&mut T::V) -> &mut S + Copy, +{ + pub(super) fn new(mut state: T, router: R1, router_mut: R2) -> Self { + let modifier = Modifier::default(); + let c_modifier = modifier.clone(); + let h = state + .raw_modifies() + .subscribe(move |v| c_modifier.raw_modifies().next(v)) + .unsubscribe_when_dropped(); + + Self { + origin_state: state, + router, + router_mut, + modifier, + connect_guard: Rc::new(Box::new(h)), + } + } +} + +impl RefShare for PartialRef +where + T: RefShare, + R1: FnOnce(&T::Target) -> &S + Copy, + R2: FnOnce(&mut T::Target) -> &mut S + Copy, +{ + #[inline] + fn silent(&mut self) -> &mut Self { + self.origin_ref.silent(); + self + } + + #[inline] + fn shallow(&mut self) -> &mut Self { + self.origin_ref.shallow(); + self + } + + #[inline] + fn forget_modifies(&self) -> ModifyScope { self.origin_ref.forget_modifies() } +} + +impl std::ops::Deref for PartialRef +where + T: RefShare, + R1: FnOnce(&T::Target) -> &S + Copy, + R2: FnOnce(&mut T::Target) -> &mut S + Copy, +{ + type Target = S; + #[inline] + fn deref(&self) -> &Self::Target { (self.router)(self.origin_ref.deref()) } +} + +impl std::ops::DerefMut for PartialRef +where + T: RefShare, + R1: FnOnce(&T::Target) -> &S + Copy, + R2: FnOnce(&mut T::Target) -> &mut S + Copy, +{ + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { (self.router_mut)(self.origin_ref.deref_mut()) } +} + +impl Drop for PartialRef +where + T: RefShare, + R1: FnOnce(&T::Target) -> &S + Copy, + R2: FnOnce(&mut T::Target) -> &mut S + Copy, +{ + fn drop(&mut self) { + let scope = self.origin_ref.forget_modifies(); + if !scope.is_empty() { + self.modifier.emit(scope) + } + } +} diff --git a/core/src/state/readonly.rs b/core/src/state/readonly.rs index 218f334bb..d998cb759 100644 --- a/core/src/state/readonly.rs +++ b/core/src/state/readonly.rs @@ -1,4 +1,4 @@ -use super::{ModifyScope, Stateful, StatefulRef}; +use super::{ModifyScope, Stateful, TrackRef}; use rxrust::{observable, ops::box_it::BoxOp, prelude::BoxIt, subject::Subject}; use std::{convert::Infallible, rc::Rc}; @@ -8,7 +8,7 @@ pub enum Readonly { } pub enum ReadRef<'a, W> { - Stateful(StatefulRef<'a, W>), + Stateful(TrackRef<'a, W>), Stateless(&'a Rc), } @@ -33,7 +33,7 @@ impl Readonly { pub fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { match self { - Readonly::Stateful(s) => s.raw_modifies(), + Readonly::Stateful(s) => s.modifier.raw_modifies(), Readonly::Stateless(_) => Subject::default(), } } @@ -43,7 +43,7 @@ impl Readonly { #[inline] pub fn modifies(&self) -> BoxOp<'static, (), Infallible> { match self { - Readonly::Stateful(s) => s.modifies(), + Readonly::Stateful(s) => s.modifier.modifies(), Readonly::Stateless(_) => observable::create(|_| {}).box_it(), } } diff --git a/core/src/state/route_state.rs b/core/src/state/route_state.rs new file mode 100644 index 000000000..431955571 --- /dev/null +++ b/core/src/state/route_state.rs @@ -0,0 +1,164 @@ +use super::{ModifyScope, RefShare, Share}; +use rxrust::{ops::box_it::BoxOp, subject::Subject}; +use std::{convert::Infallible, ops::Deref}; + +/// A route state is shortcut state for the origin state. They are share the +/// same state, but the route state always route to the target of `P` before you +/// try to access. It's also have the same modifier with the origin state. +// Keep the `S` as the first generic, so the user know the actual state type +// when ide hints. +pub struct RouteState +where + T: Share, + R1: FnOnce(&T::V) -> &S + Copy, + R2: FnOnce(&mut T::V) -> &mut S + Copy, +{ + origin_state: T, + router: R1, + router_mut: R2, +} + +impl Share for RouteState +where + T: Share, + R1: FnOnce(&T::V) -> &S + Copy, + R2: FnOnce(&mut T::V) -> &mut S + Copy, +{ + type V = S; + type Origin = T::S; + type S = RouteState; + type RefTrack<'a> = RouteRef, R1, R2> + where + Self: 'a; + type Ref<'a> = Ref, R1> where Self: 'a; + + #[inline] + fn as_ref(&self) -> Self::Ref<'_> { Ref::new(self.origin_state.as_ref(), self.router) } + + #[inline] + fn clone_state(&mut self) -> Self::S { + RouteState { + origin_state: self.origin_state.clone_state(), + router: self.router, + router_mut: self.router_mut, + } + } + + #[inline] + fn modifies(&mut self) -> BoxOp<'static, (), Infallible> { self.origin_state.modifies() } + + #[inline] + fn raw_modifies(&mut self) -> Subject<'static, ModifyScope, Infallible> { + self.origin_state.raw_modifies() + } + + #[inline] + fn origin_state(&mut self) -> Self::Origin { self.origin_state.clone_state() } + + #[inline] + fn state_ref(&'_ mut self) -> Self::RefTrack<'_> { + RouteRef { + origin_ref: self.origin_state.state_ref(), + router: self.router, + router_mut: self.router_mut, + } + } + + #[inline] + fn own_data(&mut self, data: impl std::any::Any) { self.origin_state.own_data(data); } +} + +impl RouteState +where + T: Share, + R1: FnOnce(&T::V) -> &S + Copy, + R2: FnOnce(&mut T::V) -> &mut S + Copy, +{ + #[inline] + pub fn new(state: T, router: R1, router_mut: R2) -> Self { + Self { + origin_state: state, + router, + router_mut, + } + } +} + +/// The reference of `PartialState`. +// Keep the `S` as the first generic, so the user know the actual state type +// when ide hints. +pub struct RouteRef +where + T: RefShare, + R1: FnOnce(&T::Target) -> &S + Copy, + R2: FnOnce(&mut T::Target) -> &mut S + Copy, +{ + origin_ref: T, + router: R1, + router_mut: R2, +} + +pub struct Ref { + origin_ref: O, + router: R, +} + +impl &S + Copy> Ref { + #[inline] + pub fn new(origin_ref: O, router: R) -> Self { Self { origin_ref, router } } +} + +impl Deref for Ref +where + O: Deref, + R: FnOnce(&O::Target) -> &S + Copy, +{ + type Target = S; + + #[inline] + fn deref(&self) -> &Self::Target { (self.router)(self.origin_ref.deref()) } +} + +impl Deref for RouteRef +where + T: RefShare, + R1: FnOnce(&T::Target) -> &S + Copy, + R2: FnOnce(&mut T::Target) -> &mut S + Copy, +{ + type Target = S; + + #[inline] + fn deref(&self) -> &Self::Target { (self.router)(self.origin_ref.deref()) } +} + +impl std::ops::DerefMut for RouteRef +where + T: RefShare, + R1: FnOnce(&T::Target) -> &S + Copy, + R2: FnOnce(&mut T::Target) -> &mut S + Copy, +{ + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { (self.router_mut)(self.origin_ref.deref_mut()) } +} + +impl RefShare for RouteRef +where + T: RefShare, + R1: FnOnce(&T::Target) -> &S + Copy, + R2: FnOnce(&mut T::Target) -> &mut S + Copy, +{ + #[inline] + fn silent(&mut self) -> &mut Self { + self.origin_ref.silent(); + self + } + + #[inline] + fn shallow(&mut self) -> &mut Self { + self.origin_ref.shallow(); + self + } + + #[inline] + fn forget_modifies(&self) -> ModifyScope { self.origin_ref.forget_modifies() } +} diff --git a/core/src/state/stateful.rs b/core/src/state/stateful.rs index 7f13523a0..53a5d065a 100644 --- a/core/src/state/stateful.rs +++ b/core/src/state/stateful.rs @@ -1,9 +1,7 @@ -use crate::{impl_proxy_query, impl_query_self_only, prelude::*}; -pub use guards::ModifyGuard; +use crate::{impl_proxy_query, impl_proxy_render, impl_query_self_only, prelude::*}; use rxrust::{ops::box_it::BoxOp, prelude::*}; use std::{ cell::{Cell, RefCell, UnsafeCell}, - collections::LinkedList, convert::Infallible, ops::{Deref, DerefMut}, rc::Rc, @@ -11,46 +9,39 @@ use std::{ /// Stateful object use to watch the modifies of the inner data. pub struct Stateful { - inner: Rc>, - pub(crate) modify_notifier: StateChangeNotifier, + pub(crate) inner: Rc>, + pub(crate) modifier: Modifier, } -/// notify downstream when widget state changed, the value mean if the change it -/// as silent or not. +/// The modifier is a `RxRust` stream that emit notification when the state +/// changed. #[derive(Default, Clone)] // todo: remove rc -pub(crate) struct StateChangeNotifier(Rc>>); +pub struct Modifier(Rc>>); /// A reference of `Stateful which tracked the state change across if user /// mutable deref this reference. -/// -/// `StateRef` also help implicit borrow inner widget mutable or not by deref or -/// deref_mut. And early drop the inner borrow if need, so the borrow lifetime -/// not bind to struct lifetime. Useful to avoid borrow conflict. For example -/// -/// ```ignore -/// (this.ref_y() > 0).then(move || this.mut_y()); -/// ``` -/// -/// Assume above code in `widget!` macro and `this` is a tracked stateful -/// widget, Two variables of borrow result of two `this` have lifetime overlap. -/// But in logic, first borrow needn't live as long as the statement. See -/// relative rust issue https://github.com/rust-lang/rust/issues/37612 - -pub struct StatefulRef<'a, W> { - /// - None, Not used the value - /// - Some(false), borrow used the value - /// - Some(true), mutable borrow used the value - mut_accessed_flag: Cell>, +pub struct TrackRef<'a, W> { + ref_state: Cell, modify_scope: ModifyScope, - value: ModifyGuard<'a, W>, + value: &'a Stateful, +} + +pub struct Ref<'a, W>(&'a Stateful); + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] +enum BorrowState { + #[default] + None, + Reading, + Writing, } bitflags! { - #[derive(Clone, Copy, PartialEq, Eq, Debug)] + #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] pub struct ModifyScope: u8 { /// state change only effect the data, transparent to ribir framework. - const DATA = 0x001; + const DATA = 0x01; /// state change only effect to framework, transparent to widget data. const FRAMEWORK = 0x010; /// state change effect both widget data and framework. @@ -58,70 +49,141 @@ bitflags! { } } -mod guards { - use super::*; - pub struct ModifyGuard<'a, W>(&'a Stateful); +impl Share for Stateful { + type V = W; + type Origin = Self; + type S = Self; + type Ref<'a> = Ref<'a,W> + where + Self: 'a; + type RefTrack<'a> = TrackRef<'a, W> + where + Self: 'a; + + fn as_ref(&self) -> Ref { + let b = &self.inner.borrow_cnt; + b.set(b.get() + 1); + Ref(self) + } - impl<'a, W> ModifyGuard<'a, W> { - pub(crate) fn new(data: &'a Stateful) -> Self { - let guards = &data.inner.guard_cnt; - guards.set(guards.get() + 1); - Self(data) - } + /// Return a reference of `Stateful`, modify across this reference will notify + /// data and framework. + #[inline] + fn state_ref(&mut self) -> TrackRef { TrackRef::new(self, ModifyScope::BOTH) } - pub(crate) fn inner_ref(&self) -> &'a Stateful { self.0 } + #[inline] + fn clone_state(&mut self) -> Self::S { self.clone() } + + #[inline] + fn origin_state(&mut self) -> Self::Origin { self.clone() } + + #[inline] + fn modifies(&mut self) -> BoxOp<'static, (), Infallible> { self.modifier.modifies() } + + #[inline] + fn raw_modifies(&mut self) -> Subject<'static, ModifyScope, Infallible> { + self.modifier.raw_modifies() } - impl<'a, W> Drop for ModifyGuard<'a, W> { - fn drop(&mut self) { - let guards = &self.0.inner.guard_cnt; - guards.set(guards.get() - 1); - - if guards.get() == 0 { - let inner = &self.0.inner; - assert_eq!(UNUSED, inner.borrow_flag.get()); - let scope = inner.modify_scope.take(); - if let Some(scope) = scope { - self.0.raw_modifies().next(scope); - } - } + #[inline] + fn own_data(&mut self, data: impl Any) { + let link = unsafe { &mut *self.inner.slot_link.get() }; + let mut node = SlotNode { _value: Box::new(data), next: None }; + if let Some(next) = link.take() { + node.next = Some(Box::new(next)); } + *link = Some(node); } +} - impl<'a, W> std::ops::Deref for ModifyGuard<'a, W> { - type Target = Stateful; +impl<'a, W> RefShare for TrackRef<'a, W> { + /// Convert this state reference to silent which only notify the modifies + /// but be ignored by the framework. + fn silent(&mut self) -> &mut Self { + self.modify_scope.remove(ModifyScope::FRAMEWORK); + self + } - #[inline] - fn deref(&self) -> &Self::Target { self.0 } + fn shallow(&mut self) -> &mut Self { + self.modify_scope.remove(ModifyScope::DATA); + self + } + + fn forget_modifies(&self) -> ModifyScope { + let ref_state = self.ref_state.replace(BorrowState::None); + let b = &self.value.inner.borrow_cnt; + match ref_state { + BorrowState::Reading => b.set(b.get() - 1), + BorrowState::Writing => b.set(b.get() + 1), + BorrowState::None => {} + } + + if BorrowState::Writing == ref_state { + self.modify_scope + } else { + ModifyScope::empty() + } } } +/// A single linked list. +struct SlotNode { + _value: Box, + next: Option>, +} + type BorrowFlag = isize; const UNUSED: BorrowFlag = 0; -#[inline(always)] -fn is_reading(x: BorrowFlag) -> bool { x > UNUSED } - -struct InnerStateful { - borrow_flag: Cell, - modify_scope: Cell>, +pub(crate) struct StateData { + data: UnsafeCell, + borrow_cnt: Cell, #[cfg(debug_assertions)] + /// the place where the data be borrowed. borrowed_at: Cell>>, - guard_cnt: Cell, - data: UnsafeCell, /// A link list to store anonymous data, so keep it live as long as the - /// `Stateful` data. When this `Stateful` subscribe to a stream, append the - /// unsubscribe handle to this list let you can unsubscribe when this - /// `Stateful` drop. - slot_link: UnsafeCell>>, + /// `data`. For example, when this `State` subscribe to a upstream, append + /// the unsubscribe handle to this list let you can unsubscribe when this + /// `data` drop. + slot_link: UnsafeCell>, +} + +macro_rules! debug_borrow_location { + ($this: ident) => { + #[cfg(debug_assertions)] + { + let caller = std::panic::Location::caller(); + $this.value.inner.borrowed_at.set(Some(caller)); + } + }; +} + +macro_rules! already_borrow_panic { + ($this: ident) => { + #[cfg(debug_assertions)] + { + let location = $this.value.inner.borrowed_at.get().unwrap(); + panic!("already borrowed at {}", location); + } + #[cfg(not(debug_assertions))] + panic!("already borrowed"); + }; } +#[repr(transparent)] +pub(crate) struct RenderFul(pub(crate) Stateful); + +impl_proxy_query!(paths [0], RenderFul, , where R: Render + 'static); +impl_proxy_render!(proxy 0.query_ref(), RenderFul, , where R: Render + 'static); +impl_proxy_query!(paths[0.query_ref()], RenderFul>); +impl_proxy_render!(proxy 0.query_ref(), RenderFul>); + impl Clone for Stateful { #[inline] fn clone(&self) -> Self { Self { inner: self.inner.clone(), - modify_notifier: self.modify_notifier.clone(), + modifier: self.modifier.clone(), } } } @@ -129,27 +191,21 @@ impl Clone for Stateful { impl Stateful { pub fn new(data: W) -> Self { Stateful { - inner: Rc::new(InnerStateful { + inner: Rc::new(StateData { data: UnsafeCell::new(data), - borrow_flag: Cell::new(0), - modify_scope: Cell::new(None), + borrow_cnt: Cell::new(0), #[cfg(debug_assertions)] borrowed_at: Cell::new(None), - guard_cnt: Cell::new(0), slot_link: <_>::default(), }), - modify_notifier: <_>::default(), + modifier: <_>::default(), } } - /// Return a guard that batch the modify event. - #[inline] - pub fn modify_guard(&self) -> ModifyGuard { ModifyGuard::new(self) } - /// Return a reference of `Stateful`, modify across this reference will notify /// data and framework. #[inline] - pub fn state_ref(&self) -> StatefulRef { StatefulRef::new(self, ModifyScope::BOTH) } + pub fn state_ref(&self) -> TrackRef { TrackRef::new(self, ModifyScope::BOTH) } /// Return a reference of `Stateful`, modify across this reference will notify /// data only, the relayout or paint depends on this object will not be skip. @@ -157,43 +213,22 @@ impl Stateful { /// If you not very clear how `silent_ref` work, use [`Stateful::state_ref`]! /// instead of. #[inline] - pub fn silent_ref(&self) -> StatefulRef { StatefulRef::new(self, ModifyScope::DATA) } - - /// Return a reference of `Stateful`, modify across this reference will notify - /// framework only. That means this modify only effect framework but not - /// effect on data. And usually use it to temporary to modify the `Stateful`. - /// - /// If you not very clear how `shallow_ref` work, use [`Stateful::state_ref`]! - /// instead of. - #[inline] - pub(crate) fn shallow_ref(&self) -> StatefulRef { - StatefulRef::new(self, ModifyScope::FRAMEWORK) - } + pub fn silent_ref(&self) -> TrackRef { TrackRef::new(self, ModifyScope::DATA) } pub fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { - self.modify_notifier.raw_modifies() + self.modifier.raw_modifies() } /// Notify when this widget be mutable accessed, no mather if the widget /// really be modified, the value is hint if it's only access by silent ref. #[inline] - pub fn modifies(&self) -> BoxOp<'static, (), Infallible> { - self - .raw_modifies() - .filter_map(|s: ModifyScope| s.contains(ModifyScope::DATA).then_some(())) - .box_it() - } + pub fn modifies(&self) -> BoxOp<'static, (), Infallible> { self.modifier.modifies() } /// Clone the stateful widget of which the reference point to. Require mutable /// reference because we try to early release inner borrow when clone occur. #[inline] pub fn clone_stateful(&self) -> Stateful { self.clone() } - pub fn own_data(&self, data: impl Any) { - let ptr = self.state_ref().value.inner.slot_link.get(); - unsafe { &mut *ptr }.push_back(Box::new(data)); - } - pub(crate) fn try_into_inner(self) -> Result { if Rc::strong_count(&self.inner) == 1 { let inner = Rc::try_unwrap(self.inner).unwrap_or_else(|_| unreachable!()); @@ -202,89 +237,63 @@ impl Stateful { Err(self) } } -} -macro_rules! debug_borrow_location { - ($this: ident) => { - #[cfg(debug_assertions)] - { - let caller = std::panic::Location::caller(); - $this.value.inner.borrowed_at.set(Some(caller)); - } - }; -} + /// This is a unsafe method to get the inner data reference. This method will + /// avoid to mark the borrow flag of the inner data, and is only designed for + /// the framework query. So this method will not bring any borrow restriction. + /// + /// # Safety + /// + /// The caller should guarantee that only use to query the inner data and do a + /// callback on it. And not use the return value to do any other thing. + /// Otherwise, the borrow check may be broken. + /// + /// Panic if there is any other mutable borrow to the inner data. + /// + /// # Notice + /// + /// Always keep this method be private. + fn query_ref(&self) -> &W { + assert!(!self.is_writing()); + unsafe { &*self.inner.data.get() } + } -macro_rules! already_borrow_panic { - ($this: ident) => { - #[cfg(debug_assertions)] - { - let location = $this.value.inner.borrowed_at.get().unwrap(); - panic!("already borrowed at {}", location); - } - #[cfg(not(debug_assertions))] - panic!("already borrowed"); - }; + #[inline(always)] + fn is_reading(&self) -> bool { self.inner.borrow_cnt.get() > UNUSED } + + #[inline(always)] + fn is_writing(&self) -> bool { self.inner.borrow_cnt.get() < UNUSED } } -impl<'a, W> Deref for StatefulRef<'a, W> { +impl<'a, W> Deref for Ref<'a, W> { type Target = W; #[track_caller] fn deref(&self) -> &Self::Target { - if self.mut_accessed_flag.get().is_none() { - self.mut_accessed_flag.set(Some(false)); - let b = &self.value.inner.borrow_flag; - b.set(b.get() + 1); + // SAFETY: `BorrowFlag` guarantees unique access. + unsafe { &*self.0.inner.data.get() } + } +} - match b.get() { - 1 => { - debug_borrow_location!(self); - } - flag if !is_reading(flag) => { - already_borrow_panic!(self); - } - _ => {} - } - if !is_reading(b.get()) {} - } +impl<'a, W> Deref for TrackRef<'a, W> { + type Target = W; + #[track_caller] + fn deref(&self) -> &Self::Target { + self.read(); // SAFETY: `BorrowFlag` guarantees unique access. - let ptr = self.value.inner.data.get(); - unsafe { &*ptr } + unsafe { &*self.value.inner.data.get() } } } -impl<'a, W> DerefMut for StatefulRef<'a, W> { +impl<'a, W> DerefMut for TrackRef<'a, W> { #[track_caller] fn deref_mut(&mut self) -> &mut Self::Target { - let b = &self.value.inner.borrow_flag; if log::log_enabled!(log::Level::Debug) { let caller = std::panic::Location::caller(); - log::debug!("state modified at {caller}"); - } - - match self.mut_accessed_flag.get() { - None => { - debug_borrow_location!(self); - b.set(b.get() - 1); - self.mut_accessed_flag.set(Some(true)) - } - Some(false) => { - debug_borrow_location!(self); - - // current ref is borrowed value, we release the borrow and mutably - // borrow the value. - b.set(b.get() - 2); - self.mut_accessed_flag.set(Some(true)) - } - Some(true) => { - // Already mutably the value, directly return. - } - } - - if b.get() != -1 { - already_borrow_panic!(self); + log::trace!("state modified at {caller}"); } + self.write(); // SAFETY: `BorrowFlag` guarantees unique access. let ptr = self.value.inner.data.get(); @@ -292,26 +301,7 @@ impl<'a, W> DerefMut for StatefulRef<'a, W> { } } -impl<'a, W> StatefulRef<'a, W> { - /// Fork a silent reference - pub fn silent(&self) -> StatefulRef<'a, W> { - self.release_borrow(); - StatefulRef::new(self.value.inner_ref(), ModifyScope::DATA) - } - - /// Forget all modifies record in this reference. So the downstream will no - /// know the inner value be modified if this reference not be mut accessed - /// anymore. - pub fn forget_modifies(&self) { - let b = &self.value.inner.borrow_flag; - match self.mut_accessed_flag.get() { - Some(false) => b.set(b.get() - 1), - Some(true) => b.set(b.get() + 1), - None => {} - } - self.mut_accessed_flag.set(None); - } - +impl<'a, W> TrackRef<'a, W> { #[inline] pub fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { self.value.raw_modifies() @@ -322,56 +312,110 @@ impl<'a, W> StatefulRef<'a, W> { fn new(value: &'a Stateful, modify_scope: ModifyScope) -> Self { Self { - mut_accessed_flag: Cell::new(None), + value, + ref_state: <_>::default(), modify_scope, - value: ModifyGuard::new(value), } } + /// Clone the stateful widget of which the reference point to. Require mutable + /// reference because we try to early release inner borrow when clone occur. #[inline] - fn release_borrow(&self) { - let b = &self.value.inner.borrow_flag; - match self.mut_accessed_flag.get() { - Some(false) => b.set(b.get() - 1), - Some(true) => { - b.set(b.get() + 1); - let scope = &self.value.inner.modify_scope; - let union_scope = scope - .get() - .map_or(self.modify_scope, |s| s.union(self.modify_scope)); - scope.set(Some(union_scope)); + pub fn clone_stateful(&self) -> Stateful { self.value.clone() } + + #[track_caller] + pub(crate) fn read(&self) { + // if this references already during reading or writing, we needn't to mark + // borrow flag. + if self.ref_state.get() == BorrowState::None { + self.ref_state.set(BorrowState::Reading); + let b = &self.value.inner.borrow_cnt; + b.set(b.get() + 1); + if b.get() == 1 { + debug_borrow_location!(self); + } else if !self.value.is_reading() { + already_borrow_panic!(self); } - None => {} } - self.mut_accessed_flag.set(None); } - /// Clone the stateful widget of which the reference point to. Require mutable - /// reference because we try to early release inner borrow when clone occur. + #[track_caller] + pub(crate) fn write(&self) { + if log::log_enabled!(log::Level::Debug) { + let caller = std::panic::Location::caller(); + log::trace!("state modified at {caller}"); + } - #[inline] - pub fn clone_stateful(&self) -> Stateful { self.value.clone() } + let b = &self.value.inner.borrow_cnt; + match self.ref_state.get() { + BorrowState::None => { + debug_borrow_location!(self); + b.set(b.get() - 1); + self.ref_state.set(BorrowState::Writing) + } + BorrowState::Reading => { + // current ref is borrowed value, we release the borrow and mutably + // borrow the value. + b.set(b.get() - 2); + self.ref_state.set(BorrowState::Writing) + } + BorrowState::Writing => { + // Already mutably the value, directly return. + } + } + + if b.get() != -1 { + already_borrow_panic!(self); + } + } } impl SingleChild for Stateful {} impl MultiChild for Stateful {} impl_proxy_query!( - paths [modify_notifier, state_ref()], + paths [modifier, query_ref()], Stateful, , where R: Query + 'static ); -impl_query_self_only!(StateChangeNotifier); +impl_query_self_only!(Modifier); + +impl<'a, W> Drop for TrackRef<'a, W> { + fn drop(&mut self) { + let scope = self.forget_modifies(); + if !scope.is_empty() { + let mut subject = self.value.modifier.raw_modifies(); + observable::timer(scope, std::time::Duration::ZERO, AppCtx::scheduler()) + .subscribe(move |scope| subject.next(scope)); + } + } +} -impl<'a, W> Drop for StatefulRef<'a, W> { - fn drop(&mut self) { self.release_borrow(); } +impl<'a, W> Drop for Ref<'a, W> { + fn drop(&mut self) { + let b = &self.0.inner.borrow_cnt; + b.set(b.get() - 1); + } } -impl StateChangeNotifier { +impl Modifier { + pub fn modifies(&self) -> BoxOp<'static, (), Infallible> { + self + .raw_modifies() + .filter_map(|s: ModifyScope| s.contains(ModifyScope::DATA).then_some(())) + .box_it() + } + pub(crate) fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { self.0.borrow().clone() } pub(crate) fn reset(&self) { *self.0.borrow_mut() = <_>::default(); } + + pub(crate) fn emit(&self, scope: ModifyScope) { + if !scope.is_empty() { + self.0.borrow_mut().next(scope) + } + } } #[cfg(test)] @@ -379,11 +423,11 @@ mod tests { use std::cell::RefCell; use super::*; - use crate::test_helper::*; + use crate::{test_helper::*, timer::Timer}; #[test] fn smoke() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + crate::rest_test_env!(); // Simulate `MockBox` widget need modify its size in event callback. Can use the // `cell_ref` in closure. @@ -396,7 +440,7 @@ mod tests { #[test] fn state_notify_and_relayout() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + crate::rest_test_env!(); use std::{cell::RefCell, rc::Rc}; let notified_count = Rc::new(RefCell::new(0)); @@ -424,6 +468,8 @@ mod tests { { state.state_ref().size = Size::new(1., 1.); } + Timer::wake_timeout_futures(); + AppCtx::run_until_stalled(); assert!(wnd.widget_tree.borrow().is_dirty()); wnd.draw_frame(); assert_eq!(*notified_count.borrow(), 1); @@ -432,7 +478,7 @@ mod tests { #[test] fn fix_pin_widget_node() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + crate::rest_test_env!(); let mut wnd = TestWindow::new(widget! { MockBox { size: Size::new(100., 100.) } }); wnd.draw_frame(); @@ -442,7 +488,7 @@ mod tests { #[test] fn change_notify() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + crate::rest_test_env!(); let notified = Rc::new(RefCell::new(vec![])); let c_notified = notified.clone(); @@ -453,11 +499,17 @@ mod tests { { let _ = &mut w.state_ref().size; } + Timer::wake_timeout_futures(); + AppCtx::run_until_stalled(); + assert_eq!(notified.borrow().deref(), &[ModifyScope::BOTH]); { let _ = &mut w.silent_ref().size; } + + Timer::wake_timeout_futures(); + AppCtx::run_until_stalled(); assert_eq!( notified.borrow().deref(), &[ModifyScope::BOTH, ModifyScope::DATA] @@ -471,6 +523,9 @@ mod tests { let _ = &mut silent_ref; let _ = &mut silent_ref; } + + Timer::wake_timeout_futures(); + AppCtx::run_until_stalled(); assert_eq!( notified.borrow().deref(), &[ModifyScope::BOTH, ModifyScope::DATA] diff --git a/core/src/test_helper.rs b/core/src/test_helper.rs index 715f7a45f..7b48eae4f 100644 --- a/core/src/test_helper.rs +++ b/core/src/test_helper.rs @@ -17,6 +17,16 @@ pub struct Frame { } pub struct TestWindow(pub Rc); +pub fn rest_test_env() {} + +#[macro_export] +macro_rules! rest_test_env { + () => { + let _ = rxrust::scheduler::NEW_TIMER_FN.set(Timer::new_timer_future); + let _guard = unsafe { AppCtx::new_lock_scope() }; + }; +} + impl TestWindow { /// Create a 1024x1024 window for test pub fn new(root: impl Into) -> Self { Self::new_wnd(root, None) } @@ -68,8 +78,17 @@ impl TestWindow { pub fn draw_frame(&mut self) { // Test window not have a eventloop, manually wake-up every frame. Timer::wake_timeout_futures(); + AppCtx::run_until_stalled(); + + let need_draw = self.0.need_draw(); + + while self.widget_tree.borrow().is_dirty() { + self.frame_process(); + Timer::wake_timeout_futures(); + AppCtx::run_until_stalled() + } - self.0.draw_frame(); + self.0.draw_frame(need_draw); } } diff --git a/core/src/widget.rs b/core/src/widget.rs index 23d2521a7..3cb80c4cb 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -306,24 +306,9 @@ impl WidgetBuilder for Stateful { fn build(self, ctx: &BuildCtx) -> WidgetId { State::Stateful(self).build(ctx) } } -#[repr(transparent)] -pub(crate) struct RenderFul(pub(crate) Stateful); - -impl_proxy_query!(paths [0], RenderFul, , where R: Render + 'static); -impl_proxy_render!(proxy 0.state_ref(), RenderFul, , where R: Render + 'static); -impl_proxy_query!(paths[0.state_ref()], RenderFul>); -impl_proxy_render!(proxy 0.state_ref(), RenderFul>); - impl Compose for R { fn compose(this: State) -> Widget { - FnWidget::new(move |ctx| { - let node: Box = match this { - State::Stateless(r) => Box::new(r), - State::Stateful(s) => Box::new(RenderFul(s)), - }; - ctx.alloc_widget(node) - }) - .into() + FnWidget::new(move |ctx| ctx.alloc_widget(this.into())).into() } } diff --git a/core/src/widget_children/compose_child_impl.rs b/core/src/widget_children/compose_child_impl.rs index 481301584..c5c335766 100644 --- a/core/src/widget_children/compose_child_impl.rs +++ b/core/src/widget_children/compose_child_impl.rs @@ -288,7 +288,6 @@ where #[cfg(test)] mod tests { - use std::{cell::Cell, rc::Rc}; use super::*; use crate::{prelude::*, test_helper::MockBox}; @@ -391,9 +390,7 @@ mod tests { .build(ctx) }); let _ = FnWidget::new(|ctx| { - let cursor = Cursor { - cursor: Rc::new(Cell::new(CursorIcon::Hand)), - }; + let cursor = Cursor { cursor: CursorIcon::Hand }; let x = cursor.with_child(Tml.with_child(A, ctx), ctx); WithDecorate .with_child(mb.with_child(x, ctx), ctx) diff --git a/core/src/widget_tree.rs b/core/src/widget_tree.rs index 4957b5e61..78159cd9d 100644 --- a/core/src/widget_tree.rs +++ b/core/src/widget_tree.rs @@ -76,10 +76,7 @@ impl WidgetTree { pub(crate) fn count(&self) -> usize { self.root().descendants(&self.arena).count() } pub(crate) fn window(&self) -> Rc { - self - .wnd - .upgrade() - .expect("The window of `FocusManager` has already dropped.") + self.wnd.upgrade().expect("The window has already dropped.") } #[allow(unused)] diff --git a/core/src/widget_tree/widget_id.rs b/core/src/widget_tree/widget_id.rs index 6b3216a8c..37b5976df 100644 --- a/core/src/widget_tree/widget_id.rs +++ b/core/src/widget_tree/widget_id.rs @@ -5,7 +5,7 @@ use super::WidgetTree; use crate::{ builtin_widgets::Void, context::{PaintingCtx, WidgetCtx}, - state::{ModifyScope, StateChangeNotifier}, + state::{Modifier, ModifyScope}, widget::{QueryOrder, Render}, window::DelayEvent, }; @@ -137,7 +137,7 @@ impl WidgetId { pub(crate) fn on_mounted(self, tree: &WidgetTree) { self.assert_get(&tree.arena).query_all_type( - |notifier: &StateChangeNotifier| { + |notifier: &Modifier| { let state_changed = tree.dirty_set.clone(); notifier .raw_modifies() diff --git a/core/src/window.rs b/core/src/window.rs index 72081ca4e..c1d43149e 100644 --- a/core/src/window.rs +++ b/core/src/window.rs @@ -1,5 +1,4 @@ use crate::{ - animation::AnimateTrack, context::AppCtx, events::{ dispatcher::Dispatcher, @@ -18,7 +17,7 @@ use ribir_geom::Point; use rxrust::{scheduler::FuturesLocalScheduler, subject::Subject}; use std::{ borrow::BorrowMut, - cell::RefCell, + cell::{Cell, RefCell}, collections::VecDeque, convert::Infallible, ops::{Deref, DerefMut}, @@ -41,7 +40,7 @@ pub struct Window { pub(crate) widget_tree: RefCell, pub(crate) frame_ticker: FrameTicker, pub(crate) focus_mgr: RefCell, - pub(crate) running_animates: Rc>, + pub(crate) running_animates: Rc>, /// This vector store the task to emit events. When perform layout, dispatch /// event and so on, some part of window may be already mutable borrowed and /// the user event callback may also query borrow that part, so we can't emit @@ -110,27 +109,21 @@ impl Window { self.frame_ticker.frame_tick_stream() } - pub fn animate_track(&self) -> AnimateTrack { - AnimateTrack { - actived: false, - actived_cnt: self.running_animates.clone(), - } - } - - /// Draw an image what current render tree represent. - #[track_caller] - pub fn draw_frame(&self) { - self.emit_events(); + pub fn inc_running_animate(&self) { self.running_animates.set(self.running_animates.get() + 1); } - if !self.need_draw() { - return; - } + pub fn dec_running_animate(&self) { self.running_animates.set(self.running_animates.get() - 1); } + pub fn frame_process(&self) { self.frame_ticker.emit(FrameMsg::NewFrame(Instant::now())); self.shell_wnd.borrow_mut().begin_frame(); loop { + // focus refresh and event emit may cause widget tree dirty again. + if !self.widget_tree.borrow().is_dirty() { + break; + } + self.layout(); self.emit_events(); @@ -140,16 +133,22 @@ impl Window { if !self.widget_tree.borrow().is_dirty() { self.focus_mgr.borrow_mut().refresh_focus(); self.emit_events(); - - // focus refresh and event emit may cause widget tree dirty again. - if !self.widget_tree.borrow().is_dirty() { - break; - } } } + } - self.widget_tree.borrow().draw(); + /// Draw an image what current render tree represent. + #[track_caller] + pub fn draw_frame(&self, force: bool) { + self.emit_events(); + if !force && !self.need_draw() { + return; + } + + self.frame_process(); + + self.widget_tree.borrow().draw(); let surface = match AppCtx::app_theme() { Theme::Full(theme) => theme.palette.surface(), Theme::Inherit(_) => unreachable!(), @@ -177,7 +176,7 @@ impl Window { } pub fn need_draw(&self) -> bool { - self.widget_tree.borrow().is_dirty() || *self.running_animates.borrow() > 0 + self.widget_tree.borrow().is_dirty() || self.running_animates.get() > 0 } pub fn new(root: Widget, shell_wnd: Box) -> Rc { @@ -194,7 +193,7 @@ impl Window { focus_mgr, delay_emitter: <_>::default(), frame_ticker: FrameTicker::default(), - running_animates: Rc::new(RefCell::new(0)), + running_animates: Rc::new(Cell::new(0)), frame_pool: RefCell::new(<_>::default()), shell_wnd: RefCell::new(shell_wnd), }; diff --git a/dev-helper/src/widget_test.rs b/dev-helper/src/widget_test.rs index f43d79557..c12eb665b 100644 --- a/dev-helper/src/widget_test.rs +++ b/dev-helper/src/widget_test.rs @@ -345,11 +345,11 @@ macro_rules! assert_layout_result_by_path { ) => { $( let info = $test_wnd.layout_info_by_path(&$path).unwrap(); - $(assert_eq!($x_expect, info.pos.x, "unexpected x");)? - $(assert_eq!($y_expect, info.pos.y, "unexpected y");)? - $(assert_eq!($width_expect, info.size.unwrap().width, "unexpected width");)? - $(assert_eq!($height_expect, info.size.unwrap().height, "unexpected height");)? - $(assert_eq!($size_expect, info.size.unwrap(), "unexpected size");)? + $(assert_eq!(info.pos.x, $x_expect, "unexpected x");)? + $(assert_eq!(info.pos.y, $y_expect, "unexpected y");)? + $(assert_eq!(info.size.unwrap().width, $width_expect, "unexpected width");)? + $(assert_eq!(info.size.unwrap().height, $height_expect, "unexpected height");)? + $(assert_eq!(info.size.unwrap(), $size_expect, "unexpected size");)? $( let size = info.size.unwrap(); assert_eq!(Rect::new(info.pos, size), $rect_expect, "unexpected rect"); diff --git a/examples/counter/src/counter.rs b/examples/counter/src/counter.rs index 58f2ac86b..92e457b61 100644 --- a/examples/counter/src/counter.rs +++ b/examples/counter/src/counter.rs @@ -2,19 +2,19 @@ use ribir::prelude::*; pub fn counter() -> impl Into { fn_widget! { - let cnt = Stateful::new(0); + let mut cnt = Stateful::new(0); @Column { h_align: HAlign::Center, align_items: Align::Center, @FilledButton { on_tap: move |_: &mut _| *$cnt += 1, - @ { Label::new("Add") } + @{ Label::new("Add") } } @H1 { text: pipe!($cnt.to_string()) } @FilledButton { on_tap: move |_: &mut _| *$cnt += -1, - @ { Label::new("Sub") } + @{ Label::new("Sub") } } } } diff --git a/examples/greet/Cargo.toml b/examples/greet/Cargo.toml deleted file mode 100644 index 572ffc178..000000000 --- a/examples/greet/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -authors.workspace = true -categories.workspace = true -description.workspace = true -documentation.workspace = true -edition.workspace = true -homepage.workspace = true -keywords.workspace = true -license.workspace = true -name = "greet" -publish = false -version.workspace = true - -[dependencies] -paste.workspace = true -# we disable `default-features`, because we want more control over testing. -ribir = {path = "../../ribir", features = ["material", "widgets"]} -ribir_dev_helper = {path = "../../dev-helper"} - -[features] -wgpu = ["ribir/wgpu"] diff --git a/examples/greet/README.md b/examples/greet/README.md deleted file mode 100644 index 9e9757f49..000000000 --- a/examples/greet/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Greet - -This is just a `widget!` syntax teaching, demo No consideration to its completeness and reasonableness. - - -You can run with: -``` -cargo run --p greet -``` diff --git a/examples/greet/src/greet.rs b/examples/greet/src/greet.rs deleted file mode 100644 index b8cc10a68..000000000 --- a/examples/greet/src/greet.rs +++ /dev/null @@ -1,68 +0,0 @@ -//! This is just a `widget!` syntax teaching demo No consideration to its -//! completeness and reasonableness. - -use ribir::prelude::*; - -pub fn greet() -> Widget { - widget! { - states { counter: Stateful::new(0) } - init ctx => { - let style = TypographyTheme::of(ctx).display_large.text.clone(); - } - Column { - Row { - align_items: Align::Center, - Input { - id: input, - Placeholder::new("Enter the name you want to greet.") - } - FilledButton { - on_tap: move |_| *counter += 1, - Label::new({ - let counter = counter.to_string(); - format!("Greet!({counter})") - }) - } - } - DynWidget { - dyns := assign_watch!(*counter > 0) - .stream_map(|o| o.distinct_until_changed()) - .map(move |need_greet| { - let style = style.clone(); - need_greet.then(move || { - widget! { - init ctx => { - let ease_in = transitions::EASE_IN.of(ctx); - } - Row { - Text { text: "Hello ", text_style: style.clone() } - Text { - id: greet, - text: no_watch!(input.text()), - text_style: style.clone() - } - Text { text: "!" , text_style: style } - } - Animate { - id: greet_new, - transition: ease_in, - prop: prop!(greet.transform), - from: Transform::translation(0., greet.layout_height() * 2.) - } - finally { - let_watch!(*counter) - .subscribe(move |_| { - greet.text = input.text(); - input.set_text(""); - }); - let_watch!(greet.text.clone()) - .subscribe(move |_| greet_new.run()); - } - } - }) - }) - } - } - } - .into() -} diff --git a/examples/greet/src/main.rs b/examples/greet/src/main.rs deleted file mode 100644 index 95b602930..000000000 --- a/examples/greet/src/main.rs +++ /dev/null @@ -1,8 +0,0 @@ -#![feature(test)] - -mod greet; -use greet::greet; -use ribir::prelude::*; -use ribir_dev_helper::*; - -example_framework!(greet, wnd_size = Size::new(640., 400.)); diff --git a/examples/todos/src/todos.rs b/examples/todos/src/todos.rs index 3fe21d001..5b2a62d1a 100644 --- a/examples/todos/src/todos.rs +++ b/examples/todos/src/todos.rs @@ -8,65 +8,55 @@ struct Task { label: String, } #[derive(Debug)] -struct TodoMVP { +struct Todos { tasks: Vec, + id_gen: usize, } -impl Compose for TodoMVP { - fn compose(this: State) -> Widget { - widget! { - states { this: this.into_writable(), id_gen: Stateful::new(this.state_ref().tasks.len()) } - init ctx => { - let surface_variant = Palette::of(ctx).surface_variant(); - let text_style = TypographyTheme::of(ctx).display_large.text.clone(); - } - Column { +impl Compose for Todos { + fn compose(mut this: State) -> Widget { + fn_widget! { + @Column { padding: EdgeInsets::all(10.), - Text { + @H1 { margin: EdgeInsets::only_bottom(10.), text: "Todo", - text_style, } - Row { - Container { - size: Size::new(240., 30.), - border: Border::only_bottom(BorderSide { width:1., color: surface_variant.into() }), - Input { - id: input, - Placeholder::new("What do you want to do ?") + @{ + let mut input = @Input { }; + let add_task = move |_: &mut _| if !$input.text().is_empty() { + $this.new_task($input.text().to_string()); + $input.set_text(""); + }; + @Row { + @Container { + size: Size::new(240., 30.), + border: { + let color = Palette::of(ctx!()).surface_variant().into(); + Border::only_bottom(BorderSide { width:1., color }) + }, + @ $input { @{ Placeholder::new("What do you want to do ?") } } + } + @FilledButton { + margin: EdgeInsets::only_left(20.), + on_tap: add_task, + @{ Label::new("ADD") } } - } - FilledButton { - margin: EdgeInsets::only_left(20.), - on_tap: move |_| { - if !input.text().is_empty() { - let label = input.text().to_string(); - this.tasks.push(Task { - id: *id_gen, - label, - finished: false, - }); - *id_gen += 1; - input.set_text(""); - } - }, - Label::new("ADD") } } - - Tabs { + @Tabs { pos: Position::Top, - Tab { - TabItem { Label::new("ALL") } - TabPane { Self::pane(no_watch!(this), |_| true) } + @Tab { + @TabItem { @{ Label::new("ALL") } } + @TabPane { @{ Self::pane(this.clone_state(), |_| true) } } } - Tab { - TabItem { Label::new("ACTIVE") } - TabPane { Self::pane(no_watch!(this), |task| !task.finished) } + @Tab { + @TabItem { @{ Label::new("ACTIVE") } } + @TabPane { @{ Self::pane(this.clone_state(), |task| !task.finished)} } } - Tab { - TabItem { Label::new("DONE") } - TabPane { Self::pane(no_watch!(this), |task| task.finished) } + @Tab { + @TabItem { @{ Label::new("DONE") } } + @TabPane { @{ Self::pane(this.clone_state(), |task| task.finished) } } } } } @@ -75,100 +65,85 @@ impl Compose for TodoMVP { } } -impl TodoMVP { - fn pane(this: StatefulRef, cond: fn(&Task) -> bool) -> Widget { - let this = this.clone_stateful(); - widget! { - states { this, mount_task_cnt: Stateful::new(0) } - VScrollBar { - Lists { +impl Todos { + fn pane(mut this: Stateful, cond: fn(&Task) -> bool) -> Widget { + fn_widget! { + let mut mount_task_cnt = Stateful::new(0); + let mut mount_idx = Stateful::new(0); + @VScrollBar { @ { pipe! { + @Lists { // when performed layout, means all task are mounted, we reset the mount count. - on_performed_layout: move |_| *mount_task_cnt = 0, + on_performed_layout: move |_| *$mount_task_cnt = 0, padding: EdgeInsets::vertical(8.), - DynWidget { - dyns: { - let tasks = this.tasks.clone(); - Multi::new( - tasks - .into_iter() - .enumerate() - .filter(move |(_, task)| { cond(task) }) - .map(move |(idx, task)| { - no_watch!(Self::task(this, task, idx, mount_task_cnt)) - }) - ) - } + @ { + let mut this2 = this.clone_state(); + let task_widget_iter = $this + .tasks + .iter() + .enumerate() + .filter_map(move |(idx, task)| { cond(task).then_some(idx) }) + .map(move |idx| { + let mut task = partial_state!($this2.tasks[idx]); + let mut key = @KeyWidget { key: $task.id, value: () }; + let mut mount_animate = @Animate { + transition: @Transition { + delay: Some(Duration::from_millis(100).mul_f32((*$mount_idx + 1) as f32)), + duration: Duration::from_millis(150), + easing: easing::EASE_IN, + }.unwrap(), + state: partial_state!($key.transform), + from: Transform::translation(-400., 0. ), + }; + @ $key { + @ListItem { + on_mounted: move |_| { + if $key.is_enter() { + *$mount_idx = *$mount_task_cnt; + *$mount_task_cnt += 1; + mount_animate.run(); + } + }, + @{ HeadlineText(Label::new($task.label.clone())) } + @Leading { + @{ + let mut checkbox = @Checkbox { + checked: pipe!($task.finished), + margin: EdgeInsets::vertical(4.), + }; + watch!($checkbox.checked).subscribe(move |v| $task.finished = v); + CustomEdgeWidget(checkbox.into()) + } + } + @Trailing { + cursor: CursorIcon::Hand, + visible: $key.mouse_hover(), + on_tap: move |_| { $this2.tasks.remove(idx); }, + @{ svgs::CLOSE } + } + } + } + }).collect::>(); + Multi::new(task_widget_iter) } + } - } + }}} } .into() } - fn task( - this: StatefulRef, - task: Task, - idx: usize, - mount_task_cnt: StatefulRef, - ) -> Widget { - let this = this.clone_stateful(); - let mount_task_cnt = mount_task_cnt.clone_stateful(); - widget! { - states { this, mount_task_cnt, mount_idx: Stateful::new(0) } - KeyWidget { - id: key, - key: Key::from(task.id), - value: Some(task.label.clone()), - ListItem { - id: item, - transform: Transform::default(), - on_mounted: move |_| { - if key.is_enter() { - *mount_idx = *mount_task_cnt; - *mount_task_cnt += 1; - mount_animate.run(); - } - }, - HeadlineText(Label::new(task.label.clone())) - Leading { - CustomEdgeWidget(widget! { - Checkbox { - id: checkbox, - checked: task.finished, - margin: EdgeInsets::vertical(4.), - } - finally { - let_watch!(checkbox.checked) - .subscribe(move |v| this.tasks[idx].finished = v); - } - }.into()) - } - Trailing { - cursor: CursorIcon::Hand, - visible: item.mouse_hover(), - on_tap: move |_| { this.tasks.remove(idx); }, - svgs::CLOSE - } - } - } - Animate { - id: mount_animate, - transition: Transition { - delay: Some(Duration::from_millis(100).mul_f32((*mount_idx + 1) as f32)), - duration: Duration::from_millis(150), - easing: easing::EASE_IN, - repeat: None, - }, - prop: prop!(item.transform), - from: Transform::translation(-400., 0. ), - } - } - .into() + fn new_task(&mut self, label: String) { + self.tasks.push(Task { + id: self.id_gen, + label, + finished: false, + }); + self.id_gen += 1; } } pub fn todos() -> Widget { - TodoMVP { + Todos { tasks: vec![ Task { id: 0, @@ -186,6 +161,7 @@ pub fn todos() -> Widget { label: "Support Virtual Scroll".to_string(), }, ], + id_gen: 3, } .into() } diff --git a/macros/src/declare_derive2.rs b/macros/src/declare_derive2.rs index 671a7067e..9d43f31e9 100644 --- a/macros/src/declare_derive2.rs +++ b/macros/src/declare_derive2.rs @@ -28,6 +28,7 @@ struct DeclareAttr { custom: Option, // field with `skip` attr, will not generate setter method and use default to init value. skip: Option, + strict: Option, } struct DeclareField<'a> { @@ -41,6 +42,7 @@ mod kw { custom_keyword!(default); custom_keyword!(custom); custom_keyword!(skip); + custom_keyword!(strict); // todo: tmp code, only for compatibility. custom_keyword!(convert); } @@ -82,6 +84,8 @@ impl Parse for DeclareAttr { attr.default = Some(input.parse()?); } else if lookahead.peek(kw::skip) { attr.skip = Some(input.parse()?); + } else if lookahead.peek(kw::strict) { + attr.strict = Some(input.parse()?); } else if lookahead.peek(kw::convert) { input.parse::()?; input.parse::()?; @@ -97,6 +101,15 @@ impl Parse for DeclareAttr { d.set_spans(vec![rename.span().unwrap(), builtin.span().unwrap()]); d.emit(); } + if let (Some(convert), Some(skip)) = (attr.strict.as_ref(), attr.skip.as_ref()) { + let mut d = Diagnostic::new( + Level::Error, + "field is marked as `skip` is not allowed to use `convert`.", + ); + d.set_spans(vec![convert.span().unwrap(), skip.span().unwrap()]); + d.emit(); + } + if !input.is_empty() { input.parse::()?; } @@ -134,8 +147,6 @@ fn struct_with_fields_gen( generics: &syn::Generics, name: &syn::Ident, ) -> syn::Result { - let (g_impl, g_ty, g_where) = generics.split_for_impl(); - let mut builder_fields = collect_filed_and_attrs(stt)?; // reverse name check. @@ -153,16 +164,25 @@ fn struct_with_fields_gen( let field_name = f.field.ident.as_ref().unwrap(); let ty = &f.field.ty; let set_method = f.set_method_name(); - - builder_methods.extend(quote! { - #[inline] - #vis fn #set_method<_M, _V>(mut self, v: _V) -> Self - where DeclareInit<#ty>: DeclareFrom<_V, _M> - { - self.#field_name = Some(DeclareInit::declare_from(v)); - self - } - }); + if f.attr.as_ref().map_or(false, |attr| attr.strict.is_some()) { + builder_methods.extend(quote! { + #[inline] + #vis fn #set_method(mut self, v: #ty) -> Self { + self.#field_name = Some(DeclareInit::Value(v)); + self + } + }); + } else { + builder_methods.extend(quote! { + #[inline] + #vis fn #set_method<_M, _V>(mut self, v: _V) -> Self + where DeclareInit<#ty>: DeclareFrom<_V, _M> + { + self.#field_name = Some(DeclareInit::declare_from(v)); + self + } + }); + } }); // builder define @@ -207,11 +227,12 @@ fn struct_with_fields_gen( let unzip_fields = builder_fields.iter().map(|df| { let field_name = df.field.ident.as_ref().unwrap(); let method = df.set_method_name(); + let err = format!( + "Required field `{}::{}` not set, use method `{}` init it", + name, field_name, method + ); quote_spanned! { field_name.span() => - let #field_name = self.#field_name.expect(&format!( - "Required field `{}::{}` not set, use method `{}` init it", - stringify!(#name), stringify!(#field_name), stringify!(#method) - )).unzip(); + let #field_name = self.#field_name.expect(#err).unzip(); } }); @@ -219,8 +240,15 @@ fn struct_with_fields_gen( let field_names2 = field_names.clone(); let field_names3 = field_names.clone(); + let (g_impl, g_ty, g_where) = generics.split_for_impl(); + let syn::Generics { + lt_token, + params, + gt_token, + where_clause, + } = generics; let tokens = quote! { - #vis struct #declarer #g_impl #g_where { + #vis struct #declarer #lt_token #params #gt_token #where_clause { #(#def_fields)* } @@ -248,10 +276,10 @@ fn struct_with_fields_gen( }); #( if let Some(u) = #field_names3.1 { - let mut _ribir2 = _ribir.clone_stateful(); + let mut _ribir2 = _ribir.clone_state(); let h = u.subscribe(move |v| _ribir2.state_ref().#field_names3 = v) .unsubscribe_when_dropped(); - _ribir.to_stateful().own_data(h); + _ribir.own_data(h); } );* diff --git a/macros/src/declare_obj.rs b/macros/src/declare_obj.rs index 47cf5de54..c04932418 100644 --- a/macros/src/declare_obj.rs +++ b/macros/src/declare_obj.rs @@ -4,20 +4,19 @@ use crate::{ }; use inflector::Inflector; use proc_macro2::{Span, TokenStream}; -use quote::quote; use quote::{quote_spanned, ToTokens}; use smallvec::SmallVec; use syn::{ parse_str, spanned::Spanned, - token::{Brace, Comma, Paren}, + token::{Brace, Paren}, Ident, Macro, Path, }; pub struct DeclareObj<'a> { this: ObjNode<'a>, span: Span, - builtin: ahash::HashMap<&'static str, SmallVec<[&'a DeclareField; 1]>>, + builtin: Vec<(&'static str, SmallVec<[&'a DeclareField; 1]>)>, children: &'a Vec, } enum ObjNode<'a> { @@ -32,7 +31,20 @@ enum ObjNode<'a> { impl<'a> DeclareObj<'a> { pub fn from_literal(mac: &'a StructLiteral) -> Result { let StructLiteral { parent, brace, fields, children } = mac; - let mut builtin: ahash::HashMap<_, SmallVec<[&DeclareField; 1]>> = <_>::default(); + let mut builtin = vec![]; + + fn get_builtin_obj<'a, 'b>( + ty_name: &'static str, + builtins: &'a mut Vec<(&'static str, SmallVec<[&'b DeclareField; 1]>)>, + ) -> &'a mut SmallVec<[&'b DeclareField; 1]> { + if let Some(idx) = builtins.iter_mut().position(|(ty, _)| *ty == ty_name) { + &mut builtins[idx].1 + } else { + builtins.push((ty_name, SmallVec::default())); + &mut builtins.last_mut().unwrap().1 + } + } + let span = match parent { RdlParent::Type(ty) => ty.span(), RdlParent::Var(name) => name.span(), @@ -47,7 +59,7 @@ impl<'a> DeclareObj<'a> { .get(f.member.to_string().as_str()) .filter(|builtin_ty| !ty.is_ident(builtin_ty)) { - builtin.entry(*ty).or_default().push(f); + get_builtin_obj(ty, &mut builtin).push(f); } else { self_fields.push(f) } @@ -57,7 +69,7 @@ impl<'a> DeclareObj<'a> { RdlParent::Var(name) => { for f in fields { if let Some(ty) = WIDGET_OF_BUILTIN_FIELD.get(f.member.to_string().as_str()) { - builtin.entry(*ty).or_default().push(f); + get_builtin_obj(ty, &mut builtin).push(f); } else { return Err(quote_spanned! { f.span() => compile_error!("Not allow to declare a field of a variable parent.") @@ -82,7 +94,7 @@ impl<'a> ToTokens for DeclareObj<'a> { if children.is_empty() && builtin.is_empty() { quote_spanned! { *span => FatObj::new(#this) }.to_tokens(tokens) } else { - Brace::default().surround(tokens, |tokens| { + Brace(*span).surround(tokens, |tokens| { let mut builtin_names = vec![]; for (ty_str, fields) in builtin { // 'b is live longer than 'a, safe convert, but we can't directly convert @@ -167,8 +179,7 @@ fn recursive_compose_with( } else { leaf(tokens) } - Comma::default().to_tokens(tokens); - quote! { ctx!() }.to_tokens(tokens); + quote_spanned!(p.span() => , ctx!()).to_tokens(tokens); }); } diff --git a/macros/src/fn_widget_macro.rs b/macros/src/fn_widget_macro.rs index 20159c9d5..ac1a1c80e 100644 --- a/macros/src/fn_widget_macro.rs +++ b/macros/src/fn_widget_macro.rs @@ -1,28 +1,27 @@ +use crate::{ok, pipe_macro::BodyExpr, symbol_process::symbol_to_macro}; +use proc_macro::TokenStream as TokenStream1; +use proc_macro2::TokenStream; use quote::{quote, ToTokens}; -use syn::{ - fold::Fold, - parse::{Parse, ParseStream}, - Stmt, -}; +use syn::{fold::Fold, parse_macro_input, Stmt}; use crate::symbol_process::DollarRefs; -pub struct FnWidgetMacro { - stmts: Vec, -} +pub struct FnWidgetMacro(Vec); + +impl FnWidgetMacro { + pub(crate) fn gen_code(input: TokenStream, outside: Option<&mut DollarRefs>) -> TokenStream1 { + let input = ok!(symbol_to_macro(input.into())); + let body = parse_macro_input!(input as BodyExpr); + let mut refs = DollarRefs::new(outside.map_or(0, |o| o.in_capture_level())); + let stmts = body.0.into_iter().map(|s| refs.fold_stmt(s)).collect(); -impl Parse for FnWidgetMacro { - fn parse(input: ParseStream) -> syn::Result { - let stmts = syn::Block::parse_within(input)?; - let mut refs = DollarRefs::default(); - let stmts = stmts.into_iter().map(|s| refs.fold_stmt(s)).collect(); - Ok(Self { stmts }) + FnWidgetMacro(stmts).to_token_stream().into() } } impl ToTokens for FnWidgetMacro { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let Self { stmts } = self; + let Self(stmts) = self; quote! { FnWidget::new(move |ctx: &BuildCtx| { set_build_ctx!(ctx); diff --git a/macros/src/lib.rs b/macros/src/lib.rs index f822fc1d9..471af6712 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -10,18 +10,20 @@ mod widget_macro; use fn_widget_macro::FnWidgetMacro; use proc_macro::TokenStream; use quote::{quote, ToTokens}; -use symbol_process::symbol_to_macro; +use symbol_process::{symbol_to_macro, DollarRefs}; use syn::{parse_macro_input, DeriveInput}; use widget_macro::gen_widget_macro; mod child_template; mod fn_widget_macro; mod pipe_macro; mod rdl_macro; +mod route_state_macro; mod watch_macro; pub(crate) use rdl_macro::*; -use crate::pipe_macro::PipeExpr; +use crate::pipe_macro::PipeMacro; use crate::watch_macro::WatchMacro; +use route_state_macro::RouteStateMacro; pub(crate) mod declare_obj; pub(crate) mod symbol_process; @@ -33,13 +35,23 @@ pub(crate) const ASSIGN_WATCH_MACRO_NAME: &str = "assign_watch"; pub(crate) const LET_WATCH_MACRO_NAME: &str = "let_watch"; pub(crate) const PROP_MACRO_NAME: &str = "prop"; +macro_rules! ok { + ($e: expr) => { + match $e { + Ok(ok) => ok, + Err(err) => return err.into(), + } + }; +} +pub(crate) use ok; + #[proc_macro_derive(SingleChild)] pub fn single_marco_derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); let name = input.ident; quote! { - impl #impl_generics SingleChild for #name #ty_generics #where_clause {} + impl #impl_generics SingleChild for #name #ty_generics #where_clause {} } .into() } @@ -50,7 +62,7 @@ pub fn multi_macro_derive(input: TokenStream) -> TokenStream { let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); let name = input.ident; quote! { - impl #impl_generics MultiChild for #name #ty_generics #where_clause {} + impl #impl_generics MultiChild for #name #ty_generics #where_clause {} } .into() } @@ -132,13 +144,7 @@ pub fn widget(input: TokenStream) -> TokenStream { gen_widget_macro(input, None) /// like: `let row = rdl!{ Widget::new(Void) };` #[proc_macro] pub fn rdl(input: TokenStream) -> TokenStream { - symbol_to_macro(input).map_or_else( - |err| err, - |input| { - let declare = parse_macro_input! { input as RdlBody }; - declare.to_token_stream().into() - }, - ) + RdlMacro::gen_code(input.into(), &mut DollarRefs::default()) } /// The `fn_widget` is a macro that create a widget from a function widget from @@ -146,15 +152,12 @@ pub fn rdl(input: TokenStream) -> TokenStream { /// `$` in the expression, the `@` is a short hand of `rdl` macro, and `$name` /// use to expression a state reference of `name`. #[proc_macro] -pub fn fn_widget(input: TokenStream) -> TokenStream { - symbol_to_macro(input).map_or_else( - |err| err, - |input| { - let widget_macro = parse_macro_input!(input as FnWidgetMacro); - widget_macro.to_token_stream().into() - }, - ) -} +pub fn fn_widget(input: TokenStream) -> TokenStream { FnWidgetMacro::gen_code(input.into(), None) } + +/// This macro just return the input token stream. It's do nothing but help +/// `ribir` mark that a macro has been expanded. +#[proc_macro] +pub fn ribir_expanded_ಠ_ಠ(input: TokenStream) -> TokenStream { input } /// set the `BuildCtx` to a special variable `_ctx_ಠ_ಠ`, so the user can use /// `ctx!` to access it. @@ -179,34 +182,46 @@ pub fn ctx(input: TokenStream) -> TokenStream { /// expression modify. Use the `$` mark the state reference and auto subscribe /// to its modify. #[proc_macro] -pub fn pipe(input: TokenStream) -> TokenStream { - symbol_to_macro(input).map_or_else( - |err| err, - |input| { - let expr = parse_macro_input! { input as PipeExpr }; - expr.to_token_stream().into() - }, - ) -} +pub fn pipe(input: TokenStream) -> TokenStream { PipeMacro::gen_code(input.into(), None) } /// `watch!` macro use to convert a expression to a `Observable` stream. Use the /// `$` mark the state reference and auto subscribe to its modify. #[proc_macro] -pub fn watch(input: TokenStream) -> TokenStream { - symbol_to_macro(input).map_or_else( - |err| err, - |input| { - let expr = parse_macro_input! { input as WatchMacro }; - expr.to_token_stream().into() - }, - ) +pub fn watch(input: TokenStream) -> TokenStream { WatchMacro::gen_code(input.into(), None) } + +/// macro split state from another state as a new state. For example, +/// `partial_state!($label.visible, Visibility -> bool );` will return a state +/// of `bool` that is partial of the `Visibility`. +/// +/// This macro implement a `StateRouter` and use `split_partial` to split from +/// the origin state. +#[proc_macro] +pub fn partial_state(input: TokenStream) -> TokenStream { + let input = ok!(symbol_to_macro(input)); + let route_state = parse_macro_input! { input as RouteStateMacro }; + route_state.to_token_stream().into() +} + +/// macro split a state from another state as a new state. For example, +/// `orphan_partial_state!($label.visible, Visibility -> bool );` will return a +/// state of `bool` that is partial of the `Visibility` and have independent +/// `Modifier` with the origin state. +/// +/// This macro implement a `StateRouter` and use `orphan_split_partial` to +/// split from the origin state. +#[proc_macro] +pub fn route_state(input: TokenStream) -> TokenStream { + let input = ok!(symbol_to_macro(input)); + let mut route_state = parse_macro_input! { input as RouteStateMacro }; + route_state.only_route = true; + route_state.to_token_stream().into() } /// The macro to use a state as its StateRef. Transplanted from the `$`. #[proc_macro] pub fn _dollar_ಠ_ಠ(input: TokenStream) -> TokenStream { - let name = parse_macro_input! { input as syn::Ident }; - quote! { #name.state_ref() }.into() + let input: proc_macro2::TokenStream = input.into(); + quote! { #input.state_ref() }.into() } #[proc_macro] diff --git a/macros/src/pipe_macro.rs b/macros/src/pipe_macro.rs index b4fba7708..bc1464839 100644 --- a/macros/src/pipe_macro.rs +++ b/macros/src/pipe_macro.rs @@ -1,42 +1,38 @@ -use crate::symbol_process::DollarRefs; +use crate::symbol_process::{fold_body_in_capture_and_require_refs, DollarRefs}; +use crate::{ok, symbol_process::symbol_to_macro}; +use proc_macro::TokenStream as TokenStream1; +use proc_macro2::TokenStream; use quote::{quote, ToTokens}; use syn::{ - fold::Fold, parse::{Parse, ParseStream}, + parse_macro_input, + spanned::Spanned, Stmt, }; -pub(crate) struct PipeExpr { +pub(crate) struct BodyExpr(pub(crate) Vec); +pub(crate) struct PipeMacro { refs: DollarRefs, expr: Vec, } -impl Parse for PipeExpr { - fn parse(input: ParseStream) -> syn::Result { - let (refs, stmts) = fold_expr_as_in_closure(input)?; - Ok(Self { refs, expr: stmts }) - } -} +impl PipeMacro { + pub fn gen_code(input: TokenStream, outside: Option<&mut DollarRefs>) -> TokenStream1 { + let span = input.span(); + let input = ok!(symbol_to_macro(input.into())); + let expr = parse_macro_input! { input as BodyExpr }; -pub fn fold_expr_as_in_closure(input: ParseStream) -> syn::Result<(DollarRefs, Vec)> { - let mut refs = DollarRefs::default(); - refs.in_capture += 1; - let stmts = syn::Block::parse_within(input)?; - let stmts = stmts.into_iter().map(|s| refs.fold_stmt(s)).collect(); - refs.in_capture -= 1; - if refs.is_empty() { - let err = syn::Error::new( - input.span(), - "expression not subscribe anything, it must contain at least one $", - ); - Err(err) - } else { + let (expr, mut refs) = ok!(fold_body_in_capture_and_require_refs(span, expr.0, outside)); refs.dedup(); - Ok((refs, stmts)) + PipeMacro { refs, expr }.to_token_stream().into() } } -impl ToTokens for PipeExpr { +impl Parse for BodyExpr { + fn parse(input: ParseStream) -> syn::Result { Ok(Self(syn::Block::parse_within(input)?)) } +} + +impl ToTokens for PipeMacro { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { let Self { refs, expr } = self; diff --git a/macros/src/rdl_macro.rs b/macros/src/rdl_macro.rs index 9a7140796..60b3e8b9f 100644 --- a/macros/src/rdl_macro.rs +++ b/macros/src/rdl_macro.rs @@ -1,3 +1,9 @@ +use crate::{ + declare_obj::DeclareObj, + ok, + symbol_process::{kw, symbol_to_macro, DollarRefs}, +}; +use proc_macro::TokenStream as TokenStream1; use proc_macro2::{Span, TokenStream}; use quote::{quote_spanned, ToTokens}; use std::collections::HashSet; @@ -5,19 +11,14 @@ use syn::{ braced, fold::Fold, parse::{Parse, ParseBuffer, ParseStream}, - parse_quote, + parse_macro_input, parse_quote, punctuated::Punctuated, spanned::Spanned, token::{At, Bang, Brace, Colon, Comma, Dollar}, Expr, Ident, Macro, Path, Result as SynResult, Stmt, }; -use crate::{ - declare_obj::DeclareObj, - symbol_process::{kw, DollarRefs}, -}; - -pub enum RdlBody { +pub enum RdlMacro { Literal(StructLiteral), /// Declare an expression as a object, like `rdl! { Widget::new(...) }` ExprObj { @@ -55,17 +56,44 @@ pub struct DeclareField { pub value: Expr, } -impl Parse for RdlBody { +impl RdlMacro { + pub fn gen_code(input: TokenStream, refs: &mut DollarRefs) -> TokenStream1 { + let input = ok!(symbol_to_macro(input.into())); + + match parse_macro_input! { input as RdlMacro } { + RdlMacro::Literal(mut l) => { + let fields = l.fields.into_iter().map(|mut f: DeclareField| { + f.value = refs.fold_expr(f.value); + f + }); + l.fields = fields.collect(); + l.children = l.children.into_iter().map(|m| refs.fold_macro(m)).collect(); + + let obj = ok!(DeclareObj::from_literal(&l)); + obj.to_token_stream().into() + } + RdlMacro::ExprObj { span, stmts } => { + let stmts = stmts.into_iter().map(|s| refs.fold_stmt(s)); + if stmts.len() > 1 { + quote_spanned! { span => { #(#stmts)* }}.into() + } else { + quote_spanned! { span => #(#stmts)* }.into() + } + } + } + } +} + +impl Parse for RdlMacro { fn parse(input: ParseStream) -> SynResult { let fork = input.fork(); if fork.parse::().is_ok() && fork.peek(Brace) { - Ok(RdlBody::Literal(input.parse()?)) + Ok(RdlMacro::Literal(input.parse()?)) } else { - let span = input.span(); - let stmts = syn::Block::parse_within(input)?; - let mut refs = DollarRefs::default(); - let stmts = stmts.into_iter().map(|s| refs.fold_stmt(s)).collect(); - Ok(RdlBody::ExprObj { span, stmts }) + Ok(RdlMacro::ExprObj { + span: input.span(), + stmts: syn::Block::parse_within(input)?, + }) } } } @@ -131,34 +159,13 @@ impl Parse for DeclareField { let value = if colon_tk.is_none() { parse_quote!(#member) } else { - let mut refs = DollarRefs::default(); - refs.fold_expr(input.parse()?) + input.parse()? }; Ok(DeclareField { member, colon_tk, value }) } } -impl ToTokens for RdlBody { - fn to_tokens(&self, tokens: &mut TokenStream) { - match self { - RdlBody::Literal(l) => match DeclareObj::from_literal(l) { - Ok(declare) => declare.to_tokens(tokens), - Err(err) => err.to_tokens(tokens), - }, - RdlBody::ExprObj { span, stmts } => { - if stmts.len() > 1 { - Brace(*span).surround(tokens, |tokens| { - stmts.iter().for_each(|s| s.to_tokens(tokens)); - }) - } else { - stmts.iter().for_each(|s| s.to_tokens(tokens)); - } - } - } - } -} - impl ToTokens for DeclareField { fn to_tokens(&self, tokens: &mut TokenStream) { let DeclareField { member, value, .. } = self; diff --git a/macros/src/route_state_macro.rs b/macros/src/route_state_macro.rs new file mode 100644 index 000000000..cb9353aea --- /dev/null +++ b/macros/src/route_state_macro.rs @@ -0,0 +1,72 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn::{ + fold::Fold, + parse::{Parse, ParseStream}, + parse_quote, + token::Brace, + ExprMacro, Result, +}; + +use crate::symbol_process::{DollarRefs, KW_DOLLAR_STR}; + +pub(crate) struct RouteStateMacro { + part_state: PartStateExpr, + pub only_route: bool, +} + +struct PartStateExpr { + host: TokenStream, + path: TokenStream, +} + +impl Parse for PartStateExpr { + fn parse(input: ParseStream) -> Result { + let mac = input.parse::()?; + if !mac.mac.path.is_ident(KW_DOLLAR_STR) { + return Err(syn::Error::new_spanned( + mac, + "expected `$` as the first token", + )); + } + + Ok(Self { + host: mac.mac.tokens, + path: input.parse()?, + }) + } +} + +impl Parse for RouteStateMacro { + fn parse(input: ParseStream) -> Result { + let mut refs = DollarRefs::default(); + let expr = refs.fold_expr(input.parse()?); + let part_state = parse_quote!(#expr); + + Ok(Self { part_state, only_route: false }) + } +} + +impl ToTokens for RouteStateMacro { + fn to_tokens(&self, tokens: &mut TokenStream) { + let Self { + part_state: PartStateExpr { host, path }, + only_route, + } = self; + + Brace::default().surround(tokens, |tokens| { + let route_method = if *only_route { + quote!(route_state) + } else { + quote!(partial_state) + }; + quote!( + #host.#route_method( + move |origin: &_| &origin #path, + move |origin: &mut _| &mut origin #path + ) + ) + .to_tokens(tokens) + }) + } +} diff --git a/macros/src/symbol_process.rs b/macros/src/symbol_process.rs index 51519bb9e..5df01a819 100644 --- a/macros/src/symbol_process.rs +++ b/macros/src/symbol_process.rs @@ -1,22 +1,29 @@ -use crate::widget_macro::{ - ribir_suffix_variable, WIDGET_OF_BUILTIN_FIELD, WIDGET_OF_BUILTIN_METHOD, +use crate::fn_widget_macro::FnWidgetMacro; +use crate::pipe_macro::PipeMacro; +use crate::rdl_macro::RdlMacro; +use crate::{ + watch_macro::WatchMacro, + widget_macro::{ribir_suffix_variable, WIDGET_OF_BUILTIN_FIELD, WIDGET_OF_BUILTIN_METHOD}, }; use inflector::Inflector; -use proc_macro2::{Ident, TokenStream}; +use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, quote_spanned, ToTokens}; use smallvec::SmallVec; +use syn::parse_quote_spanned; use syn::{ fold::Fold, parse::{Parse, ParseStream}, - parse_quote, spanned::Spanned, token::Dollar, - Expr, ExprField, ExprMethodCall, Macro, Member, + Expr, ExprField, ExprMethodCall, Macro, Member, Stmt, }; pub const KW_DOLLAR_STR: &str = "_dollar_ಠ_ಠ"; pub const KW_CTX: &str = "ctx"; pub const KW_RDL: &str = "rdl"; +pub const KW_PIPE: &str = "pipe"; +pub const KW_WATCH: &str = "watch"; +pub const KW_FN_WIDGET: &str = "fn_widget"; pub use tokens_pre_process::*; pub mod kw { @@ -24,18 +31,27 @@ pub mod kw { syn::custom_keyword!(rdl); } +#[derive(Hash, PartialEq, Eq, Debug, Clone)] +pub struct BuiltinInfo { + host: Ident, + member: Ident, +} + #[derive(Hash, PartialEq, Eq, Debug, Clone)] pub struct DollarRef { pub name: Ident, - pub builtin_shadow: Option, + pub builtin: Option, } -#[derive(Default)] +#[derive(Debug)] pub struct DollarRefs { refs: SmallVec<[DollarRef; 1]>, ctx_used: bool, - pub in_capture: usize, + in_capture: usize, + variable_stacks: Vec>, } +pub struct StackGuard<'a>(&'a mut DollarRefs); + mod tokens_pre_process { use proc_macro::{TokenTree, *}; @@ -158,6 +174,67 @@ mod tokens_pre_process { } impl Fold for DollarRefs { + fn fold_block(&mut self, i: syn::Block) -> syn::Block { + let mut this = self.push_stack(); + syn::fold::fold_block(&mut *this, i) + } + + fn fold_item_const(&mut self, i: syn::ItemConst) -> syn::ItemConst { + self.new_local_var(&i.ident); + syn::fold::fold_item_const(self, i) + } + + fn fold_local(&mut self, mut i: syn::Local) -> syn::Local { + // we fold right expression first, then fold pattern, because the `=` is a + // right operator. + i.init = i + .init + .map(|(assign, e)| (assign, Box::new(self.fold_expr(*e)))); + i.pat = self.fold_pat(i.pat); + + i + } + + fn fold_expr_block(&mut self, i: syn::ExprBlock) -> syn::ExprBlock { + let mut this = self.push_stack(); + syn::fold::fold_expr_block(&mut *this, i) + } + + fn fold_expr_for_loop(&mut self, i: syn::ExprForLoop) -> syn::ExprForLoop { + let mut this = self.push_stack(); + syn::fold::fold_expr_for_loop(&mut *this, i) + } + + fn fold_expr_loop(&mut self, i: syn::ExprLoop) -> syn::ExprLoop { + let mut this = self.push_stack(); + syn::fold::fold_expr_loop(&mut *this, i) + } + + fn fold_expr_if(&mut self, i: syn::ExprIf) -> syn::ExprIf { + let mut this = self.push_stack(); + syn::fold::fold_expr_if(&mut *this, i) + } + + fn fold_arm(&mut self, i: syn::Arm) -> syn::Arm { + let mut this = self.push_stack(); + syn::fold::fold_arm(&mut *this, i) + } + + fn fold_expr_unsafe(&mut self, i: syn::ExprUnsafe) -> syn::ExprUnsafe { + let mut this = self.push_stack(); + syn::fold::fold_expr_unsafe(&mut *this, i) + } + + fn fold_expr_while(&mut self, i: syn::ExprWhile) -> syn::ExprWhile { + let mut this = self.push_stack(); + syn::fold::fold_expr_while(&mut *this, i) + } + + fn fold_pat_ident(&mut self, i: syn::PatIdent) -> syn::PatIdent { + self.new_local_var(&i.ident); + syn::fold::fold_pat_ident(self, i) + } + fn fold_expr_field(&mut self, mut i: ExprField) -> ExprField { let ExprField { base, member, .. } = &mut i; if let Member::Named(member) = member { @@ -180,21 +257,40 @@ impl Fold for DollarRefs { fn fold_macro(&mut self, mut mac: Macro) -> Macro { if let Some(DollarMacro { name, .. }) = parse_clean_dollar_macro(&mac) { mac.tokens = name.to_token_stream(); - self.refs.push(DollarRef { name, builtin_shadow: None }); - mac + if !self.is_local_variable(&name) { + self.refs.push(DollarRef { name, builtin: None }); + } + } else if mac.path.is_ident(KW_WATCH) { + mac.tokens = WatchMacro::gen_code(mac.tokens, Some(self)).into(); + mac.path = parse_quote_spanned! { mac.path.span() => ribir_expanded_ಠ_ಠ }; + } else if mac.path.is_ident(KW_PIPE) { + self.ctx_used = true; + mac.tokens = PipeMacro::gen_code(mac.tokens, Some(self)).into(); + mac.path = parse_quote_spanned! { mac.path.span() => ribir_expanded_ಠ_ಠ }; + } else if mac.path.is_ident(KW_RDL) { + self.ctx_used = true; + mac.tokens = RdlMacro::gen_code(mac.tokens, self).into(); + mac.path = parse_quote_spanned! { mac.path.span() => ribir_expanded_ಠ_ಠ }; + } else if mac.path.is_ident(KW_FN_WIDGET) { + mac.tokens = FnWidgetMacro::gen_code(mac.tokens, Some(self)).into(); + mac.path = parse_quote_spanned! { mac.path.span() => ribir_expanded_ಠ_ಠ }; + } else if mac.path.is_ident(KW_CTX) { + self.ctx_used = true; } else { - self.ctx_used = mac.path.is_ident(KW_RDL) || mac.path.is_ident(KW_CTX); - syn::fold::fold_macro(self, mac) + mac = syn::fold::fold_macro(self, mac); } + mac } - fn fold_expr(&mut self, i: Expr) -> Expr { match i { Expr::Closure(c) if c.capture.is_some() => { - let mut closure_refs = DollarRefs::default(); + let mut closure_refs = DollarRefs { + in_capture: self.in_capture, + ..Default::default() + }; + closure_refs.in_capture += 1; let mut c = closure_refs.fold_expr_closure(c); - closure_refs.in_capture -= 1; if !closure_refs.is_empty() || closure_refs.ctx_used { closure_refs.dedup(); @@ -212,13 +308,16 @@ impl Fold for DollarRefs { .ctx_used .then(|| quote_spanned! { c.span() => let _ctx_handle = ctx!().handle(); }); - // todo: seems need merge `closure_refs` into `self`. - - Expr::Verbatim(quote_spanned!(c.span() => { + let expr = Expr::Verbatim(quote_spanned!(c.span() => { #closure_refs #handle #c - })) + })); + + self.merge(&mut closure_refs); + closure_refs.in_capture -= 1; + + expr } else { Expr::Closure(c) } @@ -228,25 +327,58 @@ impl Fold for DollarRefs { } } -impl ToTokens for DollarRef { - fn to_tokens(&self, tokens: &mut TokenStream) { - let DollarRef { name, builtin_shadow: shadow } = self; - if let Some(shadow) = shadow { - quote_spanned! { shadow.span() => let mut #name = #shadow.clone();}.to_tokens(tokens); - } else { - quote_spanned! { shadow.span() => let mut #name = #name.clone();}.to_tokens(tokens); - } - } -} impl ToTokens for DollarRefs { fn to_tokens(&self, tokens: &mut TokenStream) { - for dollar_ref in &self.refs { - dollar_ref.to_tokens(tokens); + for DollarRef { name, builtin } in &self.refs { + match builtin { + Some(builtin) => { + let BuiltinInfo { host, member } = builtin; + quote_spanned! { + name.span() => let mut #name = #host.#member(ctx!()).clone_state(); + } + .to_tokens(tokens); + } + _ => { + quote_spanned! { name.span() => let mut #name = #name.clone_state();}.to_tokens(tokens); + } + } } } } impl DollarRefs { + pub fn new(in_capture: usize) -> Self { Self { in_capture, ..Default::default() } } + + pub fn in_capture_level(&self) -> usize { self.in_capture } + + pub fn push_stack(&mut self) -> StackGuard<'_> { + self.variable_stacks.push(vec![]); + StackGuard(self) + } + + pub fn merge(&mut self, other: &mut Self) { + for r in other.refs.iter_mut() { + if let Some(builtin) = r.builtin.as_mut() { + if !self.is_local_variable(&builtin.host) { + if r.name == "host_has_focus_ಠ_ಠ" { + println!( + "make {} : `host_has_focus_ಠ_ಠ` as not builtin widget", + builtin.host + ); + + println!("local variables: {:?}", self.variable_stacks); + } + self.refs.push(r.clone()); + r.builtin.take(); + } + } else if !self.is_local_variable(&r.name) { + self.refs.push(r.clone()) + } + } + + self.ctx_used |= other.ctx_used; + } + pub fn used_ctx(&self) -> bool { self.ctx_used } pub fn dedup(&mut self) { @@ -258,8 +390,8 @@ impl DollarRefs { match self.len() { 0 => quote! {}, 1 => { - let DollarRef { name, builtin_shadow: value } = &self.refs[0]; - quote_spanned! { value.span() => #name.modifies() } + let DollarRef { name, .. } = &self.refs[0]; + quote_spanned! { name.span() => #name.modifies() } } _ => { let upstream = self.iter().map(|DollarRef { name, .. }| { @@ -284,25 +416,43 @@ impl DollarRefs { e => e, }; + // When a builtin widget captured by a `move |_| {...}` closure, we need split + // the builtin widget from the `FatObj` so we only capture the builtin part that + // we used. + let Expr::Macro(m) = e else { return None }; let DollarMacro { name: host, .. } = parse_clean_dollar_macro(&m.mac)?; - let builtin_name = ribir_suffix_variable(&host, builtin_member); + let name = ribir_suffix_variable(&host, builtin_member); let builtin_member = Ident::new(builtin_member, host.span()); - // When a builtin widget captured by a `move |_| {...}` closure, we need split - // the builtin widget from the `FatObj` so we only capture the used builtin - // part. - m.mac.tokens = if self.in_capture > 0 { - builtin_name.to_token_stream() + // if used in embedded closure, we directly use the builtin variable, the + // variable that the closure capture is already a separate builtin variable. + let is_local_declare = self.is_local_variable(&host); + + m.mac.tokens = if !is_local_declare && self.in_capture > 0 { + name.to_token_stream() } else { quote_spanned! { host.span() => #host.#builtin_member(ctx!()) } }; - self.refs.push(DollarRef { - name: builtin_name, - builtin_shadow: Some(parse_quote! { #host.#builtin_member(ctx!()) }), - }); + + if !is_local_declare { + let builtin = Some(BuiltinInfo { host, member: builtin_member }); + self.refs.push(DollarRef { name, builtin }); + } + self.last() } + + fn new_local_var(&mut self, name: &Ident) { + self.variable_stacks.last_mut().unwrap().push(name.clone()) + } + + fn is_local_variable(&self, name: &Ident) -> bool { + self + .variable_stacks + .iter() + .any(|stack| stack.contains(name)) + } } fn parse_clean_dollar_macro(mac: &Macro) -> Option { @@ -332,3 +482,48 @@ impl Parse for DollarMacro { }) } } + +impl<'a> std::ops::Deref for StackGuard<'a> { + type Target = DollarRefs; + fn deref(&self) -> &Self::Target { self.0 } +} + +impl<'a> std::ops::DerefMut for StackGuard<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { self.0 } +} + +impl<'a> Drop for StackGuard<'a> { + fn drop(&mut self) { self.0.variable_stacks.pop(); } +} + +impl Default for DollarRefs { + fn default() -> Self { + Self { + refs: SmallVec::new(), + ctx_used: false, + in_capture: 0, + variable_stacks: vec![vec![]], + } + } +} + +pub fn fold_body_in_capture_and_require_refs( + span: Span, + stmts: Vec, + outside: Option<&mut DollarRefs>, +) -> Result<(Vec, DollarRefs), TokenStream> { + let in_capture = outside.as_ref().map_or(0, |o| o.in_capture) + 1; + let mut refs = DollarRefs::new(in_capture); + + let stmts = stmts.into_iter().map(|s| refs.fold_stmt(s)).collect(); + if refs.is_empty() { + Err(quote_spanned!(span => + compile_error!("expression not subscribe anything, it must contain at least one $") + )) + } else { + if let Some(outside) = outside { + outside.merge(&mut refs); + } + Ok((stmts, refs)) + } +} diff --git a/macros/src/watch_macro.rs b/macros/src/watch_macro.rs index 2c4e27b9c..485499c33 100644 --- a/macros/src/watch_macro.rs +++ b/macros/src/watch_macro.rs @@ -1,25 +1,33 @@ -use crate::{pipe_macro::fold_expr_as_in_closure, symbol_process::DollarRefs}; -use quote::{quote, ToTokens}; -use syn::{ - parse::{Parse, ParseStream}, - Stmt, +use crate::{ + ok, + pipe_macro::BodyExpr, + symbol_process::{fold_body_in_capture_and_require_refs, symbol_to_macro, DollarRefs}, }; +use proc_macro::TokenStream as TokenStream1; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn::{parse_macro_input, spanned::Spanned, Stmt}; pub(crate) struct WatchMacro { refs: DollarRefs, expr: Vec, } -impl Parse for WatchMacro { - fn parse(input: ParseStream) -> syn::Result { - let (refs, stmts) = fold_expr_as_in_closure(input)?; - Ok(Self { refs, expr: stmts }) +impl WatchMacro { + pub fn gen_code(input: TokenStream, outside: Option<&mut DollarRefs>) -> TokenStream1 { + let span = input.span(); + let input = ok!(symbol_to_macro(input.into())); + let expr = parse_macro_input! { input as BodyExpr }; + + let (expr, mut refs) = ok!(fold_body_in_capture_and_require_refs(span, expr.0, outside)); + refs.dedup(); + WatchMacro { refs, expr }.to_token_stream().into() } } impl ToTokens for WatchMacro { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let Self { refs, expr } = self; + let Self { refs, expr, .. } = self; let upstream = refs.upstream_tokens(); diff --git a/macros/src/widget_macro/code_gen.rs b/macros/src/widget_macro/code_gen.rs index 78c563765..88b4779eb 100644 --- a/macros/src/widget_macro/code_gen.rs +++ b/macros/src/widget_macro/code_gen.rs @@ -205,7 +205,7 @@ impl Desugared { quote! { .into(), #guards_vec }.to_tokens(tokens) }); } else { - w.gen_compose_node(named_objs, tokens) + w.gen_compose_node(named_objs, tokens); } }); quote! { ; FnWidget::new(#name) }.to_tokens(tokens); diff --git a/macros/src/widget_macro/name_used_info.rs b/macros/src/widget_macro/name_used_info.rs index 5ff38d5fc..ba8fe9932 100644 --- a/macros/src/widget_macro/name_used_info.rs +++ b/macros/src/widget_macro/name_used_info.rs @@ -155,9 +155,7 @@ impl ScopeUsedInfo { pub fn state_refs_tokens(&self, tokens: &mut TokenStream) { if let Some(names) = self.ref_widgets() { let c_names = names.clone(); - if names.clone().nth(1).is_some() { - quote! {let _guard = (#(#names.modify_guard(),)*);}.to_tokens(tokens); - } + c_names.for_each(|n| { quote_spanned! {n.span() => let mut #n = #n.state_ref();}.to_tokens(tokens); }); diff --git a/painter/src/style.rs b/painter/src/style.rs index bc5d08d97..d95641b8c 100644 --- a/painter/src/style.rs +++ b/painter/src/style.rs @@ -24,6 +24,11 @@ impl From for Brush { fn from(c: Color) -> Self { Brush::Color(c) } } +impl From for Option { + #[inline] + fn from(c: Color) -> Self { Some(c.into()) } +} + impl From> for Brush { #[inline] fn from(img: ShareResource) -> Self { Brush::Image(img) } diff --git a/ribir/src/app.rs b/ribir/src/app.rs index 289418089..9abc4eff8 100644 --- a/ribir/src/app.rs +++ b/ribir/src/app.rs @@ -92,7 +92,7 @@ impl App { /// thread until the application exit. #[track_caller] pub fn exec() { - Self::active_window().draw_frame(); + Self::active_window().draw_frame(false); let event_loop = &mut unsafe { App::shared_mut() }.event_loop; @@ -139,7 +139,7 @@ impl App { } Event::RedrawRequested(id) => { if let Some(wnd) = AppCtx::get_window(new_id(id)) { - wnd.draw_frame() + wnd.draw_frame(false) } } Event::RedrawEventsCleared => { diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 9a3d45208..fd37439f8 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -29,10 +29,6 @@ path = "compile_message.rs" name = "rdl_macro_test" path = "rdl_macro_test.rs" -[[test]] -name = "animations_syntax" -path = "animations_syntax_test.rs" - [[test]] name = "child_template_derive" path = "child_template_derive_test.rs" diff --git a/tests/animations_syntax_test.rs b/tests/animations_syntax_test.rs deleted file mode 100644 index 537610574..000000000 --- a/tests/animations_syntax_test.rs +++ /dev/null @@ -1,93 +0,0 @@ -use ribir::{core::test_helper::*, prelude::*}; -use std::{cell::Cell, rc::Rc, time::Duration}; -use winit::event::{DeviceId, MouseScrollDelta, TouchPhase, WindowEvent}; - -fn wheel_widget(w: Widget) -> TestWindow { - let mut wnd = TestWindow::new(w); - - wnd.draw_frame(); - let device_id = unsafe { DeviceId::dummy() }; - #[allow(deprecated)] - wnd.processes_native_event(WindowEvent::MouseWheel { - device_id, - delta: MouseScrollDelta::LineDelta(1.0, 1.0), - phase: TouchPhase::Started, - modifiers: ModifiersState::default(), - }); - wnd -} - -#[test] -fn listener_trigger_have_handler() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - let handler_call_times = Rc::new(Cell::new(0)); - let h1 = handler_call_times.clone(); - let animate_state = Stateful::new(false); - - let w = widget! { - states { animate_state: animate_state.clone() } - init ctx => { - let linear_transition = transitions::LINEAR.of(ctx); - } - SizedBox { - id: sized_box, - size: Size::new(100., 100.), - on_wheel: move |_| { - h1.set(h1.get() + 1); - leak_animate.run(); - }, - } - Animate { - id: leak_animate, - transition: linear_transition, - prop: prop!(sized_box.size), - from: ZERO_SIZE, - } - finally { - watch!(leak_animate.is_running()) - .subscribe(move |v| *animate_state = v); - } - }; - - wheel_widget(w.into()); - - assert!(*animate_state.state_ref()); - assert_eq!(handler_call_times.get(), 1); -} - -#[test] -fn listener_trigger() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - let animate_state = Stateful::new(false); - - let w = widget! { - states { animate_state: animate_state.clone() } - init ctx => { - let linear_transition = Transition::declare_builder() - .easing(easing::LINEAR) - .duration(Duration::from_millis(100)) - .build(ctx); - } - SizedBox { - id: sized_box, - size: Size::new(100., 100.), - on_wheel: move |_| leak_animate.run() - } - Animate { - id: leak_animate, - prop: prop!(sized_box.size), - from: ZERO_SIZE, - transition: linear_transition, - } - finally { - watch!(leak_animate.is_running()) - .subscribe(move |v| *animate_state = v); - } - }; - - wheel_widget(w.into()); - - assert!(*animate_state.state_ref()); -} diff --git a/tests/compile_msg/declare/animations_base_syntax_pass.rs b/tests/compile_msg/declare/animations_base_syntax_pass.rs deleted file mode 100644 index 6736a339e..000000000 --- a/tests/compile_msg/declare/animations_base_syntax_pass.rs +++ /dev/null @@ -1,63 +0,0 @@ -use ribir::prelude::*; - -fn main() { - let _def_ref = widget! { - init ctx => { - let linear = Transition::declare_builder() - .easing(easing::LINEAR) - .build(ctx); - } - SizedBox { - id: sized_box, - size: Size::zero() - } - - Animate { - id: animate1, - transition: linear, - prop: prop!(sized_box.size), - from: Size::new(10., 10.), - } - - finally { - let_watch!(sized_box.size) - .distinct_until_changed() - .subscribe(move |_| animate1.run()); - } - }; - - let _implicit_from_state = widget! { - SizedBox { - id: sized_box, - size: Size::zero() - } - transition prop!(sized_box.size) { - easing: easing::LINEAR, - duration: std::time::Duration::from_millis(200), - } - - }; - - let _transition_by = widget! { - init ctx => { - let linear = transitions::LINEAR.of(ctx); - } - SizedBox { - id: sized_box, - size: Size::zero() - } - transition prop!(sized_box.size) { by: linear } - }; - - let _fix_shorthand_with_builtin_field = widget! { - SizedBox { - id: sized_box, - background: Color::RED, - size: Size::zero() - } - transition prop!(sized_box.background) { - easing: easing::LINEAR, - duration: std::time::Duration::from_millis(200), - } - }; -} diff --git a/tests/rdl_macro_test.rs b/tests/rdl_macro_test.rs index c066269c3..b4847de23 100644 --- a/tests/rdl_macro_test.rs +++ b/tests/rdl_macro_test.rs @@ -1,6 +1,6 @@ use ribir::{core::test_helper::*, prelude::*}; use ribir_dev_helper::*; -use std::{cell::Cell, rc::Rc, time::Duration}; +use std::{cell::Cell, rc::Rc}; use winit::event::{DeviceId, ElementState, MouseButton, WindowEvent}; fn simplest_leaf_rdl() -> impl Into { @@ -131,7 +131,7 @@ widget_layout_test!(dollar_as_middle_parent, width == 500., height == 500.,); fn pipe_as_field_value() -> impl Into { let size = Stateful::new(Size::zero()); - let size2 = size.clone(); + let mut size2 = size.clone(); let w = fn_widget! { rdl! { SizedBox { size: pipe!(*$size2) }} }; @@ -142,7 +142,7 @@ widget_layout_test!(pipe_as_field_value, width == 100., height == 100.,); fn pipe_as_builtin_field_value() -> impl Into { let margin = Stateful::new(EdgeInsets::all(0.)); - let margin2 = margin.clone(); + let mut margin2 = margin.clone(); let w = fn_widget! { rdl! { SizedBox { @@ -156,7 +156,7 @@ fn pipe_as_builtin_field_value() -> impl Into { widget_layout_test!(pipe_as_builtin_field_value, width == 100., height == 100.,); fn pipe_with_ctx() -> impl Into { - let scale = Stateful::new(1.); + let mut scale = Stateful::new(1.); let scale2 = scale.clone(); let w = fn_widget! { rdl! { SizedBox { @@ -194,7 +194,7 @@ widget_layout_test!(capture_closure_used_ctx, width == 18., height == 18.,); fn pipe_single_parent() { let _guard = unsafe { AppCtx::new_lock_scope() }; - let outside_blank = Stateful::new(true); + let mut outside_blank = Stateful::new(true); let outside_blank2 = outside_blank.clone(); let w = fn_widget! { let edges = EdgeInsets::all(5.); @@ -225,7 +225,7 @@ fn pipe_single_parent() { fn pipe_multi_parent() { let _guard = unsafe { AppCtx::new_lock_scope() }; - let stack_or_flex = Stateful::new(true); + let mut stack_or_flex = Stateful::new(true); let stack_or_flex2 = stack_or_flex.clone(); let w = fn_widget! { let container = pipe! { @@ -259,7 +259,7 @@ fn pipe_as_child() { let _guard = unsafe { AppCtx::new_lock_scope() }; let box_or_not = Stateful::new(true); - let box_or_not2 = box_or_not.clone(); + let mut box_or_not2 = box_or_not.clone(); let w = fn_widget! { let blank: Pipe = pipe!{ if *$box_or_not2 { @@ -286,7 +286,7 @@ fn pipe_as_multi_child() { let _guard = unsafe { AppCtx::new_lock_scope() }; let fix_box = SizedBox { size: Size::new(100., 100.) }; - let cnt = Stateful::new(0); + let mut cnt = Stateful::new(0); let cnt2 = cnt.clone(); let w = fn_widget! { let boxes = pipe! { @@ -357,7 +357,7 @@ widget_layout_test!( fn closure_in_fn_widget_capture() { let _guard = unsafe { AppCtx::new_lock_scope() }; - let hi_res = Stateful::new(CowArc::borrowed("")); + let mut hi_res = Stateful::new(CowArc::borrowed("")); let hi_res2 = hi_res.clone(); let w = fn_widget! { let mut text = @ Text { text: "hi" }; @@ -416,7 +416,7 @@ fn simple_ref_bind_work() { tap_at(&mut wnd, (1, 1)); - wnd.layout(); + wnd.draw_frame(); assert_layout_result_by_path!(wnd, { path = [0], size == flex_size * 2., }); } @@ -506,7 +506,7 @@ fn expression_for_children() { assert_layout_result_by_path!(wnd, { path = [0, 4], size == ZERO_SIZE,}); tap_at(&mut wnd, (0, 0)); - wnd.layout(); + wnd.draw_frame(); assert_layout_result_by_path!(wnd, { path = [0], width == 25., height == 5.,}); assert_layout_result_by_path!(wnd, { path = [0, 0], size == size_five,}); assert_layout_result_by_path!(wnd, { path = [0, 1], size == size_five,}); @@ -610,8 +610,8 @@ fn builtin_ref() { size: Size::new(5., 5.), cursor: CursorIcon::Hand, on_tap: move |_| { - tap_box.cursor.set(CursorIcon::AllScroll); - c_icon_track.set(tap_box.cursor.get()); + tap_box.cursor = CursorIcon::AllScroll; + c_icon_track.set(tap_box.cursor); } } } @@ -832,7 +832,7 @@ fn fix_silent_not_relayout_dyn_widget() { wnd.draw_frame(); assert_layout_result_by_path!(wnd, { path = [0], size == ZERO_SIZE,}); { - *trigger_size.state_ref().silent() = Size::new(100., 100.); + **trigger_size.state_ref().silent() = Size::new(100., 100.); } // after silent modified, dyn widget not rebuild. wnd.draw_frame(); @@ -874,104 +874,3 @@ fn embed_shadow_states() { }) }; } - -#[test] -fn untrack_prop_with_pure_lambda() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - let trigger = Stateful::new(0_u32); - let counter = Stateful::new(0_u32); - let w = widget! { - states { - trigger: trigger.clone(), - counter: counter.clone() - } - SizedBox { - id: host, - size: Size::new(50., 50.), - } - Animate { - id: animate, - transition: Transition { - delay: None, - duration: Duration::from_millis(150), - easing: easing::EASE_IN, - repeat: None, - }, - prop: prop!(host.size, move |from, to, rate| { - let _ = *trigger; - to.lerp(from, rate) - }), - from: Size::zero(), - } - finally { - let_watch!(animate.from) - .subscribe(move |_| { - *counter += 1; - }); - } - }; - - let mut wnd = TestWindow::new(w); - wnd.draw_frame(); - - assert_eq!(*counter.state_ref(), 0); - *trigger.state_ref() += 1; - wnd.draw_frame(); - - assert_eq!(*counter.state_ref(), 0); -} - -#[test] -fn embed_widget() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - let _ = widget! { - Column { - widget! { // simple case & as first child - Container { - size: Size::new(10., 10.), - } - } - Container { - size: Size::zero(), - } - widget! { // all feature & as middle child - init ctx => { - let linear_transition = transitions::LINEAR.of(ctx); - } - Row { - SizedBox { - id: sized_box, - size: Size::new(100., 100.), - on_tap: move |_| { - leak_animate.run(); - }, - } - } - Animate { - id: leak_animate, - transition: linear_transition, - prop: prop!(sized_box.size), - from: ZERO_SIZE, - } - finally { - watch!(leak_animate.is_running()) - .subscribe(move |v| println!("{v}")); - } - } - Container { - size: Size::zero(), - } - widget! { // embed multi widget! & as last child - Row { - widget! { - Container { - size: Size::zero(), - } - } - } - } - } - }; -} diff --git a/themes/material/src/lib.rs b/themes/material/src/lib.rs index 74f815413..66507e3eb 100644 --- a/themes/material/src/lib.rs +++ b/themes/material/src/lib.rs @@ -267,72 +267,77 @@ fn override_compose_decorator(theme: &mut FullTheme) { } let styles = &mut theme.compose_decorators; - styles.override_compose_decorator::(|this, host| { - widget! { - states { this } - init ctx => { - let smooth_scroll = transitions::SMOOTH_SCROLL.of(ctx); - } - DynWidget { - id: thumb, - left_anchor: this.offset, - dyns: scrollbar_thumb(host, EdgeInsets::vertical(1.)) - } + styles.override_compose_decorator::(|mut this, host| { + fn_widget! { + let host = scrollbar_thumb(host, EdgeInsets::vertical(1.)); + let mut thumb = @ $host { left_anchor: pipe!($this.offset) }; - transition prop!(thumb.left_anchor, PositionUnit::lerp_fn(thumb.layout_width())) { - by: smooth_scroll, - } + let left_trans = partial_state!($thumb.left_anchor); + left_trans.transition_with( + transitions::LINEAR.of(ctx!()), + move |from, to, rate| PositionUnit::lerp_fn(from, to, rate, $thumb.layout_width()), + ctx!() + ); + + thumb } .into() }); - styles.override_compose_decorator::(|this, host| { - widget! { - states { this } - init ctx => { - let smooth_scroll = transitions::SMOOTH_SCROLL.of(ctx); - } - DynWidget { - id: thumb, - top_anchor: this.offset, - dyns: scrollbar_thumb(host, EdgeInsets::vertical(1.)) - } + styles.override_compose_decorator::(|mut this, host| { + fn_widget! { + let host = scrollbar_thumb(host, EdgeInsets::vertical(1.)); + let mut thumb = @ $host { top_anchor: pipe!($this.offset) }; - transition prop!(thumb.top_anchor, PositionUnit::lerp_fn(thumb.layout_height())) { - by: smooth_scroll - } + let top_trans = partial_state!($thumb.top_anchor); + top_trans.transition_with( + transitions::LINEAR.of(ctx!()), + move |from, to, rate| PositionUnit::lerp_fn(from, to, rate, $thumb.layout_height()), + ctx!() + ); + + thumb } .into() }); - styles.override_compose_decorator::(|style, host| { - widget! { - states { style } - init ctx => { - let ease_in = transitions::EASE_IN.of(ctx); - } - DynWidget { - id: indicator, - left_anchor: match style.pos { - Position::Top | Position::Bottom => style.rect.origin.x - + (style.rect.size.width - INDICATOR_SIZE) / 2., - Position::Left => style.rect.size.width - style.extent, - Position::Right => 0., + styles.override_compose_decorator::(|mut style, host| { + fn_widget! { + let mut indicator = @ $host { + left_anchor: pipe!{ + let mut style = $style; + match style.pos { + Position::Top | Position::Bottom => style.rect.origin.x + + (style.rect.size.width - INDICATOR_SIZE) / 2., + Position::Left => style.rect.size.width - style.extent, + Position::Right => 0., + } }, - top_anchor: match style.pos { - Position::Left | Position::Right => style.rect.origin.y - + (style.rect.size.height - INDICATOR_SIZE) / 2., - Position::Top => style.rect.size.height - style.extent, - Position::Bottom => 0., + top_anchor: pipe!{ + let style = $style; + match style.pos { + Position::Left | Position::Right => style.rect.origin.y + + (style.rect.size.height - INDICATOR_SIZE) / 2., + Position::Top => style.rect.size.height - style.extent, + Position::Bottom => 0., + } }, - dyns: host, - } - transition prop!( - indicator.left_anchor, - PositionUnit::lerp_fn(style.rect.size.width) - ) { by: ease_in.clone() } - transition prop!( - indicator.top_anchor, - PositionUnit::lerp_fn(style.rect.size.height) - ) { by: ease_in } + }; + + let ease_in = transitions::EASE_IN.of(ctx!()); + let left = partial_state!($indicator.left_anchor); + left.transition_with( + ease_in.clone(), + move |from, to, rate| PositionUnit::lerp_fn(from, to, rate, $style.rect.width()), + ctx!() + ); + + let top = partial_state!($indicator.top_anchor); + top.transition_with( + ease_in, + move |from, to, rate| PositionUnit::lerp_fn(from, to, rate, $style.rect.height()), + ctx!() + ); + + indicator } .into() }); diff --git a/themes/material/src/ripple.rs b/themes/material/src/ripple.rs index aecaff681..0d5245209 100644 --- a/themes/material/src/ripple.rs +++ b/themes/material/src/ripple.rs @@ -25,7 +25,7 @@ pub struct Ripple { } /// Config how ripples show outside of the host widget box. -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone, Copy)] pub enum RippleBound { /// Ripples visible outside of the host widget. Unbounded, @@ -38,87 +38,87 @@ pub enum RippleBound { impl ComposeChild for Ripple { type Child = Widget; - fn compose_child(this: State, child: Self::Child) -> Widget { - widget! { - states { this: this.into_writable() } - init ctx => { - let linear_transition = transitions::LINEAR.of(ctx); - let ease_out = transitions::EASE_OUT.of(ctx); - } - Stack { - id: container, - fit: StackFit::Passthrough, - on_pointer_down: move |e| this.launch_pos = if this.center { - let center = container.layout_size() / 2.; - Some(Point::new(center.width, center.height)) - } else { - Some(e.position()) - }, - widget::from(child) - Option::map(this.launch_pos, |launch_at| { - let radius = this.radius.unwrap_or_else(|| { - let size = container.layout_size(); + fn compose_child(mut this: State, child: Self::Child) -> Widget { + fn_widget! { + let mut container = @Stack { fit: StackFit::Passthrough }; + + let ripple_widget = pipe!{ + let launch_at = $this.launch_pos; + launch_at.map(|launch_at|{ + let radius = $this.radius.unwrap_or_else(|| { + let size = $container.layout_size(); let distance_x = f32::max(launch_at.x , size.width - launch_at.x); let distance_y = f32::max(launch_at.y, size.height - launch_at.y); (distance_x.powf(2.) + distance_y.powf(2.)).sqrt() }); - let linear_transition = linear_transition.clone(); - let ease_out = ease_out.clone(); - widget!{ - IgnorePointer { - id: ripple, - opacity: 1., - delay_drop_until: !ripper_fade_out.is_running(), - on_disposed: move |_| ripper_fade_out.run(), - DynWidget { - dyns: widget::then(this.bounded != RippleBound::Unbounded, || { - let rect = Rect::from_size(container.layout_size()); - let path = match this.bounded { - RippleBound::Unbounded => unreachable!(), - RippleBound::Bounded => Path::rect(&rect), - RippleBound::Radius(radius) => { - Path::rect_round(&rect, &radius) - } - }; - Clip { clip: ClipType::Path(path) } - }), - Container { - size: container.layout_size(), - PathPaintKit { - id: ripple_path, - brush: StateRole::pressed().calc_color(this.color), - path: Path::circle(launch_at, radius), - on_mounted: move |_| { ripper_enter.run(); } - } - } - } - } - Animate { - id: ripper_enter, - transition: linear_transition, - prop: prop!(ripple_path.path, move |_, _, rate| { + let mut ripple = @IgnorePointer {}; + + let mut ripple_path = @PathPaintKit { + brush: pipe!(StateRole::pressed().calc_color($this.color)), + path: Path::circle(launch_at, radius), + }; + + let mut ripper_enter = @Animate { + transition: transitions::LINEAR.of(ctx!()), + state: LerpFnState::new( + partial_state!($ripple_path.path), + move |_, _, rate| { let radius = Lerp::lerp(&0., &radius, rate); - let center = this.launch_pos.clone().unwrap(); + let center = $this.launch_pos.unwrap(); Path::circle(center, radius) - }), - from: Path::circle(Point::zero(), 0.) - } - Animate { - id: ripper_fade_out, - from: 0., - prop: prop!(ripple.opacity, |from, to, rate| to.lerp(from,rate)), - transition: ease_out, - } - finally { - let_watch!(!container.pointer_pressed() && !ripper_enter.is_running()) - .filter(|b| *b) - .subscribe(move |_| { - this.launch_pos.take(); - }); + } + ), + from: Path::circle(Point::zero(), 0.) + }.unwrap(); + + let h = watch!(!$container.pointer_pressed() && !$ripper_enter.is_running()) + .filter(|b| *b) + .subscribe(move |_| { $this.launch_pos.take();}); + ripper_enter.own_data(h); + + + let mut ripper_fade_out = @Animate { + from: 0., + state: partial_state!($ripple.opacity), + transition: transitions::EASE_OUT.of(ctx!()), + }; + + let bounded = $this.bounded; + let clipper = (bounded != RippleBound::Unbounded).then(|| { + let rect = Rect::from_size($container.layout_size()); + let path = match bounded { + RippleBound::Unbounded => unreachable!(), + RippleBound::Bounded => Path::rect(&rect), + RippleBound::Radius(radius) => { + Path::rect_round(&rect, &radius) + } + }; + @Clip { clip: ClipType::Path(path) } + }); + + @ $ripple { + delay_drop_until: !$ripper_fade_out.is_running(), + on_disposed: move |_| ripper_fade_out.run(), + @Container { + size: $container.layout_size(), + @$clipper { + @$ripple_path { on_mounted: move |_| { ripper_enter.run(); } } + } } } }) + }; + + @ $container { + on_pointer_down: move |e| $this.launch_pos = if $this.center { + let center = $container.layout_size() / 2.; + Some(Point::new(center.width, center.height)) + } else { + Some(e.position()) + }, + @{ child } + @{ ripple_widget } } } .into() diff --git a/widgets/src/checkbox.rs b/widgets/src/checkbox.rs index 67d3d5981..157bf4fbe 100644 --- a/widgets/src/checkbox.rs +++ b/widgets/src/checkbox.rs @@ -5,7 +5,7 @@ use crate::{ use ribir_core::prelude::*; /// Represents a control that a user can select and clear. -#[derive(Clone, Declare)] +#[derive(Clone, Declare, Declare2)] pub struct Checkbox { #[declare(default)] pub checked: bool, @@ -77,10 +77,9 @@ impl Checkbox { .into() } - fn label(mut label: State