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..19e7e1cd9 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::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: ::Value, #[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::Value>>, + #[declare(skip, default = ctx.window().id())] + window_id: WindowId, +} + +pub trait AnimateState { + type State: StateWriter; + fn state(&self) -> &Self::State; + + fn calc_lerp_value( + &mut self, + from: &::Value, + to: &::Value, + rate: f32, + ) -> ::Value; } -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,75 @@ 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, + ::Value: 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.write(); + let this = &mut *animate_ref; + let wnd_id = this.window_id; + let new_to = this.state.state().read().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_writer(); + let ticker = wnd.frame_ticker.frame_tick_stream(); + let unsub = ticker.subscribe(move |msg| { + match msg { + FrameMsg::NewFrame(time) => { + let p = animate.read().running_info.as_ref().unwrap().last_progress; + // Stop the animate at the next frame of animate finished, to ensure draw the + // last frame of the animate. + if matches!(p, AnimateProgress::Finish) { + let wnd = AppCtx::get_window(wnd_id).unwrap(); + let animate = animate.clone_writer(); + wnd + .frame_spawn(async move { animate.silent().stop() }) + .unwrap(); + } else { + animate.shallow().lerp_by_instant(time); + } + } + FrameMsg::LayoutReady(_) => {} + // use silent_ref because the state of animate change, bu no need to effect the framework. + FrameMsg::Finish(_) => { + let animate = &mut *animate.silent(); + let info = animate.running_info.as_mut().unwrap(); + *animate.state.state().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.write(); + 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 lerp_by_instant(&mut self, now: Instant) -> AnimateProgress + where + ::Value: Clone, + { let AnimateInfo { from, to, @@ -102,15 +139,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().shallow(); // 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 = value; } - AnimateProgress::Dismissed => prop.set(from.clone()), + AnimateProgress::Dismissed => *self.state.state().shallow() = from.clone(), AnimateProgress::Finish => {} } @@ -120,90 +157,89 @@ 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: StateWriter, + V: Lerp, +{ + type State = S; + + fn state(&self) -> &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: StateWriter, + F: FnMut(&V, &V, f32) -> V, +{ + type State = S; + + fn state(&self) -> &Self::State { &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: StateReader, + 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}; + use std::time::Duration; #[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, + }.into_inner(), + 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..ffb038ff9 100644 --- a/core/src/animation/transition.rs +++ b/core/src/animation/transition.rs @@ -2,17 +2,67 @@ 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 { + /// Use an animate to transition the state after it modified. + fn transition(self, transition: T, ctx: &BuildCtx) -> Writer> + where + Self: AnimateState, + ::Value: Clone, + { + let state = self.state().clone_writer(); + let from = state.read().clone(); + let mut animate: State> = Animate::declare2_builder() + .transition(transition) + .from(from) + .state(self) + .build_declare(ctx); + + let c_animate = animate.clone_writer(); + let init_value = observable::of(state.read().clone()); + state + .modifies() + .map(move |_| state.read().clone()) + .merge(init_value) + .pairwise() + .subscribe(move |(old, _)| { + animate.write().from = old; + animate.run(); + }); + c_animate + } + + /// Transition the state with a lerp function. + fn transition_with( + self, + transition: T, + lerp_fn: F, + ctx: &BuildCtx, + ) -> Writer>> + where + Self: StateWriter, + Self::Value: Clone, + F: FnMut(&Self::Value, &Self::Value, f32) -> Self::Value + 'static, + T: Roc + 'static, + { + 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 +96,20 @@ 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::Value: Roc, +{ + fn rate_of_change(&self, dur: Duration) -> AnimateProgress { self.read().rate_of_change(dur) } +} + +impl TransitionState for S {} + +impl TransitionState for LerpFnState +where + S: StateWriter + '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..abed9e403 100644 --- a/core/src/builtin_widgets.rs +++ b/core/src/builtin_widgets.rs @@ -7,8 +7,8 @@ pub mod key; pub use key::{Key, KeyWidget}; pub mod image_widget; pub use image_widget::*; -pub mod delay_drop_widget; -pub use delay_drop_widget::DelayDropWidget; +pub mod delay_drop; +pub use delay_drop::DelayDrop; mod theme; pub use theme::*; mod cursor; @@ -61,30 +61,34 @@ pub use focus_scope::*; use crate::{prelude::*, widget::WidgetBuilder}; -macro_rules! impl_fat_obj { +macro_rules! impl_builtin_obj { ($($builtin_ty: ty),*) => { paste::paste! { - #[doc="A fat object that extend the `T` object with all builtin widgets \ - ability.\ - During the compose phase, the `FatObj` will be created when a object \ - use the builtin fields and methods. And they are only a help obj to \ - build the finally widget, so they will be dropped after composed." - ] - pub struct FatObj { - pub host: T, + #[doc="A builtin object contains all builtin widgets, and can be used to \ + extend other object in the declare syntax, so other objects can use the \ + builtin fields and methods like self fields and methods."] + #[derive(Default)] + pub struct BuiltinObj { $([< $builtin_ty: snake:lower >]: Option>),* } - impl FatObj { - pub fn new(host: T) -> Self { - Self { - host, - $([< $builtin_ty: snake:lower >]: None),* - } + impl BuiltinObj { + pub fn is_empty(&self) -> bool { + $(self.[< $builtin_ty: snake:lower >].is_none())&& * } $( - pub fn [< with_ $builtin_ty: snake:lower >]( + pub fn [< $builtin_ty: snake:lower >](&mut self, ctx: &BuildCtx) + -> &mut State<$builtin_ty> + { + self + .[< $builtin_ty: snake:lower >] + .get_or_insert_with(|| $builtin_ty::declare2_builder().build_declare(ctx)) + } + )* + + $( + pub fn [< set_builtin_ $builtin_ty: snake:lower >]( mut self, builtin: State<$builtin_ty> ) -> Self { self.[< $builtin_ty: snake:lower >] = Some(builtin); @@ -93,73 +97,39 @@ macro_rules! impl_fat_obj { )* $( - pub fn [< $builtin_ty: snake:lower >](&mut self, ctx: &BuildCtx) + pub fn [< get_builtin_ $builtin_ty: snake:lower >](&mut self, ctx: &BuildCtx) -> &mut State<$builtin_ty> { self .[< $builtin_ty: snake:lower >] - .get_or_insert_with(|| $builtin_ty::declare2_builder().build(ctx)) + .get_or_insert_with(|| $builtin_ty::declare2_builder().build_declare(ctx)) } )* - } - impl WidgetBuilder for FatObj - where - T: Into - { - fn build(self, ctx: &BuildCtx) -> WidgetId { - let mut host: Widget = self.host.into(); + pub fn compose_with_host(self, mut host: Widget, ctx: &BuildCtx) -> Widget { $( if let Some(builtin) = self.[< $builtin_ty: snake:lower >] { host = builtin.with_child(host, ctx).into(); } )* - host.build(ctx) + host } } - impl, C> SingleWithChild for FatObj{ - type Target = FatObj; - fn with_child(self, child: C, ctx: &BuildCtx) -> Self::Target { - let Self { host, $([< $builtin_ty: snake:lower >]),* } = self; - let host = host.with_child(child, ctx); - FatObj { - host, - $([< $builtin_ty: snake:lower >]),* - } - } - } - - impl, C> MultiWithChild for FatObj{ - type Target = FatObj; - fn with_child(self, child: C, ctx: &BuildCtx) -> Self::Target { - let Self { host, $([< $builtin_ty: snake:lower >]),* } = self; - let host = host.with_child(child, ctx); - FatObj { - host, - $([< $builtin_ty: snake:lower >]),* - } - } - } - impl ComposeWithChild for FatObj - where - T: ComposeWithChild - { - type Target = FatObj; - fn with_child(self, child: C, ctx: &BuildCtx) -> Self::Target { - let Self { host, $([< $builtin_ty: snake:lower >]),* } = self; - let host = host.with_child(child, ctx); - FatObj { - host, - $([< $builtin_ty: snake:lower >]),* + impl FatObj { + $( + pub fn [< get_builtin_ $builtin_ty: snake:lower >](&mut self, ctx: &BuildCtx) + -> &mut State<$builtin_ty> + { + self.builtin.[](ctx) } - } + )* } } }; } -impl_fat_obj!( +impl_builtin_obj!( PointerListener, FocusNode, RequestFocus, @@ -188,9 +158,95 @@ impl_fat_obj!( Visibility, Opacity, LifecycleListener, - DelayDropWidget + DelayDrop ); +/// A fat object that extend the `T` object with all builtin widgets ability. A +/// `FatObj` will create during the compose phase, and compose with the builtin +/// widgets it actually use, and drop after composed. +pub struct FatObj { + host: T, + builtin: BuiltinObj, +} + +impl FatObj { + pub fn from_host(host: T) -> Self { Self { host, builtin: BuiltinObj::default() } } + + pub fn new(host: T, builtin: BuiltinObj) -> Self { Self { host, builtin } } + + pub fn unzip(self) -> (T, BuiltinObj) { (self.host, self.builtin) } + + pub fn into_inner(self) -> T { + assert!( + self.builtin.is_empty(), + "Unwrap a FatObj with contains builtin widgets is not allowed." + ); + self.host + } +} + +impl> WidgetBuilder for FatObj { + fn build(self, ctx: &BuildCtx) -> WidgetId { + let Self { host, builtin } = self; + builtin.compose_with_host(host.into(), ctx).build(ctx) + } +} + +impl SingleChild for FatObj {} +impl MultiChild for FatObj {} +impl ComposeChild for FatObj> { + type Child = T::Child; + + fn compose_child(this: State, child: Self::Child) -> Widget { + let this = this.into_value(); + let Self { host, builtin } = this; + FnWidget::new(move |ctx| { + let this = host.with_child(child, ctx); + builtin.compose_with_host(this.into(), ctx) + }) + .into() + } +} + +impl ComposeChild for FatObj { + type Child = T::Child; + + fn compose_child(this: State, child: Self::Child) -> Widget { + let this = this.into_value(); + let Self { host, builtin } = this; + FnWidget::new(move |ctx| { + let this = host.with_child(child, ctx); + builtin.compose_with_host(this.into(), ctx) + }) + .into() + } +} + +impl SingleParent for FatObj { + fn append_child(self, child: WidgetId, ctx: &mut BuildCtx) -> WidgetId { + let Self { host, builtin } = self; + let p = host.append_child(child, ctx); + builtin.compose_with_host(p.into(), ctx).build(ctx) + } +} + +impl MultiParent for FatObj { + fn append_children(self, children: Vec, ctx: &mut BuildCtx) -> WidgetId { + let Self { host, builtin } = self; + let host = host.append_children(children, ctx); + builtin.compose_with_host(host.into(), ctx).build(ctx) + } +} + +impl ComposeChild for BuiltinObj { + type Child = Widget; + + fn compose_child(this: State, child: Self::Child) -> Widget { + let this = this.into_value(); + fn_widget! { this.compose_with_host(child, ctx!()) }.into() + } +} + impl std::ops::Deref for FatObj { type Target = T; #[inline] @@ -201,17 +257,3 @@ impl std::ops::DerefMut for FatObj { #[inline] fn deref_mut(&mut self) -> &mut Self::Target { &mut self.host } } - -impl BoxedSingleParent for FatObj -where - W: SingleChild + Into> + Into + 'static, -{ - fn into_parent(self: Box, ctx: &mut BuildCtx) -> WidgetId { self.build(ctx) } -} - -impl BoxMultiParent for FatObj -where - W: MultiChild + Into> + Into + 'static, -{ - fn into_parent(self: Box, ctx: &mut BuildCtx) -> WidgetId { self.build(ctx) } -} diff --git a/core/src/builtin_widgets/anchor.rs b/core/src/builtin_widgets/anchor.rs index 9a4862101..4726497d1 100644 --- a/core/src/builtin_widgets/anchor.rs +++ b/core/src/builtin_widgets/anchor.rs @@ -13,21 +13,21 @@ pub enum PositionUnit { /// Widget use to anchor child constraints with the left edge of parent widget. #[derive(Declare, Declare2, SingleChild)] pub struct LeftAnchor { - #[declare(convert=into, builtin)] + #[declare(convert=into, builtin, default=0.)] pub left_anchor: PositionUnit, } /// Widget use to anchor child constraints with the right edge of parent widget. #[derive(Declare, Declare2, SingleChild)] pub struct RightAnchor { - #[declare(convert=into, builtin)] + #[declare(convert=into, builtin, default=0.)] pub right_anchor: PositionUnit, } /// Widget use to anchor child constraints with the top edge of parent widget. #[derive(Declare, Declare2, SingleChild)] pub struct TopAnchor { - #[declare(convert=into, builtin)] + #[declare(convert=into, builtin, default=0.)] pub top_anchor: PositionUnit, } @@ -35,7 +35,7 @@ pub struct TopAnchor { /// widget. #[derive(Declare, Declare2, SingleChild)] pub struct BottomAnchor { - #[declare(convert=into, builtin)] + #[declare(convert=into, builtin, default=0.)] pub bottom_anchor: PositionUnit, } @@ -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/box_decoration.rs b/core/src/builtin_widgets/box_decoration.rs index aa77739cd..3ab7fbb78 100644 --- a/core/src/builtin_widgets/box_decoration.rs +++ b/core/src/builtin_widgets/box_decoration.rs @@ -228,7 +228,7 @@ mod tests { let dummy = std::mem::MaybeUninit::uninit(); // just for test, we know BoxDecoration not use `ctx` to build. let ctx: BuildCtx<'static> = unsafe { dummy.assume_init() }; - let w = BoxDecoration::declare_builder().build(&ctx); + let w = BoxDecoration::declare_builder().build_declare(&ctx); assert_eq!(w.border, None); assert_eq!(w.border_radius, None); 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..9b4d76ef7 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)] @@ -126,7 +76,7 @@ mod tests { }, 1., ); - wnd.emit_events(); + wnd.run_frame_tasks(); assert_eq!(wnd.get_cursor(), CursorIcon::Help); let device_id = unsafe { DeviceId::dummy() }; @@ -138,7 +88,7 @@ mod tests { }, 1., ); - wnd.emit_events(); + wnd.run_frame_tasks(); assert_eq!(wnd.get_cursor(), CursorIcon::Hand); let device_id = unsafe { DeviceId::dummy() }; @@ -150,7 +100,7 @@ mod tests { }, 1., ); - wnd.emit_events(); + wnd.run_frame_tasks(); assert_eq!(wnd.get_cursor(), CursorIcon::AllScroll); let device_id = unsafe { DeviceId::dummy() }; @@ -162,7 +112,7 @@ mod tests { }, 1., ); - wnd.emit_events(); + wnd.run_frame_tasks(); assert_eq!(wnd.get_cursor(), CursorIcon::Hand); let device_id = unsafe { DeviceId::dummy() }; @@ -174,7 +124,7 @@ mod tests { }, 1., ); - wnd.emit_events(); + wnd.run_frame_tasks(); assert_eq!(wnd.get_cursor(), CursorIcon::Help); } } diff --git a/core/src/builtin_widgets/delay_drop.rs b/core/src/builtin_widgets/delay_drop.rs new file mode 100644 index 000000000..44d77cde8 --- /dev/null +++ b/core/src/builtin_widgets/delay_drop.rs @@ -0,0 +1,29 @@ +use crate::{impl_query_self_only, prelude::*}; + +/// A widget that can delay drop its child until the `delay_drop_until` field be +/// set to `true`. +/// +/// This widget not effect the widget lifecycle, if the widget is dispose but +/// the `delay_drop_until` is `false`, it's not part of the widget tree anymore +/// but not drop immediately, is disposed in `logic`, but not release resource. +/// It's be isolated from the widget tree and can layout and paint normally. +/// +/// Once the `delay_drop_until` field be set to `true`, the widget will be +/// dropped. +/// +/// It's useful when you need run a leave animation for a widget. +#[derive(Declare, Declare2)] +pub struct DelayDrop { + #[declare(builtin)] + pub delay_drop_until: bool, +} + +impl ComposeChild for DelayDrop { + type Child = Widget; + #[inline] + fn compose_child(this: State, child: Self::Child) -> Widget { + DataWidget::attach(child, this.into_writable()) + } +} + +impl_query_self_only!(DelayDrop); diff --git a/core/src/builtin_widgets/delay_drop_widget.rs b/core/src/builtin_widgets/delay_drop_widget.rs deleted file mode 100644 index 9c11ff019..000000000 --- a/core/src/builtin_widgets/delay_drop_widget.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::{impl_query_self_only, prelude::*}; - -#[derive(Declare, Declare2)] -pub struct DelayDropWidget { - #[declare(builtin)] - pub delay_drop_until: bool, -} - -impl ComposeChild for DelayDropWidget { - type Child = Widget; - #[inline] - fn compose_child(this: State, child: Self::Child) -> Widget { - DataWidget::attach(child, this.into_writable()) - } -} - -impl_query_self_only!(DelayDropWidget); 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_node.rs b/core/src/builtin_widgets/focus_node.rs index 796ee0fb2..ecf90df95 100644 --- a/core/src/builtin_widgets/focus_node.rs +++ b/core/src/builtin_widgets/focus_node.rs @@ -53,16 +53,16 @@ impl ComposeChild for FocusNode { .unwrap_or_else(|| { let listener = LifecycleListener::default(); let subject = listener.lifecycle_stream(); - attach_to_id(id, ctx.force_as_mut(), |child| { + attach_to_id(id, &mut *ctx.tree.borrow_mut(), |child| { Box::new(DataWidget::new(child, listener)) }); subject }); - fn subscribe_fn(this: Stateful) -> impl FnMut(&'_ mut AllLifecycle) + 'static { + fn subscribe_fn(this: Reader) -> impl FnMut(&'_ mut AllLifecycle) + 'static { move |e| match e { AllLifecycle::Mounted(e) => { - let auto_focus = this.state_ref().auto_focus; + let auto_focus = this.read().auto_focus; e.window().add_focus_node(e.id, auto_focus, FocusType::Node) } AllLifecycle::PerformedLayout(_) => {} @@ -70,10 +70,10 @@ impl ComposeChild for FocusNode { } } let h = subject - .subscribe(subscribe_fn(this.clone())) + .subscribe(subscribe_fn(this.clone_reader())) .unsubscribe_when_dropped(); - attach_to_id(id, ctx.force_as_mut(), |child| { + attach_to_id(id, &mut *ctx.tree.borrow_mut(), |child| { let d = DataWidget::new(child, this); Box::new(DataWidget::new( Box::new(d), @@ -104,18 +104,17 @@ pub struct RequestFocus { impl ComposeChild for RequestFocus { type Child = Widget; fn compose_child(this: State, child: Self::Child) -> Widget { - let this = this.into_writable(); - let w: Widget = widget! { - states { this: this.clone() } - DynWidget { - on_mounted: move |ctx| { - this.silent().handle = Some(ctx.window().focus_mgr.borrow().focus_handle(ctx.id)); - }, - dyns: child + let this2 = this.clone_reader(); + let w: Widget = fn_widget! { + @$child { + on_mounted: move |e| { + let handle = e.window().focus_mgr.borrow().focus_handle(e.id); + $this.silent().handle = Some(handle); + } } } .into(); - DataWidget::attach(w, this) + DataWidget::attach(w, this2) } } impl RequestFocus { diff --git a/core/src/builtin_widgets/focus_scope.rs b/core/src/builtin_widgets/focus_scope.rs index 15168e757..a623dee04 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 @@ -207,7 +207,7 @@ mod tests { is_synthetic: false, }); - wnd.emit_events(); + wnd.run_frame_tasks(); wnd.draw_frame(); assert_eq!(*result.borrow(), 2); } 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 0526dada6..5813d5eb3 100644 --- a/core/src/builtin_widgets/key.rs +++ b/core/src/builtin_widgets/key.rs @@ -69,12 +69,7 @@ where V: Default + 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.read().key.clone() } fn record_prev_key_widget(&self, key: &dyn AnyKey) { assert_eq!(self.key(), key.key()); @@ -82,19 +77,10 @@ where log::warn!("Different value type for same key."); return; }; - 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_ref() - .record_before_value(key.state_ref().value.clone()), - }, - } + self.write().record_before_value(key.read().value.clone()); } - fn record_next_key_widget(&self, _: &dyn AnyKey) { self.as_ref().has_successor.set(true); } + fn record_next_key_widget(&self, _: &dyn AnyKey) { self.write().has_successor.set(true); } fn as_any(&self) -> &dyn Any { self } } diff --git a/core/src/builtin_widgets/opacity.rs b/core/src/builtin_widgets/opacity.rs index ebc611435..b2d1a2fca 100644 --- a/core/src/builtin_widgets/opacity.rs +++ b/core/src/builtin_widgets/opacity.rs @@ -3,7 +3,7 @@ use crate::prelude::*; #[derive(Declare, Declare2, Default, Clone, SingleChild)] pub struct Opacity { - #[declare(builtin)] + #[declare(builtin, default = 1.)] pub opacity: f32, } 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..05c758af8 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.read().theme.clone(); AppCtx::load_font_from_theme(&theme); ctx.push_theme(theme.clone()); @@ -110,6 +106,7 @@ impl_query_self_only!(Theme); impl Default for Theme { fn default() -> Self { Theme::Full(<_>::default()) } } + impl Default for FullTheme { fn default() -> Self { let icon_size = IconSize { 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..0343803bb 100644 --- a/core/src/context/app_ctx.rs +++ b/core/src/context/app_ctx.rs @@ -1,7 +1,8 @@ use crate::{ builtin_widgets::{FullTheme, InheritTheme, Theme}, clipboard::{Clipboard, MockClipboard}, - window::{Window, WindowId}, + widget::Widget, + window::{ShellWindow, Window, WindowId}, }; use pin_project_lite::pin_project; use std::{ @@ -44,9 +45,14 @@ pub struct AppCtx { typography_store: TypographyStore, clipboard: RefCell>, runtime_waker: Box, + scheduler: FuturesLocalScheduler, executor: RefCell, + triggers: TriggerMap, } +type TriggerMap = RefCell>>; +pub struct AppCtxScopeGuard(MutexGuard<'static, ()>); + static mut INIT_THREAD_ID: Option = None; static mut APP_CTX_INIT: Once = Once::new(); static mut APP_CTX: Option = None; @@ -61,6 +67,16 @@ impl AppCtx { #[track_caller] pub fn app_theme() -> &'static Theme { &Self::shared().app_theme } + pub fn new_window(shell_wnd: Box, content: Widget) -> Rc { + let wnd = Window::new(shell_wnd); + let id = wnd.id(); + + Self::shared().windows.borrow_mut().insert(id, wnd.clone()); + wnd.set_content_widget(content); + + wnd + } + /// Get the window by the window id. Return an count reference of the window. /// /// If you want store the `Window`, you'd better store the `WindowId` instead. @@ -102,7 +118,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] @@ -116,10 +132,41 @@ impl AppCtx { #[track_caller] pub fn font_db() -> &'static Rc> { &Self::shared().font_db } + /// Add a trigger task to the application, this task use an pointer address as + /// its identity. You can use the identity to trigger this task in a + /// deterministic time by calling `AppCtx::trigger_task`. + pub fn add_trigger_task(trigger: *const (), task: Box) { + let mut tasks = AppCtx::shared().triggers.borrow_mut(); + let task: Box = if let Some(t) = tasks.remove(&trigger) { + Box::new(move || { + t(); + task(); + }) + } else { + Box::new(task) + }; + tasks.insert(trigger, Box::new(task)); + } + + /// Trigger the task by the pointer address identity. Returns true if the task + /// is found. + pub fn trigger_task(trigger: *const ()) -> bool { + let task = Self::shared().triggers.borrow_mut().remove(&trigger); + if let Some(task) = task { + task(); + true + } else { + false + } + } + /// Runs all tasks in the local(usually means on the main thread) pool and /// returns if no more progress can be made on any task. #[track_caller] - pub fn run_until_stalled() { Self::shared().executor.borrow_mut().run_until_stalled() } + pub fn run_until_stalled() { + let mut executor = Self::shared().executor.borrow_mut(); + while executor.try_run_one() {} + } /// Loads the font from the theme config and import it into the font database. #[track_caller] @@ -195,7 +242,7 @@ impl AppCtx { /// If your application want create multi `AppCtx` instances, hold a scope for /// every instance. Otherwise, the behavior is undefined. #[track_caller] - pub unsafe fn new_lock_scope() -> MutexGuard<'static, ()> { + pub unsafe fn new_lock_scope() -> AppCtxScopeGuard { static LOCK: Mutex<()> = Mutex::new(()); let locker = LOCK.lock().unwrap_or_else(|e| { @@ -205,14 +252,14 @@ impl AppCtx { e.into_inner() }); - APP_CTX_INIT = Once::new(); - locker + + AppCtxScopeGuard(locker) } #[track_caller] pub(crate) fn end_frame() { - // todo: frame cache is not a good choice? because not every text will relayout - // in every frame. + // todo: frame cache is not a good algorithm? because not every text will + // relayout in every frame. let ctx = unsafe { Self::shared_mut() }; ctx.shaper.end_frame(); ctx.reorder.end_frame(); @@ -231,6 +278,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,9 +288,11 @@ 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()), + triggers: RefCell::new(ahash::HashMap::default()), }; INIT_THREAD_ID = Some(std::thread::current().id()); @@ -261,8 +313,7 @@ impl AppCtx { Fut: Future + 'static, { let ctx = AppCtx::shared(); - ctx.runtime_waker.wake(); - ctx.executor.borrow().spawner().spawn_local(LocalFuture { + ctx.scheduler.spawn_local(LocalFuture { fut: future, waker: ctx.runtime_waker.clone(), }) @@ -367,6 +418,26 @@ pub fn load_font_from_theme(theme: &Theme, font_db: &mut FontDB) { } } +impl Drop for AppCtxScopeGuard { + fn drop(&mut self) { + let ctx = AppCtx::shared(); + ctx.windows.borrow_mut().clear(); + + while !ctx.triggers.borrow().is_empty() { + // drop task may generate new trigger task, use a vector to collect the tasks + // to delay drop these tasks after the borrow scope. + let _vec = ctx.triggers.borrow_mut().drain().collect::>(); + } + + // Safety: this guard guarantee only one thread can access the `AppCtx`. + unsafe { + APP_CTX = None; + INIT_THREAD_ID = None; + APP_CTX_INIT = Once::new(); + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/core/src/context/build_context.rs b/core/src/context/build_context.rs index ff2e57d2b..6af648750 100644 --- a/core/src/context/build_context.rs +++ b/core/src/context/build_context.rs @@ -3,15 +3,20 @@ use crate::{ widget::{widget_id::new_node, TreeArena, WidgetTree}, window::{DelayEvent, WindowId}, }; -use std::{ops::Deref, rc::Rc}; +use std::{ + cell::{Ref, RefCell, UnsafeCell}, + ops::Deref, + rc::Rc, +}; /// A context provide during build the widget tree. pub struct BuildCtx<'a> { - pub(crate) themes: Option>>, + // tmp `UnsafeCell` before use `BuildCtx` as mutable reference. + pub(crate) themes: UnsafeCell>>>, /// The widget which this `BuildCtx` is created from. It's not means this /// is the parent of the widget which is builded by this `BuildCtx`. ctx_from: Option, - pub(crate) tree: &'a mut WidgetTree, + pub(crate) tree: &'a RefCell, } /// A handle of `BuildCtx` that you can store it and access the `BuildCtx` later @@ -24,16 +29,16 @@ pub struct BuildCtxHandle { impl<'a> BuildCtx<'a> { /// Return the window of this context is created from. - pub fn window(&self) -> Rc { self.tree.window() } + pub fn window(&self) -> Rc { self.tree.borrow().window() } /// Get the widget which this `BuildCtx` is created from. - pub fn ctx_from(&self) -> WidgetId { self.ctx_from.unwrap_or_else(|| self.tree.root()) } + pub fn ctx_from(&self) -> WidgetId { self.ctx_from.unwrap_or_else(|| self.tree.borrow().root()) } /// 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 { BuildCtxHandle { - wnd_id: self.tree.window().id(), + wnd_id: self.window().id(), ctx_from: self.ctx_from, } } @@ -43,8 +48,12 @@ impl<'a> BuildCtx<'a> { } #[inline] - pub(crate) fn new(from: Option, tree: &'a mut WidgetTree) -> Self { - Self { themes: None, ctx_from: from, tree } + pub(crate) fn new(from: Option, tree: &'a RefCell) -> Self { + Self { + themes: UnsafeCell::new(None), + ctx_from: from, + tree, + } } pub(crate) fn find_cfg(&self, f: impl Fn(&Theme) -> Option<&T>) -> Option<&T> { @@ -60,41 +69,37 @@ impl<'a> BuildCtx<'a> { } /// Get the widget back of `id`, panic if not exist. - pub(crate) fn assert_get(&self, id: WidgetId) -> &dyn Render { id.assert_get(&self.tree.arena) } - - pub(crate) fn assert_get_mut(&self, id: WidgetId) -> &mut Box { - id.assert_get_mut(&mut self.force_as_mut().tree.arena) + pub(crate) fn assert_get(&self, id: WidgetId) -> Ref { + Ref::map(self.tree.borrow(), |tree| id.assert_get(&tree.arena)) } pub(crate) fn alloc_widget(&self, widget: Box) -> WidgetId { - let arena = &mut self.force_as_mut().tree.arena; - new_node(arena, widget) + new_node(&mut self.tree.borrow_mut().arena, widget) } pub(crate) fn append_child(&mut self, parent: WidgetId, child: WidgetId) { - parent.append(child, &mut self.tree.arena); + parent.append(child, &mut self.tree.borrow_mut().arena); } /// Insert `next` after `prev` pub(crate) fn insert_after(&mut self, prev: WidgetId, next: WidgetId) { - let arena = &mut self.tree.arena; - prev.insert_after(next, arena); + prev.insert_after(next, &mut self.tree.borrow_mut().arena); } /// After insert new subtree to the widget tree, call this to watch the /// subtree and fire mount events. pub(crate) fn on_subtree_mounted(&self, id: WidgetId) { - id.descendants(&self.tree.arena) + id.descendants(&self.tree.borrow().arena) .for_each(|w| self.on_widget_mounted(w)); - self.tree.mark_dirty(id); + self.tree.borrow_mut().mark_dirty(id); } /// After insert new widget to the widget tree, call this to watch the widget /// and fire mount events. pub(crate) fn on_widget_mounted(&self, id: WidgetId) { self.assert_get(id).query_all_type( - |notifier: &StateChangeNotifier| { - let state_changed = self.tree.dirty_set.clone(); + |notifier: &Notifier| { + let state_changed = self.tree.borrow().dirty_set.clone(); notifier .raw_modifies() .filter(|b| b.contains(ModifyScope::FRAMEWORK)) @@ -111,17 +116,17 @@ impl<'a> BuildCtx<'a> { /// Dispose the whole subtree of `id`, include `id` itself. pub(crate) fn dispose_subtree(&self, id: WidgetId) { - // todo: delay drop query - let tree = &mut self.force_as_mut().tree; + let mut tree = self.tree.borrow_mut(); + let parent = id.parent(&tree.arena); tree.detach(id); - self + tree .window() - .add_delay_event(DelayEvent::Disposed { id, delay_drop: false }); + .add_delay_event(DelayEvent::Disposed { id, parent }); let (arena1, arena2) = unsafe { split_arena(&mut tree.arena) }; id.descendants(arena1).for_each(|id| id.mark_drop(arena2)) } - pub(crate) fn mark_dirty(&mut self, id: WidgetId) { self.tree.mark_dirty(id); } + pub(crate) fn mark_dirty(&mut self, id: WidgetId) { self.tree.borrow_mut().mark_dirty(id); } #[inline] pub(crate) fn push_theme(&self, theme: Rc) { self.themes().push(theme); } @@ -141,13 +146,14 @@ impl<'a> BuildCtx<'a> { } } + #[allow(clippy::mut_from_ref)] fn themes(&self) -> &mut Vec> { let this = self.force_as_mut(); - this.themes.get_or_insert_with(|| { + unsafe { &mut *this.themes.get() }.get_or_insert_with(|| { let mut themes = vec![]; let Some(p) = self.ctx_from else { return themes }; - let arena = &mut this.tree.arena; + let arena = &this.tree.borrow().arena; p.ancestors(arena).any(|p| { p.assert_get(arena).query_all_type( |t: &Rc| { @@ -168,9 +174,8 @@ impl BuildCtxHandle { /// the window is closed or widget is removed. pub fn with_ctx(self, f: impl FnOnce(&BuildCtx) -> R) -> Option { 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); - f(&ctx) + let mut ctx = BuildCtx::new(self.ctx_from, &wnd.widget_tree); + f(&mut ctx) }) } } @@ -194,6 +199,7 @@ mod tests { struct LightDarkThemes(Rc>>); let themes: Stateful>> = Stateful::new(vec![]); + let c_themes = themes.clone_writer(); let light_palette = Palette { brightness: Brightness::Light, ..Default::default() @@ -202,27 +208,29 @@ mod tests { brightness: Brightness::Dark, ..Default::default() }; - let light_dark = widget! { - states { themes: themes.clone() } - ThemeWidget { + let light_dark = fn_widget! { + @ThemeWidget { theme: Rc::new(Theme::Inherit(InheritTheme { palette: Some(Rc::new(light_palette)), ..<_>::default() })), - MockBox { + @MockBox { size: INFINITY_SIZE, - ThemeWidget { + @ThemeWidget { theme: Rc::new(Theme::Inherit(InheritTheme { palette: Some(Rc::new(dark_palette)), ..<_>::default() })), - MockBox { + @MockBox { size: ZERO_SIZE, - FnWidget::new(move |ctx: &BuildCtx| { - no_watch!(*themes) = ctx.themes().clone(); - Void - }) + @ { + FnWidget::new(move |ctx: &BuildCtx| { + *$c_themes.write() = ctx.themes().clone(); + Void + }) + } + } } } diff --git a/core/src/data_widget.rs b/core/src/data_widget.rs index d2b619412..e2449ce6a 100644 --- a/core/src/data_widget.rs +++ b/core/src/data_widget.rs @@ -2,7 +2,9 @@ //! is same as origin widget. use crate::{ - impl_proxy_query, impl_proxy_render, impl_query_self_only, prelude::*, widget::FnWidget, + impl_proxy_query, impl_proxy_render, impl_query_self_only, + prelude::*, + widget::{FnWidget, WidgetTree}, }; pub struct DataWidget { @@ -16,7 +18,7 @@ impl DataWidget { pub fn attach(widget: Widget, data: D) -> Widget { FnWidget::new(move |ctx| { let id = widget.build(ctx); - attach_to_id(id, ctx.force_as_mut(), |child| { + attach_to_id(id, &mut *ctx.tree.borrow_mut(), |child| { Box::new(Self::new(child, data)) }); id @@ -25,30 +27,34 @@ impl DataWidget { } pub fn attach_state(widget: Widget, data: State) -> Widget { - match data { - State::Stateless(data) => DataWidget::attach(widget, data), - State::Stateful(data) => DataWidget::attach(widget, data), + match data.0.into_inner() { + InnerState::Data(data) => { + let data = data.into_inner(); + DataWidget::attach(widget, data) + } + InnerState::Stateful(data) => DataWidget::attach(widget, data), } } } -pub fn attach_to_id( +pub(crate) fn attach_to_id( id: WidgetId, - ctx: &mut BuildCtx, + tree: &mut WidgetTree, attach_data: impl FnOnce(Box) -> Box, ) { let mut tmp: Box = Box::new(Void); - let node = ctx.assert_get_mut(id); - std::mem::swap(&mut tmp, node); + let node = id.assert_get_mut(&mut tree.arena); + std::mem::swap(&mut tmp, &mut *node); let mut attached = attach_data(tmp); - std::mem::swap(node, &mut attached); + std::mem::swap(&mut *node, &mut attached); } impl_proxy_query!(paths [data, render], DataWidget, , where D: Query + 'static); impl_proxy_render!(proxy render, DataWidget, , where D: Query + 'static); /// Data attach widget that we don't care about its type. +/// todo: directly use Box instead of AnonymousData pub struct AnonymousData(Box); impl AnonymousData { diff --git a/core/src/declare.rs b/core/src/declare.rs index d36d77d6f..df23d01a9 100644 --- a/core/src/declare.rs +++ b/core/src/declare.rs @@ -1,4 +1,4 @@ -use crate::{context::BuildCtx, prelude::Pipe}; +use crate::{context::BuildCtx, prelude::Pipe, state::ModifyScope}; use rxrust::ops::box_it::BoxOp; use std::convert::Infallible; @@ -18,7 +18,9 @@ pub trait Declare2 { /// document](declare) to know how to use it. pub trait DeclareBuilder { type Target; - fn build(self, ctx: &BuildCtx) -> Self::Target; + /// build the object with the given context, return the object and not care + /// about if this object is subscribed to other or not. + fn build_declare(self, ctx: &BuildCtx) -> Self::Target; } /// The type use to store the init value of the field when declare a object. @@ -27,8 +29,10 @@ pub enum DeclareInit { Pipe(Pipe), } +type ValueStream = BoxOp<'static, (ModifyScope, V), Infallible>; + impl DeclareInit { - pub fn unzip(self) -> (V, Option>) { + pub fn unzip(self) -> (V, Option>) { match self { Self::Value(v) => (v, None), Self::Pipe(v) => { @@ -67,23 +71,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 deleted file mode 100644 index 7f3dc4e52..000000000 --- a/core/src/dynamic_widget.rs +++ /dev/null @@ -1,899 +0,0 @@ -use smallvec::SmallVec; - -use crate::{ - builtin_widgets::key::AnyKey, - impl_proxy_query, impl_query_self_only, - prelude::*, - widget::{ - widget_id::{empty_node, split_arena}, - *, - }, - window::DelayEvent, -}; -use std::{ - cell::RefCell, - collections::{HashMap, HashSet}, - rc::Rc, -}; - -#[derive(Clone, Copy)] -/// the information of a widget generated by `DynWidget`. -pub(crate) enum DynWidgetGenInfo { - /// DynWidget generate single result, and have static children. The depth - /// describe the distance from first dynamic widget (self) to the static - /// child. - DynDepth(usize), - /// `DynWidget` without static children, and the whole subtree of generated - /// widget are dynamic widgets. The value record how many dynamic siblings - /// have. - WholeSubtree(usize), -} - -// todo: we can remove `DynWidget` after syntax refactor. -// - 1. Stateful Compose/ComposeChild as a parent needn't keep -// Stateful>. -// - 2. Stateful Render can be directly replace the widget in the tree. -// - 3. Stateful Multi Widget in Stateful> that should be include -// in `Multi` widget. - -/// Widget that as a container of dynamic widgets - -#[derive(Declare)] -pub struct DynWidget { - #[declare(convert=custom)] - pub(crate) dyns: Option, -} - -impl DynWidgetDeclarer { - pub fn dyns(mut self, d: D) -> Self { - self.dyns = Some(Some(d)); - self - } -} - -impl DynWidget { - pub fn set_declare_dyns(&mut self, dyns: D) { self.dyns = Some(dyns); } - - pub fn into_inner(mut self) -> D { - self - .dyns - .take() - .unwrap_or_else(|| unreachable!("stateless `DynWidget` must be initialized.")) - } -} - -/// Widget help to limit which `DynWidget` can be a parent widget and which can -/// be a child. -pub(crate) struct DynRender { - dyn_widgets: Stateful>, - self_render: RefCell>, - gen_info: RefCell>, - dyns_to_widgets: fn(D) -> Box>, - drop_until_widgets: WidgetsHost, -} - -// A dynamic widget must be stateful, depends others. -impl Render for DynRender { - fn perform_layout(&self, clamp: BoxClamp, ctx: &mut LayoutCtx) -> Size { - if !self.regen_if_need(ctx) { - self.self_render.borrow().perform_layout(clamp, ctx) - } else { - ctx.new_layouter(ctx.id).perform_widget_layout(clamp) - } - } - - fn paint(&self, ctx: &mut PaintingCtx) { - if !self.drop_until_widgets.is_empty() { - ctx.painter.save(); - // set the position back to parent. - let rc = ctx.box_rect().unwrap(); - ctx.painter.translate(-rc.min_x(), -rc.min_y()); - self.drop_until_widgets.paint(ctx); - ctx.painter.restore(); - } - - self.self_render.borrow().paint(ctx); - } - - fn only_sized_by_parent(&self) -> bool { - // Dyn widget effect the children of its parent. Even if its self render is only - // sized by parent, but itself effect siblings, sibling effect parent, means - // itself not only sized by parent but also its sibling. - false - } - - fn hit_test(&self, ctx: &HitTestCtx, pos: Point) -> HitTest { - self.self_render.borrow().hit_test(ctx, pos) - } - - fn get_transform(&self) -> Option { self.self_render.borrow().get_transform() } -} - -#[derive(Default, Clone)] -struct WidgetsHost { - wids: Rc>>, -} - -impl WidgetsHost { - fn add(&self, wid: WidgetId) { self.wids.borrow_mut().insert(wid); } - - fn is_empty(&self) -> bool { self.wids.borrow().is_empty() } - - fn paint(&self, ctx: &mut PaintingCtx) { - self.wids.borrow().iter().for_each(|wid| { - wid.paint_subtree(ctx); - }); - } -} - -impl_query_self_only!(WidgetsHost); - -impl DynRender { - pub(crate) fn single(dyns: Stateful>) -> Self - where - Widget: From, - { - DynRender { - dyn_widgets: dyns, - self_render: RefCell::new(Box::new(Void)), - gen_info: <_>::default(), - dyns_to_widgets: move |w| Box::new(std::iter::once(w.into())), - drop_until_widgets: <_>::default(), - } - } - - fn regen_if_need(&self, ctx: &mut LayoutCtx) -> bool { - let Some(new_widgets) = self.dyn_widgets.silent_ref().dyns.take() else { - return false; - }; - - let mut new_widgets = (self.dyns_to_widgets)(new_widgets) - .map(|w| { - let build_ctx = BuildCtx::new(ctx.parent(), ctx.tree); - w.build(&build_ctx) - }) - .collect::>(); - - if new_widgets.is_empty() { - new_widgets.push(empty_node(&mut ctx.tree.arena)); - } - - let gen_info = *self.gen_info.borrow_mut().get_or_insert_with(|| { - if ctx.has_child() { - DynWidgetGenInfo::DynDepth(1) - } else { - DynWidgetGenInfo::WholeSubtree(1) - } - }); - - self.update_key_state(ctx.id, &new_widgets, &ctx.tree.arena); - - let mut tmp_render = std::mem::replace(&mut *self.self_render.borrow_mut(), Box::new(Void {})); - // 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(); - - let wrap_render = |gen_info, tree: &mut WidgetTree| { - let new_render = DynRender { - dyn_widgets: self.dyn_widgets.clone(), - self_render: RefCell::new(Box::new(Void {})), - gen_info: RefCell::new(Some(gen_info)), - dyns_to_widgets: self.dyns_to_widgets, - drop_until_widgets: self.drop_until_widgets.clone(), - }; - - // Place the first new render in `DynRender`. - std::mem::swap( - &mut *new_render.self_render.borrow_mut(), - new_widgets[0].assert_get_mut(&mut tree.arena), - ); - // use the dyn render as the first new widget. - *new_widgets[0].assert_get_mut(&mut tree.arena) = Box::new(new_render); - }; - - let tree = &mut ctx.tree; - - match gen_info { - DynWidgetGenInfo::DynDepth(depth) => { - assert_eq!(new_widgets.len(), 1); - - let declare_child_parent = single_down(ctx.id, &tree.arena, depth as isize - 1); - let (new_leaf, down_level) = down_to_leaf(new_widgets[0], &tree.arena); - wrap_render(DynWidgetGenInfo::DynDepth(down_level + 1), tree); - - if let Some(declare_child_parent) = declare_child_parent { - // Safety: control two subtree not intersect. - let (arena1, arena2) = unsafe { split_arena(&mut tree.arena) }; - declare_child_parent - .children(arena1) - .for_each(|c| new_leaf.append(c, arena2)); - } - - ctx.id.insert_after(new_widgets[0], &mut tree.arena); - self.remove_old_subtree(ctx.id, self.drop_until_widgets.clone(), tree); - - let mut w = new_widgets[0]; - loop { - w.on_mounted(tree); - if w == new_leaf { - break; - } - w = w.single_child(&tree.arena).unwrap(); - } - } - - DynWidgetGenInfo::WholeSubtree(siblings) => { - wrap_render(DynWidgetGenInfo::WholeSubtree(new_widgets.len()), tree); - let mut cursor = Some(ctx.id); - new_widgets - .iter() - .for_each(|w| ctx.id.insert_before(*w, &mut tree.arena)); - - (0..siblings).for_each(|_| { - let o = cursor.unwrap(); - cursor = o.next_sibling(&tree.arena); - self.remove_old_subtree(o, self.drop_until_widgets.clone(), tree); - }); - - new_widgets.iter().for_each(|w| w.on_mounted_subtree(tree)); - } - }; - - if ctx.id == tree.root() { - tree.root = new_widgets.first().copied(); - } - ctx.id = new_widgets[0]; - - true - } - - fn remove_old_subtree(&self, wid: WidgetId, host: WidgetsHost, tree: &mut WidgetTree) { - fn detach( - host: WidgetsHost, - wid: WidgetId, - drop_until: Stateful, - tree: &mut WidgetTree, - ) { - let mut handlers = SmallVec::<[_; 1]>::new(); - tree.detach(wid); - - let arena = &mut tree.arena; - wid.assert_get(arena).query_all_type( - |notifier: &StateChangeNotifier| { - let state_changed = tree.dirty_set.clone(); - // abandon the old subscribe - notifier.reset(); - let h = notifier - .raw_modifies() - .filter(|b| b.contains(ModifyScope::FRAMEWORK)) - .subscribe(move |_| { - state_changed.borrow_mut().insert(wid); - }) - .unsubscribe_when_dropped(); - handlers.push(h); - true - }, - QueryOrder::OutsideFirst, - ); - - let wnd_id = tree.window().id(); - let tmp = drop_until.clone(); - drop_until - .raw_modifies() - .filter(move |b| b.contains(ModifyScope::FRAMEWORK) && tmp.state_ref().delay_drop_until) - .take(1) - .delay(std::time::Duration::ZERO, tree.window().frame_scheduler()) - .subscribe(move |_| { - if let Some(wnd) = AppCtx::get_window(wnd_id) { - wnd.widget_tree.borrow_mut().remove_subtree(wid); - } - host.wids.borrow_mut().remove(&wid); - handlers.clear(); - }); - } - - let wnd = tree.window(); - - let drop_until = wid - .assert_get(&tree.arena) - .query_on_first_type(QueryOrder::OutsideFirst, |w: &Stateful| { - w.clone_stateful() - }); - - let is_drop = drop_until - .as_ref() - .map_or(true, |w| w.state_ref().delay_drop_until); - wnd.add_delay_event(DelayEvent::Disposed { id: wid, delay_drop: !is_drop }); - if !is_drop { - detach(host, wid, drop_until.unwrap(), tree); - self.drop_until_widgets.add(wid); - tree.dirty_set.borrow_mut().insert(wid); - } else { - tree.detach(wid); - - let (arena1, arena2) = unsafe { split_arena(&mut tree.arena) }; - wid - .descendants(arena1) - .for_each(|wid| wid.mark_drop(arena2)) - } - } - - fn update_key_state(&self, sign_id: WidgetId, new_widgets: &[WidgetId], arena: &TreeArena) { - let mut old_key_list = HashMap::new(); - - let mut gen_info = self.gen_info.borrow_mut(); - let Some(gen_info) = &mut *gen_info else { return }; - - let siblings = match gen_info { - DynWidgetGenInfo::DynDepth(_) => 1, - DynWidgetGenInfo::WholeSubtree(width) => *width, - }; - let mut remove = Some(sign_id); - (0..siblings).for_each(|_| { - let o = remove.unwrap(); - inspect_key(&o, arena, |old_key_widget: &dyn AnyKey| { - let key = old_key_widget.key(); - old_key_list.insert(key, o); - }); - - remove = o.next_sibling(arena); - }); - - new_widgets.iter().for_each(|n| { - inspect_key(n, arena, |new_key_widget: &dyn AnyKey| { - let key = &new_key_widget.key(); - if let Some(wid) = old_key_list.get(key) { - inspect_key(wid, arena, |old_key_widget: &dyn AnyKey| { - new_key_widget.record_prev_key_widget(old_key_widget); - old_key_widget.record_next_key_widget(new_key_widget); - }); - } - }); - }); - } -} - -impl DynRender> { - pub(crate) fn multi(dyns: Stateful>>) -> Self - where - D: IntoIterator + 'static, - Widget: From, - { - Self { - dyn_widgets: dyns, - self_render: RefCell::new(Box::new(Void)), - gen_info: <_>::default(), - dyns_to_widgets: move |d| Box::new(d.into_inner().into_iter().map(|w| w.into())), - drop_until_widgets: <_>::default(), - } - } -} - -impl DynRender> -where - Widget: From, -{ - pub(crate) fn option(dyns: Stateful>>) -> Self { - DynRender { - dyn_widgets: dyns, - self_render: RefCell::new(Box::new(Void)), - gen_info: <_>::default(), - dyns_to_widgets: move |w| Box::new(w.into_iter().map(From::from)), - drop_until_widgets: <_>::default(), - } - } -} - -impl_proxy_query!(paths [self_render.borrow(), dyn_widgets], DynRender, , where D: 'static ); -impl_query_self_only!(DynWidget, , where D: 'static); - -fn inspect_key(id: &WidgetId, tree: &TreeArena, mut cb: impl FnMut(&dyn AnyKey)) { - #[allow(clippy::borrowed_box)] - id.assert_get(tree) - .query_on_first_type(QueryOrder::OutsideFirst, |key_widget: &Box| { - cb(&**key_widget) - }); -} - -fn single_down(id: WidgetId, arena: &TreeArena, mut down_level: isize) -> Option { - let mut res = Some(id); - while down_level > 0 { - down_level -= 1; - res = res.unwrap().single_child(arena); - } - res -} - -fn down_to_leaf(id: WidgetId, arena: &TreeArena) -> (WidgetId, usize) { - let mut leaf = id; - let mut depth = 0; - while let Some(c) = leaf.single_child(arena) { - leaf = c; - depth += 1; - } - (leaf, depth) -} - -// impl IntoWidget - -// only `DynWidget` gen single widget can as a parent widget -impl WidgetBuilder for Stateful> -where - Widget: From, -{ - fn build(self, ctx: &BuildCtx) -> WidgetId { DynRender::single(self).build(ctx) } -} - -impl WidgetBuilder for Stateful>> -where - Widget: From, -{ - fn build(self, ctx: &BuildCtx) -> WidgetId { - DynRender { - dyn_widgets: self, - self_render: RefCell::new(Box::new(Void)), - gen_info: <_>::default(), - dyns_to_widgets: move |w| Box::new(w.into_iter().map(From::from)), - drop_until_widgets: <_>::default(), - } - .build(ctx) - } -} - -impl SingleChild for DynWidget {} - -#[cfg(test)] -mod tests { - use std::{ - cell::{Ref, RefCell}, - rc::Rc, - }; - - use crate::{ - builtin_widgets::key::KeyChange, impl_query_self_only, prelude::*, test_helper::*, - widget::TreeArena, - }; - - #[test] - fn expr_widget_as_root() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - let size = Stateful::new(Size::zero()); - let w = widget! { - states { size: size.clone() } - DynWidget { - dyns: MockBox { size: *size }, - Void {} - } - }; - let wnd = TestWindow::new(w); - let mut tree = wnd.widget_tree.borrow_mut(); - tree.layout(Size::zero()); - let ids = tree.root().descendants(&tree.arena).collect::>(); - assert_eq!(ids.len(), 2); - { - *size.state_ref() = Size::new(1., 1.); - } - tree.layout(Size::zero()); - let new_ids = tree.root().descendants(&tree.arena).collect::>(); - assert_eq!(new_ids.len(), 2); - - assert_eq!(ids[1], new_ids[1]); - } - - #[test] - fn expr_widget_with_declare_child() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - let size = Stateful::new(Size::zero()); - let w = widget! { - states { size: size.clone() } - MockBox { - size: Size::zero(), - DynWidget { - dyns: MockBox { size: *size }, - Void {} - } - } - }; - let wnd = TestWindow::new(w); - let mut tree = wnd.widget_tree.borrow_mut(); - tree.layout(Size::zero()); - let ids = tree.root().descendants(&tree.arena).collect::>(); - assert_eq!(ids.len(), 3); - { - *size.state_ref() = Size::new(1., 1.); - } - tree.layout(Size::zero()); - let new_ids = tree.root().descendants(&tree.arena).collect::>(); - assert_eq!(new_ids.len(), 3); - - assert_eq!(ids[0], new_ids[0]); - assert_eq!(ids[2], new_ids[2]); - } - - #[test] - fn expr_widget_mounted_new() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - let v = Stateful::new(vec![1, 2, 3]); - - let new_cnt = Stateful::new(0); - let drop_cnt = Stateful::new(0); - let w = widget! { - states { - v: v.clone(), - new_cnt: new_cnt.clone(), - drop_cnt: drop_cnt.clone(), - } - - MockMulti { - Multi::new(v.clone().into_iter().map(move |_| { - widget! { - MockBox{ - size: Size::zero(), - on_mounted: move |_| *new_cnt += 1, - on_disposed: move |_| *drop_cnt += 1 - } - } - })) - } - }; - - let mut wnd = TestWindow::new(w); - wnd.on_wnd_resize_event(Size::zero()); - wnd.draw_frame(); - assert_eq!(*new_cnt.state_ref(), 3); - assert_eq!(*drop_cnt.state_ref(), 0); - - v.state_ref().push(4); - wnd.draw_frame(); - assert_eq!(*new_cnt.state_ref(), 7); - assert_eq!(*drop_cnt.state_ref(), 3); - - v.state_ref().pop(); - wnd.draw_frame(); - assert_eq!(*new_cnt.state_ref(), 10); - assert_eq!(*drop_cnt.state_ref(), 7); - } - - #[test] - fn dyn_widgets_with_key() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - let v = Stateful::new(vec![(1, '1'), (2, '2'), (3, '3')]); - let enter_list: Stateful> = Stateful::new(vec![]); - let update_list: Stateful> = Stateful::new(vec![]); - let leave_list: Stateful> = Stateful::new(vec![]); - let key_change: Stateful> = Stateful::new(KeyChange::default()); - let w = widget! { - states { - v: v.clone(), - enter_list: enter_list.clone(), - update_list: update_list.clone(), - leave_list: leave_list.clone(), - key_change: key_change.clone(), - } - - MockMulti { - Multi::new(v.clone().into_iter().map(move |(i, c)| { - widget! { - KeyWidget { - id: key, - key: Key::from(i), - value: c, - - MockBox { - size: Size::zero(), - on_mounted: move |_| { - if key.is_enter() { - (*enter_list).push(key.value); - } - - if key.is_changed() { - (*update_list).push(key.value); - *key_change = key.get_change(); - } - }, - on_disposed: move |_| { - if key.is_leave() { - (*leave_list).push(key.value); - } - } - } - } - } - })) - } - }; - - // 1. 3 item enter - let mut wnd = TestWindow::new(w); - wnd.draw_frame(); - let expect_vec = vec!['1', '2', '3']; - assert_eq!((*enter_list.state_ref()).len(), 3); - assert!( - (*enter_list.state_ref()) - .iter() - .all(|item| expect_vec.contains(item)) - ); - // clear enter list vec - (*enter_list.state_ref()).clear(); - - // 2. add 1 item - v.state_ref().push((4, '4')); - wnd.on_wnd_resize_event(ZERO_SIZE); - wnd.draw_frame(); - - let expect_vec = vec!['4']; - assert_eq!((*enter_list.state_ref()).len(), 1); - assert!( - (*enter_list.state_ref()) - .iter() - .all(|item| expect_vec.contains(item)) - ); - // clear enter list vec - (*enter_list.state_ref()).clear(); - - // 3. update the second item - v.state_ref()[1].1 = 'b'; - wnd.draw_frame(); - - let expect_vec = vec![]; - assert_eq!((*enter_list.state_ref()).len(), 0); - assert!( - (*enter_list.state_ref()) - .iter() - .all(|item| expect_vec.contains(item)) - ); - - let expect_vec = vec!['b']; - assert_eq!((*update_list.state_ref()).len(), 1); - assert!( - (*update_list.state_ref()) - .iter() - .all(|item| expect_vec.contains(item)) - ); - assert_eq!(*key_change.state_ref(), KeyChange(Some('2'), 'b')); - (*update_list.state_ref()).clear(); - - // 4. remove the second item - v.state_ref().remove(1); - wnd.draw_frame(); - let expect_vec = vec!['b']; - assert_eq!((*leave_list.state_ref()), expect_vec); - assert_eq!((*leave_list.state_ref()).len(), 1); - assert!( - (*leave_list.state_ref()) - .iter() - .all(|item| expect_vec.contains(item)) - ); - (*leave_list.state_ref()).clear(); - - // 5. update the first item - v.state_ref()[0].1 = 'a'; - wnd.draw_frame(); - - assert_eq!((*enter_list.state_ref()).len(), 0); - - let expect_vec = vec!['a']; - assert_eq!((*update_list.state_ref()).len(), 1); - assert!( - (*update_list.state_ref()) - .iter() - .all(|item| expect_vec.contains(item)) - ); - assert_eq!(*key_change.state_ref(), KeyChange(Some('1'), 'a')); - (*update_list.state_ref()).clear(); - } - - #[test] - fn delay_drop_widgets() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - #[derive(Default, Clone)] - struct Task { - mounted: u32, - pin: bool, - paint_cnt: Rc>, - layout_cnt: Rc>, - trigger: u32, - wid: Option, - } - - fn build(item: Stateful) -> Widget { - widget! { - states { task: item.clone() } - TaskWidget { - delay_drop_until: !task.pin, - layout_cnt: task.layout_cnt.clone(), - paint_cnt: task.paint_cnt.clone(), - trigger: task.trigger, - on_mounted: move |ctx| { - task.mounted += 1; - task.wid = Some(ctx.id); - }, - on_disposed: move |ctx| { - let wid = task.wid.take(); - assert_eq!(wid, Some(ctx.id)); - } - } - } - .into() - } - - #[derive(Declare)] - struct TaskWidget { - trigger: u32, - paint_cnt: Rc>, - layout_cnt: Rc>, - } - - impl Render for TaskWidget { - fn perform_layout(&self, _: BoxClamp, _: &mut LayoutCtx) -> Size { - *self.layout_cnt.borrow_mut() += 1; - Size::new(1., 1.) - } - - fn paint(&self, _: &mut PaintingCtx) { *self.paint_cnt.borrow_mut() += 1; } - } - - impl_query_self_only!(TaskWidget); - - fn child_count(wnd: &Window) -> usize { - let tree = wnd.widget_tree.borrow(); - let root = tree.root(); - root.children(&tree.arena).count() - } - - let tasks = (0..3) - .map(|_| Stateful::new(Task::default())) - .collect::>(); - let tasks = Stateful::new(tasks); - let w = widget! { - states {tasks: tasks.clone()} - MockMulti { - Multi::new(tasks.clone().into_iter().map(build)) - } - }; - - let mut wnd = TestWindow::new(w); - let mut removed = vec![]; - - wnd.draw_frame(); - assert_eq!(child_count(&wnd), 3); - - // the first pined widget will still paint it - tasks.state_ref()[0].state_ref().pin = true; - removed.push(tasks.state_ref().remove(0)); - wnd.draw_frame(); - assert_eq!(child_count(&wnd), 2); - assert_eq!(*removed[0].state_ref().paint_cnt.borrow(), 2); - - // the remove pined widget will paint and no layout when no changed - let first_layout_cnt = *removed[0].state_ref().layout_cnt.borrow(); - tasks.state_ref().get(0).unwrap().state_ref().pin = true; - removed.push(tasks.state_ref().remove(0)); - wnd.draw_frame(); - assert_eq!(child_count(&wnd), 1); - assert_eq!(*removed[0].state_ref().paint_cnt.borrow(), 3); - assert_eq!(*removed[1].state_ref().paint_cnt.borrow(), 3); - assert_eq!( - *removed[0].state_ref().layout_cnt.borrow(), - first_layout_cnt - ); - - // the remove pined widget only mark self dirty - let first_layout_cnt = *removed[0].state_ref().layout_cnt.borrow(); - let secord_layout_cnt = *removed[1].state_ref().layout_cnt.borrow(); - let host_layout_cnt = *tasks.state_ref()[0].state_ref().layout_cnt.borrow(); - removed[0].state_ref().trigger += 1; - wnd.draw_frame(); - assert_eq!( - *removed[0].state_ref().layout_cnt.borrow(), - first_layout_cnt + 1 - ); - assert_eq!(*removed[0].state_ref().paint_cnt.borrow(), 4); - assert_eq!( - *removed[1].state_ref().layout_cnt.borrow(), - secord_layout_cnt - ); - assert_eq!( - *tasks.state_ref()[0].state_ref().layout_cnt.borrow(), - host_layout_cnt - ); - - // when unpined, it will no paint anymore - removed[0].state_ref().pin = false; - wnd.draw_frame(); - assert_eq!(*removed[0].state_ref().paint_cnt.borrow(), 4); - assert_eq!(*removed[1].state_ref().paint_cnt.borrow(), 5); - - // after removed, it will no paint and layout anymore - let first_layout_cnt = *removed[0].state_ref().layout_cnt.borrow(); - removed[0].state_ref().trigger += 1; - wnd.draw_frame(); - assert_eq!(*removed[0].state_ref().paint_cnt.borrow(), 4); - assert_eq!(*removed[1].state_ref().paint_cnt.borrow(), 5); - assert_eq!( - *removed[0].state_ref().layout_cnt.borrow(), - first_layout_cnt - ); - - // other pined widget is work fine. - let first_layout_cnt = *removed[0].state_ref().layout_cnt.borrow(); - let second_layout_cnt = *removed[1].state_ref().layout_cnt.borrow(); - removed[1].state_ref().trigger += 1; - wnd.draw_frame(); - assert_eq!(*removed[0].state_ref().paint_cnt.borrow(), 4); - assert_eq!(*removed[1].state_ref().paint_cnt.borrow(), 6); - assert_eq!( - *removed[0].state_ref().layout_cnt.borrow(), - first_layout_cnt - ); - assert_eq!( - *removed[1].state_ref().layout_cnt.borrow(), - second_layout_cnt + 1, - ); - } - - #[test] - fn remove_delay_drop_widgets() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - let child = Stateful::new(Some(())); - let child_destroy_until = Stateful::new(false); - let grandson = Stateful::new(Some(())); - let grandson_destroy_until = Stateful::new(false); - let w = widget! { - states { - child: child.clone(), - child_destroy_until: child_destroy_until.clone(), - grandson: grandson.clone(), - grandson_destroy_until: grandson_destroy_until.clone(), - } - MockMulti { - Option::map(child.as_ref(), move|_| widget! { - MockMulti { - delay_drop_until: *child_destroy_until, - Option::map(grandson.as_ref(), move|_| widget! { - MockBox { - delay_drop_until: *grandson_destroy_until, - size: Size::zero(), - } - }) - } - }) - } - }; - let mut wnd = TestWindow::new(w); - wnd.draw_frame(); - - fn tree_arena(wnd: &TestWindow) -> Ref { - let tree = wnd.widget_tree.borrow(); - Ref::map(tree, |t| &t.arena) - } - - let grandson_id = { - let arena = tree_arena(&wnd); - let root = wnd.widget_tree.borrow().root(); - root - .first_child(&arena) - .unwrap() - .first_child(&arena) - .unwrap() - }; - - wnd.draw_frame(); - assert!(!grandson_id.is_dropped(&tree_arena(&wnd))); - - child.state_ref().take(); - wnd.draw_frame(); - assert!(!grandson_id.is_dropped(&tree_arena(&wnd))); - - *child_destroy_until.state_ref() = true; - wnd.draw_frame(); - assert!(grandson_id.is_dropped(&tree_arena(&wnd))); - } -} diff --git a/core/src/events/dispatcher.rs b/core/src/events/dispatcher.rs index aa833df98..6b253a446 100644 --- a/core/src/events/dispatcher.rs +++ b/core/src/events/dispatcher.rs @@ -192,12 +192,12 @@ impl Dispatcher { .copied(); if let Some(old) = old { - let ancestor = new_hit.and_then(|w| w.common_ancestors(old, &tree.arena).next()); + let ancestor = new_hit.and_then(|w| w.lowest_common_ancestor(old, &tree.arena)); wnd.add_delay_event(DelayEvent::PointerLeave { bottom: old, up: ancestor }); }; if let Some(new) = new_hit { - let ancestor = old.and_then(|o| o.common_ancestors(new, &tree.arena).next()); + let ancestor = old.and_then(|o| o.lowest_common_ancestor(new, &tree.arena)); wnd.add_delay_event(DelayEvent::PointerEnter { bottom: new, up: ancestor }); } diff --git a/core/src/events/focus_mgr.rs b/core/src/events/focus_mgr.rs index 9d97b8fd4..f67bbf370 100644 --- a/core/src/events/focus_mgr.rs +++ b/core/src/events/focus_mgr.rs @@ -696,7 +696,7 @@ mod tests { log.borrow_mut().clear(); wnd.focus_mgr.borrow_mut().focus(parent); - wnd.emit_events(); + wnd.run_frame_tasks(); assert_eq!( &*log.borrow(), &["blur child", "focusout child", "focus parent",] @@ -704,7 +704,7 @@ mod tests { log.borrow_mut().clear(); wnd.focus_mgr.borrow_mut().blur(); - wnd.emit_events(); + wnd.run_frame_tasks(); assert_eq!(&*log.borrow(), &["blur parent", "focusout parent",]); } diff --git a/core/src/lib.rs b/core/src/lib.rs index b900d9138..283a537d9 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -14,7 +14,6 @@ pub(crate) mod widget_tree; pub mod clipboard; pub mod declare; -pub mod dynamic_widget; pub mod events; pub mod pipe; pub mod ticker; @@ -22,6 +21,7 @@ pub mod timer; pub mod widget; pub mod widget_children; pub mod window; +pub use rxrust; pub mod prelude { pub use crate::animation::*; @@ -34,10 +34,7 @@ pub mod prelude { #[doc(no_inline)] pub use crate::declare::*; #[doc(no_inline)] - 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 +57,8 @@ 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, map_writer, pipe, rdl, ribir_expanded_ಠ_ಠ, set_build_ctx, + split_writer, watch, widget, Declare, Declare2, Lerp, MultiChild, SingleChild, Template, }; #[doc(no_inline)] pub use ribir_painter::*; diff --git a/core/src/pipe.rs b/core/src/pipe.rs index d696ff4bf..641b66a3e 100644 --- a/core/src/pipe.rs +++ b/core/src/pipe.rs @@ -1,3 +1,4 @@ +use ribir_algo::Sc; use rxrust::{ ops::box_it::BoxOp, prelude::{BoxIt, ObservableExt, ObservableItem}, @@ -6,35 +7,52 @@ use rxrust::{ use std::{ cell::{Cell, RefCell}, convert::Infallible, - rc::Rc, + ops::{Deref, Range}, }; use crate::{ builtin_widgets::{key::AnyKey, Void}, - context::BuildCtx, + context::{AppCtx, BuildCtx}, data_widget::attach_to_id, - prelude::{AnonymousData, DataWidget, Multi, MultiChild, RenderParent, SingleChild}, - widget::{QueryOrder, Widget, WidgetBuilder, WidgetId}, + prelude::{ + AnonymousData, BoxedSingleParent, DataWidget, Multi, MultiChild, MultiParent, SingleChild, + SingleParent, + }, + ticker::FrameMsg, + widget::{QueryOrder, Widget, WidgetBuilder, WidgetId, WidgetTree}, + window::WindowId, }; /// A value that can be subscribed its continuous change from the observable /// stream. pub struct Pipe { value: V, - observable: BoxOp<'static, V, Infallible>, + observable: BoxOp<'static, (ModifyScope, V), Infallible>, +} + +macro_rules! new_frame_sampler { + ($ctx: ident) => { + $ctx + .window() + .frame_tick_stream() + .filter(|f| matches!(f, FrameMsg::NewFrame(_))) + }; } impl Pipe { #[inline] - pub fn new(init: V, observable: BoxOp<'static, V, Infallible>) -> Self { + pub fn new(init: V, observable: BoxOp<'static, (ModifyScope, V), Infallible>) -> Self { Self { value: init, observable } } /// map the inner observable stream to another observable that emit same type /// value. - pub fn stream_map(self, f: impl FnOnce(BoxOp<'static, V, Infallible>) -> R) -> Pipe + pub fn stream_map( + self, + f: impl FnOnce(BoxOp<'static, (ModifyScope, V), Infallible>) -> R, + ) -> Pipe where - R: BoxIt>, + R: BoxIt>, { let Self { value, observable } = self; let observable = f(observable).box_it(); @@ -51,14 +69,16 @@ impl Pipe { let Self { value, observable } = self; Pipe { value: f(value), - observable: observable.map(f).box_it(), + observable: observable.map(move |(scope, v)| (scope, f(v))).box_it(), } } /// Unzip the `Pipe` into its inner value and the changes stream of the /// value. #[inline] - pub fn unzip(self) -> (V, BoxOp<'static, V, Infallible>) { (self.value, self.observable) } + pub fn unzip(self) -> (V, BoxOp<'static, (ModifyScope, V), Infallible>) { + (self.value, self.observable) + } #[inline] pub fn value(&self) -> &V { &self.value } @@ -67,79 +87,168 @@ impl Pipe { pub fn value_mut(&mut self) -> &mut V { &mut self.value } } -impl> WidgetBuilder for Pipe { - #[inline] +impl + 'static> WidgetBuilder for Pipe { fn build(self, ctx: &crate::context::BuildCtx) -> WidgetId { let (v, modifies) = self.unzip(); let id = v.into().build(ctx); - let id_share = Rc::new(Cell::new(id)); + let id_share = Sc::new(Cell::new(id)); + let id_share2 = id_share.clone(); let handle = ctx.handle(); - let h = modifies - .subscribe(move |v| { + let wnd_id = ctx.window().id(); + let unsub = modifies + // Collects all the subtrees need to be regenerated before executing the regeneration in the + // `subscribe` method. Because the `sampler` will delay the `subscribe` until a new frame + // start. + .filter(|(scope, _)| scope.contains(ModifyScope::FRAMEWORK)) + .tap(move |_| { + if let Some(wnd) = AppCtx::get_window(wnd_id) { + wnd.mark_widgets_regenerating(id_share2.get(), None) + } + }) + .sample(new_frame_sampler!(ctx)) + .subscribe(move |(_, v)| { handle.with_ctx(|ctx| { let id = id_share.get(); + + // async clean the mark when all regenerating is done to avoid other pipe + // regenerate in the regenerating scope. + let wnd = ctx.window(); + AppCtx::spawn_local(async move { wnd.remove_regenerating_mark(id) }).unwrap(); + let ctx = ctx.force_as_mut(); - let new_id = v.into().build(ctx); + if !ctx.window().is_in_another_regenerating(id) { + let new_id = v.into().build(ctx); - update_key_status_single(id, new_id, ctx); + update_key_status_single(id, new_id, ctx); - ctx.insert_after(id, new_id); - ctx.dispose_subtree(id); - ctx.on_subtree_mounted(new_id); - id_share.set(new_id); - ctx.mark_dirty(new_id) + ctx.insert_after(id, new_id); + ctx.dispose_subtree(id); + ctx.on_subtree_mounted(new_id); + id_share.set(new_id); + ctx.mark_dirty(new_id) + } }); - }) - .unsubscribe_when_dropped(); - - let h = AnonymousData::new(Box::new(h)); - attach_to_id(id, ctx.force_as_mut(), |d| Box::new(DataWidget::new(d, h))); + }); + attach_unsubscribe_guard(id, ctx.window().id(), unsub); id } } -impl Pipe { - pub(crate) fn into_only_parent(self, ctx: &mut BuildCtx) -> WidgetId - where - R: RenderParent, - { - let (v, modifies) = self.unzip(); - let id = v.into_render_parent(ctx); - let id_share = Rc::new(Cell::new(id)); - let handle = ctx.handle(); - let h = modifies - .subscribe(move |v| { - handle.with_ctx(|ctx| { - let id = id_share.get(); - let ctx = ctx.force_as_mut(); - let new_id = v.into_render_parent(ctx); +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) + } +} - update_key_status_single(id, new_id, ctx); - let mut cursor = id.first_child(&ctx.tree.arena); - while let Some(c) = cursor { - cursor = c.next_sibling(&ctx.tree.arena); - ctx.append_child(new_id, c); - } +impl SingleParent for Pipe { + fn append_child(self, child: WidgetId, ctx: &mut BuildCtx) -> WidgetId { + let (v, modifies) = self.unzip(); + let p = v.append_child(child, ctx); + let rg = half_to_close_interval(p..child, &ctx.tree.borrow()); + update_pipe_parent(rg, modifies, ctx, |new_p, old_p, ctx| { + let child = old_p.single_child(&ctx.tree.borrow().arena).unwrap(); + new_p.append_child(child, ctx) + }); + p + } +} - ctx.insert_after(id, new_id); - ctx.dispose_subtree(id); +impl MultiParent for Pipe { + fn append_children(self, children: Vec, ctx: &mut BuildCtx) -> WidgetId { + // if children is empty, we can let the pipe parent as the whole subtree. + if children.is_empty() { + self.build(ctx) + } else { + let (v, modifies) = self.unzip(); + let first_child = children[0]; + let p = v.append_children(children, ctx); + let rg = half_to_close_interval(p..first_child, &ctx.tree.borrow()); + update_pipe_parent(rg, modifies, ctx, |new_p, old_p, ctx| { + let children = old_p.children(&ctx.tree.borrow().arena).collect::>(); + new_p.append_children(children, ctx) + }); + p + } + } +} - ctx.on_widget_mounted(new_id); - id_share.set(new_id); - ctx.mark_dirty(new_id); - }); +impl SingleParent for Pipe> { + fn append_child(self, child: WidgetId, ctx: &mut BuildCtx) -> WidgetId { + self + .map(|p| -> Box { + if let Some(p) = p { + Box::new(p) + } else { + Box::new(Void) + } }) - .unsubscribe_when_dropped(); + .append_child(child, ctx) + } +} - let h = AnonymousData::new(Box::new(h)); - attach_to_id(id, ctx.force_as_mut(), |d| Box::new(DataWidget::new(d, h))); +fn half_to_close_interval(rg: Range, tree: &WidgetTree) -> Range { + rg.start..rg.end.parent(&tree.arena).unwrap() +} - id - } +fn update_pipe_parent( + // The range of the pipe parent widget ids. + parent: Range, + // transplant the children of the old parent to the new widget. + modifies: BoxOp<'static, (ModifyScope, W), Infallible>, + ctx: &mut BuildCtx, + transplant: impl Fn(W, WidgetId, &mut BuildCtx) -> WidgetId + 'static, +) { + let id_share = Sc::new(RefCell::new(parent.clone())); + let id_share2 = id_share.clone(); + let handle = ctx.handle(); + let wnd_id = ctx.window().id(); + let unsub = modifies + .filter(|(scope, _)| scope.contains(ModifyScope::FRAMEWORK)) + .tap(move |_| { + if let Some(wnd) = AppCtx::get_window(wnd_id) { + let rg = id_share2.borrow().clone(); + wnd.mark_widgets_regenerating(rg.start, Some(rg.end)) + } + }) + .sample(new_frame_sampler!(ctx)) + .subscribe(move |(_, v)| { + handle.with_ctx(|ctx| { + let ctx = ctx.force_as_mut(); + let rg = id_share.borrow().clone(); + + let wnd = ctx.window(); + // async clean the mark when all regenerating is done to avoid other pipe + // regenerate in the regenerating scope. + AppCtx::spawn_local(async move { wnd.remove_regenerating_mark(rg.start) }).unwrap(); + + if !ctx.window().is_in_another_regenerating(rg.start) { + let first_child = rg.end.first_child(&ctx.tree.borrow().arena).unwrap(); + let p = transplant(v, rg.end, ctx.force_as_mut()); + let new_rg = half_to_close_interval(p..first_child, &ctx.tree.borrow()); + + update_key_status_single(rg.start, new_rg.start, ctx); + + ctx.insert_after(rg.start, new_rg.start); + ctx.dispose_subtree(rg.start); + new_rg + .end + .ancestors(&ctx.tree.borrow().arena) + .take_while(|w| w != &new_rg.start) + .for_each(|p| ctx.on_widget_mounted(p)); + + ctx.mark_dirty(new_rg.start); + *id_share.borrow_mut() = new_rg; + } + }); + }); + + attach_unsubscribe_guard(parent.start, ctx.window().id(), unsub); } -impl Pipe> { +impl Pipe> { pub(crate) fn build_multi(self, vec: &mut Vec, ctx: &mut BuildCtx) where W: IntoIterator, @@ -148,8 +257,7 @@ impl Pipe> { fn build_multi( v: Multi>>, ctx: &mut BuildCtx, - s_guard: impl Clone + 'static, - ) -> Box<[WidgetId]> { + ) -> Vec { let mut ids = v .into_inner() .into_iter() @@ -159,92 +267,676 @@ impl Pipe> { if ids.is_empty() { ids.push(Void.build(ctx)); } - for id in &ids { - attach_to_id(*id, ctx, |d| { - let h = AnonymousData::new(Box::new(s_guard.clone())); - Box::new(DataWidget::new(d, h)) - }); - } - ids.into_boxed_slice() + ids } - let s_guard = Rc::new(RefCell::new(None)); let (m, modifies) = self.unzip(); - let ids = build_multi(m, ctx, s_guard.clone()); - vec.extend(&*ids); - - let ids_share = Rc::new(RefCell::new(ids)); + let ids = build_multi(m, ctx); + let first_id = ids[0]; + vec.extend(&ids); + let ids_share = Sc::new(RefCell::new(ids)); + let id_share2 = ids_share.clone(); + let wnd_id = ctx.window().id(); let handle = ctx.handle(); - - let s_guard2 = s_guard.clone(); - let guard = modifies - .subscribe(move |m| { + let unsub = modifies + .filter(|(scope, _)| scope.contains(ModifyScope::FRAMEWORK)) + // Collects all the subtrees need to be regenerated before executing the regeneration in the + // `subscribe` method. Because the `sampler` will delay the `subscribe` until a new frame + // start. + .tap(move |_| { + if let Some(wnd) = AppCtx::get_window(wnd_id) { + for id in id_share2.borrow().iter() { + wnd.mark_widgets_regenerating(*id, None) + } + } + }) + .sample(new_frame_sampler!(ctx)) + .box_it() + .subscribe(move |(_, m)| { handle.with_ctx(|ctx| { let ctx = ctx.force_as_mut(); - let old = ids_share.borrow(); - let new = build_multi(m, ctx, s_guard.clone()); + let mut old = ids_share.borrow_mut(); + let removed_subtree = old.clone(); + + // async clean the mark when all regenerating is done to avoid other pipe + // regenerate in the regenerating scope. + let wnd = ctx.window(); + AppCtx::spawn_local(async move { + for id in removed_subtree { + wnd.remove_regenerating_mark(id); + } + }) + .unwrap(); + + if !ctx.window().is_in_another_regenerating(old[0]) { + let new = build_multi(m, ctx); + + update_key_state_multi(old.iter().copied(), new.iter().copied(), ctx); + + new.iter().for_each(|w| ctx.insert_after(old[0], *w)); + old.iter().for_each(|id| ctx.dispose_subtree(*id)); + new.iter().for_each(|w| { + ctx.on_subtree_mounted(*w); + ctx.mark_dirty(*w) + }); + *old = new; + } + }); + }); + attach_unsubscribe_guard(first_id, ctx.window().id(), unsub); + } +} - update_key_state_multi(&new, &old, ctx); +fn attach_unsubscribe_guard(id: WidgetId, wnd: WindowId, unsub: impl Subscription + 'static) { + AppCtx::spawn_local(async move { + let Some(wnd) = AppCtx::get_window(wnd) else { + unsub.unsubscribe(); + return; + }; + let mut tree = wnd.widget_tree.borrow_mut(); + + if tree.root() != id { + let guard = unsub.unsubscribe_when_dropped(); + // auto unsubscribe when the widget is not a root and its parent is None. + if let Some(p) = id.parent(&tree.arena) { + let guard = AnonymousData::new(Box::new(guard)); + attach_to_id(p, &mut *tree, |d| Box::new(DataWidget::new(d, guard))) + } + } + }) + .unwrap(); +} - new.iter().for_each(|w| ctx.insert_after(old[0], *w)); - old.iter().for_each(|id| ctx.dispose_subtree(*id)); - new.iter().for_each(|w| { - ctx.on_subtree_mounted(*w); - ctx.mark_dirty(*w) +fn update_children_key_status(old: WidgetId, new: WidgetId, ctx: &BuildCtx) { + let tree = &ctx.tree.borrow().arena; + + match ( + old.first_child(tree), + old.last_child(tree), + new.first_child(tree), + new.last_child(tree), + ) { + // old or new children is empty. + (None, _, _, _) | (_, _, None, _) => {} + (Some(_), None, _, _) | (_, _, Some(_), None) => { + unreachable!("first child is some, but last child is none") + } + (Some(o_first), Some(o_last), Some(n_first), Some(n_last)) => { + match (o_first == o_last, n_first == n_last) { + (true, true) => update_key_status_single(o_first, n_first, ctx), + (true, false) => { + inspect_key(o_first, ctx, |old_key| { + let o_key = old_key.key(); + new.children(tree).any(|n| { + inspect_key(n, ctx, |new_key| { + let same_key = o_key == new_key.key(); + if same_key { + update_key_states(old_key, o_first, new_key, n, ctx); + } + same_key + }) + .unwrap_or(false) + }); }); - }); - }) - .unsubscribe_when_dropped(); - - s_guard2.borrow_mut().replace(guard); + } + (false, true) => { + inspect_key(n_first, ctx, |new_key| { + let n_key = new_key.key(); + old.children(tree).any(|o| { + inspect_key(o, ctx, |old_key| { + let same_key = old_key.key() == n_key; + if same_key { + update_key_states(old_key, o, new_key, n_first, ctx); + } + same_key + }) + .unwrap_or(false) + }) + }); + } + (false, false) => update_key_state_multi(old.children(tree), new.children(tree), ctx), + } + } } } -fn update_key_status_single(new: WidgetId, old: WidgetId, ctx: &BuildCtx) { +fn update_key_status_single(old: WidgetId, new: WidgetId, ctx: &BuildCtx) { inspect_key(old, ctx, |old_key| { inspect_key(new, ctx, |new_key| { if old_key.key() == new_key.key() { - new_key.record_prev_key_widget(old_key); - old_key.record_next_key_widget(new_key); + update_key_states(old_key, old, new_key, new, ctx) } }) - }) + }); } -fn update_key_state_multi(old: &[WidgetId], new: &[WidgetId], ctx: &BuildCtx) { +fn update_key_state_multi( + old: impl Iterator, + new: impl Iterator, + ctx: &BuildCtx, +) { let mut old_key_list = ahash::HashMap::default(); - for o in old { - inspect_key(*o, ctx, |old_key: &dyn AnyKey| { - let key = old_key.key(); - old_key_list.insert(key, *o); + inspect_key(o, ctx, |old_key: &dyn AnyKey| { + old_key_list.insert(old_key.key(), o); }); } - for n in new { - inspect_key(*n, ctx, |new_key: &dyn AnyKey| { - let key = &new_key.key(); - if let Some(o) = old_key_list.get(key) { - inspect_key(*o, ctx, |old_key: &dyn AnyKey| { - new_key.record_prev_key_widget(old_key); - old_key.record_next_key_widget(new_key); - }); - } - }); + if !old_key_list.is_empty() { + for n in new { + inspect_key(n, ctx, |new_key| { + if let Some(o) = old_key_list.get(&new_key.key()).copied() { + inspect_key(o, ctx, |old_key| { + update_key_states(old_key, o, new_key, n, ctx) + }); + } + }); + } } } -fn inspect_key(id: WidgetId, ctx: &BuildCtx, mut cb: impl FnMut(&dyn AnyKey)) { - #[allow(clippy::borrowed_box)] +fn inspect_key(id: WidgetId, ctx: &BuildCtx, mut cb: impl FnMut(&dyn AnyKey) -> R) -> Option { ctx .assert_get(id) .query_on_first_type::, _>(QueryOrder::OutsideFirst, |key_widget| { - cb(&**key_widget) - }); + cb(key_widget.deref()) + }) +} + +fn update_key_states( + old_key: &dyn AnyKey, + old: WidgetId, + new_key: &dyn AnyKey, + new: WidgetId, + ctx: &BuildCtx, +) { + new_key.record_prev_key_widget(old_key); + old_key.record_next_key_widget(new_key); + update_children_key_status(old, new, ctx) } impl SingleChild for Pipe {} impl MultiChild for Pipe {} + +#[cfg(test)] +mod tests { + use std::{ + cell::{Cell, Ref}, + rc::Rc, + }; + + use crate::{ + builtin_widgets::key::KeyChange, impl_query_self_only, prelude::*, reset_test_env, + test_helper::*, widget::TreeArena, + }; + + #[test] + fn pipe_widget_as_root() { + reset_test_env!(); + + let size = Stateful::new(Size::zero()); + let c_size = size.clone_writer(); + let w = fn_widget! { + let p = pipe! { MockBox { size: *$size }}; + @$p { @Void {} } + }; + let wnd = TestWindow::new(w); + let mut tree = wnd.widget_tree.borrow_mut(); + tree.layout(Size::zero()); + let ids = tree.root().descendants(&tree.arena).collect::>(); + assert_eq!(ids.len(), 2); + { + *c_size.write() = Size::new(1., 1.); + } + tree.layout(Size::zero()); + let new_ids = tree.root().descendants(&tree.arena).collect::>(); + assert_eq!(new_ids.len(), 2); + + assert_eq!(ids[1], new_ids[1]); + } + + #[test] + fn expr_widget_with_declare_child() { + reset_test_env!(); + + let size = Stateful::new(Size::zero()); + let c_size = size.clone_writer(); + let w = fn_widget! { + @MockBox { + size: Size::zero(), + @ { + let p = pipe! { MockBox { size: *$size }}; + @$p { @Void {} } + } + } + }; + let wnd = TestWindow::new(w); + let mut tree = wnd.widget_tree.borrow_mut(); + tree.layout(Size::zero()); + let ids = tree.root().descendants(&tree.arena).collect::>(); + assert_eq!(ids.len(), 3); + { + *c_size.write() = Size::new(1., 1.); + } + tree.layout(Size::zero()); + let new_ids = tree.root().descendants(&tree.arena).collect::>(); + assert_eq!(new_ids.len(), 3); + + assert_eq!(ids[0], new_ids[0]); + assert_eq!(ids[2], new_ids[2]); + } + + #[test] + fn pipe_widget_mounted_new() { + reset_test_env!(); + + let v = Stateful::new(vec![1, 2, 3]); + let new_cnt = Stateful::new(0); + let drop_cnt = Stateful::new(0); + + let c_v = v.clone_writer(); + let c_new_cnt = new_cnt.clone_reader(); + let c_drop_cnt = drop_cnt.clone_reader(); + let w = fn_widget! { + @MockMulti { + @ { + pipe!($v.clone()).map(move |v| { + let iter = v.into_iter().map(move |_| { + @MockBox{ + size: Size::zero(), + on_mounted: move |_| *$new_cnt.write() += 1, + on_disposed: move |_| *$drop_cnt.write() += 1 + } + }); + Multi::new(iter) + }) + } + } + }; + + let mut wnd = TestWindow::new(w); + wnd.on_wnd_resize_event(Size::zero()); + wnd.draw_frame(); + assert_eq!(*c_new_cnt.read(), 3); + assert_eq!(*c_drop_cnt.read(), 0); + + c_v.write().push(4); + wnd.draw_frame(); + assert_eq!(*c_new_cnt.read(), 7); + assert_eq!(*c_drop_cnt.read(), 3); + + c_v.write().pop(); + wnd.draw_frame(); + assert_eq!(*c_new_cnt.read(), 10); + assert_eq!(*c_drop_cnt.read(), 7); + } + + #[test] + fn pipe_widget_in_pipe() { + reset_test_env!(); + let p_trigger = Stateful::new(false); + let c_trigger = Stateful::new(false); + let mnt_cnt = Stateful::new(0); + let c_p_trigger = p_trigger.clone_writer(); + let c_c_trigger = c_trigger.clone_writer(); + let mnt_cnt2 = mnt_cnt.clone_reader(); + + let w = fn_widget! { + pipe!(*$p_trigger).map(move |_| { + @MockBox { + size: Size::zero(), + on_mounted: move |_| *$mnt_cnt.write() +=1, + @{ + pipe!(*$c_trigger).map(move |_| { + @MockBox { + size: Size::zero(), + on_mounted: move |_| *$mnt_cnt.write() +=1, + } + }) + } + } + }) + }; + + let mut wnd = TestWindow::new(w); + wnd.draw_frame(); + assert_eq!(*mnt_cnt2.read(), 2); + + // trigger the parent update + *c_p_trigger.write() = true; + wnd.run_frame_tasks(); + // then trigger the child update. + *c_c_trigger.write() = true; + + wnd.draw_frame(); + assert_eq!(*mnt_cnt2.read(), 4); + } + + #[test] + fn pipe_widgets_with_key() { + reset_test_env!(); + + let v = Stateful::new(vec![(1, '1'), (2, '2'), (3, '3')]); + let enter_list: Stateful> = Stateful::new(vec![]); + let update_list: Stateful> = Stateful::new(vec![]); + let leave_list: Stateful> = Stateful::new(vec![]); + let key_change: Stateful> = Stateful::new(KeyChange::default()); + + let c_v = v.clone_writer(); + let c_enter_list = enter_list.clone_writer(); + let c_update_list = update_list.clone_writer(); + let c_leave_list = leave_list.clone_writer(); + let c_key_change = key_change.clone_writer(); + let w: Widget = fn_widget! { + @MockMulti { + @ { + pipe!($v.clone()).map(move |v| { + let iter = v.into_iter().map(move |(i, c)| { + let mut key = @KeyWidget { key: i, value: c }; + @$key { + @MockBox { + size: Size::zero(), + on_mounted: move |_| { + if $key.is_enter() { + $c_enter_list.write().push($key.value); + } + + if $key.is_changed() { + $c_update_list.write().push($key.value); + *$c_key_change.write() = $key.get_change(); + } + }, + on_disposed: move |_| if $key.is_leave() { + $c_leave_list.write().push($key.value); + } + } + } + }); + Multi::new(iter) + }) + } + } + } + .into(); + + // 1. 3 item enter + let mut wnd = TestWindow::new(w); + wnd.draw_frame(); + let expect_vec = vec!['1', '2', '3']; + assert_eq!((*enter_list.state_ref()).len(), 3); + assert!( + (*enter_list.state_ref()) + .iter() + .all(|item| expect_vec.contains(item)) + ); + // clear enter list vec + (*enter_list.state_ref()).clear(); + + // 2. add 1 item + c_v.write().push((4, '4')); + wnd.on_wnd_resize_event(ZERO_SIZE); + wnd.draw_frame(); + + let expect_vec = vec!['4']; + assert_eq!((*enter_list.state_ref()).len(), 1); + assert!( + (*enter_list.state_ref()) + .iter() + .all(|item| expect_vec.contains(item)) + ); + // clear enter list vec + (*enter_list.state_ref()).clear(); + + // 3. update the second item + c_v.write()[1].1 = 'b'; + wnd.draw_frame(); + + let expect_vec = vec![]; + assert_eq!((*enter_list.state_ref()).len(), 0); + assert!( + (*enter_list.state_ref()) + .iter() + .all(|item| expect_vec.contains(item)) + ); + + let expect_vec = vec!['b']; + assert_eq!((*update_list.state_ref()).len(), 1); + assert!( + (*update_list.state_ref()) + .iter() + .all(|item| expect_vec.contains(item)) + ); + assert_eq!(*key_change.state_ref(), KeyChange(Some('2'), 'b')); + (*update_list.state_ref()).clear(); + + // 4. remove the second item + c_v.write().remove(1); + wnd.draw_frame(); + let expect_vec = vec!['b']; + assert_eq!((*leave_list.state_ref()), expect_vec); + assert_eq!((*leave_list.state_ref()).len(), 1); + assert!( + (*leave_list.state_ref()) + .iter() + .all(|item| expect_vec.contains(item)) + ); + (*leave_list.state_ref()).clear(); + + // 5. update the first item + c_v.write()[0].1 = 'a'; + wnd.draw_frame(); + + assert_eq!((*enter_list.state_ref()).len(), 0); + + let expect_vec = vec!['a']; + assert_eq!((*update_list.state_ref()).len(), 1); + assert!( + (*update_list.state_ref()) + .iter() + .all(|item| expect_vec.contains(item)) + ); + assert_eq!(*key_change.state_ref(), KeyChange(Some('1'), 'a')); + (*update_list.state_ref()).clear(); + } + + #[test] + fn delay_drop_widgets() { + reset_test_env!(); + + #[derive(Default, Clone)] + struct Task { + mounted: u32, + pin: bool, + paint_cnt: Rc>, + layout_cnt: Rc>, + trigger: u32, + wid: Option, + } + + fn build(item: Writer) -> Widget { + let item = item.into_inner(); + widget! { + states { task: item.clone_stateful() } + TaskWidget { + delay_drop_until: !task.pin, + layout_cnt: task.layout_cnt.clone(), + paint_cnt: task.paint_cnt.clone(), + trigger: task.trigger, + on_mounted: move |ctx| { + task.mounted += 1; + task.wid = Some(ctx.id); + }, + on_disposed: move |ctx| { + let wid = task.wid.take(); + assert_eq!(wid, Some(ctx.id)); + } + } + } + .into() + } + + #[derive(Declare)] + struct TaskWidget { + trigger: u32, + paint_cnt: Rc>, + layout_cnt: Rc>, + } + + impl Render for TaskWidget { + fn perform_layout(&self, _: BoxClamp, _: &mut LayoutCtx) -> Size { + self.layout_cnt.set(self.layout_cnt.get() + 1); + Size::new(1., 1.) + } + + fn paint(&self, _: &mut PaintingCtx) { self.paint_cnt.set(self.paint_cnt.get() + 1); } + } + + impl_query_self_only!(TaskWidget); + + fn child_count(wnd: &Window) -> usize { + let tree = wnd.widget_tree.borrow(); + let root = tree.root(); + root.children(&tree.arena).count() + } + + let tasks = (0..3) + .map(|_| Stateful::new(Task::default())) + .collect::>(); + let tasks = Stateful::new(tasks); + let c_tasks = tasks.clone_reader(); + let w = fn_widget! { + @MockMulti { + @ { pipe!{ + let iter = $c_tasks.iter().map(|t| build(t.clone_writer())).collect::>(); + Multi::new(iter) + }} + } + }; + + let mut wnd = TestWindow::new(w); + let mut removed = vec![]; + + wnd.draw_frame(); + assert_eq!(child_count(&wnd), 3); + + // the first pined widget will still paint it + tasks.state_ref()[0].state_ref().pin = true; + removed.push(tasks.state_ref().remove(0)); + wnd.draw_frame(); + assert_eq!(child_count(&wnd), 2); + assert_eq!(removed[0].state_ref().paint_cnt.get(), 2); + + // the remove pined widget will paint and no layout when no changed + let first_layout_cnt = removed[0].state_ref().layout_cnt.get(); + tasks.state_ref().get(0).unwrap().state_ref().pin = true; + removed.push(tasks.state_ref().remove(0)); + wnd.draw_frame(); + assert_eq!(child_count(&wnd), 1); + assert_eq!(removed[0].state_ref().paint_cnt.get(), 3); + assert_eq!(removed[1].state_ref().paint_cnt.get(), 3); + assert_eq!(removed[0].state_ref().layout_cnt.get(), first_layout_cnt); + + // the remove pined widget only mark self dirty + let first_layout_cnt = removed[0].state_ref().layout_cnt.get(); + let secord_layout_cnt = removed[1].state_ref().layout_cnt.get(); + let host_layout_cnt = tasks.state_ref()[0].state_ref().layout_cnt.get(); + removed[0].state_ref().trigger += 1; + wnd.draw_frame(); + assert_eq!( + removed[0].state_ref().layout_cnt.get(), + first_layout_cnt + 1 + ); + assert_eq!(removed[0].state_ref().paint_cnt.get(), 4); + assert_eq!(removed[1].state_ref().layout_cnt.get(), secord_layout_cnt); + assert_eq!( + tasks.state_ref()[0].state_ref().layout_cnt.get(), + host_layout_cnt + ); + + // when unpined, it will no paint anymore + removed[0].state_ref().pin = false; + wnd.draw_frame(); + assert_eq!(removed[0].state_ref().paint_cnt.get(), 4); + assert_eq!(removed[1].state_ref().paint_cnt.get(), 5); + + // after removed, it will no paint and layout anymore + let first_layout_cnt = removed[0].state_ref().layout_cnt.get(); + removed[0].state_ref().trigger += 1; + wnd.draw_frame(); + assert_eq!(removed[0].state_ref().paint_cnt.get(), 4); + assert_eq!(removed[1].state_ref().paint_cnt.get(), 5); + assert_eq!(removed[0].state_ref().layout_cnt.get(), first_layout_cnt); + + // other pined widget is work fine. + let first_layout_cnt = removed[0].state_ref().layout_cnt.get(); + let second_layout_cnt = removed[1].state_ref().layout_cnt.get(); + removed[1].state_ref().trigger += 1; + wnd.draw_frame(); + assert_eq!(removed[0].state_ref().paint_cnt.get(), 4); + assert_eq!(removed[1].state_ref().paint_cnt.get(), 6); + assert_eq!(removed[0].state_ref().layout_cnt.get(), first_layout_cnt); + assert_eq!( + removed[1].state_ref().layout_cnt.get(), + second_layout_cnt + 1, + ); + } + + #[test] + fn remove_delay_drop_widgets() { + reset_test_env!(); + + let child = Stateful::new(Some(())); + let child_destroy_until = Stateful::new(false); + let grandson = Stateful::new(Some(())); + let grandson_destroy_until = Stateful::new(false); + let c_child = child.clone_writer(); + let c_child_destroy_until = child_destroy_until.clone_writer(); + + let w = fn_widget! { + @MockMulti { + @ { pipe!(*$child).map(move |_| { + @MockMulti { + delay_drop_until: pipe!(*$child_destroy_until), + @ { pipe!(*$grandson).map(move |_| { + @MockBox { + delay_drop_until: pipe!(*$grandson_destroy_until), + size: Size::zero(), + } + })} + } + })} + } + }; + let mut wnd = TestWindow::new(w); + wnd.draw_frame(); + + fn tree_arena(wnd: &TestWindow) -> Ref { + let tree = wnd.widget_tree.borrow(); + Ref::map(tree, |t| &t.arena) + } + + let grandson_id = { + let arena = tree_arena(&wnd); + let root = wnd.widget_tree.borrow().root(); + root + .first_child(&arena) + .unwrap() + .first_child(&arena) + .unwrap() + }; + + wnd.draw_frame(); + assert!(!grandson_id.is_dropped(&tree_arena(&wnd))); + + c_child.write().take(); + wnd.draw_frame(); + assert!(!grandson_id.is_dropped(&tree_arena(&wnd))); + + *c_child_destroy_until.write() = true; + wnd.draw_frame(); + assert!(grandson_id.is_dropped(&tree_arena(&wnd))); + } +} diff --git a/core/src/state.rs b/core/src/state.rs index aab5fd52a..b52da0e48 100644 --- a/core/src/state.rs +++ b/core/src/state.rs @@ -1,58 +1,191 @@ -mod readonly; +mod map_state; +mod splitted_state; mod stateful; -use std::{convert::Infallible, mem::MaybeUninit, rc::Rc}; -pub use readonly::*; -use rxrust::{ops::box_it::BoxOp, prelude::ObservableItem}; +use std::{ + cell::UnsafeCell, + convert::Infallible, + mem::MaybeUninit, + ops::{Deref, DerefMut}, +}; + +pub use map_state::*; +use rxrust::{ops::box_it::BoxOp, subject::Subject}; +pub use splitted_state::*; pub use stateful::*; use crate::{ context::BuildCtx, - dynamic_widget::DynWidget, - prelude::{BoxMultiParent, BoxedSingleParent, MultiChild, SingleChild}, - widget::{Compose, Render, RenderFul, WidgetBuilder, WidgetId}, + prelude::{MultiChild, SingleChild}, + widget::{Compose, Render, WidgetBuilder, WidgetId}, }; +/// The `StateReader` trait allows for reading, clone and map the state. +pub trait StateReader: Sized { + /// The value type of this state. + type Value; + /// The origin state type that this state map or split from . Otherwise + /// return itself. + type OriginReader: StateReader; + type Reader: StateReader; + /// The reference type that can read the value of the state. + type Ref<'a>: Deref + where + Self: 'a; + + /// Return a reference of this state. + fn read(&'_ self) -> Self::Ref<'_>; + /// get a clone of this state that only can read. + fn clone_reader(&self) -> Self::Reader; + /// Maps an reader to another by applying a function to a contained + /// value. The return reader and the origin reader are the same reader. So + /// when one of them is modified, they will both be notified. + fn map_reader(&self, f: F) -> MapReader + where + F: FnOnce(&Self::Value) -> &Target + Copy, + { + MapReader::new(self.clone_reader(), f) + } + /// Return the origin reader that this state map or split from . Otherwise + /// return itself. + fn origin_reader(&self) -> &Self::OriginReader; + /// Return a modifies `Rx` stream of the state, user can subscribe it to + /// response the state changes. + fn modifies(&self) -> BoxOp<'static, ModifyScope, Infallible>; + fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible>; +} + +pub trait StateWriter: StateReader { + type Writer: StateWriter; + type OriginWriter: StateWriter; + type RefWrite<'a>: RefWrite + where + Self: 'a; + + /// Return a write reference of this state. + fn write(&'_ self) -> Self::RefWrite<'_>; + /// Return a silent write reference which notifies will be ignored by the + /// framework. + fn silent(&'_ self) -> Self::RefWrite<'_>; + /// 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(&'_ self) -> Self::RefWrite<'_>; + /// Clone this state writer. + fn clone_writer(&self) -> Self::Writer; + /// Return the origin writer that this state map or split from. + fn origin_writer(&self) -> &Self::OriginWriter; + /// Return a new writer by applying a function to the contained value. + /// + /// This writer share the same state with the origin writer. But has it's own + /// notifier. When modifies across the return writer, the downstream + /// subscribed on the origin state will not be notified. But when modifies + /// across the origin writer, the downstream subscribed on the return writer + /// will be notified. + /// + /// If you want split a new writer that has same behavior with the origin + /// writer, you can use `map_reader(..).into_writer(..)`. + fn split_writer( + &self, + read_map: R, + writer_map: W, + ) -> SplittedWriter + where + R: FnOnce(&Self::Value) -> &Target + Copy, + W: FnOnce(&mut Self::Value) -> &mut Target + Copy, + { + SplittedWriter::new(self.clone_writer(), read_map, writer_map) + } + + fn map_writer( + &self, + read_map: R, + writer_map: W, + ) -> MapWriter + where + R: FnOnce(&Self::Value) -> &Target + Copy, + W: FnOnce(&mut Self::Value) -> &mut Target + Copy, + { + MapWriter::new(self.clone_writer(), read_map, writer_map) + } +} + +pub trait RefWrite: DerefMut { + /// Forget all modifies of this reference. So all the modifies occurred on + /// this reference before this call will not be notified. Return true if there + /// is any modifies on this reference. + fn forget_modifies(&mut self) -> bool; +} + /// Enum to store both stateless and stateful object. -pub enum State { - Stateless(W), +pub struct State(pub(crate) UnsafeCell>); + +pub(crate) enum InnerState { + Data(StateData), Stateful(Stateful), } -pub enum StateRef<'a, W> { - Stateful(StatefulRef<'a, W>), - Stateless(&'a mut W), -} +impl StateReader for State { + type Value = T; + type OriginReader = Self; + type Reader = Reader; + type Ref<'a> = ReadRef<'a,T> + where + Self: 'a; -impl SingleChild for State {} -impl MultiChild for State {} + fn read(&'_ self) -> Self::Ref<'_> { + match self.inner_ref() { + InnerState::Data(w) => w.read(), + InnerState::Stateful(w) => w.read(), + } + } + + fn clone_reader(&self) -> Self::Reader { self.as_stateful().clone_reader() } -impl BoxedSingleParent for State { - fn into_parent(self: Box, ctx: &mut BuildCtx) -> WidgetId { - let r: Box = match *self { - State::Stateless(w) => Box::new(w), - State::Stateful(w) => Box::new(RenderFul(w)), - }; - ctx.alloc_widget(r) + #[inline] + fn origin_reader(&self) -> &Self::OriginReader { self } + + fn modifies(&self) -> BoxOp<'static, ModifyScope, Infallible> { self.as_stateful().modifies() } + + fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { + self.as_stateful().raw_modifies() } } -impl BoxMultiParent for State { - fn into_parent(self: Box, ctx: &mut BuildCtx) -> WidgetId { - let r: Box = match *self { - State::Stateless(w) => Box::new(w), - State::Stateful(w) => Box::new(RenderFul(w)), - }; - ctx.alloc_widget(r) - } +impl StateWriter for State { + type Writer = Writer; + type OriginWriter = Self; + type RefWrite<'a> = WriteRef<'a,T> + where + Self: 'a; + + #[inline] + fn write(&'_ self) -> Self::RefWrite<'_> { self.as_stateful().write() } + + #[inline] + fn silent(&'_ self) -> Self::RefWrite<'_> { self.as_stateful().silent() } + + #[inline] + fn shallow(&'_ self) -> Self::RefWrite<'_> { self.as_stateful().shallow() } + + #[inline] + fn clone_writer(&self) -> Self::Writer { self.as_stateful().clone_writer() } + + #[inline] + fn origin_writer(&self) -> &Self::OriginWriter { self } } +impl SingleChild for State {} +impl MultiChild for State {} + impl From> for Box { #[inline] fn from(s: State) -> Self { - match s { - State::Stateless(w) => w.into(), - State::Stateful(w) => w.into(), + match s.0.into_inner() { + InnerState::Data(w) => w.into_inner().into(), + InnerState::Stateful(w) => w.into(), } } } @@ -63,59 +196,58 @@ impl WidgetBuilder for State { } impl State { - pub fn into_writable(self) -> Stateful { - match self { - State::Stateless(w) => Stateful::new(w), - State::Stateful(w) => w, - } + pub fn stateful(stateful: Stateful) -> Self { + State(UnsafeCell::new(InnerState::Stateful(stateful))) } - pub fn into_readonly(self) -> Readonly { - match self { - State::Stateless(w) => Readonly::Stateless(Rc::new(w)), - State::Stateful(w) => match w.try_into_inner() { - Ok(w) => Readonly::Stateless(Rc::new(w)), - Err(s) => Readonly::Stateful(s), - }, + pub fn value(value: W) -> Self { State(UnsafeCell::new(InnerState::Data(StateData::new(value)))) } + + /// Unwrap the state and return the inner value. + /// + /// # Panics + /// panic if the state is stateful or already attached data. + pub fn into_value(self) -> W { + match self.0.into_inner() { + InnerState::Data(w) => w.into_inner(), + InnerState::Stateful(w) => w + .try_into_inner() + .unwrap_or_else(|_| panic!("can't convert stateful to value")), } } - pub fn clone_stateful(&mut self) -> Stateful { self.to_stateful().clone() } + pub fn into_writable(self) -> Stateful { self.as_stateful().clone_stateful() } - pub fn modifies(&mut self) -> BoxOp<'static, (), Infallible> { self.to_stateful().modifies() } + pub fn clone_stateful(&mut self) -> Stateful { self.as_stateful().clone_stateful() } - pub fn clone(&mut self) -> State { - let stateful = self.to_stateful().clone(); - State::Stateful(stateful) - } + pub fn as_stateful(&self) -> &Stateful { + match self.inner_ref() { + InnerState::Data(w) => { + w.assert_is_not_used(); + + let mut uninit: MaybeUninit<_> = MaybeUninit::uninit(); + // Safety: we already check there is no other reference to the state data. + unsafe { + std::ptr::copy(w, uninit.as_mut_ptr(), 1); + let stateful = InnerState::Stateful(Stateful::from_state_data(uninit.assume_init())); + let copy = std::mem::replace(&mut *self.0.get(), stateful); + // this is a copy of the inner data so we need forget it. + std::mem::forget(copy); + }; - pub fn stateful_ref(&mut self) -> StatefulRef { self.to_stateful().state_ref() } - - pub fn to_stateful(&mut self) -> &mut Stateful { - match self { - State::Stateless(w) => { - // convert the stateless value to stateful first. - let uninit: MaybeUninit<_> = MaybeUninit::zeroed(); - let v = std::mem::replace(w, unsafe { uninit.assume_init() }); - let stateful = State::Stateful(Stateful::new(v)); - let uninit = std::mem::replace(self, stateful); - // the tmp value not init, so we need forget it. - std::mem::forget(uninit); - - match self { - State::Stateful(w) => w, + match self.inner_ref() { + InnerState::Stateful(w) => w, _ => unreachable!(), } } - State::Stateful(w) => w, + InnerState::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()), - } + fn inner_ref(&self) -> &InnerState { + // Safety: we only use this method to get the inner state, and no way to get the + // mutable reference of the inner state except the `as_stateful` method and the + // `as_stateful` will check the inner borrow state. + unsafe { &*self.0.get() } } } @@ -125,47 +257,12 @@ pub(crate) trait StateFrom { impl StateFrom for State { #[inline] - fn state_from(value: W) -> State { State::Stateless(value) } + fn state_from(value: W) -> State { State::value(value) } } impl StateFrom> for State { #[inline] - fn state_from(value: Stateful) -> State { State::Stateful(value) } -} - -impl StateFrom>> for State { - #[inline] - fn state_from(value: Stateful>) -> State { - let c_value = value.clone(); - let v = value.silent_ref().dyns.take().unwrap(); - let v = Stateful::new(v); - let c_v = v.clone(); - value.modifies().subscribe(move |_| { - if c_value.silent_ref().dyns.is_some() { - let mut c_value = c_value.silent_ref(); - *c_v.state_ref() = c_value.dyns.take().unwrap(); - - // In this widget, we subscribed the `child` modifies, then spread it. - // When we spread it, we modifies it, a circular occur. So we forget - // the modify of take its children to break the circular. - // - // In other side, `child` is a stateful dynamic widget and use as - // child here, and all its content all a black box, so others - // should not depends on it. - c_value.forget_modifies(); - } - }); - State::Stateful(v) - } -} - -impl<'a, T> StateRef<'a, T> { - pub fn forget_modifies(&self) { - match self { - StateRef::Stateless(_) => {} - StateRef::Stateful(w) => w.forget_modifies(), - } - } + fn state_from(value: Stateful) -> State { State::stateful(value) } } impl From for State @@ -175,32 +272,17 @@ where fn from(value: T) -> Self { StateFrom::state_from(value) } } -impl<'a, W> std::ops::Deref for StateRef<'a, W> { - type Target = W; - - fn deref(&self) -> &Self::Target { - match self { - StateRef::Stateful(s) => s.deref(), - StateRef::Stateless(r) => r, - } - } -} - -impl<'a, W> std::ops::DerefMut for StateRef<'a, W> { - fn deref_mut(&mut self) -> &mut Self::Target { - match self { - StateRef::Stateful(s) => s.deref_mut(), - StateRef::Stateless(r) => r, - } - } -} - #[cfg(test)] mod tests { + use std::cell::Cell; + use super::*; + use crate::{context::AppCtx, prelude::*, reset_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 +290,61 @@ mod tests { c_dyns.state_ref().dyns = Some(2); } } + + struct Origin { + a: i32, + b: i32, + } + + #[test] + fn path_state_router_test() { + reset_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/map_state.rs b/core/src/state/map_state.rs new file mode 100644 index 000000000..ee9100fe2 --- /dev/null +++ b/core/src/state/map_state.rs @@ -0,0 +1,259 @@ +use super::{ModifyScope, RefWrite, StateReader, StateWriter}; +use rxrust::{ops::box_it::BoxOp, subject::Subject}; +use std::{ + convert::Infallible, + ops::{Deref, DerefMut}, +}; + +/// A state reader that map a reader to another by applying a function on the +/// value. This reader is the same reader with the origin reader, It's also have +/// the same modifier with the origin state. +// Keep the `V` as the first generic, so the user know the actual value type +// when ide hints. +pub struct MapReader &V + Copy> { + origin_reader: R, + map_fn: F, +} + +pub struct MapWriter< + V, + W: StateWriter, + RM: FnOnce(&W::Value) -> &V + Copy, + WM: FnOnce(&mut W::Value) -> &mut V + Copy, +> { + origin_writer: W, + map_reader: RM, + map_writer: WM, +} + +/// The read reference of `MapReader`. +pub struct MapReadRef &V + Copy> { + origin_ref: O, + map_fn: F, +} + +pub struct MapWriteRef +where + O: DerefMut, + R: FnOnce(&O::Target) -> &V + Copy, + W: FnOnce(&mut O::Target) -> &mut V + Copy, +{ + origin_ref: O, + map_read: R, + map_write: W, +} + +impl MapReader +where + R: StateReader, + F: FnOnce(&R::Value) -> &V + Copy, +{ + #[inline] + pub fn new(origin_state: R, map_fn: F) -> Self { Self { origin_reader: origin_state, map_fn } } +} + +impl MapReader +where + W: StateWriter, + RM: FnOnce(&W::Value) -> &V + Copy, +{ + /// Convert a `MapRender` to a `MapWriter` by add a write map function. + #[inline] + pub fn into_writer &mut V + Copy>( + self, + map_fn: WM, + ) -> MapWriter { + MapWriter { + map_reader: self.map_fn, + map_writer: map_fn, + origin_writer: self.origin_reader, + } + } +} + +impl MapWriter +where + W: StateWriter, + RM: FnOnce(&W::Value) -> &V + Copy, + WM: FnOnce(&mut W::Value) -> &mut V + Copy, +{ + #[inline] + pub fn new(origin_state: W, map_reader: RM, map_writer: WM) -> Self { + Self { + origin_writer: origin_state, + map_reader, + map_writer, + } + } +} + +impl &V + Copy> MapReadRef { + #[inline] + pub fn new(origin_ref: O, map_fn: F) -> Self { MapReadRef { origin_ref, map_fn } } +} + +impl StateReader for MapReader +where + R: StateReader, + M: FnOnce(&R::Value) -> &V + Copy, +{ + type Value = V; + type OriginReader = R; + type Reader = MapReader; + type Ref<'a> = MapReadRef, M> where Self:'a; + + #[inline] + fn read(&'_ self) -> Self::Ref<'_> { + MapReadRef { + origin_ref: self.origin_reader.read(), + map_fn: self.map_fn, + } + } + + #[inline] + fn clone_reader(&self) -> Self::Reader { + let origin_state = self.origin_reader.clone_reader(); + MapReader { + origin_reader: origin_state, + map_fn: self.map_fn, + } + } + + #[inline] + fn origin_reader(&self) -> &Self::OriginReader { &self.origin_reader } + #[inline] + fn modifies(&self) -> BoxOp<'static, ModifyScope, Infallible> { self.origin_reader.modifies() } + #[inline] + fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { + self.origin_reader.raw_modifies() + } +} + +impl StateReader for MapWriter +where + W: StateWriter, + RM: FnOnce(&W::Value) -> &V + Copy, + WM: FnOnce(&mut W::Value) -> &mut V + Copy, +{ + type Value = V; + type OriginReader = W; + type Reader = MapReader; + type Ref<'a> = MapReadRef, RM> where Self:'a; + + #[inline] + fn read(&'_ self) -> Self::Ref<'_> { + MapReadRef { + origin_ref: self.origin_writer.read(), + map_fn: self.map_reader, + } + } + + #[inline] + fn clone_reader(&self) -> Self::Reader { + MapReader { + origin_reader: self.origin_writer.clone_reader(), + map_fn: self.map_reader, + } + } + + #[inline] + fn origin_reader(&self) -> &Self::OriginReader { &self.origin_writer } + + #[inline] + fn modifies(&self) -> BoxOp<'static, ModifyScope, Infallible> { self.origin_writer.modifies() } + + #[inline] + fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { + self.origin_writer.raw_modifies() + } +} + +impl StateWriter for MapWriter +where + W: StateWriter, + RM: FnOnce(&W::Value) -> &V + Copy, + WM: FnOnce(&mut W::Value) -> &mut V + Copy, +{ + type Writer = MapWriter; + type OriginWriter = W; + type RefWrite<'a> = MapWriteRef, RM, WM> where Self:'a; + + fn write(&'_ self) -> Self::RefWrite<'_> { + MapWriteRef { + origin_ref: self.origin_writer.write(), + map_read: self.map_reader, + map_write: self.map_writer, + } + } + + #[inline] + fn silent(&'_ self) -> Self::RefWrite<'_> { + MapWriteRef { + origin_ref: self.origin_writer.silent(), + map_read: self.map_reader, + map_write: self.map_writer, + } + } + + #[inline] + fn shallow(&'_ self) -> Self::RefWrite<'_> { + MapWriteRef { + origin_ref: self.origin_writer.shallow(), + map_read: self.map_reader, + map_write: self.map_writer, + } + } + + #[inline] + fn clone_writer(&self) -> Self::Writer { + MapWriter { + origin_writer: self.origin_writer.clone_writer(), + map_reader: self.map_reader, + map_writer: self.map_writer, + } + } + + #[inline] + fn origin_writer(&self) -> &Self::OriginWriter { &self.origin_writer } +} + +impl std::ops::Deref for MapReadRef +where + O: Deref, + F: FnOnce(&O::Target) -> &V + Copy, +{ + type Target = V; + #[inline] + fn deref(&self) -> &Self::Target { (self.map_fn)(self.origin_ref.deref()) } +} + +impl std::ops::Deref for MapWriteRef +where + O: DerefMut, + R: FnOnce(&O::Target) -> &V + Copy, + W: FnOnce(&mut O::Target) -> &mut V + Copy, +{ + type Target = V; + #[inline] + fn deref(&self) -> &Self::Target { (self.map_read)(self.origin_ref.deref()) } +} + +impl std::ops::DerefMut for MapWriteRef +where + O: DerefMut, + R: FnOnce(&O::Target) -> &V + Copy, + W: FnOnce(&mut O::Target) -> &mut V + Copy, +{ + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { (self.map_write)(self.origin_ref.deref_mut()) } +} + +impl RefWrite for MapWriteRef +where + O: RefWrite, + R: FnOnce(&O::Target) -> &V + Copy, + W: FnOnce(&mut O::Target) -> &mut V + Copy, +{ + #[inline] + fn forget_modifies(&mut self) -> bool { self.origin_ref.forget_modifies() } +} diff --git a/core/src/state/readonly.rs b/core/src/state/readonly.rs deleted file mode 100644 index 218f334bb..000000000 --- a/core/src/state/readonly.rs +++ /dev/null @@ -1,103 +0,0 @@ -use super::{ModifyScope, Stateful, StatefulRef}; -use rxrust::{observable, ops::box_it::BoxOp, prelude::BoxIt, subject::Subject}; -use std::{convert::Infallible, rc::Rc}; - -pub enum Readonly { - Stateful(Stateful), - Stateless(Rc), -} - -pub enum ReadRef<'a, W> { - Stateful(StatefulRef<'a, W>), - Stateless(&'a Rc), -} - -impl Readonly { - /// Readonly never modify the inner data, use a `()` to mock the api. - #[inline] - pub fn modify_guard(&self) {} - - #[inline] - pub fn state_ref(&self) -> ReadRef { - match self { - Readonly::Stateful(s) => ReadRef::Stateful(s.state_ref()), - Readonly::Stateless(w) => ReadRef::Stateless(w), - } - } - - #[inline] - pub fn silent_ref(&self) -> ReadRef { - // For a read only reference, `state_ref` and `silent_ref` no difference. - self.state_ref() - } - - pub fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { - match self { - Readonly::Stateful(s) => s.raw_modifies(), - Readonly::Stateless(_) => Subject::default(), - } - } - - /// 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> { - match self { - Readonly::Stateful(s) => s.modifies(), - Readonly::Stateless(_) => observable::create(|_| {}).box_it(), - } - } - - /// 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) -> Readonly { self.clone() } -} - -impl<'a, W> ReadRef<'a, W> { - #[inline] - pub fn silent(&self) -> &W { self } - - #[inline] - pub fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { - match self { - ReadRef::Stateful(s) => s.raw_modifies(), - ReadRef::Stateless(_) => Subject::default(), - } - } - - #[inline] - pub fn modifies(&self) -> BoxOp<'static, (), Infallible> { - match self { - ReadRef::Stateful(s) => s.modifies(), - ReadRef::Stateless(_) => observable::create(|_| {}).box_it(), - } - } - - pub fn clone_stateful(&self) -> Readonly { - match self { - ReadRef::Stateful(s) => Readonly::Stateful(s.clone_stateful()), - ReadRef::Stateless(r) => Readonly::Stateless((*r).clone()), - } - } -} - -impl Clone for Readonly { - fn clone(&self) -> Self { - match self { - Self::Stateful(arg0) => Self::Stateful(arg0.clone()), - Self::Stateless(arg0) => Self::Stateless(arg0.clone()), - } - } -} - -impl<'a, W> std::ops::Deref for ReadRef<'a, W> { - type Target = W; - - fn deref(&self) -> &Self::Target { - match self { - ReadRef::Stateful(s) => s.deref(), - ReadRef::Stateless(r) => r, - } - } -} diff --git a/core/src/state/splitted_state.rs b/core/src/state/splitted_state.rs new file mode 100644 index 000000000..98205a466 --- /dev/null +++ b/core/src/state/splitted_state.rs @@ -0,0 +1,205 @@ +use crate::context::AppCtx; + +use super::{MapReadRef, MapReader, ModifyScope, Notifier, RefWrite, StateReader, StateWriter}; +use ribir_algo::Sc; +use rxrust::{ + ops::box_it::BoxOp, + prelude::{ObservableItem, Observer}, + subject::Subject, + subscription::Subscription, +}; +use std::{any::Any, cell::Cell, rc::Rc}; + +/// A writer splitted writer from another writer, and has its own notifier. +pub struct SplittedWriter +where + O: StateWriter, + R: FnOnce(&O::Value) -> &V + Copy, + W: FnOnce(&mut O::Value) -> &mut V + Copy, +{ + origin_writer: O, + reader: R, + writer: W, + notifier: Notifier, + batched_modify: Sc>, + connect_guard: Rc>, +} + +impl StateReader for SplittedWriter +where + O: StateWriter, + R: FnOnce(&O::Value) -> &V + Copy, + W: FnOnce(&mut O::Value) -> &mut V + Copy, +{ + type Value = V; + type OriginReader = O; + type Reader = MapReader; + + type Ref<'a> = MapReadRef, R> where Self: 'a; + + #[inline] + fn read(&'_ self) -> Self::Ref<'_> { MapReadRef::new(self.origin_writer.read(), self.reader) } + + #[inline] + fn clone_reader(&self) -> Self::Reader { + MapReader::new(self.origin_writer.clone_reader(), self.reader) + } + + #[inline] + fn origin_reader(&self) -> &Self::OriginReader { &self.origin_writer } + + #[inline] + fn modifies(&self) -> BoxOp<'static, ModifyScope, std::convert::Infallible> { + self.notifier.modifies() + } + + #[inline] + fn raw_modifies(&self) -> Subject<'static, ModifyScope, std::convert::Infallible> { + self.notifier.raw_modifies() + } +} + +impl StateWriter for SplittedWriter +where + O: StateWriter, + R: FnOnce(&O::Value) -> &V + Copy, + W: FnOnce(&mut O::Value) -> &mut V + Copy, +{ + type Writer = SplittedWriter; + type OriginWriter = O; + type RefWrite<'a> = SplittedWriteRef<'a, V, O::RefWrite<'a>, R, W> where Self: 'a; + + #[inline] + fn write(&'_ self) -> Self::RefWrite<'_> { self.write_ref(ModifyScope::BOTH) } + + fn silent(&'_ self) -> Self::RefWrite<'_> { self.write_ref(ModifyScope::DATA) } + + fn shallow(&'_ self) -> Self::RefWrite<'_> { self.write_ref(ModifyScope::FRAMEWORK) } + + #[inline] + fn clone_writer(&self) -> Self::Writer { + SplittedWriter { + origin_writer: self.origin_writer.clone_writer(), + reader: self.reader, + writer: self.writer, + notifier: self.notifier.clone(), + batched_modify: self.batched_modify.clone(), + connect_guard: self.connect_guard.clone(), + } + } + + #[inline] + fn origin_writer(&self) -> &Self::OriginWriter { &self.origin_writer } +} + +pub struct SplittedWriteRef<'a, V, O, R, W> +where + O: RefWrite, + R: FnOnce(&O::Target) -> &V + Copy, + W: FnOnce(&mut O::Target) -> &mut V + Copy, +{ + origin_ref: O, + modify_scope: ModifyScope, + batched_modify: &'a Sc>, + notifier: &'a Notifier, + reader_fn: R, + writer_fn: W, +} + +impl<'a, V, O, R, W> std::ops::Deref for SplittedWriteRef<'a, V, O, R, W> +where + O: RefWrite, + R: FnOnce(&O::Target) -> &V + Copy, + W: FnOnce(&mut O::Target) -> &mut V + Copy, +{ + type Target = V; + + #[inline] + fn deref(&self) -> &Self::Target { (self.reader_fn)(&*self.origin_ref) } +} + +impl<'a, V, O, R, W> std::ops::DerefMut for SplittedWriteRef<'a, V, O, R, W> +where + O: RefWrite, + R: FnOnce(&O::Target) -> &V + Copy, + W: FnOnce(&mut O::Target) -> &mut V + Copy, +{ + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { (self.writer_fn)(&mut *self.origin_ref) } +} + +impl<'a, V, O, R, W> RefWrite for SplittedWriteRef<'a, V, O, R, W> +where + O: RefWrite, + R: FnOnce(&O::Target) -> &V + Copy, + W: FnOnce(&mut O::Target) -> &mut V + Copy, +{ + #[inline] + fn forget_modifies(&mut self) -> bool { self.origin_ref.forget_modifies() } +} + +impl SplittedWriter +where + O: StateWriter, + R: FnOnce(&O::Value) -> &V + Copy, + W: FnOnce(&mut O::Value) -> &mut V + Copy, +{ + pub(super) fn new(origin_writer: O, reader: R, writer: W) -> Self { + let notifier = Notifier::default(); + let c_modifier = notifier.clone(); + + let h = origin_writer + .raw_modifies() + .subscribe(move |v| c_modifier.raw_modifies().next(v)) + .unsubscribe_when_dropped(); + + Self { + origin_writer, + reader, + writer, + notifier, + batched_modify: <_>::default(), + connect_guard: Rc::new(Box::new(h)), + } + } + + fn write_ref(&'_ self, scope: ModifyScope) -> SplittedWriteRef<'_, V, O::RefWrite<'_>, R, W> { + SplittedWriteRef { + origin_ref: self.origin_writer.write(), + modify_scope: scope, + batched_modify: &self.batched_modify, + notifier: &self.notifier, + reader_fn: self.reader, + writer_fn: self.writer, + } + } +} + +impl<'a, V, O, R, W> Drop for SplittedWriteRef<'a, V, O, R, W> +where + O: RefWrite, + R: FnOnce(&O::Target) -> &V + Copy, + W: FnOnce(&mut O::Target) -> &mut V + Copy, +{ + fn drop(&mut self) { + if !self.origin_ref.forget_modifies() { + return; + } + + let scope = self.modify_scope; + let batched_modify = self.batched_modify.get(); + if batched_modify.is_empty() && !scope.is_empty() { + self.batched_modify.set(scope); + + let mut subject = self.notifier.raw_modifies(); + let batched_modify = self.batched_modify.clone(); + AppCtx::spawn_local(async move { + let scope = batched_modify.replace(ModifyScope::empty()); + subject.next(scope); + }) + .unwrap(); + } else { + self.batched_modify.set(batched_modify | scope); + } + } +} diff --git a/core/src/state/stateful.rs b/core/src/state/stateful.rs index 7f13523a0..115500e04 100644 --- a/core/src/state/stateful.rs +++ b/core/src/state/stateful.rs @@ -1,56 +1,42 @@ -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 ribir_algo::Sc; use rxrust::{ops::box_it::BoxOp, prelude::*}; use std::{ - cell::{Cell, RefCell, UnsafeCell}, - collections::LinkedList, + cell::{Cell, Ref, RefCell, RefMut}, convert::Infallible, ops::{Deref, DerefMut}, - rc::Rc, }; /// Stateful object use to watch the modifies of the inner data. pub struct Stateful { - inner: Rc>, - pub(crate) modify_notifier: StateChangeNotifier, + pub(crate) inner: Sc>, + pub(crate) notifier: Notifier, } -/// notify downstream when widget state changed, the value mean if the change it -/// as silent or not. -#[derive(Default, Clone)] -// todo: remove rc -pub(crate) struct StateChangeNotifier(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 Reader(Stateful); + +pub struct Writer(Stateful); + +pub struct ReadRef<'a, W>(Ref<'a, W>); + +pub struct WriteRef<'a, W> { + modified: bool, modify_scope: ModifyScope, - value: ModifyGuard<'a, W>, + value: RefMut<'a, W>, + batched_modify: &'a Sc>, + notifier: Option<&'a Notifier>, } +/// The notifier is a `RxRust` stream that emit notification when the state +/// changed. +#[derive(Default, Clone)] +pub struct Notifier(Subject<'static, ModifyScope, Infallible>); + 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,320 +44,362 @@ bitflags! { } } -mod guards { - use super::*; - pub struct ModifyGuard<'a, W>(&'a Stateful); +impl StateReader for Stateful { + type Value = W; + type OriginReader = Self; + type Reader = Reader; + type Ref<'a> = ReadRef<'a, W> where Self: 'a,; - 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) - } + #[inline] + fn read(&self) -> ReadRef<'_, W> { self.inner.read() } + + #[inline] + fn clone_reader(&self) -> Self::Reader { Reader::from_stateful(self) } + + #[inline] + fn origin_reader(&self) -> &Self::OriginReader { self } + + #[inline] + fn modifies(&self) -> BoxOp<'static, ModifyScope, Infallible> { self.notifier.modifies() } - pub(crate) fn inner_ref(&self) -> &'a Stateful { self.0 } + #[inline] + fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { + self.notifier.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); - } - } - } +impl StateWriter for Stateful { + type Writer = Writer; + type OriginWriter = Self; + type RefWrite<'a> = WriteRef<'a, W> where Self: 'a; + + fn write(&'_ self) -> Self::RefWrite<'_> { self.write_ref(ModifyScope::BOTH) } + + fn silent(&'_ self) -> Self::RefWrite<'_> { self.write_ref(ModifyScope::DATA) } + + fn shallow(&'_ self) -> Self::RefWrite<'_> { self.write_ref(ModifyScope::FRAMEWORK) } + + fn clone_writer(&self) -> Self::Writer { Writer::from_stateful(self) } + + fn origin_writer(&self) -> &Self::OriginWriter { self } +} + +impl StateReader for Reader { + type Value = W; + type OriginReader = Self; + type Reader = Self; + type Ref<'a> = ReadRef<'a, W> where W:'a; + + #[inline] + fn read(&'_ self) -> Self::Ref<'_> { self.0.read() } + + #[inline] + fn clone_reader(&self) -> Self { self.0.clone_reader() } + + #[inline] + fn origin_reader(&self) -> &Self::OriginReader { self } + + #[inline] + fn modifies(&self) -> BoxOp<'static, ModifyScope, Infallible> { self.0.modifies() } + + #[inline] + fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { self.0.raw_modifies() } +} + +impl StateReader for Writer { + type Value = W; + type OriginReader = Self; + type Reader = Reader; + type Ref<'a> = ReadRef<'a, W> where W:'a; + + #[inline] + fn read(&'_ self) -> Self::Ref<'_> { self.0.read() } + + #[inline] + fn clone_reader(&self) -> Self::Reader { self.0.clone_reader() } + + #[inline] + fn origin_reader(&self) -> &Self::OriginReader { self } + + #[inline] + fn modifies(&self) -> BoxOp<'static, ModifyScope, Infallible> { self.0.modifies() } + + #[inline] + fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { self.0.raw_modifies() } +} + +impl StateWriter for Writer { + type Writer = Self; + type OriginWriter = Self; + type RefWrite<'a> = WriteRef<'a, W> where W:'a; + + #[inline] + fn write(&'_ self) -> Self::RefWrite<'_> { self.0.write() } + + #[inline] + fn silent(&'_ self) -> Self::RefWrite<'_> { self.0.silent() } + + #[inline] + fn shallow(&'_ self) -> Self::RefWrite<'_> { self.0.shallow() } + + #[inline] + fn clone_writer(&self) -> Self { self.0.clone_writer() } + + #[inline] + fn origin_writer(&self) -> &Self::OriginWriter { self } +} + +impl Drop for Reader { + fn drop(&mut self) { + // The `Stateful` is a writer but used as a reader in `Reader` that not + // increment the writer count. So we increment the writer count before drop the + // `Stateful` to keep its count correct. + self.0.inc_writer(); } +} - impl<'a, W> std::ops::Deref for ModifyGuard<'a, W> { - type Target = Stateful; +impl Drop for Stateful { + fn drop(&mut self) { + self.dec_writer(); + // can cancel the notifier, because no one will modify the data. + if self.writer_count() == 0 { + let notifier = self.notifier.clone(); + // we use an async task to unsubscribe to wait the batched modifies to be + // notified. + AppCtx::spawn_local(async move { + notifier.0.unsubscribe(); + }) + .unwrap(); + } - #[inline] - fn deref(&self) -> &Self::Target { self.0 } + // Declare object may add task to disconnect to upstream, trigger that task if + // this is the last reference. We not hold that task in `Stateful` to avoid + // cycle reference. + if self.inner.ref_count() == 1 { + AppCtx::trigger_task(self.heap_ptr() as *const ()); + } } } -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>, - #[cfg(debug_assertions)] - 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>>, +impl Reader { + fn from_stateful(stateful: &Stateful) -> Self { + Reader(Stateful { + inner: stateful.inner.clone(), + notifier: stateful.notifier.clone(), + }) + } } -impl Clone for Stateful { +impl Writer { #[inline] - fn clone(&self) -> Self { - Self { - inner: self.inner.clone(), - modify_notifier: self.modify_notifier.clone(), - } + pub fn into_inner(self) -> Stateful { self.0 } + + fn from_stateful(stateful: &Stateful) -> Self { + stateful.inc_writer(); + Writer(Stateful { + inner: stateful.inner.clone(), + notifier: stateful.notifier.clone(), + }) } } +pub(crate) struct StateData { + data: RefCell, + /// The batched modifies of the `State` which will be notified. + batch_modified: Sc>, + /// The counter of the writer may be modified the data. + writer_count: Cell, +} + +#[repr(transparent)] +pub(crate) struct RenderFul(pub(crate) Stateful); + +impl_proxy_query!(paths [0], RenderFul, , where R: Render + 'static); +impl_proxy_render!(proxy 0.read(), RenderFul, , where R: Render + 'static); +impl_proxy_query!(paths[0.read()], RenderFul>); +impl_proxy_render!(proxy 0.read(), RenderFul>); + impl Stateful { pub fn new(data: W) -> Self { Stateful { - inner: Rc::new(InnerStateful { - data: UnsafeCell::new(data), - borrow_flag: Cell::new(0), - modify_scope: Cell::new(None), - #[cfg(debug_assertions)] - borrowed_at: Cell::new(None), - guard_cnt: Cell::new(0), - slot_link: <_>::default(), - }), - modify_notifier: <_>::default(), + inner: Sc::new(StateData::new(data)), + notifier: <_>::default(), } } - /// Return a guard that batch the modify event. + /// 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 modify_guard(&self) -> ModifyGuard { ModifyGuard::new(self) } + pub fn clone_stateful(&self) -> Stateful { self.clone_writer().0 } - /// Return a reference of `Stateful`, modify across this reference will notify - /// data and framework. + /// just for compatibility with `Stateful` in old syntax. #[inline] - pub fn state_ref(&self) -> StatefulRef { StatefulRef::new(self, ModifyScope::BOTH) } + pub fn state_ref(&self) -> WriteRef { self.write() } - /// 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. - /// - /// 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. + /// Run the `task` when the inner state data will drop. #[inline] - pub(crate) fn shallow_ref(&self) -> StatefulRef { - StatefulRef::new(self, ModifyScope::FRAMEWORK) - } - - pub fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { - self.modify_notifier.raw_modifies() + pub fn on_state_drop(&self, task: impl FnOnce() + 'static) { + AppCtx::add_trigger_task(self.heap_ptr() as *const _, Box::new(task)) } - /// 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. + // unsubscribe the `subscription` when the inner state data will drop. #[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 unsubscribe_on_drop(&self, subscription: impl Subscription + 'static) { + self.on_state_drop(move || subscription.unsubscribe()) } - /// Clone the stateful widget of which the reference point to. Require mutable - /// reference because we try to early release inner borrow when clone occur. + /// return the heap pointer of the data. #[inline] - pub fn clone_stateful(&self) -> Stateful { self.clone() } + fn heap_ptr(&self) -> *const W { self.inner.data.as_ptr() } - 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 from_state_data(data: StateData) -> Self { + Self { + inner: Sc::new(data), + notifier: <_>::default(), + } } 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!()); + if Sc::ref_count(&self.inner) == 1 { + let inner = self.inner.clone(); + drop(self); + // SAFETY: `Rc::strong_count(&self.inner) == 1` guarantees unique access. + let inner = unsafe { Sc::try_unwrap(inner).unwrap_unchecked() }; Ok(inner.data.into_inner()) } else { 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)); - } - }; + fn write_ref(&self, scope: ModifyScope) -> WriteRef<'_, W> { + let value = self.inner.data.borrow_mut(); + let batched_modify = &self.inner.batch_modified; + let modifier = &self.notifier; + WriteRef::new(value, scope, batched_modify, Some(modifier)) + } + + fn writer_count(&self) -> usize { self.inner.writer_count.get() } + fn inc_writer(&self) { self.inner.writer_count.set(self.writer_count() + 1); } + fn dec_writer(&self) { self.inner.writer_count.set(self.writer_count() - 1); } } -macro_rules! already_borrow_panic { - ($this: ident) => { - #[cfg(debug_assertions)] - { - let location = $this.value.inner.borrowed_at.get().unwrap(); - panic!("already borrowed at {}", location); +impl StateData { + /// Assert the state data is not used by any reader and writer. + #[inline] + #[track_caller] + pub(crate) fn assert_is_not_used(&self) { self.data.borrow_mut(); } + + #[inline] + pub(crate) fn new(data: W) -> Self { + Self { + // the `StateData` in `Stateful` or `State` is a writer + writer_count: Cell::new(1), + data: RefCell::new(data), + batch_modified: <_>::default(), } - #[cfg(not(debug_assertions))] - panic!("already borrowed"); - }; + } + + pub(crate) fn into_inner(self) -> W { self.data.into_inner() } + + pub(crate) fn read(&self) -> ReadRef { ReadRef(self.data.borrow()) } } -impl<'a, W> Deref for StatefulRef<'a, W> { +impl<'a, W> Deref for ReadRef<'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); - - match b.get() { - 1 => { - debug_borrow_location!(self); - } - flag if !is_reading(flag) => { - already_borrow_panic!(self); - } - _ => {} - } - if !is_reading(b.get()) {} - } + fn deref(&self) -> &Self::Target { &self.0 } +} - // SAFETY: `BorrowFlag` guarantees unique access. - let ptr = self.value.inner.data.get(); - unsafe { &*ptr } - } +impl<'a, W> Deref for WriteRef<'a, W> { + type Target = W; + #[track_caller] + fn deref(&self) -> &Self::Target { &self.value } } -impl<'a, W> DerefMut for StatefulRef<'a, W> { +impl<'a, W> DerefMut for WriteRef<'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); - } - - // SAFETY: `BorrowFlag` guarantees unique access. - let ptr = self.value.inner.data.get(); - unsafe { &mut *ptr } + self.modified = true; + &mut self.value } } -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); - } - - #[inline] - pub fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { - self.value.raw_modifies() - } - +impl<'a, W> RefWrite for WriteRef<'a, W> { #[inline] - pub fn modifies(&self) -> BoxOp<'static, (), Infallible> { self.value.modifies() } + fn forget_modifies(&mut self) -> bool { std::mem::replace(&mut self.modified, false) } +} - fn new(value: &'a Stateful, modify_scope: ModifyScope) -> Self { +impl<'a, W> WriteRef<'a, W> { + pub(crate) fn new( + value: RefMut<'a, W>, + scope: ModifyScope, + batch_scope: &'a Sc>, + modifier: Option<&'a Notifier>, + ) -> Self { Self { - mut_accessed_flag: Cell::new(None), - modify_scope, - value: ModifyGuard::new(value), + modified: false, + modify_scope: scope, + value, + batched_modify: batch_scope, + notifier: modifier, } } - - #[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)); - } - 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. - - #[inline] - pub fn clone_stateful(&self) -> Stateful { self.value.clone() } } impl SingleChild for Stateful {} impl MultiChild for Stateful {} impl_proxy_query!( - paths [modify_notifier, state_ref()], + paths [notifier, read()], Stateful, , where R: Query + 'static ); -impl_query_self_only!(StateChangeNotifier); +impl_query_self_only!(Notifier); +impl_proxy_query!(paths [0], Reader, , where T: Query + 'static); +impl_proxy_query!(paths [0], Writer, , where T: Query + 'static); + +impl<'a, W> Drop for WriteRef<'a, W> { + fn drop(&mut self) { + if !self.modified { + return; + } -impl<'a, W> Drop for StatefulRef<'a, W> { - fn drop(&mut self) { self.release_borrow(); } + let scope = self.modify_scope; + let batch_scope = self.batched_modify.get(); + if batch_scope.is_empty() && !scope.is_empty() { + self.batched_modify.set(scope); + if let Some(m) = self.notifier.as_mut() { + let mut subject = m.raw_modifies(); + let share_scope = self.batched_modify.clone(); + AppCtx::spawn_local(async move { + let scope = share_scope.replace(ModifyScope::empty()); + subject.next(scope); + }) + .unwrap(); + } + } else { + self.batched_modify.set(batch_scope | scope); + } + } } -impl StateChangeNotifier { - pub(crate) fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { - self.0.borrow().clone() +impl Notifier { + pub fn modifies(&self) -> BoxOp<'static, ModifyScope, Infallible> { + self + .raw_modifies() + .filter(|s| s.contains(ModifyScope::DATA)) + .box_it() } - pub(crate) fn reset(&self) { *self.0.borrow_mut() = <_>::default(); } + pub(crate) fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { self.0.clone() } + + pub(crate) fn reset(&self) { *self.0.clone().complete() } +} + +impl std::fmt::Debug for Stateful { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("Stateful").field(&*self.read()).finish() + } } #[cfg(test)] @@ -379,11 +407,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::reset_test_env!(); // Simulate `MockBox` widget need modify its size in event callback. Can use the // `cell_ref` in closure. @@ -396,7 +424,7 @@ mod tests { #[test] fn state_notify_and_relayout() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + crate::reset_test_env!(); use std::{cell::RefCell, rc::Rc}; let notified_count = Rc::new(RefCell::new(0)); @@ -409,12 +437,12 @@ mod tests { let changed_size = Rc::new(RefCell::new(Size::zero())); let c_changed_size = changed_size.clone(); - let c_box = sized_box.clone(); + let c_box = sized_box.clone_writer(); sized_box.modifies().subscribe(move |_| { - *c_changed_size.borrow_mut() = c_box.state_ref().size; + *c_changed_size.borrow_mut() = c_box.write().size; }); - let state = sized_box.clone(); + let state = sized_box.clone_writer(); let mut wnd = TestWindow::new(sized_box); wnd.draw_frame(); assert_eq!(*notified_count.borrow(), 0); @@ -422,8 +450,10 @@ mod tests { assert_eq!(&*changed_size.borrow(), &Size::new(0., 0.)); { - state.state_ref().size = Size::new(1., 1.); + state.write().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 +462,7 @@ mod tests { #[test] fn fix_pin_widget_node() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + crate::reset_test_env!(); let mut wnd = TestWindow::new(widget! { MockBox { size: Size::new(100., 100.) } }); wnd.draw_frame(); @@ -442,9 +472,9 @@ mod tests { #[test] fn change_notify() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + crate::reset_test_env!(); - let notified = Rc::new(RefCell::new(vec![])); + let notified = Sc::new(RefCell::new(vec![])); let c_notified = notified.clone(); let w = Stateful::new(MockBox { size: Size::zero() }); w.raw_modifies() @@ -453,24 +483,37 @@ 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; + let _ = &mut w.silent().size; } + + Timer::wake_timeout_futures(); + AppCtx::run_until_stalled(); assert_eq!( notified.borrow().deref(), &[ModifyScope::BOTH, ModifyScope::DATA] ); { - let mut state_ref = w.state_ref(); - let mut silent_ref = w.silent_ref(); - let _ = &mut state_ref; - let _ = &mut state_ref; - let _ = &mut silent_ref; - let _ = &mut silent_ref; + let _ = &mut w.write(); } + { + let _ = &mut w.write(); + } + { + let _ = &mut w.silent(); + } + { + let _ = &mut w.silent(); + } + + 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..16d874592 100644 --- a/core/src/test_helper.rs +++ b/core/src/test_helper.rs @@ -15,8 +15,18 @@ pub struct Frame { pub viewport: Rect, pub surface: Color, } + +#[derive(Clone)] pub struct TestWindow(pub Rc); +#[macro_export] +macro_rules! reset_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) } @@ -27,10 +37,8 @@ impl TestWindow { fn new_wnd(root: impl Into, size: Option) -> Self { let _ = NEW_TIMER_FN.set(Timer::new_timer_future); - let wnd = Window::new(root.into(), Box::new(TestShellWindow::new(size))); - let id = wnd.id(); - AppCtx::windows().borrow_mut().insert(wnd.id(), wnd.clone()); - AppCtx::get_window(id).unwrap().emit_events(); + let wnd = AppCtx::new_window(Box::new(TestShellWindow::new(size)), root.into()); + wnd.run_frame_tasks(); Self(wnd) } @@ -68,6 +76,8 @@ 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(); + self.run_frame_tasks(); self.0.draw_frame(); } diff --git a/core/src/ticker.rs b/core/src/ticker.rs index b4aad0b58..114bdaef8 100644 --- a/core/src/ticker.rs +++ b/core/src/ticker.rs @@ -12,16 +12,24 @@ pub struct FrameTicker { #[derive(Clone)] pub enum FrameMsg { - /// This msg emit when all event has processed and framework ready to do - /// layout & paint. + /// This msg emit when all event has processed and framework begin to do + /// layout & paint of the frame. + /// + /// only the first frame of continuous frames that do not need to be drawn + /// will be sent this message. NewFrame(Instant), /// This Msg emit when performed layout finished, and widget tree ready to - /// draw. Notice, this message may emit more than once, if someone listen - /// this message and do some stuff to lead to some widget need relayout, be - /// careful to modify widget in the listener of this message. + /// draw. + /// # Notice + /// - this message may emit more than once, if someone listen this message and + /// do some stuff to lead to some widget need relayout, be careful to modify + /// widget in the listener of this message. LayoutReady(Instant), /// This msg emit after render data has submitted that mean all stuffs of /// current frame need to processed by framework done. + /// + /// only the first frame of continuous frames that do not need to be drawn + /// will be sent this message. Finish(Instant), } diff --git a/core/src/widget.rs b/core/src/widget.rs index 23d2521a7..3cb1b6fa1 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -298,32 +298,17 @@ macro_rules! impl_proxy_render { impl WidgetBuilder for C { #[inline] - fn build(self, ctx: &BuildCtx) -> WidgetId { State::Stateless(self).build(ctx) } + fn build(self, ctx: &BuildCtx) -> WidgetId { State::value(self).build(ctx) } } impl WidgetBuilder for Stateful { #[inline] - fn build(self, ctx: &BuildCtx) -> WidgetId { State::Stateful(self).build(ctx) } + 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.rs b/core/src/widget_children.rs index b2dbfd092..b75b84482 100644 --- a/core/src/widget_children.rs +++ b/core/src/widget_children.rs @@ -29,7 +29,7 @@ //! clone, this seems too strict and if `A` is not support clone, the compile //! error is too complex to diagnostic. -use crate::prelude::*; +use crate::{prelude::*, widget::WidgetBuilder}; mod compose_child_impl; mod multi_child_impl; mod single_child_impl; @@ -43,15 +43,21 @@ pub trait SingleChild {} /// A boxed render widget that support accept one child. pub trait BoxedSingleParent { - fn into_parent(self: Box, ctx: &mut BuildCtx) -> WidgetId; + fn boxed_append_child(self: Box, child: WidgetId, ctx: &mut BuildCtx) -> WidgetId; + fn boxed_build(self: Box, ctx: &mut BuildCtx) -> WidgetId; } /// Trait to tell Ribir a object that has multi children. pub trait MultiChild {} /// A boxed render widget that support accept multi children. -pub trait BoxMultiParent { - fn into_parent(self: Box, ctx: &mut BuildCtx) -> WidgetId; +pub trait BoxedMultiParent { + fn boxed_append_children( + self: Box, + children: Vec, + ctx: &mut BuildCtx, + ) -> WidgetId; + fn boxed_build(self: Box, ctx: &mut BuildCtx) -> WidgetId; } /// Trait mark widget can have one child and also have compose logic for widget @@ -66,23 +72,81 @@ pub trait ComposeChild: Sized { pub type WidgetOf = SinglePair; impl SingleChild for Box {} -impl MultiChild for Box {} +impl MultiChild for Box {} -impl RenderParent for Box { - fn into_render_parent(self, ctx: &mut BuildCtx) -> WidgetId { self.into_parent(ctx) } +impl WidgetBuilder for Box { + #[inline] + fn build(self, ctx: &BuildCtx) -> WidgetId { self.boxed_build(ctx.force_as_mut()) } +} + +impl SingleParent for Box { + #[inline] + fn append_child(self, child: WidgetId, ctx: &mut BuildCtx) -> WidgetId { + self.boxed_append_child(child, ctx) + } } -impl RenderParent for Box { - fn into_render_parent(self, ctx: &mut BuildCtx) -> WidgetId { self.into_parent(ctx) } +impl WidgetBuilder for Box { + #[inline] + fn build(self, ctx: &BuildCtx) -> WidgetId { self.boxed_build(ctx.force_as_mut()) } } -pub(crate) trait RenderParent { - fn into_render_parent(self, ctx: &mut BuildCtx) -> WidgetId; +impl MultiParent for Box { + #[inline] + fn append_children(self, children: Vec, ctx: &mut BuildCtx) -> WidgetId { + self.boxed_append_children(children, ctx) + } +} + +pub(crate) trait SingleParent: WidgetBuilder { + fn append_child(self, child: WidgetId, ctx: &mut BuildCtx) -> WidgetId; +} + +pub(crate) trait MultiParent: WidgetBuilder { + fn append_children(self, children: Vec, ctx: &mut BuildCtx) -> WidgetId; +} + +impl> + SingleChild + WidgetBuilder> SingleParent for T { + fn append_child(self, child: WidgetId, ctx: &mut BuildCtx) -> WidgetId { + let p = self.build(ctx); + ctx.append_child(p, child); + p + } } -impl>> RenderParent for T { +impl> + MultiChild + WidgetBuilder> MultiParent for T { + #[inline] + fn append_children(self, children: Vec, ctx: &mut BuildCtx) -> WidgetId { + let p = self.build(ctx); + for c in children { + ctx.append_child(p, c); + } + p + } +} + +impl BoxedSingleParent for W { + #[inline] + fn boxed_append_child(self: Box, child: WidgetId, ctx: &mut BuildCtx) -> WidgetId { + (*self).append_child(child, ctx) + } + + #[inline] + fn boxed_build(self: Box, ctx: &mut BuildCtx) -> WidgetId { (*self).build(ctx) } +} + +impl BoxedMultiParent for W { + #[inline] + fn boxed_append_children( + self: Box, + children: Vec, + ctx: &mut BuildCtx, + ) -> WidgetId { + (*self).append_children(children, ctx) + } + #[inline] - fn into_render_parent(self, ctx: &mut BuildCtx) -> WidgetId { ctx.alloc_widget(self.into()) } + fn boxed_build(self: Box, ctx: &mut BuildCtx) -> WidgetId { (*self).build(ctx) } } #[cfg(test)] @@ -91,7 +155,6 @@ mod tests { use crate::test_helper::*; use crate::widget::WidgetBuilder; use ribir_dev_helper::*; - use std::{cell::RefCell, rc::Rc}; #[test] fn compose_template_child() { diff --git a/core/src/widget_children/child_convert.rs b/core/src/widget_children/child_convert.rs index d2101b1a4..76832678e 100644 --- a/core/src/widget_children/child_convert.rs +++ b/core/src/widget_children/child_convert.rs @@ -3,8 +3,8 @@ use super::{ TmlFlag, }; use crate::{ - dynamic_widget::{DynRender, DynWidget}, - state::{State, StateFrom, Stateful}, + builtin_widgets::FatObj, + state::{State, StateFrom}, widget::*, }; @@ -68,28 +68,38 @@ where impl FromAnother, [(M1, M2); 0]> for SinglePair where W: FromAnother, - C: ChildFrom, + C: FromAnother, { #[inline] fn from_another(value: SinglePair) -> Self { let SinglePair { widget, child } = value; SinglePair { widget: W::from_another(widget), - child: C::child_from(child), + child: C::from_another(child), } } } -impl FromAnother, [(M1, M2); 1]> for SinglePair +impl FromAnother, [M; 1]> for SinglePair where - W: ChildFrom, - C: FromAnother, + W: FromAnother, { #[inline] - fn from_another(value: SinglePair) -> Self { + fn from_another(value: SinglePair) -> Self { + let SinglePair { widget, child } = value; + SinglePair { widget: W::child_from(widget), child } + } +} + +impl FromAnother, [M; 2]> for SinglePair +where + C: FromAnother, +{ + #[inline] + fn from_another(value: SinglePair) -> Self { let SinglePair { widget, child } = value; SinglePair { - widget: W::child_from(widget), + widget, child: C::from_another(child), } } @@ -120,6 +130,24 @@ where } } +impl FromAnother, [M; 0]> for FatObj +where + T2: FromAnother, +{ + fn from_another(value: FatObj) -> Self { + let (host, builtin) = value.unzip(); + FatObj::new(T2::from_another(host), builtin) + } +} + +impl FromAnother for FatObj +where + T2: ChildFrom, +{ + #[inline] + fn from_another(value: T1) -> Self { FatObj::from_host(ChildFrom::child_from(value)) } +} + pub(crate) trait FillVec { fn fill_vec(self, vec: &mut Vec); } @@ -151,11 +179,3 @@ where vec.extend(self.into_inner().into_iter().map(ChildFrom::child_from)) } } - -impl FillVec for Stateful>> -where - D: IntoIterator + 'static, - Widget: From, -{ - fn fill_vec(self, vec: &mut Vec) { vec.push(DynRender::multi(self).into()) } -} diff --git a/core/src/widget_children/compose_child_impl.rs b/core/src/widget_children/compose_child_impl.rs index ae11b8b77..db660d400 100644 --- a/core/src/widget_children/compose_child_impl.rs +++ b/core/src/widget_children/compose_child_impl.rs @@ -1,6 +1,5 @@ use crate::{ context::BuildCtx, - dynamic_widget::DynWidget, prelude::ChildFrom, state::{State, Stateful}, widget::{Widget, WidgetBuilder, WidgetId}, @@ -52,7 +51,7 @@ where { type Target = as ComposeWithChild>::Target; fn with_child(self, child: C, ctx: &BuildCtx) -> Self::Target { - State::Stateless(self).with_child(child, ctx) + State::value(self).with_child(child, ctx) } } @@ -63,18 +62,7 @@ where { type Target = as ComposeWithChild>::Target; fn with_child(self, child: C, ctx: &BuildCtx) -> Self::Target { - State::Stateful(self).with_child(child, ctx) - } -} - -impl ComposeWithChild for Stateful> -where - T: ComposeChild + 'static, - State: ComposeWithChild, -{ - type Target = as ComposeWithChild>::Target; - fn with_child(self, child: C, ctx: &BuildCtx) -> Self::Target { - State::from(self).with_child(child, ctx) + State::stateful(self).with_child(child, ctx) } } @@ -287,7 +275,6 @@ where #[cfg(test)] mod tests { - use std::{cell::Cell, rc::Rc}; use super::*; use crate::{prelude::*, test_helper::MockBox}; @@ -390,9 +377,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_children/multi_child_impl.rs b/core/src/widget_children/multi_child_impl.rs index 76c2ce7f9..c69b66be1 100644 --- a/core/src/widget_children/multi_child_impl.rs +++ b/core/src/widget_children/multi_child_impl.rs @@ -8,8 +8,8 @@ pub trait MultiWithChild { fn with_child(self, child: C, ctx: &BuildCtx) -> Self::Target; } -pub struct MultiPair { - pub parent: WidgetId, +pub struct MultiPair

{ + pub parent: P, pub children: Vec, } @@ -57,16 +57,6 @@ where } } -impl FillVec for Stateful>> -where - D: IntoIterator + 'static, - Widget: From, -{ - fn fill_vec(self, vec: &mut Vec, ctx: &BuildCtx) { - vec.push(DynRender::multi(self).build(ctx)) - } -} - impl FillVec for Pipe> where D: IntoIterator + 'static, @@ -77,36 +67,21 @@ where } } -trait MultiParent { - fn into_multi_parent(self, ctx: &mut BuildCtx) -> WidgetId; -} - -impl MultiParent for R { - #[inline] - fn into_multi_parent(self, ctx: &mut BuildCtx) -> WidgetId { self.into_render_parent(ctx) } -} - -impl MultiParent for Pipe { - #[inline] - fn into_multi_parent(self, ctx: &mut BuildCtx) -> WidgetId { self.into_only_parent(ctx) } -} - -impl MultiWithChild for R +impl MultiWithChild for P where - R: MultiParent, + P: MultiChild, C: FillVec, { - type Target = MultiPair; + type Target = MultiPair

; fn with_child(self, child: C, ctx: &BuildCtx) -> Self::Target { - let parent = self.into_multi_parent(ctx.force_as_mut()); let mut children = vec![]; child.fill_vec(&mut children, ctx); - MultiPair { parent, children } + MultiPair { parent: self, children } } } -impl MultiWithChild for MultiPair +impl MultiWithChild for MultiPair

where C: FillVec, { @@ -118,13 +93,9 @@ where } } -impl WidgetBuilder for MultiPair { +impl WidgetBuilder for MultiPair

{ fn build(self, ctx: &BuildCtx) -> WidgetId { let MultiPair { parent, children } = self; - children - .into_iter() - .for_each(|child| ctx.force_as_mut().append_child(parent, child)); - - parent + parent.append_children(children, ctx.force_as_mut()) } } diff --git a/core/src/widget_children/single_child_impl.rs b/core/src/widget_children/single_child_impl.rs index 8d01c2f24..05d2d9306 100644 --- a/core/src/widget_children/single_child_impl.rs +++ b/core/src/widget_children/single_child_impl.rs @@ -10,8 +10,20 @@ pub trait SingleWithChild { /// A node of widget with not compose its child. pub struct SinglePair { - pub widget: W, - pub child: C, + pub(crate) widget: W, + pub(crate) child: C, +} + +impl SinglePair { + #[inline] + pub fn unzip(self) -> (W, C) { + let Self { widget, child } = self; + (widget, child) + } + #[inline] + pub fn child(self) -> C { self.child } + #[inline] + pub fn parent(self) -> W { self.widget } } impl SingleChild for Option {} @@ -35,79 +47,29 @@ impl SingleWithChild for SinglePair { } } -trait SingleParent { - fn into_single_parent(self, ctx: &mut BuildCtx) -> WidgetId; -} - -trait WidgetChild { - fn child_build(self, ctx: &BuildCtx) -> WidgetId; -} - -impl SingleParent for W { - #[inline] - fn into_single_parent(self, ctx: &mut BuildCtx) -> WidgetId { self.into_render_parent(ctx) } -} - -impl SingleParent for Pipe { - #[inline] - fn into_single_parent(self, ctx: &mut BuildCtx) -> WidgetId { self.into_only_parent(ctx) } -} - -impl SingleParent for Stateful> -where - D: Render + SingleChild + WidgetBuilder + 'static, -{ - #[inline] - fn into_single_parent(self, ctx: &mut BuildCtx) -> WidgetId { - Box::new(DynRender::single(self)).build(ctx) - } -} - -impl SingleParent for Stateful>> -where - D: Render + SingleChild + WidgetBuilder + 'static, -{ - #[inline] - fn into_single_parent(self, ctx: &mut BuildCtx) -> WidgetId { - Box::new(DynRender::option(self)).build(ctx) - } -} - -impl WidgetChild for Widget { - #[inline] - fn child_build(self, ctx: &BuildCtx) -> WidgetId { self.build(ctx) } -} - -impl WidgetChild for W { - #[inline] - fn child_build(self, ctx: &BuildCtx) -> WidgetId { self.build(ctx) } -} - impl WidgetBuilder for SinglePair where W: SingleParent, - C: WidgetChild, + C: Into, { fn build(self, ctx: &BuildCtx) -> WidgetId { let Self { widget, child } = self; - let p = widget.into_single_parent(ctx.force_as_mut()); - let child = child.child_build(ctx); - ctx.force_as_mut().append_child(p, child); - p + let child = child.into().build(ctx); + widget.append_child(child, ctx.force_as_mut()) } } impl WidgetBuilder for SinglePair, C> where W: SingleParent, - C: WidgetChild, + C: Into, { fn build(self, ctx: &BuildCtx) -> WidgetId { let Self { widget, child } = self; if let Some(widget) = widget { SinglePair { widget, child }.build(ctx) } else { - child.child_build(ctx) + child.into().build(ctx) } } } @@ -115,14 +77,14 @@ where impl WidgetBuilder for SinglePair> where W: SingleParent, - C: WidgetChild, + SinglePair: WidgetBuilder, { fn build(self, ctx: &BuildCtx) -> WidgetId { let Self { widget, child } = self; if let Some(child) = child { SinglePair { widget, child }.build(ctx) } else { - widget.into_single_parent(ctx.force_as_mut()) + widget.build(ctx) } } } diff --git a/core/src/widget_tree.rs b/core/src/widget_tree.rs index 4957b5e61..6590afee3 100644 --- a/core/src/widget_tree.rs +++ b/core/src/widget_tree.rs @@ -12,11 +12,12 @@ mod layout_info; use crate::prelude::*; pub use layout_info::*; +use self::widget_id::empty_node; + pub(crate) type DirtySet = Rc>>; -#[derive(Default)] pub(crate) struct WidgetTree { - pub(crate) root: Option, + pub(crate) root: WidgetId, wnd: Weak, pub(crate) arena: TreeArena, pub(crate) store: LayoutStore, @@ -24,21 +25,16 @@ pub(crate) struct WidgetTree { } impl WidgetTree { - pub(crate) fn new() -> WidgetTree { Self::default() } - - pub fn init(&mut self, widget: Widget, wnd: Weak) { - self.wnd = wnd; - let build_ctx = BuildCtx::new(None, self); - let root = widget.build(&build_ctx); - self.root = Some(root); - self.mark_dirty(root); - root.on_mounted_subtree(self); - } + pub fn init(&mut self, wnd: Weak) { self.wnd = wnd; } - pub(crate) fn root(&self) -> WidgetId { - self.root.expect("Try to access a not init `WidgetTree`") + pub fn set_root(&mut self, root: WidgetId) { + self.root = root; + self.mark_dirty(self.root); + self.root.on_mounted_subtree(self); } + pub(crate) fn root(&self) -> WidgetId { self.root } + /// Draw current tree by painter. pub(crate) fn draw(&self) { let wnd = self.window(); @@ -79,7 +75,7 @@ impl WidgetTree { self .wnd .upgrade() - .expect("The window of `FocusManager` has already dropped.") + .expect("Must initialize the widget tree before use it.") } #[allow(unused)] @@ -167,7 +163,7 @@ impl WidgetTree { .next_sibling(&self.arena) .or_else(|| root.prev_sibling(&self.arena)) .expect("Try to remove the root and there is no other widget can be the new root."); - self.root = Some(new_root); + self.root = new_root; } id.0.detach(&mut self.arena); @@ -187,6 +183,19 @@ impl WidgetTree { } } +impl Default for WidgetTree { + fn default() -> Self { + let mut arena = TreeArena::new(); + Self { + root: empty_node(&mut arena), + wnd: Weak::new(), + arena, + store: LayoutStore::default(), + dirty_set: Rc::new(RefCell::new(HashSet::default())), + } + } +} + #[cfg(test)] mod tests { extern crate test; @@ -262,9 +271,8 @@ mod tests { fn bench_recursive_inflate(width: usize, depth: usize, b: &mut Bencher) { let wnd = TestWindow::new(Void {}); b.iter(move || { - let mut tree = wnd.widget_tree.borrow_mut(); - tree.init(Recursive { width, depth }.into(), Rc::downgrade(&wnd.0)); - tree.layout(Size::new(512., 512.)); + wnd.set_content_widget(Recursive { width, depth }.into()); + wnd.layout(); }); } @@ -272,12 +280,11 @@ mod tests { let w = Stateful::new(Recursive { width, depth }); let trigger = w.clone(); let wnd = TestWindow::new(w); - let mut tree = wnd.widget_tree.borrow_mut(); b.iter(|| { { let _: &mut Recursive = &mut trigger.state_ref(); } - tree.layout(Size::new(512., 512.)); + wnd.layout(); }); } @@ -392,7 +399,7 @@ mod tests { b.iter(move || { let post = Embed { width: 5, depth: 1000 }; - WidgetTree::new().init(post.into(), Rc::downgrade(&wnd.0)); + wnd.set_content_widget(post.into()); }); } diff --git a/core/src/widget_tree/widget_id.rs b/core/src/widget_tree/widget_id.rs index 6b3216a8c..cd1b5a0a4 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::{ModifyScope, Notifier}, widget::{QueryOrder, Render}, window::DelayEvent, }; @@ -30,7 +30,7 @@ pub(crate) struct RenderNode { impl WidgetId { /// Returns a reference to the node data. - pub(crate) fn get(self, tree: &TreeArena) -> Option<&dyn Render> { + pub(crate) fn get<'a, 'b>(self, tree: &'a TreeArena) -> Option<&'a (dyn Render + 'b)> { self.get_node(tree).map(|n| n.data.as_ref()) } @@ -116,6 +116,10 @@ impl WidgetId { self.node_feature(tree, |node| node.previous_sibling()) } + pub(crate) fn ancestor_of(self, other: WidgetId, tree: &TreeArena) -> bool { + other.ancestors(tree).find(|p| self == *p).is_some() + } + pub(crate) fn ancestors(self, tree: &TreeArena) -> impl Iterator + '_ { self.0.ancestors(tree).map(WidgetId) } @@ -137,7 +141,7 @@ impl WidgetId { pub(crate) fn on_mounted(self, tree: &WidgetTree) { self.assert_get(&tree.arena).query_all_type( - |notifier: &StateChangeNotifier| { + |notifier: &Notifier| { let state_changed = tree.dirty_set.clone(); notifier .raw_modifies() @@ -183,7 +187,7 @@ impl WidgetId { tree.get(self.0).and_then(method).map(WidgetId) } - pub(crate) fn assert_get(self, tree: &TreeArena) -> &dyn Render { + pub(crate) fn assert_get<'a, 'b>(self, tree: &'a TreeArena) -> &'a (dyn Render + 'b) { self.get(tree).expect("Widget not exists in the `tree`") } diff --git a/core/src/window.rs b/core/src/window.rs index 8bb0f370a..9e565e405 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 @@ -52,6 +51,14 @@ pub struct Window { /// all task finished before current frame end. frame_pool: RefCell, shell_wnd: RefCell>, + /// A vector store the widget id pair of (parent, child). The child need to + /// drop after its `DelayDrop::delay_drop_until` be false or its parent + /// is dropped. + /// + /// This widgets it's detached from its parent, but still need to paint. + delay_drop_widgets: RefCell, WidgetId)>>, + /// A hash set store the root of the subtree need to regenerate. + regenerating_subtree: RefCell>>, } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Hash)] @@ -86,7 +93,7 @@ impl Window { pub fn processes_native_event(&self, event: WindowEvent) { let ratio = self.device_pixel_ratio() as f64; self.dispatcher.borrow_mut().dispatch(event, ratio); - self.emit_events(); + self.run_frame_tasks(); } /// Request switch the focus to next widget. @@ -110,79 +117,83 @@ impl Window { self.frame_ticker.frame_tick_stream() } - pub fn animate_track(&self) -> AnimateTrack { - AnimateTrack { - actived: false, - actived_cnt: self.running_animates.clone(), - } - } + pub fn inc_running_animate(&self) { self.running_animates.set(self.running_animates.get() + 1); } + + pub fn dec_running_animate(&self) { self.running_animates.set(self.running_animates.get() - 1); } /// Draw an image what current render tree represent. #[track_caller] - pub fn draw_frame(&self) { - self.emit_events(); - - if !self.need_draw() || self.size().is_empty() { - return; - } - + pub fn draw_frame(&self) -> bool { + self.run_frame_tasks(); self.frame_ticker.emit(FrameMsg::NewFrame(Instant::now())); - self.shell_wnd.borrow_mut().begin_frame(); + let draw = self.need_draw() && !self.size().is_empty(); + if draw { + self.shell_wnd.borrow_mut().begin_frame(); - loop { self.layout(); - self.emit_events(); - - // wait all frame task finished. - self.frame_pool.borrow_mut().run(); - if !self.widget_tree.borrow().is_dirty() { - self.focus_mgr.borrow_mut().refresh_focus(); - self.emit_events(); + self.widget_tree.borrow().draw(); + self.draw_delay_drop_widgets(); - // focus refresh and event emit may cause widget tree dirty again. - if !self.widget_tree.borrow().is_dirty() { - break; - } - } - } + let surface = match AppCtx::app_theme() { + Theme::Full(theme) => theme.palette.surface(), + Theme::Inherit(_) => unreachable!(), + }; - self.widget_tree.borrow().draw(); + let mut shell = self.shell_wnd.borrow_mut(); + let inner_size = shell.inner_size(); + let paint_cmds = self.painter.borrow_mut().finish(); + shell.draw_commands(Rect::from_size(inner_size), paint_cmds, surface); - let surface = match AppCtx::app_theme() { - Theme::Full(theme) => theme.palette.surface(), - Theme::Inherit(_) => unreachable!(), - }; - - let mut shell = self.shell_wnd.borrow_mut(); - let inner_size = shell.inner_size(); - let paint_cmds = self.painter.borrow_mut().finish(); - shell.draw_commands(Rect::from_size(inner_size), paint_cmds, surface); + shell.end_frame(); + } - shell.end_frame(); - self.frame_ticker.emit(FrameMsg::Finish(Instant::now())); AppCtx::end_frame(); + self.frame_ticker.emit(FrameMsg::Finish(Instant::now())); + + draw } pub fn layout(&self) { - self - .widget_tree - .borrow_mut() - .layout(self.shell_wnd.borrow().inner_size()); + loop { + self.run_frame_tasks(); - self - .frame_ticker - .emit(FrameMsg::LayoutReady(Instant::now())); + self + .widget_tree + .borrow_mut() + .layout(self.shell_wnd.borrow().inner_size()); + + if !self.widget_tree.borrow().is_dirty() { + self.focus_mgr.borrow_mut().refresh_focus(); + } + + // we need to run frame tasks before we emit `FrameMsg::LayoutReady` to keep the + // task and event emit order. + if !self.widget_tree.borrow().is_dirty() { + self.run_frame_tasks(); + } + if !self.widget_tree.borrow().is_dirty() { + let ready = FrameMsg::LayoutReady(Instant::now()); + self.frame_ticker.emit(ready); + } + + if !self.widget_tree.borrow().is_dirty() { + break; + } + } } 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 + // if a `pipe` widget is regenerating, need a new frame to finish it. + || !self.regenerating_subtree.borrow().is_empty() } - pub fn new(root: Widget, shell_wnd: Box) -> Rc { + pub fn new(shell_wnd: Box) -> Rc { let focus_mgr = RefCell::new(FocusManager::new()); - let widget_tree = RefCell::new(WidgetTree::new()); + let widget_tree = RefCell::new(WidgetTree::default()); let dispatcher = RefCell::new(Dispatcher::new()); let size = shell_wnd.inner_size(); let mut painter = Painter::new(Rect::from_size(size)); @@ -194,21 +205,26 @@ impl Window { focus_mgr, delay_emitter: <_>::default(), frame_ticker: FrameTicker::default(), - running_animates: Rc::new(RefCell::new(0)), - frame_pool: RefCell::new(<_>::default()), + running_animates: <_>::default(), + frame_pool: <_>::default(), shell_wnd: RefCell::new(shell_wnd), + delay_drop_widgets: <_>::default(), + regenerating_subtree: <_>::default(), }; let window = Rc::new(window); window.dispatcher.borrow_mut().init(Rc::downgrade(&window)); window.focus_mgr.borrow_mut().init(Rc::downgrade(&window)); - window - .widget_tree - .borrow_mut() - .init(root, Rc::downgrade(&window)); + window.widget_tree.borrow_mut().init(Rc::downgrade(&window)); window } + pub fn set_content_widget(&self, root: Widget) { + let build_ctx = BuildCtx::new(None, &self.widget_tree); + let root = root.build(&build_ctx); + self.widget_tree.borrow_mut().set_root(root) + } + #[inline] pub fn id(&self) -> WindowId { self.shell_wnd.borrow().id() } @@ -270,10 +286,63 @@ impl Window { self.delay_emitter.borrow_mut().push_back(e); } + pub(crate) fn is_in_another_regenerating(&self, wid: WidgetId) -> bool { + let regen = self.regenerating_subtree.borrow(); + if regen.is_empty() { + return false; + } + let tree = self.widget_tree.borrow(); + let Some(p) = wid.parent(&tree.arena) else { return false }; + let in_another = p.ancestors(&tree.arena).any(|p| { + regen.get(&p).map_or(false, |to| { + to.map_or(true, |to| wid.ancestor_of(to, &tree.arena)) + }) + }); + in_another + } + + pub(crate) fn mark_widgets_regenerating(&self, from: WidgetId, to: Option) { + self.regenerating_subtree.borrow_mut().insert(from, to); + } + + pub(crate) fn remove_regenerating_mark(&self, from: WidgetId) { + self.regenerating_subtree.borrow_mut().remove(&from); + } + + fn draw_delay_drop_widgets(&self) { + let mut delay_widgets = self.delay_drop_widgets.borrow_mut(); + let mut painter = self.painter.borrow_mut(); + + delay_widgets.retain(|(parent, wid)| { + let tree = self.widget_tree.borrow(); + let drop_conditional = wid + .assert_get(&self.widget_tree.borrow().arena) + .query_on_first_type(QueryOrder::OutsideFirst, |d: &DelayDrop| d.delay_drop_until) + .unwrap_or(true); + let parent_dropped = parent.map_or(false, |p| { + p.ancestors(&tree.arena).last() != Some(tree.root()) + }); + let need_drop = drop_conditional || parent_dropped; + if need_drop { + drop(tree); + self.widget_tree.borrow_mut().remove_subtree(*wid); + } else { + let mut painter = painter.save_guard(); + if let Some(p) = parent { + let offset = tree.store.map_to_global(Point::zero(), *p, &tree.arena); + painter.translate(offset.x, offset.y); + } + let mut ctx = PaintingCtx::new(*wid, self.id(), &mut painter); + wid.paint_subtree(&mut ctx); + } + !need_drop + }); + } + /// Immediately emit all delay events. You should not call this method only if /// you want to interfere with the framework event dispatch process and know /// what you are doing. - pub fn emit_events(&self) { + fn emit_events(&self) { loop { let Some(e) = self.delay_emitter.borrow_mut().pop_front() else{ break}; @@ -286,14 +355,20 @@ impl Window { let e = AllLifecycle::PerformedLayout(LifecycleEvent { id, wnd_id: self.id() }); self.emit::(id, e); } - DelayEvent::Disposed { id, delay_drop } => { + DelayEvent::Disposed { id, parent } => { id.descendants(&self.widget_tree.borrow().arena) .for_each(|id| { let e = AllLifecycle::Disposed(LifecycleEvent { id, wnd_id: self.id() }); self.emit::(id, e); }); - if !delay_drop { + let delay_drop = id + .assert_get(&self.widget_tree.borrow().arena) + .contain_type::(); + + if delay_drop { + self.delay_drop_widgets.borrow_mut().push((parent, id)); + } else { self.widget_tree.borrow_mut().remove_subtree(id); } } @@ -467,6 +542,22 @@ impl Window { (**e).borrow().is_propagation() }); } + + /// Run all async tasks need finished in current frame and emit all delay + /// events. + pub fn run_frame_tasks(&self) { + loop { + // wait all frame task finished. + self.frame_pool.borrow_mut().run(); + // run all ready async tasks + AppCtx::run_until_stalled(); + if !self.delay_emitter.borrow().is_empty() { + self.emit_events(); + } else { + break; + } + } + } } /// Event that delay to emit, emit it when the window is not busy(nobody borrow @@ -476,8 +567,8 @@ pub(crate) enum DelayEvent { Mounted(WidgetId), PerformedLayout(WidgetId), Disposed { + parent: Option, id: WidgetId, - delay_drop: bool, }, Focus(WidgetId), Blur(WidgetId), diff --git a/dev-helper/src/example_framework.rs b/dev-helper/src/example_framework.rs index 8777294bd..56b5526e6 100644 --- a/dev-helper/src/example_framework.rs +++ b/dev-helper/src/example_framework.rs @@ -45,8 +45,7 @@ macro_rules! example_framework { AppCtx::set_app_theme(material::purple::light()); } let name = env!("CARGO_PKG_NAME"); - let id = App::new_window($widget_fn().into(), Some($size)); - AppCtx::get_window(id).unwrap().set_title(name); + App::new_window($widget_fn().into(), Some($size)).set_title(name); App::exec(); } }; 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..2be5f2cdd 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,86 @@ 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! { + + @VScrollBar { @ { pipe! { + let mut mount_task_cnt = Stateful::new(0); + + @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_idx = Stateful::new(0); + + let mut mount_animate = @Animate { + transition: @Transition { + delay: pipe!(Duration::from_millis(100).mul_f32(*$mount_idx as f32)), + duration: Duration::from_millis(150), + easing: easing::EASE_IN, + }.into_inner(), + state: route_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 +162,7 @@ pub fn todos() -> Widget { label: "Support Virtual Scroll".to_string(), }, ], + id_gen: 3, } .into() } diff --git a/macros/src/builtin_fields_list.rs b/macros/src/builtin_fields_list.rs index 329e65f95..a16acfdd1 100644 --- a/macros/src/builtin_fields_list.rs +++ b/macros/src/builtin_fields_list.rs @@ -299,7 +299,7 @@ builtin! { lifecycle_stream: LifecycleSubject } - DelayDropWidget { + DelayDrop { #[doc= "The widget delay the drop of its child until the field delay_drop_until is false, but not affect its dispose event emit time. It's useful to ensure the disappear-animate display fully."] delay_drop_until: bool, } diff --git a/macros/src/child_template.rs b/macros/src/child_template.rs index 999033e80..ca89b5294 100644 --- a/macros/src/child_template.rs +++ b/macros/src/child_template.rs @@ -71,7 +71,7 @@ pub(crate) fn derive_child_template(input: &mut syn::DeriveInput) -> syn::Result impl #g_impl DeclareBuilder for #builder #g_ty { type Target = Self; #[inline] - fn build(self, _: &BuildCtx) -> Self { self } + fn build_declare(self, _: &BuildCtx) -> Self { self } } diff --git a/macros/src/declare_derive.rs b/macros/src/declare_derive.rs index 16180f3d3..b3a72dfaf 100644 --- a/macros/src/declare_derive.rs +++ b/macros/src/declare_derive.rs @@ -266,7 +266,7 @@ pub(crate) fn declare_derive(input: &mut syn::DeriveInput) -> syn::Result #name #g_ty { + fn build_declare(mut self, ctx: &BuildCtx) -> #name #g_ty { #(#fill_default)* #name { #(#fields_ident : #builder_values),* } diff --git a/macros/src/declare_derive2.rs b/macros/src/declare_derive2.rs index 671a7067e..4944de726 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::()?; } @@ -111,16 +124,22 @@ pub(crate) fn declare_derive(input: &mut syn::DeriveInput) -> syn::Result quote!(#name {}), + Fields::Unnamed(_) => quote!(#name()), + Fields::Unit => quote!(#name), + }; let tokens = quote! { - impl Declare2 for #name { - type Builder = #name; - fn declare2_builder() -> Self::Builder { #name } - } + impl Declare2 for #name { + type Builder = #name; + fn declare2_builder() -> Self::Builder { #construct } + } - impl DeclareBuilder for #name { - type Target = #name; - fn build(self, _: &BuildCtx) -> Self::Target { self } - } + impl DeclareBuilder for #name { + type Target = #name; + #[inline] + fn build_declare(self, _: &BuildCtx) -> Self::Target { self } + } }; Ok(tokens) } else { @@ -134,8 +153,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 +170,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 +233,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 +246,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)* } @@ -238,24 +272,36 @@ fn struct_with_fields_gen( impl #g_impl DeclareBuilder for #declarer #g_ty #g_where { type Target = State<#name #g_ty>; - fn build(mut self, ctx: &BuildCtx) -> Self::Target { + + #[inline] + fn build_declare(mut self, ctx: &BuildCtx) -> Self::Target { set_build_ctx!(ctx); #(#fill_default)* #(#unzip_fields)* - let mut _ribir = State::Stateless(#name { + let mut _ribir_ಠ_ಠ = State::value(#name { #(#field_names2 : #field_names2.0),* }); + let mut _unsub_ಠ_ಠ = None; + #( if let Some(u) = #field_names3.1 { - let mut _ribir2 = _ribir.clone_stateful(); - let h = u.subscribe(move |v| _ribir2.state_ref().#field_names3 = v) - .unsubscribe_when_dropped(); - _ribir.to_stateful().own_data(h); + let mut _ribir2 = _ribir_ಠ_ಠ.clone_writer(); + let h = u.subscribe(move |(_, v)| _ribir2.write().#field_names3 = v); + _unsub_ಠ_ಠ = if let Some(u) = _unsub_ಠ_ಠ { + let unsub = ZipSubscription::new(u, h); + Some(BoxSubscription::new(unsub)) + } else { + Some(h) + }; } );* - _ribir + if let Some(unsub) = _unsub_ಠ_ಠ { + _ribir_ಠ_ಠ.as_stateful().unsubscribe_on_drop(unsub); + } + + _ribir_ಠ_ಠ } } }; diff --git a/macros/src/declare_obj.rs b/macros/src/declare_obj.rs index 47cf5de54..e02c38b62 100644 --- a/macros/src/declare_obj.rs +++ b/macros/src/declare_obj.rs @@ -1,23 +1,20 @@ use crate::{ rdl_macro::{DeclareField, RdlParent, StructLiteral}, - widget_macro::{ribir_variable, WIDGETS, WIDGET_OF_BUILTIN_FIELD}, + widget_macro::{WIDGETS, WIDGET_OF_BUILTIN_FIELD}, }; use inflector::Inflector; use proc_macro2::{Span, TokenStream}; -use quote::quote; -use quote::{quote_spanned, ToTokens}; +use quote::{quote, quote_spanned, ToTokens}; +use smallvec::smallvec; use smallvec::SmallVec; -use syn::{ - parse_str, - spanned::Spanned, - token::{Brace, Comma, Paren}, - Ident, Macro, Path, -}; +use syn::{parse_str, spanned::Spanned, token::Brace, Ident, Macro, Path}; pub struct DeclareObj<'a> { - this: ObjNode<'a>, span: Span, - builtin: ahash::HashMap<&'static str, SmallVec<[&'a DeclareField; 1]>>, + /// if declare a builtin widget, this is None. For example: + /// `@Margin { margin: ... }` + this: Option>, + builtin: Vec<(&'static str, SmallVec<[&'a DeclareField; 1]>)>, children: &'a Vec, } enum ObjNode<'a> { @@ -32,144 +29,151 @@ 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 span = match parent { - RdlParent::Type(ty) => ty.span(), - RdlParent::Var(name) => name.span(), - }; - let span = span.join(brace.span).unwrap(); + let mut builtin: Vec<(&'static str, SmallVec<[&'a DeclareField; 1]>)> = vec![]; - let this = match parent { - RdlParent::Type(ty) => { - let mut self_fields = SmallVec::default(); - for f in fields { - if let Some(ty) = WIDGET_OF_BUILTIN_FIELD - .get(f.member.to_string().as_str()) - .filter(|builtin_ty| !ty.is_ident(builtin_ty)) - { - builtin.entry(*ty).or_default().push(f); - } else { - self_fields.push(f) - } + let mut self_fields = SmallVec::default(); + for f in fields { + if let Some(ty) = WIDGET_OF_BUILTIN_FIELD.get(f.member.to_string().as_str()) { + if let Some((_, fields)) = builtin.iter_mut().find(|(ty_name, _)| ty_name == ty) { + fields.push(f); + } else { + builtin.push((*ty, smallvec![f])); } - ObjNode::Obj { ty, fields: self_fields, span } + } else { + self_fields.push(f); } - RdlParent::Var(name) => { + } + + fn invalid_member_err(fields: &[&DeclareField], err_msg: &str) -> Result<(), TokenStream> { + if fields.is_empty() { + Ok(()) + } else { + let mut err_tokens = quote! {}; 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); - } else { - return Err(quote_spanned! { f.span() => - compile_error!("Not allow to declare a field of a variable parent.") - }); - } + quote_spanned! { f.member.span() => #err_msg }.to_tokens(&mut err_tokens) } - ObjNode::Var(name) + Err(err_tokens) } - }; + } - Ok(Self { this, span, builtin, children }) + match parent { + RdlParent::Type(ty) => { + let span = ty.span().join(brace.span).unwrap(); + if WIDGETS.iter().any(|w| ty.is_ident(w.ty)) { + invalid_member_err( + &self_fields, + &format!("not a valid member of {}", ty.to_token_stream()), + )?; + Ok(Self { this: None, span, builtin, children }) + } else { + let this = Some(ObjNode::Obj { ty, fields: self_fields, span }); + Ok(Self { this, span, builtin, children }) + } + } + RdlParent::Var(name) => { + invalid_member_err( + &self_fields, + "only allow to declare builtin fields in a variable parent.", + )?; + let this = Some(ObjNode::Var(name)); + let span = name.span().join(brace.span).unwrap(); + Ok(Self { this, span, builtin, children }) + } + } } } impl<'a> ToTokens for DeclareObj<'a> { fn to_tokens(&self, tokens: &mut TokenStream) { - let Self { this, span, builtin, children } = self; + Brace(self.span).surround(tokens, |tokens| { + match &self.this { + Some(this @ ObjNode::Obj { span, .. }) => { + // declare the host widget before builtin widget and children. + // so that we can use variable if it moved in builtin widget and children. + // this is consistent with the user's declaration. - // if children is empty, we declare a `FatObj`, so it's can be used and - // referenced by others, otherwise directly composed. - - if children.is_empty() && builtin.is_empty() { - quote_spanned! { *span => FatObj::new(#this) }.to_tokens(tokens) - } else { - Brace::default().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 - // `SmallVec<[&'b DeclareField; 1]>` to `SmallVec<[&'a DeclareField; - // 1]>`, because `SmallVec` is invariant over `T`. - fn shorter_lifetime<'a, 'b: 'a>( - fields: SmallVec<[&'b DeclareField; 1]>, - ) -> SmallVec<[&'a DeclareField; 1]> { - unsafe { std::mem::transmute(fields.clone()) } - } - let fields = shorter_lifetime(fields.clone()); - - let builtin_span = fields[0].span(); - let ty = parse_str::(ty_str).unwrap(); - let obj = ObjNode::Obj { ty: &ty, span: builtin_span, fields }; - - let snaked_ty_str = ty_str.to_snake_case(); - let name = Ident::new(&snaked_ty_str, builtin_span); - quote_spanned! { builtin_span => let #name = #obj; }.to_tokens(tokens); - builtin_names.push(name); + quote_spanned! { *span => let _ribir_ಠ_ಠ = #this; }.to_tokens(tokens); + let name = Ident::new("_ribir_ಠ_ಠ", self.span); + self.compose_builtin_and_children(&name, tokens) } - - let mut children_names = vec![]; - for (i, c) in children.iter().enumerate() { - let child = ribir_variable(&format!("child_{i}"), c.span()); - quote_spanned! { c.span() => let #child = #c; }.to_tokens(tokens); - children_names.push(child) + Some(ObjNode::Var(var)) => self.compose_builtin_and_children(var, tokens), + None => { + let (builtin_names, children) = self.declare_builtin_objs_and_children(tokens); + let built_obj = self.to_builtin_obj(builtin_names); + quote_spanned! { self.span => #built_obj #(.with_child(#children, ctx!()))* } + .to_tokens(tokens) } + } + }) + } +} - if children.is_empty() { - let builtin_init = builtin_names - .iter() - .map(|name| Ident::new(&format!("with_{name}"), name.span())); +impl<'a> DeclareObj<'a> { + /// declare the builtin inner widgets, and return the name of them. + fn declare_builtin_objs_and_children( + &self, + tokens: &mut TokenStream, + ) -> (SmallVec<[Ident; 1]>, SmallVec<[Ident; 1]>) { + let mut builtin_names = smallvec![]; + for (ty_str, fields) in &self.builtin { + // 'b is live longer than 'a, safe convert, but we can't directly convert + // `SmallVec<[&'b DeclareField; 1]>` to `SmallVec<[&'a DeclareField; + // 1]>`, because `SmallVec` is invariant over `T`. + fn shorter_lifetime<'a, 'b: 'a>( + fields: SmallVec<[&'b DeclareField; 1]>, + ) -> SmallVec<[&'a DeclareField; 1]> { + unsafe { std::mem::transmute(fields.clone()) } + } + let fields = shorter_lifetime(fields.clone()); - quote_spanned! { - *span =>FatObj::new(#this)#(.#builtin_init(#builtin_names))* - } - .to_tokens(tokens); - } else { - // todo: tmp code, we should use FatObj to compose builtin widgets in every - // where, so we can keep the builtin widget compose order consistent. - builtin_names.sort_by_key(|name| { - WIDGETS - .iter() - .position(|b| *name == b.ty.to_snake_case()) - .unwrap() - }); - if !builtin.is_empty() { - let first = &builtin_names[0]; - let rest_builtin = &builtin_names[1..]; + let builtin_span = fields[0].span(); + let ty = parse_str::(ty_str).unwrap(); + let obj = ObjNode::Obj { ty: &ty, span: builtin_span, fields }; - recursive_compose_with(first, rest_builtin.iter(), tokens, |tokens| { - quote_spanned! { *span => - #this #(.with_child(#children_names, ctx!()))* - } - .to_tokens(tokens) - }); - } else { - quote_spanned! { *span => - #this #(.with_child(#children_names, ctx!()))* - } - .to_tokens(tokens) - } - } - }); + let snaked_ty_str = ty_str.to_snake_case(); + let name = Ident::new(&snaked_ty_str, builtin_span); + quote_spanned! { builtin_span => let #name = #obj; }.to_tokens(tokens); + builtin_names.push(name); + } + let mut children_names = smallvec![]; + for (i, c) in self.children.iter().enumerate() { + let child = Ident::new(&format!("_child_{i}_ಠ_ಠ"), c.span()); + quote_spanned! { c.span() => let #child = #c; }.to_tokens(tokens); + children_names.push(child) } + + (builtin_names, children_names) } -} -fn recursive_compose_with( - p: impl ToTokens, - mut child_chain: impl Iterator, - tokens: &mut TokenStream, - leaf: impl FnOnce(&mut TokenStream), -) { - p.to_tokens(tokens); - quote_spanned! { p.span() => .with_child}.to_tokens(tokens); - Paren(p.span()).surround(tokens, |tokens| { - let child = child_chain.next(); - if let Some(c) = child { - recursive_compose_with(c, child_chain, tokens, leaf) + /// compose the builtin objs as `BuiltinObj` + fn to_builtin_obj(&self, builtin_names: SmallVec<[Ident; 1]>) -> TokenStream { + if builtin_names.is_empty() { + quote_spanned! { self.span => BuiltinObj::default() } } else { - leaf(tokens) + let builtin_init = builtin_names + .iter() + .map(|name| Ident::new(&format!("set_builtin_{name}"), name.span())); + quote_spanned! { self.span => BuiltinObj::default()#(.#builtin_init(#builtin_names))* } } - Comma::default().to_tokens(tokens); - quote! { ctx!() }.to_tokens(tokens); - }); + } + + fn compose_builtin_and_children(&self, var: &Ident, tokens: &mut TokenStream) { + let (builtin_names, children) = self.declare_builtin_objs_and_children(tokens); + // if builtin is empty and children is not empty, we needn't to create a + // `FatObj`, because not support to use the builtin widget in this case. + if builtin_names.is_empty() && !children.is_empty() { + quote_spanned! { self.span => + #var #(.with_child(#children, ctx!()))* + } + .to_tokens(tokens); + } else { + let built_obj = self.to_builtin_obj(builtin_names); + quote_spanned! { self.span => + FatObj::new(#var, #built_obj)#(.with_child(#children, ctx!()))* + } + .to_tokens(tokens) + } + } } impl<'a> ToTokens for ObjNode<'a> { @@ -178,7 +182,7 @@ impl<'a> ToTokens for ObjNode<'a> { Self::Obj { ty, span, fields } => { quote_spanned! { *span => #ty::declare2_builder() }.to_tokens(tokens); fields.iter().for_each(|f| f.to_tokens(tokens)); - tokens.extend(quote_spanned! { *span => .build(ctx!()) }); + tokens.extend(quote_spanned! { *span => .build_declare(ctx!()) }); } Self::Var(var) => var.to_tokens(tokens), } diff --git a/macros/src/fn_widget_macro.rs b/macros/src/fn_widget_macro.rs index 20159c9d5..6a41786ab 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; +use crate::symbol_process::DollarRefsCtx; -pub struct FnWidgetMacro { - stmts: Vec, -} +pub struct FnWidgetMacro(Vec); -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 }) +impl FnWidgetMacro { + pub(crate) fn gen_code(input: TokenStream, refs_ctx: &mut DollarRefsCtx) -> TokenStream1 { + let input = ok!(symbol_to_macro(TokenStream1::from(input))); + let body = parse_macro_input!(input as BodyExpr); + refs_ctx.new_dollar_scope(true); + let stmts = body.0.into_iter().map(|s| refs_ctx.fold_stmt(s)).collect(); + let _ = refs_ctx.pop_dollar_scope(true); + 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..615b9160a 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -9,8 +9,8 @@ mod util; mod widget_macro; use fn_widget_macro::FnWidgetMacro; use proc_macro::TokenStream; -use quote::{quote, ToTokens}; -use symbol_process::symbol_to_macro; +use quote::quote; +use symbol_process::DollarRefsCtx; use syn::{parse_macro_input, DeriveInput}; use widget_macro::gen_widget_macro; mod child_template; @@ -18,9 +18,10 @@ mod fn_widget_macro; mod pipe_macro; mod rdl_macro; mod watch_macro; +mod writer_map_macro; pub(crate) use rdl_macro::*; -use crate::pipe_macro::PipeExpr; +use crate::pipe_macro::PipeMacro; use crate::watch_macro::WatchMacro; pub(crate) mod declare_obj; pub(crate) mod symbol_process; @@ -33,13 +34,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 +61,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 +143,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 DollarRefsCtx::top_level()) } /// The `fn_widget` is a macro that create a widget from a function widget from @@ -147,15 +152,20 @@ pub fn rdl(input: TokenStream) -> TokenStream { /// 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() - }, - ) + // if input.to_string().contains("$child.layout_size()") { + // println!( + // "gen code\n {}", + // FnWidgetMacro::gen_code(input.clone().into(), &mut + // DollarRefsCtx::top_level()).to_string() ) + // } + FnWidgetMacro::gen_code(input.into(), &mut DollarRefsCtx::top_level()) } +/// 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. #[proc_macro] @@ -180,33 +190,32 @@ pub fn ctx(input: TokenStream) -> TokenStream { /// 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() - }, - ) + PipeMacro::gen_code(input.into(), &mut DollarRefsCtx::top_level()) } /// `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() - }, - ) + WatchMacro::gen_code(input.into(), &mut DollarRefsCtx::top_level()) +} + +/// macro split a new writer from a state writer. For example, +/// `split_writer!($label.visible);` will return a writer of `bool` that is +/// partial of the `Visibility`. This macros is a convenient way for +/// `StateWriter::split_writer` +#[proc_macro] +pub fn split_writer(input: TokenStream) -> TokenStream { + writer_map_macro::gen_split_path_writer(input.into(), &mut DollarRefsCtx::top_level()) } -/// The macro to use a state as its StateRef. Transplanted from the `$`. +/// macro map a write to another state write. For example, +/// `map_writer!($label.visible);` will return a writer of `bool` that is +/// partial of the `Visibility`. This macros is a convenient way for +/// `StateWriter::map_writer` #[proc_macro] -pub fn _dollar_ಠ_ಠ(input: TokenStream) -> TokenStream { - let name = parse_macro_input! { input as syn::Ident }; - quote! { #name.state_ref() }.into() +pub fn map_writer(input: TokenStream) -> TokenStream { + writer_map_macro::gen_map_path_writer(input.into(), &mut DollarRefsCtx::top_level()) } #[proc_macro] diff --git a/macros/src/pipe_macro.rs b/macros/src/pipe_macro.rs index b4fba7708..ebc59462b 100644 --- a/macros/src/pipe_macro.rs +++ b/macros/src/pipe_macro.rs @@ -1,42 +1,44 @@ -use crate::symbol_process::DollarRefs; +use crate::symbol_process::{not_subscribe_anything, DollarRefsCtx, DollarRefsScope}; +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; use syn::{ - fold::Fold, parse::{Parse, ParseStream}, + parse_macro_input, + spanned::Spanned, Stmt, }; -pub(crate) struct PipeExpr { - refs: DollarRefs, +pub(crate) struct BodyExpr(pub(crate) Vec); +pub(crate) struct PipeMacro { + refs: DollarRefsScope, 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, refs_ctx: &mut DollarRefsCtx) -> TokenStream1 { + let span = input.span(); + let input = ok!(symbol_to_macro(TokenStream1::from(input))); + let expr = parse_macro_input! { input as BodyExpr }; + + refs_ctx.new_dollar_scope(true); + let expr = expr.0.into_iter().map(|s| refs_ctx.fold_stmt(s)).collect(); + let refs = refs_ctx.pop_dollar_scope(true); + if refs.is_empty() { + not_subscribe_anything(span).into() + } else { + PipeMacro { refs, expr }.to_token_stream().into() + } } } -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 { - refs.dedup(); - Ok((refs, stmts)) - } +impl Parse for BodyExpr { + fn parse(input: ParseStream) -> syn::Result { Ok(Self(syn::Block::parse_within(input)?)) } } -impl ToTokens for PipeExpr { +impl ToTokens for PipeMacro { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { let Self { refs, expr } = self; @@ -46,13 +48,15 @@ impl ToTokens for PipeExpr { quote! {{ #refs let upstream = #upstream; + let _ctx_handle_ಠ_ಠ = ctx!().handle(); let mut expr_value = move |ctx!(): &BuildCtx<'_>| { #(#expr)* }; - let _ctx_handle = ctx!().handle(); - Pipe::new( expr_value(ctx!()), upstream - .filter_map(move |_| _ctx_handle.with_ctx(&mut expr_value)) + .filter_map(move |scope| _ctx_handle_ಠ_ಠ + .with_ctx(&mut expr_value) + .map(|v| (scope, v)) + ) .box_it() ) }} @@ -64,7 +68,7 @@ impl ToTokens for PipeExpr { let mut expr_value = move || { #(#expr)* }; Pipe::new( expr_value(), - upstream.map(move |_| expr_value()).box_it() + upstream.map(move |scope| (scope, expr_value())).box_it() ) }} .to_tokens(tokens) diff --git a/macros/src/rdl_macro.rs b/macros/src/rdl_macro.rs index 9a7140796..fade30cef 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, DollarRefsCtx}, +}; +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 DollarRefsCtx) -> TokenStream1 { + let input = ok!(symbol_to_macro(TokenStream1::from(input))); + + 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/symbol_process.rs b/macros/src/symbol_process.rs index 51519bb9e..0dc82e890 100644 --- a/macros/src/symbol_process.rs +++ b/macros/src/symbol_process.rs @@ -1,22 +1,33 @@ -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::writer_map_macro::{gen_map_path_writer, gen_split_path_writer}; +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 smallvec::{smallvec, SmallVec}; use syn::{ fold::Fold, parse::{Parse, ParseStream}, - parse_quote, spanned::Spanned, token::Dollar, Expr, ExprField, ExprMethodCall, Macro, Member, }; +use syn::{parse_quote, parse_quote_spanned}; 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 const KW_SPLIT_WRITER: &str = "split_writer"; +pub const KW_MAP_WRITER: &str = "map_writer"; + pub use tokens_pre_process::*; pub mod kw { @@ -24,18 +35,34 @@ pub mod kw { syn::custom_keyword!(rdl); } +#[derive(Hash, PartialEq, Eq, Debug, Clone)] +pub struct BuiltinInfo { + pub(crate) host: Ident, + pub(crate) member: Ident, +} + #[derive(Hash, PartialEq, Eq, Debug, Clone)] pub struct DollarRef { pub name: Ident, - pub builtin_shadow: Option, + pub builtin: Option, + pub write: bool, } -#[derive(Default)] -pub struct DollarRefs { +#[derive(Debug)] +pub struct DollarRefsCtx { + scopes: SmallVec<[DollarRefsScope; 1]>, + // the head stack index of the variable stack for every capture level. + capture_level_heads: SmallVec<[usize; 1]>, + variable_stacks: Vec>, +} + +#[derive(Debug, Default)] +pub struct DollarRefsScope { refs: SmallVec<[DollarRef; 1]>, - ctx_used: bool, - pub in_capture: usize, + used_ctx: bool, } +pub struct StackGuard<'a>(&'a mut DollarRefsCtx); + mod tokens_pre_process { use proc_macro::{TokenTree, *}; @@ -63,7 +90,9 @@ mod tokens_pre_process { /// Convert `@` and `$` symbol to a `rdl!` or `_dollar_ಠ_ಠ!` macro, make it /// conform to Rust syntax - pub fn symbol_to_macro(input: TokenStream) -> Result { + pub fn symbol_to_macro( + input: impl IntoIterator, + ) -> Result { let mut iter = input.into_iter(); let mut tokens = vec![]; @@ -131,7 +160,6 @@ mod tokens_pre_process { None => return dollar_err(p.span()), }; } - Some(TokenTree::Group(mut g)) => { // not process symbol in macro, because it's maybe as part of the macro syntax. if !in_macro(&tokens) { @@ -157,12 +185,82 @@ mod tokens_pre_process { } } -impl Fold for DollarRefs { +impl Fold for DollarRefsCtx { + fn fold_block(&mut self, i: syn::Block) -> syn::Block { + let mut this = self.push_code_stack(); + syn::fold::fold_block(&mut *this, i) + } + + fn fold_expr_closure(&mut self, i: syn::ExprClosure) -> syn::ExprClosure { + let mut this = self.push_code_stack(); + syn::fold::fold_expr_closure(&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_code_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_code_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_code_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_code_stack(); + syn::fold::fold_expr_if(&mut *this, i) + } + + fn fold_arm(&mut self, i: syn::Arm) -> syn::Arm { + let mut this = self.push_code_stack(); + syn::fold::fold_arm(&mut *this, i) + } + + fn fold_expr_unsafe(&mut self, i: syn::ExprUnsafe) -> syn::ExprUnsafe { + let mut this = self.push_code_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_code_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 { - if let Some(builtin_ty) = WIDGET_OF_BUILTIN_FIELD.get(member.to_string().as_str()) { - self.replace_builtin_ident(&mut *base, &builtin_ty.to_snake_case()); + let dollar = WIDGET_OF_BUILTIN_FIELD + .get(member.to_string().as_str()) + .and_then(|builtin_ty| self.replace_builtin_ident(&mut *base, &builtin_ty.to_snake_case())); + if dollar.is_some() { + return i; } } @@ -170,57 +268,98 @@ impl Fold for DollarRefs { } fn fold_expr_method_call(&mut self, mut i: ExprMethodCall) -> ExprMethodCall { - if let Some(builtin_ty) = WIDGET_OF_BUILTIN_METHOD.get(i.method.to_string().as_str()) { - self.replace_builtin_ident(&mut i.receiver, &builtin_ty.to_snake_case()); + // fold builtin method on state + let dollar = WIDGET_OF_BUILTIN_METHOD + .get(i.method.to_string().as_str()) + .and_then(|builtin_ty| { + self.replace_builtin_ident(&mut i.receiver, &builtin_ty.to_snake_case()) + }); + if dollar.is_some() { + return i; + } + + // fold if write on state. + let write_mac = is_state_write_method(&i).then(|| { + let Expr::Macro(m) = &mut *i.receiver else { return None }; + parse_dollar_macro(&m.mac).map(|d| (d.name, &mut m.mac)) + }); + if let Some(Some((name, mac))) = write_mac { + mac.tokens = expand_write_method(name.to_token_stream()); + mark_macro_expanded(mac); + let dollar_ref = DollarRef { name, builtin: None, write: true }; + self.add_dollar_ref(dollar_ref); + return i; } syn::fold::fold_expr_method_call(self, i) } 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 let Some(DollarMacro { name, .. }) = parse_dollar_macro(&mac) { + mac.tokens = expand_read(name.to_token_stream()); + mark_macro_expanded(&mut mac); + let dollar_ref = DollarRef { name, builtin: None, write: false }; + self.add_dollar_ref(dollar_ref) + } else if mac.path.is_ident(KW_WATCH) { + mac.tokens = WatchMacro::gen_code(mac.tokens, self).into(); + mark_macro_expanded(&mut mac); + } else if mac.path.is_ident(KW_PIPE) { + self.mark_used_ctx(); + mac.tokens = PipeMacro::gen_code(mac.tokens, self).into(); + mark_macro_expanded(&mut mac); + } else if mac.path.is_ident(KW_RDL) { + self.mark_used_ctx(); + mac.tokens = RdlMacro::gen_code(mac.tokens, self).into(); + mark_macro_expanded(&mut mac); + } else if mac.path.is_ident(KW_FN_WIDGET) { + mac.tokens = FnWidgetMacro::gen_code(mac.tokens, self).into(); + mark_macro_expanded(&mut mac); + } else if mac.path.is_ident(KW_SPLIT_WRITER) { + mac.tokens = gen_split_path_writer(mac.tokens, self).into(); + mark_macro_expanded(&mut mac); + } else if mac.path.is_ident(KW_MAP_WRITER) { + mac.tokens = gen_map_path_writer(mac.tokens, self).into(); + mark_macro_expanded(&mut mac); + } else if mac.path.is_ident(KW_CTX) { + self.mark_used_ctx(); } 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(); - 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(); - - let body = &mut *c.body; - if closure_refs.ctx_used { - *body = Expr::Verbatim(quote_spanned!(body.span() => - _ctx_handle - .with_ctx(|ctx!(): &'_ BuildCtx<'_>| #body ) - .expect("The `BuildCtx` is not available.") - )); + self.new_dollar_scope(true); + let mut c = self.fold_expr_closure(c); + let dollar_scope = self.pop_dollar_scope(true); + + if dollar_scope.used_ctx() || !dollar_scope.is_empty() { + if dollar_scope.used_ctx() { + let body = &mut *c.body; + let body_with_ctx = quote_spanned! { body.span() => + _ctx_handle.with_ctx(|ctx!()| #body).expect("ctx is not available") + }; + + if matches!(c.output, syn::ReturnType::Default) { + *body = parse_quote! { #body_with_ctx }; + } else { + *body = parse_quote_spanned! { body.span() => { #body_with_ctx }}; + } } - let handle = closure_refs - .ctx_used + let handle = dollar_scope + .used_ctx() .then(|| quote_spanned! { c.span() => let _ctx_handle = ctx!().handle(); }); - // todo: seems need merge `closure_refs` into `self`. - Expr::Verbatim(quote_spanned!(c.span() => { - #closure_refs + #dollar_scope #handle #c })) } else { - Expr::Closure(c) + Expr::Closure(self.fold_expr_closure(c)) } } _ => syn::fold::fold_expr(self, i), @@ -228,93 +367,232 @@ impl Fold for DollarRefs { } } -impl ToTokens for DollarRef { +fn mark_macro_expanded(mac: &mut Macro) { + mac.path = parse_quote_spanned! { mac.path.span() => ribir_expanded_ಠ_ಠ }; +} + +impl ToTokens for DollarRefsScope { 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); + for DollarRef { name, builtin, write } in &self.refs { + let reader_writer = if *write { + "clone_writer" + } else { + "clone_reader" + }; + // todo: reader needn't mut + let reader_or_writer = Ident::new(reader_writer, name.span()); + match builtin { + Some(builtin) => { + let BuiltinInfo { host, member } = builtin; + quote_spanned! { + name.span() => let mut #name = #host.#member(ctx!()).#reader_or_writer(); + } + .to_tokens(tokens); + } + _ => { + quote_spanned! { + name.span() => let mut #name = #name.#reader_or_writer(); + } + .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); + +impl DollarRefsCtx { + #[inline] + pub fn top_level() -> Self { Self::default() } + + #[inline] + pub fn new_dollar_scope(&mut self, has_capture: bool) { + if has_capture { + self.capture_level_heads.push(self.variable_stacks.len()); + // new scope level, should start a new variables scope, otherwise the local + // variables will record in its parent level. + self.variable_stacks.push(vec![]); } + self.scopes.push(<_>::default()); } -} -impl DollarRefs { - pub fn used_ctx(&self) -> bool { self.ctx_used } + /// Pop the last dollar scope, and removes duplicate elements in it and make + /// the builtin widget first. Keep the builtin reference before the host + /// because if a obj both reference builtin widget and its host, the host + /// reference may shadow the original. + /// + /// For example, this generate code not work: + /// + /// ```ignore + /// let a = a.clone_state(); + /// // the `a` is shadowed by the before `a` variable. + /// let a_margin = a.get_builtin_margin(ctx!()); + /// ``` + /// + /// must generate `a_margin` first: + /// + /// ```ignore + /// let a_margin = a.get_builtin_margin(ctx!()); + /// let a = a.clone_state(); + /// ``` + #[inline] + pub fn pop_dollar_scope(&mut self, has_capture: bool) -> DollarRefsScope { + if has_capture { + self.variable_stacks.pop(); + self.capture_level_heads.pop(); + } + let mut scope = self.scopes.pop().unwrap(); + + // sort and remove duplicate + scope.refs.sort_by(|a, b| { + a.builtin + .is_none() + .cmp(&b.builtin.is_none()) + .then_with(|| a.name.cmp(&b.name)) + .then_with(|| b.write.cmp(&a.write)) + }); + scope.refs.dedup_by(|a, b| a.name == b.name); + + if !self.scopes.is_empty() { + self.current_dollar_scope_mut().used_ctx |= scope.used_ctx(); + + for r in scope.refs.iter_mut() { + if !self.is_local_var(r.host()) { + self.current_dollar_scope_mut().refs.push(r.clone()); + // if ref variable is not a local variable of parent capture level, should + // remove its builtin info as a normal variable, because parent will capture the + // builtin object individually. + if has_capture { + r.builtin.take(); + } + } + } + } + scope + } - pub fn dedup(&mut self) { - self.refs.sort_by_key(|r: &_| r.name.to_string()); - self.refs.dedup() + pub fn push_code_stack(&mut self) -> StackGuard<'_> { + self.variable_stacks.push(vec![]); + StackGuard(self) } - pub fn upstream_tokens(&self) -> TokenStream { - match self.len() { - 0 => quote! {}, - 1 => { - let DollarRef { name, builtin_shadow: value } = &self.refs[0]; - quote_spanned! { value.span() => #name.modifies() } - } - _ => { - let upstream = self.iter().map(|DollarRef { name, .. }| { - quote! { #name.modifies() } - }); - quote! { observable::from_iter([#(#upstream),*]).merge_all(usize::MAX) } - } + pub fn builtin_host_tokens(&self, dollar_ref: &DollarRef) -> TokenStream { + let DollarRef { name, builtin, .. } = dollar_ref; + let BuiltinInfo { host, member } = builtin.as_ref().unwrap(); + + // if used in embedded closure, we directly use the builtin variable, the + // variable that capture by the closure is already a separate builtin variable. + + if !self.is_local_var(host) && self.capture_level_heads.len() > 1 { + name.to_token_stream() + } else { + quote_spanned! { host.span() => #host.#member(ctx!()) } } } + fn mark_used_ctx(&mut self) { self.current_dollar_scope_mut().used_ctx = true; } + fn replace_builtin_ident( &mut self, caller: &mut Expr, builtin_member: &str, ) -> Option<&DollarRef> { - let e = match caller { - Expr::MethodCall(ExprMethodCall { receiver, method, args, .. }) - if args.is_empty() && (method == "shallow" || method == "silent") => - { - &mut **receiver - } - e => e, + let mut write = false; + let e = if let Expr::MethodCall(m) = caller { + write = is_state_write_method(m); + if write { &mut *m.receiver } else { caller } + } else { + caller }; 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 builtin_member = Ident::new(builtin_member, host.span()); + let DollarMacro { name: host, .. } = parse_dollar_macro(&m.mac)?; // 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() + // the builtin widget from the `FatObj` so we only capture the builtin part that + // we used. + let name = ribir_suffix_variable(&host, builtin_member); + let get_builtin_method = Ident::new(&format!("get_builtin_{builtin_member}"), host.span()); + let builtin = Some(BuiltinInfo { host, member: get_builtin_method }); + let dollar_ref = DollarRef { name, builtin, write }; + + let state = self.builtin_host_tokens(&dollar_ref); + m.mac.tokens = if write { + expand_write_method(state) } else { - quote_spanned! { host.span() => #host.#builtin_member(ctx!()) } + expand_read(state) }; - self.refs.push(DollarRef { - name: builtin_name, - builtin_shadow: Some(parse_quote! { #host.#builtin_member(ctx!()) }), - }); - self.last() + mark_macro_expanded(&mut m.mac); + + self.add_dollar_ref(dollar_ref); + self.current_dollar_scope().last() + } + + fn new_local_var(&mut self, name: &Ident) { + self.variable_stacks.last_mut().unwrap().push(name.clone()) + } + + fn add_dollar_ref(&mut self, dollar_ref: DollarRef) { + // local variable is not a outside reference. + if !self.is_local_var(dollar_ref.host()) { + let scope = self.scopes.last_mut().expect("no dollar refs scope"); + scope.refs.push(dollar_ref); + } + } + + fn current_dollar_scope(&self) -> &DollarRefsScope { + self.scopes.last().expect("no dollar refs scope") + } + + fn current_dollar_scope_mut(&mut self) -> &mut DollarRefsScope { + self.scopes.last_mut().expect("no dollar refs scope") + } + + fn is_local_var(&self, name: &Ident) -> bool { + let stack_idx = self.capture_level_heads.last().copied().unwrap_or(0); + self.variable_stacks[stack_idx..] + .iter() + .any(|stack| stack.contains(name)) + } +} + +impl DollarRefsScope { + pub fn used_ctx(&self) -> bool { self.used_ctx } + + pub fn upstream_tokens(&self) -> TokenStream { + match self.len() { + 0 => quote! {}, + 1 => { + let DollarRef { name, .. } = &self.refs[0]; + quote_spanned! { name.span() => #name.modifies() } + } + _ => { + let upstream = self + .iter() + .map(|DollarRef { name, .. }| quote! { #name.modifies() }); + quote! { observable::from_iter([#(#upstream),*]).merge_all(usize::MAX) } + } + } } } -fn parse_clean_dollar_macro(mac: &Macro) -> Option { +impl DollarRef { + pub fn host(&self) -> &Ident { + self + .builtin + .as_ref() + .map_or_else(|| &self.name, |b| &b.host) + } +} + +fn parse_dollar_macro(mac: &Macro) -> Option { if mac.path.is_ident(KW_DOLLAR_STR) { - // parse fail may occur because the macro is already fold by `DollarRefs`. - mac.parse_body::().ok() + Some(mac.parse_body::().unwrap()) } else { None } } -impl std::ops::Deref for DollarRefs { +impl std::ops::Deref for DollarRefsScope { type Target = [DollarRef]; fn deref(&self) -> &Self::Target { &self.refs } } @@ -332,3 +610,40 @@ impl Parse for DollarMacro { }) } } + +impl<'a> std::ops::Deref for StackGuard<'a> { + type Target = DollarRefsCtx; + 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 DollarRefsCtx { + fn default() -> Self { + Self { + scopes: smallvec![DollarRefsScope::default()], + capture_level_heads: smallvec![], + variable_stacks: vec![vec![]], + } + } +} + +pub fn not_subscribe_anything(span: Span) -> TokenStream { + quote_spanned!(span => + compile_error!("expression not subscribe anything, it must contain at least one $") + ) +} + +fn is_state_write_method(m: &ExprMethodCall) -> bool { + m.method == "write" || m.method == "silent" || m.method == "shallow" +} + +fn expand_write_method(host: TokenStream) -> TokenStream { host } + +fn expand_read(name: TokenStream) -> TokenStream { quote_spanned!(name.span() => #name.read()) } diff --git a/macros/src/watch_macro.rs b/macros/src/watch_macro.rs index 2c4e27b9c..d3b48d359 100644 --- a/macros/src/watch_macro.rs +++ b/macros/src/watch_macro.rs @@ -1,34 +1,46 @@ -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::{not_subscribe_anything, symbol_to_macro, DollarRefsCtx, DollarRefsScope}, }; +use proc_macro::TokenStream as TokenStream1; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn::{fold::Fold, parse_macro_input, spanned::Spanned, Stmt}; pub(crate) struct WatchMacro { - refs: DollarRefs, + refs: DollarRefsScope, 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, refs_ctx: &mut DollarRefsCtx) -> TokenStream1 { + let span = input.span(); + let input = ok!(symbol_to_macro(TokenStream1::from(input))); + let expr = parse_macro_input! { input as BodyExpr }; + refs_ctx.new_dollar_scope(true); + let expr = expr.0.into_iter().map(|s| refs_ctx.fold_stmt(s)).collect(); + let refs = refs_ctx.pop_dollar_scope(true); + if refs.is_empty() { + not_subscribe_anything(span).into() + } else { + 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(); if refs.used_ctx() { quote! {{ #refs - let _ctx_handle = ctx!().handle(); + let _ctx_handle_ಠ_ಠ = ctx!().handle(); #upstream - .map(move |_| _ctx_handle.with_ctx(|ctx!(): &BuildCtx<'_>| { #(#expr)* })) + .map(move |_| _ctx_handle_ಠ_ಠ.with_ctx(|ctx!(): &BuildCtx<'_>| { #(#expr)* })) }} .to_tokens(tokens) } else { diff --git a/macros/src/widget_macro/code_gen.rs b/macros/src/widget_macro/code_gen.rs index 78c563765..7ab0225f4 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); @@ -442,7 +442,7 @@ impl DeclareObj { Paren(value.span()).surround(tokens, |tokens| value.to_tokens(tokens)) }); let build_ctx = ctx_ident(); - tokens.extend(quote_spanned! { span => .build(#build_ctx) }); + tokens.extend(quote_spanned! { span => .build_declare(#build_ctx) }); }; let builder = |tokens: &mut TokenStream| { let is_stateful = *stateful || !watch_stmts.is_empty(); 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/macros/src/writer_map_macro.rs b/macros/src/writer_map_macro.rs new file mode 100644 index 000000000..a88699208 --- /dev/null +++ b/macros/src/writer_map_macro.rs @@ -0,0 +1,107 @@ +use crate::{ok, symbol_process::symbol_to_macro}; +use proc_macro::{TokenStream as TokenStream1, TokenTree}; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{quote, quote_spanned, ToTokens}; +use syn::{ + fold::Fold, + parse::{Parse, ParseStream}, + parse_macro_input, parse_quote, + spanned::Spanned, + Expr, ExprMacro, Result, +}; + +use crate::symbol_process::DollarRefsCtx; + +pub fn gen_map_path_writer(input: TokenStream, refs_ctx: &mut DollarRefsCtx) -> TokenStream1 { + gen_path_partial_writer(input, "map_writer", refs_ctx) +} + +pub fn gen_split_path_writer(input: TokenStream, refs_ctx: &mut DollarRefsCtx) -> TokenStream1 { + gen_path_partial_writer(input, "split_writer", refs_ctx) +} + +fn gen_path_partial_writer( + input: TokenStream, + method_name: &'static str, + refs_ctx: &mut DollarRefsCtx, +) -> TokenStream1 { + fn first_dollar_err(span: Span) -> TokenStream1 { + quote_spanned! { span => + compile_error!("expected `$` as the first token, and only one `$` is allowed") + } + .into() + } + + let mut input = TokenStream1::from(input).into_iter(); + let first = input.next(); + let is_first_dollar = first.as_ref().map_or( + false, + |f| matches!(f, TokenTree::Punct(p) if p.as_char() == '$'), + ); + if !is_first_dollar { + first_dollar_err( + first + .as_ref() + .map_or(Span::call_site(), |t| t.span().into()), + ); + } + + let input = ok!(symbol_to_macro(first.into_iter().chain(input))); + + let expr = parse_macro_input! { input as Expr }; + // Although `split_writer!` and `map_writer!` are not a capture scope, but we + // start a new capture scope to ensure found the dollar in the macro. We will + // not use the result of the `$var`, so it ok. + refs_ctx.new_dollar_scope(true); + let expr = refs_ctx.fold_expr(expr); + let refs = refs_ctx.pop_dollar_scope(true); + + if refs.len() != 1 { + quote_spanned! { expr.span() => + compile_error!("expected `$` as the first token, and only one `$` is allowed") + } + .into() + } else { + let dollar_ref = &refs[0]; + let host = if dollar_ref.builtin.is_some() { + refs_ctx.builtin_host_tokens(dollar_ref) + } else { + dollar_ref.name.to_token_stream() + }; + + let path: RouterPath = parse_quote!(#expr); + RouterMacro { host, path: path.0, method_name } + .to_token_stream() + .into() + } +} + +struct RouterPath(TokenStream); + +impl Parse for RouterPath { + fn parse(input: ParseStream) -> Result { + input.parse::()?; + Ok(Self(input.parse()?)) + } +} + +struct RouterMacro { + host: TokenStream, + path: TokenStream, + method_name: &'static str, +} + +impl ToTokens for RouterMacro { + fn to_tokens(&self, tokens: &mut TokenStream) { + let Self { host, path, method_name } = self; + let method = Ident::new(method_name, Span::call_site()); + + quote!( + #host.#method( + move |origin: &_| &origin #path, + move |origin: &mut _| &mut origin #path + ) + ) + .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 8cd99193a..7d2edae05 100644 --- a/ribir/src/app.rs +++ b/ribir/src/app.rs @@ -47,18 +47,15 @@ impl App { /// create a new window with the `root` widget and return the window id. #[track_caller] - pub fn new_window(root: Widget, size: Option) -> WindowId { + pub fn new_window(root: Widget, size: Option) -> Rc { let app = unsafe { App::shared_mut() }; let shell_wnd = WinitShellWnd::new(size, &app.event_loop); - let wnd = Window::new(root, Box::new(shell_wnd)); - let id = wnd.id(); - AppCtx::windows().borrow_mut().insert(id, wnd); - AppCtx::get_window(id).unwrap().emit_events(); + let wnd = AppCtx::new_window(Box::new(shell_wnd), root); if app.active_wnd.is_none() { - app.active_wnd = Some(id); + app.active_wnd = Some(wnd.id()); } - id + wnd } /// Get a event sender of the application event loop, you can use this to send @@ -129,17 +126,19 @@ impl App { } Event::MainEventsCleared => { AppCtx::run_until_stalled(); - AppCtx::windows().borrow().values().for_each(|wnd| { - if wnd.need_draw() { - let wnd = wnd.shell_wnd().borrow(); - let shell = wnd.as_any().downcast_ref::().unwrap(); - shell.winit_wnd.request_redraw(); - } - }) + AppCtx::windows() + .borrow() + .values() + .filter(|wnd| wnd.need_draw()) + .for_each(|wnd| request_redraw(wnd)) } Event::RedrawRequested(id) => { if let Some(wnd) = AppCtx::get_window(new_id(id)) { - wnd.draw_frame() + // if this frame is really draw, request another redraw. To make sure the draw + // always end with a empty draw and emit an extra tick cycle message. + if wnd.draw_frame() { + request_redraw(&wnd); + } } } Event::RedrawEventsCleared => { @@ -217,3 +216,9 @@ impl RuntimeWaker for EventWaker { /// EventWaker only send `RibirEvent::FuturesWake`. unsafe impl Send for EventWaker {} + +fn request_redraw(wnd: &Window) { + let wnd = wnd.shell_wnd().borrow(); + let shell = wnd.as_any().downcast_ref::().unwrap(); + shell.winit_wnd.request_redraw(); +} diff --git a/ribir/tests/timer_test.rs b/ribir/tests/timer_test.rs index cfc052ce0..befee4648 100644 --- a/ribir/tests/timer_test.rs +++ b/ribir/tests/timer_test.rs @@ -2,7 +2,8 @@ use ribir::core::timer::Timer; use rxrust::scheduler::NEW_TIMER_FN; mod test_single_thread { - use futures::executor::LocalPool; + use super::*; + use ribir_core::reset_test_env; use ribir_core::test_helper::TestWindow; use ribir_dev_helper::*; use std::{cell::RefCell, rc::Rc}; @@ -59,10 +60,12 @@ mod test_single_thread { (wnd, count) } - fn run_until(local_pool: &mut LocalPool, cond: impl Fn() -> bool) { + fn run_until(wnd: &TestWindow, cond: impl Fn() -> bool) { loop { - super::Timer::wake_timeout_futures(); - local_pool.run_until_stalled(); + Timer::wake_timeout_futures(); + AppCtx::run_until_stalled(); + wnd.run_frame_tasks(); + if (cond)() { break; } @@ -71,20 +74,21 @@ mod test_single_thread { } pub fn test_double_tap() { + reset_test_env!(); let (wnd, count) = env(2); + let c_wnd = wnd.clone(); - let mut local_pool = LocalPool::new(); let device_id = unsafe { DeviceId::dummy() }; let is_complete = Rc::new(RefCell::new(false)); let is_complete2 = is_complete.clone(); - observable::interval(Duration::from_millis(10), local_pool.spawner()) + observable::interval(Duration::from_millis(10), AppCtx::scheduler()) .take(8) .on_complete(move || { *is_complete.borrow_mut() = true; }) .subscribe(move |i| { #[allow(deprecated)] - wnd.processes_native_event(WindowEvent::MouseInput { + c_wnd.processes_native_event(WindowEvent::MouseInput { device_id, state: if i % 2 == 0 { ElementState::Pressed @@ -94,23 +98,24 @@ mod test_single_thread { button: MouseButton::Left, modifiers: ModifiersState::default(), }); - wnd.emit_events(); }); - run_until(&mut local_pool, || *is_complete2.borrow()); + run_until(&wnd, || *is_complete2.borrow()); assert_eq!(*count.borrow(), 2); let (wnd, count) = env(2); + let c_wnd = wnd.clone(); + let is_complete = Rc::new(RefCell::new(false)); let is_complete2 = is_complete.clone(); - observable::interval(Duration::from_millis(251), local_pool.spawner()) + observable::interval(Duration::from_millis(251), AppCtx::scheduler()) .take(8) .on_complete(move || { *is_complete.borrow_mut() = true; }) .subscribe(move |i| { #[allow(deprecated)] - wnd.processes_native_event(WindowEvent::MouseInput { + c_wnd.processes_native_event(WindowEvent::MouseInput { device_id, state: if i % 2 == 0 { ElementState::Pressed @@ -120,28 +125,28 @@ mod test_single_thread { button: MouseButton::Left, modifiers: ModifiersState::default(), }); - wnd.emit_events(); }); - run_until(&mut local_pool, || *is_complete2.borrow()); + run_until(&wnd, || *is_complete2.borrow()); assert_eq!(*count.borrow(), 0); } pub fn test_tripe_tap() { + reset_test_env!(); let (wnd, count) = env(3); + let c_wnd = wnd.clone(); - let mut local_pool = LocalPool::new(); let device_id = unsafe { DeviceId::dummy() }; let is_complete = Rc::new(RefCell::new(false)); let is_complete2 = is_complete.clone(); - observable::interval(Duration::from_millis(10), local_pool.spawner()) + observable::interval(Duration::from_millis(10), AppCtx::scheduler()) .take(12) .on_complete(move || { *is_complete.borrow_mut() = true; }) .subscribe(move |i| { #[allow(deprecated)] - wnd.processes_native_event(WindowEvent::MouseInput { + c_wnd.processes_native_event(WindowEvent::MouseInput { device_id, state: if i % 2 == 0 { ElementState::Pressed @@ -151,10 +156,9 @@ mod test_single_thread { button: MouseButton::Left, modifiers: ModifiersState::default(), }); - wnd.emit_events(); }); - run_until(&mut local_pool, || *is_complete2.borrow()); + run_until(&wnd, || *is_complete2.borrow()); assert_eq!(*count.borrow(), 2); } 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/declare_builder_test.rs b/tests/declare_builder_test.rs index 2621f8cd9..f55d3d45e 100644 --- a/tests/declare_builder_test.rs +++ b/tests/declare_builder_test.rs @@ -7,7 +7,7 @@ fn declare_builder_smoke() { // empty struct #[derive(Declare)] struct A; - let _: A = ADeclarer {}.build(dummy_ctx()); + let _: A = ADeclarer {}.build_declare(dummy_ctx()); #[derive(Declare)] struct B { @@ -18,7 +18,7 @@ fn declare_builder_smoke() { let b = ::declare_builder() .a(1.) .b(1) - .build(dummy_ctx()); + .build_declare(dummy_ctx()); assert_eq!(b.a, 1.); assert_eq!(b.b, 1); } @@ -31,7 +31,7 @@ fn panic_if_miss_require_field() { _a: f32, } - let _ = ::declare_builder().build(dummy_ctx()); + let _ = ::declare_builder().build_declare(dummy_ctx()); } #[test] @@ -43,7 +43,7 @@ fn default_field() { a: f32, } - let t = ::declare_builder().build(dummy_ctx()); + let t = ::declare_builder().build_declare(dummy_ctx()); assert_eq!(t.a, 0.); } @@ -56,6 +56,6 @@ fn default_field_with_value() { text: &'static str, } - let t = ::declare_builder().build(dummy_ctx()); + let t = ::declare_builder().build_declare(dummy_ctx()); assert_eq!(t.text, "hi!"); } diff --git a/tests/rdl_macro_test.rs b/tests/rdl_macro_test.rs index c066269c3..7df49c23b 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 { @@ -180,29 +180,29 @@ fn pipe_with_builtin_field() -> impl Into { } widget_layout_test!(pipe_with_builtin_field, width == 4., height == 2.,); -fn capture_closure_used_ctx() -> impl Into { - fn_widget! { - let mut size_box = @SizedBox { size: ZERO_SIZE }; - @ $size_box { - on_mounted: move |_| $size_box.size = IconSize::of(ctx!()).tiny - } - } -} -widget_layout_test!(capture_closure_used_ctx, width == 18., height == 18.,); +// fn capture_closure_used_ctx() -> impl Into { +// fn_widget! { +// let mut size_box = @SizedBox { size: ZERO_SIZE }; +// @ $size_box { +// on_mounted: move |_| $size_box.size = IconSize::of(ctx!()).tiny +// } +// } +// } +// widget_layout_test!(capture_closure_used_ctx, width == 18., height == 18.,); #[test] 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.); let blank = pipe! { if *$outside_blank { - Box::new(rdl! { Margin { margin: edges } }) as Box + Box::new(Margin { margin: edges }) as Box } else { - Box::new(rdl! { Padding { padding: edges } }) as Box + Box::new(Padding { padding: edges }) as Box } }; rdl! { @@ -225,11 +225,11 @@ 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! { - let c: Box = if *$stack_or_flex { + let c: Box = if *$stack_or_flex { Box::new(rdl! { Stack { } }) } else { Box::new(rdl! { Flex { } }) @@ -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..d490d5ff8 100644 --- a/themes/material/src/lib.rs +++ b/themes/material/src/lib.rs @@ -252,15 +252,11 @@ fn init_custom_style(theme: &mut FullTheme) { fn override_compose_decorator(theme: &mut FullTheme) { fn scrollbar_thumb(host: Widget, margin: EdgeInsets) -> Widget { - widget! { - init ctx => { - let background = Palette::of(ctx).primary(); - } - DynWidget { - dyns: host, + fn_widget! { + @$host { margin, border_radius: Radius::all(4.), - background + background: Palette::of(ctx!()).primary(), } } .into() @@ -268,142 +264,167 @@ 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.)) - } + 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 = map_writer!($thumb.left_anchor); + left_trans.transition_with( + transitions::LINEAR.of(ctx!()), + move |from, to, rate| PositionUnit::lerp(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.)) - } + 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 = map_writer!($thumb.top_anchor); + top_trans.transition_with( + transitions::LINEAR.of(ctx!()), + move |from, to, rate| PositionUnit::lerp(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., + 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, + }; + + let ease_in = transitions::EASE_IN.of(ctx!()); + match $style.pos { + Position::Top | Position::Bottom => { + let left = map_writer!($indicator.left_anchor); + left.transition_with( + ease_in.clone(), + move |from, to, rate| PositionUnit::lerp(from, to, rate, $style.rect.width()), + ctx!() + ); + + } + Position::Left | Position::Right => { + let top = map_writer!($indicator.top_anchor); + top.transition_with( + ease_in, + move |from, to, rate| PositionUnit::lerp(from, to, rate, $style.rect.height()), + ctx!() + ); + } } - 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 } + + indicator } .into() }); styles.override_compose_decorator::(move |style, host| { - widget! { - states { style } - Ripple { + fn_widget! { + @Ripple { center: true, - color: style.color, + color: pipe!($style.color), radius: 24., bounded: RippleBound::Unbounded, - InteractiveLayer { - color: style.color, border_radii: Radius::all(24.), - DynWidget { dyns: host, margin: EdgeInsets::all(12.) } + @InteractiveLayer { + color: pipe!($style.color), + border_radii: Radius::all(24.), + @$host { + margin: EdgeInsets::all(12.) + } } } } .into() }); styles.override_compose_decorator::(move |style, host| { - widget! { - init ctx => { - let palette1 = Palette::of(ctx).clone(); - let palette2 = Palette::of(ctx).clone(); - } - states { style } - Ripple { + fn_widget! { + @Ripple { center: false, - color: palette1.on_of(&palette1.base_of(&style.color)), + color: { + let palette = Palette::of(ctx!()).clone(); + pipe!(palette.on_of(&palette.base_of(&$style.color))) + }, bounded: RippleBound::Radius(Radius::all(20.)), - InteractiveLayer { - color: palette2.on_of(&palette2.base_of(&style.color)), border_radii: Radius::all(20.), - DynWidget { dyns: host, margin: EdgeInsets::all(0.) } + @InteractiveLayer { + border_radii: Radius::all(20.), + color: { + let palette = Palette::of(ctx!()).clone(); + pipe!(palette.on_of(&palette.base_of(&$style.color))) + }, + @$host { + margin: EdgeInsets::all(0.) + } } } } .into() }); styles.override_compose_decorator::(move |style, host| { - widget! { - init ctx => { - let palette1 = Palette::of(ctx).clone(); - let palette2 = Palette::of(ctx).clone(); - } - states { style } - Ripple { + fn_widget! { + @Ripple { center: false, - color: palette1.base_of(&style.color), + color: { + let palette = Palette::of(ctx!()).clone(); + pipe!(palette.base_of(&$style.color)) + }, bounded: RippleBound::Radius(Radius::all(20.)), - InteractiveLayer { - color: palette2.base_of(&style.color), border_radii: Radius::all(20.), - DynWidget { dyns: host, margin: EdgeInsets::all(0.) } + @InteractiveLayer { + border_radii: Radius::all(20.), + color: { + let palette = Palette::of(ctx!()).clone(); + pipe!(palette.base_of(&$style.color)) + }, + @$host { + margin: EdgeInsets::all(0.) + } } } } .into() }); styles.override_compose_decorator::(move |style, host| { - widget! { - init ctx => { - let palette1 = Palette::of(ctx).clone(); - let palette2 = Palette::of(ctx).clone(); - } - states { style } - Ripple { + fn_widget! { + @Ripple { center: false, - color: palette1.base_of(&style.color), + color: { + let palette = Palette::of(ctx!()).clone(); + pipe!(palette.on_of(&palette.base_of(&$style.color))) + }, bounded: RippleBound::Radius(Radius::all(20.)), - InteractiveLayer { - color: palette2.base_of(&style.color), border_radii: Radius::all(20.), - DynWidget { dyns: host, margin: EdgeInsets::all(0.) } + @InteractiveLayer { + border_radii: Radius::all(20.), + color: { + let palette = Palette::of(ctx!()).clone(); + pipe!(palette.on_of(&palette.base_of(&$style.color))) + }, + @$host { + margin: EdgeInsets::all(0.) + } } } } diff --git a/themes/material/src/ripple.rs b/themes/material/src/ripple.rs index aecaff681..4c4bb16f0 100644 --- a/themes/material/src/ripple.rs +++ b/themes/material/src/ripple.rs @@ -20,12 +20,12 @@ pub struct Ripple { /// How ripples show outside of the host widget box. pub bounded: RippleBound, /// The position of current animate launch start. - #[declare(skip)] - launch_pos: Option, + #[declare(default = Stateful::new(None))] + ripple_at: Stateful>, } /// 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,81 @@ 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 mut ripple_at = $this.ripple_at.clone_state(); + + let ripple_widget = pipe!(*$ripple_at) + .map(move |launch_at| { + let launch_at = 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 = @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( + route_state!($ripple.path), + move |_, _, rate| { let radius = Lerp::lerp(&0., &radius, rate); - let center = this.launch_pos.clone().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(); - }); + Path::circle(launch_at, radius) + } + ), + from: Path::circle(Point::zero(), 0.) + }.into_inner(); + + watch!(!$container.pointer_pressed() && !$ripper_enter.is_running()) + .filter(|b| *b) + // the ripple used only once, so we unsubscribe it after the animate finished. + .take(1) + .subscribe(move |_| { + $ripple_at.take(); + }); + + + let mut ripper_fade_out = route_state!($ripple.opacity) + .transition(transitions::EASE_OUT.of(ctx!()), 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) } + }); + + Some(@IgnorePointer { + delay_drop_until: pipe!(!$ripper_fade_out.is_running()), + on_disposed: move |_| $ripple.opacity = 0., + on_mounted: move |_| { ripper_enter.run(); }, + @Container { + size: $container.layout_size(), + @$clipper { @ { ripple } } } - } - }) + }) + }); + + @ $container { + on_pointer_down: move |e| *$ripple_at = if $this.center { + let center = $container.layout_size() / 2.; + Some(Point::new(center.width, center.height)) + } else { + Some(e.position()) + }, + @{ child } + @{ ripple_widget } } } .into() @@ -127,5 +121,5 @@ impl ComposeChild for Ripple { impl Ripple { /// Manual launch a ripple animate at `pos`. - pub fn launch_at(&mut self, pos: Point) { self.launch_pos = Some(pos); } + pub fn launch_at(&mut self, pos: Point) { *self.ripple_at.state_ref() = Some(pos); } } diff --git a/themes/material/src/state_layer.rs b/themes/material/src/state_layer.rs index a5873fdc4..94ec49130 100644 --- a/themes/material/src/state_layer.rs +++ b/themes/material/src/state_layer.rs @@ -4,7 +4,7 @@ use ribir_widgets::{layout::Stack, path::PathPaintKit}; /// Widget that as an visual indicator of material design used to present the /// interactive status of its child. -#[derive(Declare)] +#[derive(Declare, Declare2)] pub struct StateLayer { pub color: Color, pub path: Path, @@ -23,14 +23,13 @@ pub struct InteractiveLayer { } impl Compose for StateLayer { - fn compose(this: State) -> Widget { - widget!( - states { this: this.into_readonly() } - PathPaintKit { - path: this.path.clone(), - brush: this.role.calc_color(this.color), + fn compose(mut this: State) -> Widget { + fn_widget! { + @PathPaintKit { + path: pipe!($this.path.clone()), + brush: pipe!($this.role.calc_color($this.color)), } - ) + } .into() } } @@ -38,31 +37,33 @@ impl Compose for StateLayer { impl ComposeChild for InteractiveLayer { type Child = Widget; - fn compose_child(this: State, child: Self::Child) -> Widget { - widget! { - states { this: this.into_readonly() } - Stack { - fit: StackFit::Passthrough, - DynWidget { id: host, dyns: child } - IgnorePointer { - Container { - size: host.layout_size(), - StateLayer { - color: this.color, - path: Path::rect_round(&host.layout_rect(), &this.border_radii), - role: if host.pointer_pressed() { - StateRole::pressed() - } else if host.has_focus() { - StateRole::focus() - } else if host.mouse_hover() { - StateRole::hover() - } else { - // todo: not support drag & drop now - StateRole::custom(0.) - } - } + fn compose_child(mut this: State, child: Self::Child) -> Widget { + fn_widget! { + let mut host = @$child { }; + let layer = @IgnorePointer { + @Container { + size: pipe!($host.layout_size()), + @StateLayer { + color: pipe!($this.color), + path: pipe!(Path::rect_round(&$host.layout_rect(), &$this.border_radii)), + role: pipe!(if $host.pointer_pressed() { + StateRole::pressed() + } else if $host.has_focus() { + StateRole::focus() + } else if $host.mouse_hover() { + StateRole::hover() + } else { + // todo: not support drag & drop now + StateRole::custom(0.) + }) } } + }; + + @Stack { + fit: StackFit::Passthrough, + @{ host } + @{ layer } } } .into() diff --git a/widgets/src/buttons.rs b/widgets/src/buttons.rs index 8ca457a82..daeb5d8df 100644 --- a/widgets/src/buttons.rs +++ b/widgets/src/buttons.rs @@ -50,38 +50,36 @@ pub struct ButtonTemplate { impl ComposeChild for ButtonImpl { type Child = ButtonTemplate; - fn compose_child(this: State, child: Self::Child) -> Widget { + fn compose_child(mut this: State, child: Self::Child) -> Widget { let ButtonTemplate { icon, label } = child; - widget! { - states { this: this.into_readonly() } - BoxDecoration { - border_radius: this.radius.map(Radius::all), - background: this.background_color.clone(), - border: this.border_style.clone(), - ConstrainedBox { - clamp: BoxClamp::min_width(this.min_width).with_fixed_height(this.height), - DynWidget { - dyns: Option::map(this.padding_style, |padding| Padding { padding }), - Row { - justify_content: JustifyContent::Center, - align_items: Align::Center, - Option::map(icon, |icon| widget! { - Icon { - size: this.icon_size, - widget::from(icon) - } - }) - Option::map(label, |label| widget! { - states { text: label.into_readonly() } - Margin { - margin: EdgeInsets::horizontal(this.label_gap), - Text { - text: text.0.clone(), - foreground: this.foreground_color.clone(), - text_style: this.label_style.clone() - } - } - }) + fn_widget! { + @BoxDecoration { + border_radius: pipe!($this.radius.map(Radius::all)), + background: pipe!($this.background_color.clone()), + border: pipe!($this.border_style.clone()), + @ConstrainedBox { + clamp: pipe!(BoxClamp::min_width($this.min_width) + .with_fixed_height($this.height)), + @{ + let padding = pipe!($this.padding_style.map(|padding| Padding { padding })); + let icon = icon.map(|icon| @Icon { + size: pipe!($this.icon_size), + @{ icon } + }); + let label = label.map(|mut label| @Text { + margin: pipe!(EdgeInsets::horizontal($this.label_gap)), + text: pipe!($label.0.clone()), + foreground: pipe!($this.foreground_color.clone()), + text_style: pipe!($this.label_style.clone()) + }); + + @$ padding { + @Row { + justify_content: JustifyContent::Center, + align_items: Align::Center, + @ { icon } + @{ label } + } } } } 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

EdgeWidget

-where - P: TmlFlag + Default, -{ +impl EdgeWidget { fn compose_with_style(self, config: EdgeWidgetStyle) -> Widget { let EdgeWidgetStyle { icon, @@ -206,7 +203,7 @@ where dyns: icon.gap.map(|margin| Margin { margin }), Icon { size: icon.size, - widget::from(w.decorate(|_, c| c.into())) + widget::from(w) } } } @@ -214,21 +211,21 @@ where EdgeWidget::Text(w) => widget! { DynWidget { dyns: text.gap.map(|margin| Margin { margin }), - widget::from(w.decorate(|_, label| widget!{ - states { label: label.into_readonly() } + widget!{ + states { label: w.into_readonly() } Text { text: label.0.clone(), text_style: text.style.clone(), foreground: text.foreground.clone(), } - }.into())) + } } } .into(), EdgeWidget::Avatar(w) => widget! { DynWidget { dyns: avatar.gap.map(|margin| Margin { margin }), - DecorateTml::decorate(w, |_, c| c.into()) + widget::from(w) } } .into(), @@ -239,7 +236,7 @@ where size: image.size, DynWidget { box_fit: BoxFit::None, - dyns: w.decorate(|_, c| c.into()) + dyns: w } } } @@ -252,7 +249,7 @@ where size: poster.size, DynWidget { box_fit: BoxFit::None, - dyns: w.decorate(|_, c| c.0.into()) + dyns: w.0 } } } @@ -261,7 +258,7 @@ where EdgeWidget::Custom(w) => widget! { DynWidget { dyns: custom.gap.map(|margin| Margin { margin }), - widget::from(w.decorate(|_, c| c.0)) + widget::from(w.0) } } .into(), @@ -273,78 +270,83 @@ where pub struct ListItemTml { headline: State, supporting: Option>, - leading: Option>, - trailing: Option>, + leading: Option>>, + trailing: Option>>, } impl ComposeChild for ListItem { type Child = ListItemTml; - fn compose_child(this: State, child: Self::Child) -> Widget { + fn compose_child(mut this: State, child: Self::Child) -> Widget { let ListItemTml { - headline, + mut headline, supporting, leading, trailing, } = child; - widget! { - states { - this: this.into_readonly(), - headline: headline.into_readonly(), - } - init ctx => { - let palette = Palette::of(ctx); - let on_surface: Brush = palette.on_surface().clone().into(); - let on_surface_variant: Brush = palette.on_surface_variant().clone().into(); - let ListItemStyle { - padding_style, - label_gap, - headline_style, - supporting_style, - leading_config, - trailing_config, - item_align, - } = ListItemStyle::of(ctx).clone(); - let TextStyle { line_height, font_size, .. } = *supporting_style.clone(); - let line_height = line_height - .map_or(font_size, FontSize::Em) - .into_pixel(); - let text_height = line_height * this.line_number as f32; - } - ListItemDecorator { - color: this.active_background, + fn_widget! { + let palette = Palette::of(ctx!()); + let ListItemStyle { + padding_style, + label_gap, + headline_style, + supporting_style, + leading_config, + trailing_config, + item_align, + } = ListItemStyle::of(ctx).clone(); + + let padding = padding_style.map(|padding| Padding { padding }); + let label_gap = label_gap.map(|padding| Padding { padding }); + + @ListItemDecorator { + color: pipe!($this.active_background), is_active: false, - DynWidget { - dyns: padding_style.map(|padding| Padding { padding }), - Row { - align_items: item_align(this.line_number), - Option::map(leading, |w| w.compose_with_style(leading_config)) - Expanded { + @ $padding { + @Row { + align_items: pipe!(item_align($this.line_number)), + @{ + leading.map(|w| { + let (SinglePair { child,.. }, builtin) = w.unzip(); + builtin.compose_with_host(child.compose_with_style(leading_config), ctx!()) + }) + } + @Expanded { flex: 1., - DynWidget { - dyns: label_gap.map(|padding| Padding { padding }), - Column { - Text { - text: headline.0.0.clone(), - foreground: on_surface, - text_style: headline_style.clone(), + @ $label_gap { + @Column { + @Text { + text: pipe!($headline.0.0.clone()), + foreground: palette.on_surface().clone(), + text_style: headline_style, } - Option::map(supporting, |supporting| widget! { - states { supporting: supporting.into_readonly() } - ConstrainedBox { - clamp: BoxClamp::fixed_height(*text_height.0), - Text { - text: supporting.0.0.clone(), - foreground: on_surface_variant.clone(), - text_style: supporting_style.clone(), + @{ supporting.map(|mut supporting| { + @ConstrainedBox { + clamp: { + let TextStyle { line_height, font_size, .. } = &*supporting_style; + let line_height = line_height.map_or(*font_size, FontSize::Em).into_pixel(); + pipe!{ + let text_height = line_height * $this.line_number as f32; + BoxClamp::fixed_height(*text_height.0) + } + } , + @Text { + text: pipe!($supporting.0.0.clone()), + foreground: palette.on_surface_variant().clone(), + text_style: supporting_style, } } - }) + })} } } } - Option::map(trailing, |w| w.compose_with_style(trailing_config)) + @{ + trailing.map(|w| { + let (SinglePair { child,.. }, builtin) = w.unzip(); + builtin.compose_with_host(child.compose_with_style(trailing_config), ctx!()) + }) + } } } } @@ -451,7 +453,7 @@ impl CustomStyle for ListItemStyle { } } -#[derive(Clone, Declare)] +#[derive(Clone, Declare, Declare2)] pub struct ListItemDecorator { pub color: Color, pub is_active: bool, diff --git a/widgets/src/path.rs b/widgets/src/path.rs index 87ca6220b..0344284bb 100644 --- a/widgets/src/path.rs +++ b/widgets/src/path.rs @@ -2,7 +2,7 @@ use ribir_core::{impl_query_self_only, prelude::*}; /// Widget just use as a paint kit for a path and not care about its size. Use /// `[PathWidget]!` instead of. -#[derive(Declare, Clone)] +#[derive(Declare, Declare2, Clone)] pub struct PathPaintKit { pub path: Path, #[declare(convert=into)] diff --git a/widgets/src/text_field.rs b/widgets/src/text_field.rs index 79d36a3a9..29b802ef3 100644 --- a/widgets/src/text_field.rs +++ b/widgets/src/text_field.rs @@ -1,9 +1,4 @@ -use crate::{ - common_widget::{Leading, LeadingText, Trailing, TrailingText}, - input::Placeholder, - layout::{Column, ConstrainedBox, Container}, - prelude::{Expanded, Icon, Input, Label, Row, Stack, Text}, -}; +use crate::prelude::*; use ribir_core::prelude::*; use std::{collections::HashMap, hash::Hash, ops::Deref}; @@ -94,8 +89,8 @@ where fn get(&self, state: S) -> Option<&T> { self.themes.get(&state) } } -#[derive(Declare)] -struct ThemeSuitProxy +#[derive(Declare, Declare2)] +struct ThemeSuitProxy where S: Hash + Eq, { @@ -107,32 +102,27 @@ type TextFieldThemeProxy = ThemeSuitProxy; impl ComposeChild for TextFieldThemeProxy { type Child = Widget; - fn compose_child(this: State, child: Self::Child) -> Widget - where - Self: Sized, - { - widget! { - states {this: this.into_writable()} - DynWidget { - dyns: { - child - }, + fn compose_child(mut this: State, child: Self::Child) -> Widget { + fn_widget! { + @ $child { on_tap: move |_| { + let mut this = $this; match this.state { TextFieldState::Enabled => this.state = TextFieldState::Focused, TextFieldState::Hovered => this.state = TextFieldState::Focused, _ => (), }; }, - on_pointer_move: move |_| { + let mut this = $this; if this.state == TextFieldState::Enabled { this.state = TextFieldState::Hovered } }, - on_pointer_leave: move |_| { + let mut this = $this; if this.state == TextFieldState::Hovered { this.state = TextFieldState::Enabled } }, on_focus_out: move |_| { + let mut this = $this; if this.state == TextFieldState::Focused { this.state = TextFieldState::Enabled } }, } @@ -291,59 +281,46 @@ macro_rules! take_option_field { impl ComposeChild for TextField { type Child = Option; - fn compose_child(this: State, config: Self::Child) -> Widget - where - Self: Sized, - { - let mut config = config.unwrap_or_default(); - widget! { - states { - this: this.into_writable(), - } - init ctx => { - let theme_suit = TextFieldThemeSuit::of(ctx).clone(); - } - init { - take_option_field!({leading_icon, trailing_icon}, config); - } + fn compose_child(mut this: State, config: Self::Child) -> Widget { + fn_widget! { + let mut config = config.unwrap_or_default(); + take_option_field!({leading_icon, trailing_icon}, config); - TextFieldThemeProxy { - id: theme, + let theme_suit = TextFieldThemeSuit::of(ctx!()); + let mut theme = @TextFieldThemeProxy { suit: theme_suit, state: TextFieldState::default(), - - Stack { - Container { - size: Size::new(0., theme.container_height), - background: theme.container_color, - } - Row { - ConstrainedBox { - clamp: BoxClamp::EXPAND_Y, - DynWidget { - v_align: VAlign::Center, - dyns: build_icon(leading_icon.map(|l| l.child)) - } - } - Expanded { - flex: 1., - build_content_area(no_watch!(&mut this), no_watch!(&mut theme), config) - } - ConstrainedBox { - clamp: BoxClamp::EXPAND_Y, - DynWidget { - v_align: VAlign::Center, - dyns: build_icon(trailing_icon.map(|t| t.child)) - } - } - } - - Container { - v_align: VAlign::Bottom, - size: Size::new(f32::MAX, theme.indicator_height), - background: theme.indicator, - } + }; + @Stack { + @Container { + size: pipe!(Size::new(0., $theme.container_height)), + background: pipe!($theme.container_color), + } + @Row { + justify_content: JustifyContent::Center, + align_items: Align::Stretch, + @{ + leading_icon.map(|t| @Icon { + size: IconSize::of(ctx!()).small, + @{ t.child } + }) + } + @Expanded { + flex: 1., + @{ build_content_area(this.clone_state(), theme.clone_state(), config) } + } + @{ + trailing_icon.map(|t| @Icon { + size: IconSize::of(ctx!()).small, + @{ t.child } + }) } + } + @Container { + v_align: VAlign::Bottom, + size: pipe!(Size::new(f32::MAX, $theme.indicator_height)), + background: pipe!($theme.indicator), + } } } .into() @@ -351,169 +328,113 @@ impl ComposeChild for TextField { } fn build_input_area( - this: &mut StatefulRef, - theme: &mut StatefulRef, + mut this: Stateful, + mut theme: Stateful, prefix: Option, suffix: Option, placeholder: Option, ) -> Widget { - fn text_label(text: CowArc, theme: StatefulRef) -> Text { - Text { - text, - foreground: theme.foreground.clone(), - text_style: theme.text.clone(), - path_style: PathPaintStyle::Fill, - overflow: Overflow::Clip, - text_align: TextAlign::Start, - } - } + fn_widget! { + let mut input_area = @Row { + visible: pipe!(!$this.text.is_empty() || $theme.state == TextFieldState::Focused), + }; - widget! { - states { this: this.clone_stateful(), theme: theme.clone_stateful() } - init ctx => { - let linear = transitions::LINEAR.of(ctx); - let prefix = prefix.map(move |p| p.child); - let suffix = suffix.map(move|s| s.child); - } - Row { - id: input_area, - visible: !this.text.is_empty() || theme.state == TextFieldState::Focused, - Option::map(prefix.clone(), move |text| text_label(text, theme)) - Expanded { - flex: 1., - Input { - id: input, - style: theme.text.clone(), - widget::from(placeholder) - } - } - Option::map(suffix.clone(), move |text| text_label(text, theme)) + let mut visible = route_state!($input_area.visible); + visible.transition(transitions::LINEAR.of(ctx!()), ctx!()); - } - transition prop!(input_area.visible, move |_from, to, rate| *to && rate >= 1.) { - by: linear, - } + let mut input = @Input{ style: pipe!($theme.text.clone()) }; + $input.set_text($this.text.clone()); - finally { - input.set_text(this.text.clone()); - let_watch!(input.text()) - .distinct_until_changed() - .subscribe(move |val| { - this.silent().text = val; - }); - let_watch!(this.text.clone()) - .distinct_until_changed() - .subscribe(move |val| input.set_text(val)); - let_watch!(theme.state) - .distinct_until_changed() - .subscribe(move |state| { - if state == TextFieldState::Focused { - input.request_focus(); - } - }); + watch!($input.text()) + .distinct_until_changed() + .subscribe(move |val| $this.silent().text = val); + + watch!($this.text.clone()) + .distinct_until_changed() + .subscribe(move |val| $input.set_text(val)); + + let h = watch!($theme.state) + .distinct_until_changed() + .filter(|state| state == &TextFieldState::Focused) + .subscribe(move |_| $input.request_focus()) + .unsubscribe_when_dropped(); + input.own_data(h); + + + @Row { + @{ + prefix.map(|p| @Text{ + text: p.child, + foreground: pipe!($theme.foreground.clone()), + text_style: pipe!($theme.text.clone()), + }) + } + @Expanded { + flex: 1., + @ $input { @{placeholder} } + } + @{ + suffix.map(|s| @Text{ + text: s.child, + foreground: pipe!($theme.foreground.clone()), + text_style: pipe!($theme.text.clone()), + }) + } } } .into() } -#[derive(Declare)] +#[derive(Declare, Declare2)] struct TextFieldLabel { text: CowArc, style: CowArc, } impl Compose for TextFieldLabel { - fn compose(this: State) -> Widget { - widget! { - states { this: this.into_readonly() } - init ctx => { - let linear = transitions::LINEAR.of(ctx); - } - Text { - id: label, + fn compose(mut this: State) -> Widget { + fn_widget! { + let label = @Text { v_align: VAlign::Top, - text: this.text.clone(), - text_style: this.style.clone(), - } + text: pipe!($this.text.clone()), + text_style: pipe!($this.style.clone()), + }; - // todo: prop with inner field's property - // transition prop!(label.style.font_size) { - // by: transitions::LINEAR.of(ctx) - // } - transition prop!(label.text_style, move |from, to, rate| { - let from_size = from.font_size.into_pixel(); - let to_size = to.font_size.into_pixel(); - - let mut res = to.clone(); - res.to_mut().font_size = FontSize::Pixel(Pixel(from_size.0.lerp(&to_size.0, rate).into())); - res - }) { - by: linear, - } + let mut font_size = route_state!($this.style.font_size); + font_size.transition(transitions::LINEAR.of(ctx!()), ctx!()); + + label } .into() } } fn build_content_area( - this: &mut StatefulRef, - theme: &mut StatefulRef, + mut this: Stateful, + mut theme: Stateful, mut config: TextFieldTml, ) -> Widget { - widget! { - states { this: this.clone_stateful(), theme: theme.clone_stateful(), } - init ctx => { - let linear = transitions::LINEAR.of(ctx); - } - init { - take_option_field!({label, prefix, suffix, placeholder}, config); - } - Column { - id: content_area, - padding: theme.input_padding(this.text.is_empty()), - - DynWidget { - dyns: label.map(move |label| { - widget! { - Expanded { - flex: 1., - TextFieldLabel { - text: label.0.clone(), - style: theme.label_style(this.text.is_empty()), - } - } + fn_widget! { + take_option_field!({label, prefix, suffix, placeholder}, config); + let mut content_area = @Column { + padding: pipe!($theme.input_padding($this.text.is_empty())), + }; + + let mut padding = route_state!($content_area.padding); + padding.transition(transitions::LINEAR.of(ctx!()), ctx!()); + + @ $content_area { + @ { + label.map(|label| @Expanded { + flex: 1., + @TextFieldLabel { + text: label.0, + style: pipe!($theme.label_style($this.text.is_empty())), } }) } - build_input_area( - no_watch!(&mut this), - no_watch!(&mut theme), - prefix, - suffix, - placeholder - ) + @ { build_input_area(this.clone(), theme.clone(), prefix, suffix, placeholder)} } - - transition prop!(content_area.padding) { by: linear } } .into() } - -fn build_icon(icon: Option) -> Widget { - if icon.is_some() { - widget! { - init ctx => { - let icon_size = IconSize::of(ctx).small; - } - Icon { - size: icon_size, - DynWidget { - dyns: icon.unwrap() - } - } - } - .into() - } else { - Void.into() - } -}