diff --git a/Cargo.toml b/Cargo.toml index 4c6f3ed69..d7ff51800 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,7 +87,7 @@ usvg = {version = "0.33.0", default-features = false} webbrowser = "0.8.8" wgpu = {version = "0.16.0"} winit = {version = "0.28.5", default-features = false, features = ["x11", "wayland", "wayland-dlopen"]} -zerocopy = "0.7.0-alpha.3" +zerocopy = "0.7.3" [workspace.metadata.release] shared-version = true 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/algo/src/lib.rs b/algo/src/lib.rs index 272de592c..1caaa9fe2 100644 --- a/algo/src/lib.rs +++ b/algo/src/lib.rs @@ -4,3 +4,5 @@ pub use cow_rc::{CowArc, Substr}; pub use frame_cache::*; mod resource; pub use resource::*; +mod sc; +pub use sc::*; diff --git a/algo/src/sc.rs b/algo/src/sc.rs new file mode 100644 index 000000000..326cc7eb2 --- /dev/null +++ b/algo/src/sc.rs @@ -0,0 +1,227 @@ +use std::{ + any::Any, + cell::Cell, + fmt::{Debug, Display, Formatter, Pointer}, + ptr::NonNull, +}; + +/// A single-thread smart pointer with strong reference count only. +/// This is a simplified version of `std::rc::Sc` with the weak reference count. +/// Use it when you are sure that there is no cycle in the reference graph or in +/// a inner resource manage that will break the cycle by itself. +pub struct Sc(NonNull>); + +struct ScBox { + ref_cnt: Cell, + value: T, +} + +impl Sc { + /// Constructs a new `Sc`. + /// + /// # Examples + /// + /// ``` + /// use ribir_algo::Sc; + /// + /// let five = Sc::new(5); + /// ``` + #[inline] + pub fn new(value: T) -> Self { + Self::from_inner(Box::leak(Box::new(ScBox { ref_cnt: Cell::new(1), value })).into()) + } + + /// Returns the inner value, if the `Sc` has exactly one strong reference. + /// + /// Otherwise, an [`Err`] is returned with the same `Sc` that was + /// passed in. + /// + /// This will succeed even if there are outstanding weak references. + /// + /// # Examples + /// + /// ``` + /// use ribir_algo::Sc; + /// + /// let x = Sc::new(3); + /// assert_eq!(Sc::try_unwrap(x), Ok(3)); + /// + /// let x = Sc::new(4); + /// let _y = Sc::clone(&x); + /// assert_eq!(*Sc::try_unwrap(x).unwrap_err(), 4); + /// ``` + #[inline] + pub fn try_unwrap(this: Self) -> Result { + if Sc::ref_count(&this) == 1 { + unsafe { + let val = std::ptr::read(&*this); // copy the contained object + + // avoid to call `drop` but release the memory. + let layout = std::alloc::Layout::for_value(this.0.as_ref()); + let ptr = this.0.as_ptr(); + std::mem::forget(this); + std::alloc::dealloc(ptr as *mut _, layout); + + Ok(val) + } + } else { + Err(this) + } + } +} + +impl Sc { + /// + /// todo: prefer implement `CoerceUnsized` if it stable. + #[inline] + pub fn new_any(value: T) -> Self { + let inner: Box> = Box::new(ScBox { ref_cnt: Cell::new(1), value }); + Self::from_inner(Box::leak(inner).into()) + } +} + +impl Sc { + // Gets the number of pointers to this allocation. + /// + /// # Examples + /// + /// ``` + /// use ribir_algo::Sc; + /// + /// let five = Sc::new(5); + /// let _also_five = Sc::clone(&five); + /// + /// assert_eq!(2, Sc::ref_count(&five)); + /// ``` + #[inline] + pub fn ref_count(&self) -> usize { self.inner().ref_cnt() } + + /// Returns `true` if the two `Sc`s point to the same allocation in a vein + /// similar to [`ptr::eq`]. See [that function][`ptr::eq`] for caveats when + /// comparing `dyn Trait` pointers. + /// + /// # Examples + /// + /// ``` + /// use ribir_algo::Sc; + /// + /// let five = Sc::new(5); + /// let same_five = Sc::clone(&five); + /// let other_five = Sc::new(5); + /// + /// assert!(Sc::ptr_eq(&five, &same_five)); + /// assert!(!Sc::ptr_eq(&five, &other_five)); + /// ``` + pub fn ptr_eq(this: &Self, other: &Self) -> bool { this.0.as_ptr() == other.0.as_ptr() } + + fn from_inner(ptr: NonNull>) -> Self { Self(ptr) } + + fn inner(&self) -> &ScBox { + // Safety: we're guaranteed that the inner pointer is valid when the `Sc` is + // alive + unsafe { self.0.as_ref() } + } +} + +impl Sc { + /// Attempt to downcast the `Sc` to a concrete type. + /// + /// # Examples + /// + /// ``` + /// use std::any::Any; + /// use ribir_algo::Sc; + /// + /// fn print_if_string(value: Sc) { + /// if let Ok(string) = value.downcast::() { + /// println!("String ({}): {}", string.len(), string); + /// } + /// } + /// + /// let my_string = "Hello World".to_string(); + /// print_if_string(Sc::new_any(my_string)); + /// print_if_string(Sc::new_any(0i8)); + /// ``` + pub fn downcast(self) -> Result, Sc> { + if (*self).is::() { + let ptr = self.0.cast::>(); + std::mem::forget(self); + Ok(Sc::from_inner(ptr)) + } else { + Err(self) + } + } +} + +impl ScBox { + fn inc(&self) { self.ref_cnt.set(self.ref_cnt.get() + 1); } + fn dec(&self) { self.ref_cnt.set(self.ref_cnt.get() - 1) } + fn ref_cnt(&self) -> usize { self.ref_cnt.get() } +} + +impl std::ops::Deref for Sc { + type Target = T; + #[inline] + fn deref(&self) -> &Self::Target { &self.inner().value } +} + +impl Drop for Sc { + fn drop(&mut self) { + self.inner().dec(); + if self.inner().ref_cnt() == 0 { + unsafe { + let layout = std::alloc::Layout::for_value(self.0.as_ref()); + let ptr = self.0.as_ptr(); + std::ptr::drop_in_place(ptr); + std::alloc::dealloc(ptr as *mut _, layout) + } + } + } +} + +impl Clone for Sc { + #[inline] + fn clone(&self) -> Self { + self.inner().inc(); + Self(self.0) + } +} + +impl Display for Sc { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { Display::fmt(&**self, f) } +} + +impl Debug for Sc { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { Debug::fmt(&**self, f) } +} + +impl Pointer for Sc { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Pointer::fmt(&(&**self as *const T), f) + } +} + +impl Default for Sc { + #[inline] + fn default() -> Sc { Sc::new(Default::default()) } +} + +impl PartialEq for Sc { + #[inline] + fn eq(&self, other: &Sc) -> bool { **self == **other } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_sc() { + let a = Sc::new(1); + assert_eq!(Sc::ref_count(&a), 1); + let b = Sc::clone(&a); + assert_eq!(Sc::ref_count(&b), 2); + drop(a); + assert_eq!(Sc::ref_count(&b), 1); + drop(b); + } +} 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 b3c577289..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> StateRef<'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/assign_observable.rs b/core/src/assign_observable.rs deleted file mode 100644 index 3a0431b5a..000000000 --- a/core/src/assign_observable.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::convert::Infallible; - -use rxrust::{ops::map::MapOp, prelude::ObservableExt}; - -/// Is a struct contain a initialize value and an observable that should be -/// subscribed to update the value. -pub struct AssignObservable> { - value: V, - observable: S, -} - -impl AssignObservable -where - S: ObservableExt, -{ - #[inline] - pub fn new(init: V, observable: S) -> 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(S) -> R) -> AssignObservable - where - R: ObservableExt, - { - let Self { value, observable } = self; - let observable = f(observable); - AssignObservable { value, observable } - } - - /// Creates a new `AssignObservable` which calls a closure on each element and - /// uses its return as the value. - pub fn map(self, mut f: F) -> AssignObservable> - where - F: FnMut(V) -> R, - { - let Self { value, observable } = self; - AssignObservable { - value: f(value), - observable: observable.map(f), - } - } - - #[inline] - pub fn unzip(self) -> (V, S) { (self.value, self.observable) } -} diff --git a/core/src/builtin_widgets.rs b/core/src/builtin_widgets.rs index 916a85e68..abed9e403 100644 --- a/core/src/builtin_widgets.rs +++ b/core/src/builtin_widgets.rs @@ -1,9 +1,14 @@ +//! Built-in widgets is a set of minimal widgets that describes the most common +//! UI elements. The most of them 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. + 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; @@ -53,3 +58,202 @@ pub mod focus_node; pub use focus_node::*; pub mod focus_scope; pub use focus_scope::*; + +use crate::{prelude::*, widget::WidgetBuilder}; + +macro_rules! impl_builtin_obj { + ($($builtin_ty: ty),*) => { + paste::paste! { + #[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 BuiltinObj { + pub fn is_empty(&self) -> bool { + $(self.[< $builtin_ty: snake:lower >].is_none())&& * + } + + $( + 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); + self + } + )* + + $( + 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_declare(ctx)) + } + )* + + 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 + } + } + + impl FatObj { + $( + pub fn [< get_builtin_ $builtin_ty: snake:lower >](&mut self, ctx: &BuildCtx) + -> &mut State<$builtin_ty> + { + self.builtin.[](ctx) + } + )* + } + } + }; +} + +impl_builtin_obj!( + PointerListener, + FocusNode, + RequestFocus, + FocusListener, + FocusBubbleListener, + HasFocus, + KeyboardListener, + CharsListener, + WheelListener, + MouseHover, + PointerPressed, + FittedBox, + BoxDecoration, + Padding, + LayoutBox, + Cursor, + Margin, + ScrollableWidget, + TransformWidget, + HAlignWidget, + VAlignWidget, + LeftAnchor, + RightAnchor, + TopAnchor, + BottomAnchor, + Visibility, + Opacity, + LifecycleListener, + 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] + fn deref(&self) -> &Self::Target { &self.host } +} + +impl std::ops::DerefMut for FatObj { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.host } +} diff --git a/core/src/builtin_widgets/align.rs b/core/src/builtin_widgets/align.rs index 8f6650c29..979ea3992 100644 --- a/core/src/builtin_widgets/align.rs +++ b/core/src/builtin_widgets/align.rs @@ -54,14 +54,14 @@ pub enum VAlign { } /// A widget that align its child in x-axis, base on child's width. -#[derive(Declare, SingleChild)] +#[derive(Declare, Declare2, SingleChild)] pub struct HAlignWidget { #[declare(default, builtin)] pub h_align: HAlign, } /// A widget that align its child in y-axis, base on child's height. -#[derive(Declare, SingleChild)] +#[derive(Declare, Declare2, SingleChild)] pub struct VAlignWidget { #[declare(default, builtin)] pub v_align: VAlign, diff --git a/core/src/builtin_widgets/anchor.rs b/core/src/builtin_widgets/anchor.rs index be20e411e..ae3be6b3b 100644 --- a/core/src/builtin_widgets/anchor.rs +++ b/core/src/builtin_widgets/anchor.rs @@ -11,31 +11,31 @@ pub enum PositionUnit { } /// Widget use to anchor child constraints with the left edge of parent widget. -#[derive(Declare, SingleChild)] +#[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, SingleChild)] +#[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, SingleChild)] +#[derive(Declare, Declare2, SingleChild)] pub struct TopAnchor { - #[declare(convert=into, builtin)] + #[declare(convert=into, builtin, default=0.)] pub top_anchor: PositionUnit, } /// Widget use to anchor child constraints with the bottom edge of parent /// widget. -#[derive(Declare, SingleChild)] +#[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(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 70ed2ee03..3ab7fbb78 100644 --- a/core/src/builtin_widgets/box_decoration.rs +++ b/core/src/builtin_widgets/box_decoration.rs @@ -1,7 +1,7 @@ use crate::{impl_query_self_only, prelude::*}; /// The BoxDecoration provides a variety of ways to draw a box. -#[derive(SingleChild, Default, Clone, Declare)] +#[derive(SingleChild, Default, Clone, Declare, Declare2)] pub struct BoxDecoration { /// The background of the box. #[declare(builtin, default, convert=custom)] @@ -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 ab5d407b8..af0fad5ea 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)] +#[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_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.write() = 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 989c8d405..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)] -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 552424e21..ecb364ae0 100644 --- a/core/src/builtin_widgets/fitted_box.rs +++ b/core/src/builtin_widgets/fitted_box.rs @@ -19,7 +19,7 @@ pub enum BoxFit { } /// Widget set how its child should be scale to fit its box. -#[derive(Declare, SingleChild)] +#[derive(Declare, Declare2, SingleChild)] pub struct FittedBox { #[declare(builtin)] pub box_fit: BoxFit, @@ -100,18 +100,15 @@ mod tests { expected_scale, } = self; let fit = Stateful::new(FittedBox { box_fit, scale_cache: <_>::default() }); - let c_fit = fit.clone(); - let w = widget! { - DynWidget { - dyns: fit, - MockBox { size } - } + let c_fit = fit.clone_reader(); + let w = fn_widget! { + @$fit { @MockBox { size } } }; let mut wnd = TestWindow::new_with_size(w, WND_SIZE); 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.read().scale_cache.get(), expected_scale); } } diff --git a/core/src/builtin_widgets/focus_node.rs b/core/src/builtin_widgets/focus_node.rs index 56c6fdda9..f636d1120 100644 --- a/core/src/builtin_widgets/focus_node.rs +++ b/core/src/builtin_widgets/focus_node.rs @@ -1,11 +1,10 @@ use crate::{ - data_widget::attach_to_id, events::focus_mgr::{FocusHandle, FocusType}, impl_query_self_only, prelude::*, }; -#[derive(Default, Declare)] +#[derive(Default, Declare, Declare2)] pub struct FocusNode { /// Indicates that `widget` can be focused, and where it participates in /// sequential keyboard navigation (usually with the Tab key, hence the name. @@ -43,28 +42,26 @@ impl ComposeChild for FocusNode { let this = this.into_writable(); FnWidget::new(|ctx| { let id = child.build(ctx); - - if !ctx.assert_get(id).contain_type::() { + let has_focus_node = ctx.assert_get(id).contain_type::(); + if !has_focus_node { let subject = ctx .assert_get(id) .query_on_first_type(QueryOrder::OutsideFirst, |l: &LifecycleListener| { l.lifecycle_stream() - }) - .unwrap_or_else(|| { - let listener = LifecycleListener::default(); - let subject = listener.lifecycle_stream(); - attach_to_id(id, ctx.force_as_mut(), |child| { - Box::new(DataWidget::new(child, listener)) - }); - subject }); + let subject = subject.unwrap_or_else(|| { + let listener = LifecycleListener::default(); + let subject = listener.lifecycle_stream(); + id.wrap_node(&mut ctx.tree.borrow_mut().arena, |child| { + Box::new(DataWidget::new(child, listener)) + }); + subject + }); - fn subscribe_fn( - this: Stateful, - ) -> impl for<'a, 'b> FnMut(&'a mut AllLifecycle<'b>) + 'static { - move |e: &mut AllLifecycle<'_>| match e { + 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(_) => {} @@ -72,10 +69,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| { + id.wrap_node(&mut ctx.tree.borrow_mut().arena, |child| { let d = DataWidget::new(child, this); Box::new(DataWidget::new( Box::new(d), @@ -97,7 +94,7 @@ pub(crate) fn dynamic_compose_focus_node(widget: Widget) -> Widget { }) .into() } -#[derive(Declare)] +#[derive(Declare, Declare2)] pub struct RequestFocus { #[declare(default)] handle: Option, @@ -106,18 +103,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..0f6fa58ce 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 @@ -17,11 +17,10 @@ pub struct FocusScope { impl ComposeChild for FocusScope { type Child = Widget; fn compose_child(this: State, child: Self::Child) -> Widget { - let w = widget! { - DynWidget { - dyns: child, - on_mounted: move |ctx| ctx.window().add_focus_node(ctx.id, false, FocusType::Scope), - on_disposed: move|ctx| ctx.window().remove_focus_node(ctx.id, FocusType::Scope), + let w = fn_widget! { + @ $child { + on_mounted: move |e| e.window().add_focus_node(e.id, false, FocusType::Scope), + on_disposed: move|e| e.window().remove_focus_node(e.id, FocusType::Scope), } }; DataWidget::attach_state(w.into(), this) @@ -207,7 +206,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/has_focus.rs b/core/src/builtin_widgets/has_focus.rs index d5d5607b5..39a21792b 100644 --- a/core/src/builtin_widgets/has_focus.rs +++ b/core/src/builtin_widgets/has_focus.rs @@ -1,5 +1,5 @@ use crate::prelude::*; -#[derive(PartialEq, Clone, Declare)] +#[derive(PartialEq, Clone, Declare, Declare2)] pub struct HasFocus { #[declare(skip, default)] focused: bool, @@ -12,12 +12,10 @@ impl HasFocus { impl ComposeChild for HasFocus { type Child = Widget; fn compose_child(this: State, child: Self::Child) -> Widget { - widget! { - states {this: this.into_writable()} - DynWidget { - dyns: child, - on_focus_in: move|_| this.focused = true, - on_focus_out: move |_| this.focused = false, + fn_widget! { + @ $child { + on_focus_in: move|_| $this.write().focused = true, + on_focus_out: move |_| $this.write().focused = false, } } .into() diff --git a/core/src/builtin_widgets/ignore_pointer.rs b/core/src/builtin_widgets/ignore_pointer.rs index fcbc367e4..4fe23d940 100644 --- a/core/src/builtin_widgets/ignore_pointer.rs +++ b/core/src/builtin_widgets/ignore_pointer.rs @@ -1,6 +1,6 @@ use crate::{impl_query_self_only, prelude::*}; -#[derive(Declare, SingleChild, Clone)] +#[derive(Declare, Declare2, SingleChild, Clone)] pub struct IgnorePointer { #[declare(default = true)] pub ignore: bool, diff --git a/core/src/builtin_widgets/key.rs b/core/src/builtin_widgets/key.rs index f97023c28..e275d2336 100644 --- a/core/src/builtin_widgets/key.rs +++ b/core/src/builtin_widgets/key.rs @@ -1,5 +1,6 @@ use crate::{impl_query_self_only, prelude::*}; use std::{ + cell::Cell, cmp::{Eq, Ord, PartialOrd}, fmt::Debug, }; @@ -29,115 +30,62 @@ pub enum Key { K32([u8; 32]), } -/// The KeyStatus is the status of the Key, not the status of the KeyWidget. -/// The change of Key status is determined by its associated KeyWidget state -/// and its own state. -/// -/// The KeyStatus has four status: -/// The first status: The KeyWidget associated with the Key was constructed, the -/// Key didn't exist in DynWidget Key List, the key status is `KeyStatus::Init`. -/// -/// The second status: The KeyWidget associated with the Key was mounted, and -/// now its status is `KeyStatus::Init`, the key status will be changed -/// `KeyStatus::Mounted`. -/// -/// The third status: The KeyWidget associated with the Key was disposed, and -/// the same key has anther associated KeyWidget was mounted, the key status -/// will be changed `KeyStatus::Updated`. -/// -/// The last status: The KeyWidget associated with the Key was disposed, the -/// same key don't has anther associated KeyWidget, the key status will be -/// changed `KeyStatus::Disposed`. -#[derive(PartialEq, Debug, Clone, Copy)] -pub enum KeyStatus { - Init, - Mounted, - Updated, - Disposed, -} - -impl Default for KeyStatus { - fn default() -> Self { Self::Init } -} - #[derive(Clone, Debug, PartialEq, Copy)] -pub struct KeyChange(pub Option, pub Option); +pub struct KeyChange(pub Option, pub V); -impl Default for KeyChange { - fn default() -> Self { KeyChange(None, None) } +impl Default for KeyChange { + fn default() -> Self { KeyChange(None, V::default()) } } -#[derive(Declare)] -pub struct KeyWidget { + +/// A widget that can be used to track if the widget is the same widget in two +/// frames by its key. If two widget has same parent and key in two frames, the +/// new widget in the next frame will be treated as the same widget in the last +/// frame. +#[derive(Declare, Declare2)] +pub struct KeyWidget { #[declare(convert=into)] pub key: Key, - pub value: Option, - - #[declare(default)] + #[declare(default, strict)] + pub value: V, + #[declare(skip)] before_value: Option, - #[declare(default)] - status: KeyStatus, + #[declare(skip)] + has_successor: Cell, } +/// A trait for `keyWidget` that use to record information of the previous and +/// next key widget. pub(crate) trait AnyKey: Any { fn key(&self) -> Key; - fn record_before_value(&self, key: &dyn AnyKey); - fn disposed(&self); - fn mounted(&self); + /// Record the previous KeyWidget associated with the same key. + fn record_prev_key_widget(&self, key: &dyn AnyKey); + /// Record the next KeyWidget associated with the same key. + fn record_next_key_widget(&self, key: &dyn AnyKey); fn as_any(&self) -> &dyn Any; } impl AnyKey for State> where - V: Clone + PartialEq, + 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_before_value(&self, key: &dyn AnyKey) { + fn record_prev_key_widget(&self, key: &dyn AnyKey) { assert_eq!(self.key(), key.key()); let Some(key) = key.as_any().downcast_ref::() else { 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 disposed(&self) { - match self { - State::Stateless(_) => (), - State::Stateful(this) => { - this.state_ref().status = KeyStatus::Disposed; - } - } - } - - fn mounted(&self) { - match self { - State::Stateless(_) => {} - State::Stateful(this) => { - this.state_ref().status = KeyStatus::Mounted; - } - } - } + fn record_next_key_widget(&self, _: &dyn AnyKey) { self.write().has_successor.set(true); } fn as_any(&self) -> &dyn Any { self } } -impl ComposeChild for KeyWidget { +impl ComposeChild for KeyWidget { type Child = Widget; #[inline] fn compose_child(this: State, child: Self::Child) -> Widget { @@ -150,27 +98,28 @@ impl_query_self_only!(Box); impl KeyWidget where - V: Clone + PartialEq, + V: Default + Clone + PartialEq, { - fn record_before_value(&mut self, value: Option) { - self.status = KeyStatus::Updated; - self.before_value = value; + /// Detect if the key widget is a new widget, there is not predecessor widget + /// that has same key. Usually used in `on_mounted` callback. + pub fn is_enter(&self) -> bool { self.before_value.is_none() } + /// Detect if the key widget is really be disposed, there is not successor + /// widget has same key. Usually used in `on_disposed` callback. + pub fn is_leave(&self) -> bool { !self.has_successor.get() } + + /// Detect if the value of the key widget is changed + pub fn is_changed(&self) -> bool { + self.before_value.is_some() && self.before_value.as_ref() != Some(&self.value) } - pub fn is_enter(&self) -> bool { self.status == KeyStatus::Mounted } - - pub fn is_modified(&self) -> bool { self.status == KeyStatus::Updated } - - pub fn is_changed(&self) -> bool { self.is_modified() && self.before_value != self.value } - - pub fn is_disposed(&self) -> bool { self.status == KeyStatus::Disposed } - pub fn get_change(&self) -> KeyChange { KeyChange(self.before_value.clone(), self.value.clone()) } -} -impl_query_self_only!(Key); + pub fn before_value(&self) -> Option<&V> { self.before_value.as_ref() } + + fn record_before_value(&mut self, value: V) { self.before_value = Some(value); } +} macro from_key_impl($($ty: ty : $name: ident)*) { $( diff --git a/core/src/builtin_widgets/layout_box.rs b/core/src/builtin_widgets/layout_box.rs index 64c43ab64..21102f9b1 100644 --- a/core/src/builtin_widgets/layout_box.rs +++ b/core/src/builtin_widgets/layout_box.rs @@ -1,7 +1,7 @@ use crate::prelude::*; /// Widget let user to access the layout result of its child. -#[derive(Declare)] +#[derive(Declare, Declare2)] pub struct LayoutBox { #[declare(skip)] /// the rect box of its child and the coordinate is relative to its parent. @@ -11,14 +11,12 @@ pub struct LayoutBox { impl ComposeChild for LayoutBox { type Child = Widget; fn compose_child(this: State, child: Self::Child) -> Widget { - widget! { - states { this: this.into_writable() } - DynWidget { - dyns: child, - on_performed_layout: move |ctx| { - let new_rect = ctx.box_rect().unwrap(); - if this.rect != new_rect { - this.silent().rect = new_rect; + fn_widget! { + @ $child { + on_performed_layout: move |e| { + let new_rect = e.box_rect().unwrap(); + if $this.rect != new_rect { + $this.silent().rect = new_rect; } } } diff --git a/core/src/builtin_widgets/lifecycle.rs b/core/src/builtin_widgets/lifecycle.rs index 31e55f789..febe15eb6 100644 --- a/core/src/builtin_widgets/lifecycle.rs +++ b/core/src/builtin_widgets/lifecycle.rs @@ -1,18 +1,15 @@ -use std::convert::Infallible; - -use crate::{impl_all_event, impl_compose_child_for_listener, impl_query_self_only, prelude::*}; -use rxrust::{ - prelude::*, - rc::{MutRc, RcDeref, RcDerefMut}, - subscriber::{Publisher, Subscriber}, +use crate::{ + impl_all_event, impl_compose_child_for_listener, impl_query_self_only, prelude::*, + window::WindowId, }; -use smallvec::SmallVec; +use rxrust::prelude::*; +use std::{convert::Infallible, rc::Rc}; define_widget_context!(LifecycleEvent); -crate::events::impl_event_subject!(Lifecycle, event_name = AllLifecycle); +pub type LifecycleSubject = MutRefItemSubject<'static, AllLifecycle, Infallible>; -#[derive(Declare, Default)] +#[derive(Declare, Declare2, Default)] pub struct LifecycleListener { #[declare(skip)] lifecycle: LifecycleSubject, @@ -41,15 +38,12 @@ macro_rules! match_closure { (|e| match e { AllLifecycle::$event_ty(e) => Some(e), _ => None, - }) as for<'a, 'b> fn(&'a mut AllLifecycle<'b>) -> Option<&'a mut LifecycleEvent<'b>> + }) as fn(&mut AllLifecycle) -> Option<&mut LifecycleEvent> }; } impl LifecycleListenerDeclarer { - pub fn on_mounted( - mut self, - handler: impl for<'r> FnMut(&'r mut LifecycleEvent<'_>) + 'static, - ) -> Self { + pub fn on_mounted(mut self, handler: impl FnMut(&mut LifecycleEvent) + 'static) -> Self { let _ = self .subject() .filter_map(match_closure!(Mounted)) @@ -86,12 +80,51 @@ impl LifecycleListenerDeclarer { } } +impl LifecycleListenerDeclarer2 { + pub fn on_mounted(mut self, handler: impl FnMut(&mut LifecycleEvent) + 'static) -> Self { + let _ = self + .subject() + .filter_map(match_closure!(Mounted)) + .take(1) + .subscribe(handler); + + self + } + + pub fn on_performed_layout(mut self, handler: impl FnMut(&mut LifecycleEvent) + 'static) -> Self { + let _ = self + .subject() + .filter_map(match_closure!(PerformedLayout)) + .subscribe(handler); + + self + } + + pub fn on_disposed(mut self, handler: impl FnMut(&mut LifecycleEvent) + 'static) -> Self { + let _ = self + .subject() + .filter_map(match_closure!(Disposed)) + .take(1) + .subscribe(handler); + + self + } + + fn subject(&mut self) -> LifecycleSubject { + self + .lifecycle + .get_or_insert_with(DeclareInit::default) + .value() + .clone() + } +} + impl_query_self_only!(LifecycleListener); impl EventListener for LifecycleListener { - type Event<'a> = AllLifecycle<'a>; + type Event = AllLifecycle; #[inline] - fn dispatch(&self, event: &mut Self::Event<'_>) { self.lifecycle.clone().next(event) } + fn dispatch(&self, event: &mut Self::Event) { self.lifecycle.clone().next(event) } } #[cfg(test)] @@ -100,43 +133,47 @@ mod tests { use crate::{ prelude::*, + reset_test_env, test_helper::{MockBox, MockMulti, TestWindow}, }; #[test] fn full_lifecycle() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); let trigger = Stateful::new(true); let lifecycle = Stateful::new(vec![]); + let c_lc = lifecycle.clone_reader(); + let c_trigger = trigger.clone_writer(); - let w = widget! { - states { - trigger: trigger.clone(), - lifecycle: lifecycle.clone() - } - MockBox { + let w = fn_widget! { + @MockBox { size: Size::zero(), - on_mounted: move |_| lifecycle.silent().push("static mounted"), - on_performed_layout: move |_| lifecycle.silent().push("static performed layout"), - on_disposed: move |_| lifecycle.silent().push("static disposed"), - widget::then(*trigger, || widget! { - MockBox { - size: Size::zero(), - on_mounted: move |_| lifecycle.silent().push("dyn mounted"), - on_performed_layout: move |_| lifecycle.silent().push("dyn performed layout"), - on_disposed: move |_| lifecycle.silent().push("dyn disposed") - } - }) + on_mounted: move |_| $lifecycle.write().push("static mounted"), + on_performed_layout: move |_| $lifecycle.write().push("static performed layout"), + on_disposed: move |_| $lifecycle.write().push("static disposed"), + @ { + pipe!(*$trigger).map(move |b| { + b.then(move || { + @MockBox { + size: Size::zero(), + on_mounted: move |_| $lifecycle.write().push("dyn mounted"), + on_performed_layout: move |_| $lifecycle.write().push("dyn performed layout"), + on_disposed: move |_| $lifecycle.write().push("dyn disposed") + } + }) + }) + } } }; let mut wnd = TestWindow::new_with_size(w, Size::new(100., 100.)); - assert_eq!(&**lifecycle.state_ref(), ["static mounted"]); + assert_eq!(&**c_lc.read(), ["static mounted", "dyn mounted",]); + wnd.draw_frame(); assert_eq!( - &**lifecycle.state_ref(), + &**c_lc.read(), [ "static mounted", "dyn mounted", @@ -145,11 +182,11 @@ mod tests { ] ); { - *trigger.state_ref() = false; + *c_trigger.write() = false; } wnd.draw_frame(); assert_eq!( - &**lifecycle.state_ref(), + &**c_lc.read(), [ "static mounted", "dyn mounted", @@ -163,42 +200,39 @@ mod tests { #[test] fn track_lifecycle() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); let cnt = Stateful::new(3); let mounted: Stateful> = Stateful::new(HashSet::default()); let disposed: Stateful> = Stateful::new(HashSet::default()); - let w = widget! { - states { - cnt: cnt.clone(), - mounted: mounted.clone(), - disposed: disposed.clone(), - } - MockMulti { - Multi::new((0..*cnt).map(move |_| widget! { - MockBox { - size: Size::zero(), - on_mounted: move |ctx| { - mounted.insert(ctx.id); - }, - on_disposed: move |ctx| { - disposed.insert(ctx.id); - }, - } - })) + + let c_cnt = cnt.clone_writer(); + let c_mounted = mounted.clone_reader(); + let c_disposed = disposed.clone_reader(); + let w = fn_widget! { + @MockMulti { + @ { + pipe!(*$cnt).map(move |cnt| { + let iter = (0..cnt).map(move |_| @MockBox { + size: Size::zero(), + on_mounted: move |e| { $mounted.write().insert(e.id); }, + on_disposed: move |e| { $disposed.write().insert(e.id); }, + }); + Multi::new(iter) + }) + } } }; let mut wnd = TestWindow::new_with_size(w, Size::new(100., 100.)); wnd.draw_frame(); - let mounted_ids = (*mounted.state_ref()).clone(); + let mounted_ids = c_mounted.read().clone(); - *cnt.state_ref() = 5; + *c_cnt.write() = 5; wnd.on_wnd_resize_event(Size::zero()); wnd.draw_frame(); - let disposed_ids = (*disposed.state_ref()).clone(); assert_eq!(mounted_ids.len(), 3); - assert_eq!(mounted_ids, disposed_ids); + assert_eq!(&mounted_ids, &*c_disposed.read()); } } diff --git a/core/src/builtin_widgets/margin.rs b/core/src/builtin_widgets/margin.rs index 2a2e0883f..8369f087b 100644 --- a/core/src/builtin_widgets/margin.rs +++ b/core/src/builtin_widgets/margin.rs @@ -9,7 +9,7 @@ pub struct EdgeInsets { } /// A widget that create space around its child. -#[derive(SingleChild, Default, Clone, PartialEq, Declare)] +#[derive(SingleChild, Default, Clone, PartialEq, Declare, Declare2)] pub struct Margin { #[declare(builtin, default)] pub margin: EdgeInsets, diff --git a/core/src/builtin_widgets/mouse_hover.rs b/core/src/builtin_widgets/mouse_hover.rs index 913b81a61..29e7726e1 100644 --- a/core/src/builtin_widgets/mouse_hover.rs +++ b/core/src/builtin_widgets/mouse_hover.rs @@ -1,6 +1,6 @@ use crate::prelude::*; -#[derive(PartialEq, Clone, Declare)] +#[derive(PartialEq, Clone, Declare, Declare2)] pub struct MouseHover { #[declare(skip, default)] hover: bool, @@ -13,12 +13,10 @@ impl MouseHover { impl ComposeChild for MouseHover { type Child = Widget; fn compose_child(this: State, child: Self::Child) -> Widget { - widget! { - states {this: this.into_writable()} - DynWidget { - dyns: child, - on_pointer_enter: move |_| this.hover = true, - on_pointer_leave: move |_| this.hover = false, + fn_widget! { + @ $child { + on_pointer_enter: move |_| $this.write().hover = true, + on_pointer_leave: move |_| $this.write().hover = false, } } .into() diff --git a/core/src/builtin_widgets/opacity.rs b/core/src/builtin_widgets/opacity.rs index 53663c9f9..b2d1a2fca 100644 --- a/core/src/builtin_widgets/opacity.rs +++ b/core/src/builtin_widgets/opacity.rs @@ -1,9 +1,9 @@ use crate::impl_query_self_only; use crate::prelude::*; -#[derive(Declare, Default, Clone, SingleChild)] +#[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/padding.rs b/core/src/builtin_widgets/padding.rs index 4778a1956..ad33362e7 100644 --- a/core/src/builtin_widgets/padding.rs +++ b/core/src/builtin_widgets/padding.rs @@ -1,7 +1,7 @@ use crate::{impl_query_self_only, prelude::*}; /// A widget that insets its child by the given padding. -#[derive(SingleChild, Clone, Declare)] +#[derive(SingleChild, Clone, Declare, Declare2)] pub struct Padding { #[declare(builtin)] pub padding: EdgeInsets, diff --git a/core/src/builtin_widgets/pointer_pressed.rs b/core/src/builtin_widgets/pointer_pressed.rs index 18a3ee1c1..3a70daccd 100644 --- a/core/src/builtin_widgets/pointer_pressed.rs +++ b/core/src/builtin_widgets/pointer_pressed.rs @@ -2,7 +2,7 @@ use crate::prelude::*; /// Widget keep the pointer press state of its child. As a builtin widget, user /// can call `pointer_pressed` method to get the pressed state of a widget. -#[derive(Declare)] +#[derive(Declare, Declare2)] pub struct PointerPressed { #[declare(skip, builtin)] pointer_pressed: bool, @@ -17,12 +17,10 @@ impl PointerPressed { impl ComposeChild for PointerPressed { type Child = Widget; fn compose_child(this: State, child: Self::Child) -> Widget { - widget! { - states { this: this.into_writable()} - DynWidget { - dyns: child, - on_pointer_down: move|_| this.pointer_pressed = true, - on_pointer_up: move |_| this.pointer_pressed = false, + fn_widget! { + @ $child { + on_pointer_down: move|_| $this.write().pointer_pressed = true, + on_pointer_up: move |_| $this.write().pointer_pressed = false, } } .into() diff --git a/core/src/builtin_widgets/scrollable.rs b/core/src/builtin_widgets/scrollable.rs index a8d6284f6..a470617db 100644 --- a/core/src/builtin_widgets/scrollable.rs +++ b/core/src/builtin_widgets/scrollable.rs @@ -15,7 +15,7 @@ pub enum Scrollable { } /// Helper struct for builtin scrollable field. -#[derive(Declare)] +#[derive(Declare, Declare2)] pub struct ScrollableWidget { #[declare(builtin, default)] pub scrollable: Scrollable, @@ -30,45 +30,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_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), + }; + + watch!($child.layout_size()) + .distinct_until_changed() + .subscribe(move |v| $this.write().set_content_size(v)); + watch!($view.layout_size()) + .distinct_until_changed() + .subscribe(move |v| $this.write().set_page(v)); + + @Clip { + @ $view { + on_wheel: move |e| $this.write().validate_scroll(Point::new(e.delta_x, e.delta_y)), + @ { child } } } - - 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); - }); - } } .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 0650e86d5..8de5418fd 100644 --- a/core/src/builtin_widgets/theme.rs +++ b/core/src/builtin_widgets/theme.rs @@ -1,17 +1,10 @@ //! Theme use to share visual config or style compose logic. It can be defined //! to app-wide or particular part of the application. -use crate::{ - declare::DeclareBuilder, - fill_svgs, impl_query_self_only, - prelude::include_svg, - prelude::{Any, BuildCtx, ComposeChild, Declare, Query, QueryFiler, QueryOrder, TypeId}, - state::State, - widget::{Widget, WidgetBuilder}, -}; -use ribir_algo::CowArc; -pub use ribir_algo::ShareResource; +use crate::{fill_svgs, impl_query_self_only, prelude::*, widget::WidgetBuilder}; +pub use ribir_algo::{CowArc, ShareResource}; use ribir_geom::Size; +use ribir_macros::Declare2; use ribir_text::TextStyle; use std::{collections::HashMap, rc::Rc}; @@ -73,7 +66,7 @@ pub enum Theme { Inherit(InheritTheme), } -#[derive(Declare)] +#[derive(Declare, Declare2)] pub struct ThemeWidget { pub theme: Rc, } @@ -84,10 +77,8 @@ impl ComposeChild for ThemeWidget { fn compose_child(this: State, child: Self::Child) -> Widget { use crate::prelude::*; FnWidget::new(move |ctx| { - let theme = match this { - State::Stateless(t) => t.theme, - State::Stateful(s) => s.state_ref().theme.clone(), - }; + let ctx = ctx.force_as_mut(); + let theme = this.read().theme.clone(); AppCtx::load_font_from_theme(&theme); ctx.push_theme(theme.clone()); @@ -108,6 +99,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/compose_decorators.rs b/core/src/builtin_widgets/theme/compose_decorators.rs index b8b71a0a6..211dff454 100644 --- a/core/src/builtin_widgets/theme/compose_decorators.rs +++ b/core/src/builtin_widgets/theme/compose_decorators.rs @@ -90,10 +90,10 @@ mod tests { theme .compose_decorators .override_compose_decorator::(|_, host| { - widget! { - MockBox { + fn_widget! { + @MockBox { size: Size::new(100., 100.), - DynWidget { dyns: host } + @ { host } } } .into() diff --git a/core/src/builtin_widgets/theme/icon_theme.rs b/core/src/builtin_widgets/theme/icon_theme.rs index c59c29268..6fcbf7988 100644 --- a/core/src/builtin_widgets/theme/icon_theme.rs +++ b/core/src/builtin_widgets/theme/icon_theme.rs @@ -59,13 +59,7 @@ 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(this: State) -> Widget { fn_widget! { @ { $this.of_or_miss(ctx!()) }}.into() } } impl IconTheme { 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/transform_widget.rs b/core/src/builtin_widgets/transform_widget.rs index 2e389ce89..89ebc0a04 100644 --- a/core/src/builtin_widgets/transform_widget.rs +++ b/core/src/builtin_widgets/transform_widget.rs @@ -1,6 +1,6 @@ use crate::{impl_query_self_only, prelude::*, widget::hit_test_impl}; -#[derive(SingleChild, Declare, Clone)] +#[derive(SingleChild, Declare, Declare2, Clone)] pub struct TransformWidget { #[declare(builtin, default)] pub transform: Transform, 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 cec501cf4..98ca5b307 100644 --- a/core/src/builtin_widgets/visibility.rs +++ b/core/src/builtin_widgets/visibility.rs @@ -1,6 +1,6 @@ use crate::{impl_query_self_only, prelude::*}; -#[derive(Declare)] +#[derive(Declare, Declare2)] pub struct Visibility { #[declare(builtin)] pub visible: bool, @@ -9,14 +9,13 @@ 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_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/builtin_widgets/void.rs b/core/src/builtin_widgets/void.rs index b83d44cd7..334306bac 100644 --- a/core/src/builtin_widgets/void.rs +++ b/core/src/builtin_widgets/void.rs @@ -4,7 +4,7 @@ use crate::{impl_query_self_only, prelude::*}; /// node in `widget!` macro, or hold a place in tree. When it have a child /// itself will be dropped when build tree, otherwise as a render widget but do /// nothing. -#[derive(Declare)] +#[derive(Declare, SingleChild, Declare2)] pub struct Void; impl Render for Void { diff --git a/core/src/context/app_ctx.rs b/core/src/context/app_ctx.rs index 79362a8c7..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. @@ -73,6 +89,14 @@ impl AppCtx { Self::shared().windows.borrow().get(&id).cloned() } + /// Get the window by the window id. Same as `get_window` but will panic if + /// the window not found. + #[track_caller] + #[inline] + pub fn get_window_assert(id: WindowId) -> Rc { + Self::get_window(id).expect("Window not found!") + } + /// Return the windows collection of the application. pub fn windows() -> &'static RefCell>> { &Self::shared().windows @@ -94,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] @@ -108,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] @@ -187,17 +242,24 @@ 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(); - APP_CTX_INIT = Once::new(); - locker + + let locker = LOCK.lock().unwrap_or_else(|e| { + // Only clear for test, so we have a clear error message. + #[cfg(test)] + LOCK.clear_poison(); + + e.into_inner() + }); + + 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(); @@ -216,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, @@ -223,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()); @@ -246,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(), }) @@ -352,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 fe7e55856..11f9eb8ef 100644 --- a/core/src/context/build_context.rs +++ b/core/src/context/build_context.rs @@ -1,30 +1,59 @@ use crate::{ prelude::*, widget::{widget_id::new_node, WidgetTree}, + window::{DelayEvent, WindowId}, +}; +use std::{ + cell::{Ref, RefCell, UnsafeCell}, + ops::Deref, + rc::Rc, }; -use std::{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, - tree: &'a mut WidgetTree, + pub(crate) tree: &'a RefCell, +} + +/// A handle of `BuildCtx` that you can store it and access the `BuildCtx` later +/// in anywhere. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct BuildCtxHandle { + ctx_from: Option, + wnd_id: WindowId, } impl<'a> BuildCtx<'a> { - pub fn window(&self) -> Rc { self.tree.window() } + /// Return the window of this context is created from. + 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.window().id(), + ctx_from: self.ctx_from, + } + } pub fn reset_ctx_from(&mut self, reset: Option) -> Option { std::mem::replace(&mut self.ctx_from, reset) } #[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> { @@ -40,22 +69,63 @@ 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.borrow_mut().arena); + } + + /// Insert `next` after `prev` + pub(crate) fn insert_after(&mut self, prev: WidgetId, next: WidgetId) { + prev.insert_after(next, &mut self.tree.borrow_mut().arena); } - pub(crate) fn append_child(&self, parent: WidgetId, child: WidgetId) { - let arena = &mut self.force_as_mut().tree.arena; - parent.append(child, 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.borrow().arena) + .for_each(|w| self.on_widget_mounted(w)); + 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: &Notifier| { + let state_changed = self.tree.borrow().dirty_set.clone(); + notifier + .raw_modifies() + .filter(|b| b.contains(ModifyScope::FRAMEWORK)) + .subscribe(move |_| { + state_changed.borrow_mut().insert(id); + }); + true + }, + QueryOrder::OutsideFirst, + ); + + self.window().add_delay_event(DelayEvent::Mounted(id)); + } + + /// Dispose the whole subtree of `id`, include `id` itself. + pub(crate) fn dispose_subtree(&self, id: WidgetId) { + let mut tree = self.tree.borrow_mut(); + let parent = id.parent(&tree.arena); + tree.detach(id); + tree + .window() + .add_delay_event(DelayEvent::Disposed { id, parent }); + } + + 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); } @@ -74,13 +144,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| { @@ -96,6 +167,17 @@ impl<'a> BuildCtx<'a> { } } +impl BuildCtxHandle { + /// Acquires a reference to the `BuildCtx` in this handle, maybe not exist if + /// the window is closed or widget is removed. + pub fn with_ctx(self, f: impl FnOnce(&BuildCtx) -> R) -> Option { + AppCtx::get_window(self.wnd_id).map(|wnd| { + let mut ctx = BuildCtx::new(self.ctx_from, &wnd.widget_tree); + f(&mut ctx) + }) + } +} + #[cfg(test)] mod tests { use super::*; @@ -110,6 +192,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() @@ -118,27 +201,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/context/layout_ctx.rs b/core/src/context/layout_ctx.rs index 6d08d80e9..768b40a8f 100644 --- a/core/src/context/layout_ctx.rs +++ b/core/src/context/layout_ctx.rs @@ -1,10 +1,11 @@ -use super::{WidgetCtx, WidgetCtxImpl}; +use super::{AppCtx, WidgetCtx, WidgetCtxImpl}; use crate::{ widget::{BoxClamp, Layouter, WidgetTree}, widget_tree::WidgetId, - window::Window, + window::{Window, WindowId}, }; use ribir_geom::Size; +use std::rc::Rc; /// A place to compute the render object's layout. Rather than holding children /// directly, `Layout` perform layout across `LayoutCtx`. `LayoutCtx` provide @@ -12,7 +13,7 @@ use ribir_geom::Size; /// descendants position. pub struct LayoutCtx<'a> { pub(crate) id: WidgetId, - pub(crate) wnd: &'a Window, + pub(crate) wnd_id: WindowId, /// The widget tree of the window, not borrow it from `wnd` is because a /// `LayoutCtx` always in a mutable borrow. pub(crate) tree: &'a mut WidgetTree, @@ -21,7 +22,7 @@ pub struct LayoutCtx<'a> { impl<'a> WidgetCtxImpl for LayoutCtx<'a> { fn id(&self) -> WidgetId { self.id } - fn current_wnd(&self) -> &Window { self.wnd } + fn current_wnd(&self) -> Rc { AppCtx::get_window_assert(self.wnd_id) } fn with_tree R, R>(&self, f: F) -> R { f(self.tree) } } @@ -77,7 +78,7 @@ impl<'a> LayoutCtx<'a> { } pub(crate) fn new_layouter(&mut self, id: WidgetId) -> Layouter { - let LayoutCtx { wnd, tree, .. } = self; - Layouter::new(id, wnd, false, tree) + let LayoutCtx { wnd_id, tree, .. } = self; + Layouter::new(id, *wnd_id, false, tree) } } diff --git a/core/src/context/painting_ctx.rs b/core/src/context/painting_ctx.rs index ffb3b09c2..8938705e3 100644 --- a/core/src/context/painting_ctx.rs +++ b/core/src/context/painting_ctx.rs @@ -1,19 +1,29 @@ +use super::{AppCtx, WidgetCtxImpl}; use crate::{ prelude::{Painter, WidgetId}, - window::Window, + window::{Window, WindowId}, }; -use std::cell::RefMut; +use std::rc::Rc; -use super::{define_widget_context, WidgetCtxImpl}; +pub struct PaintingCtx<'a> { + pub(crate) id: WidgetId, + pub(crate) wnd_id: WindowId, + pub(crate) painter: &'a mut Painter, +} + +impl<'a> WidgetCtxImpl for PaintingCtx<'a> { + #[inline] + fn id(&self) -> WidgetId { self.id } -define_widget_context!(PaintingCtx, painter: RefMut<'a, Painter>); + #[inline] + fn current_wnd(&self) -> Rc { AppCtx::get_window_assert(self.wnd_id) } +} impl<'a> PaintingCtx<'a> { - pub fn new(id: WidgetId, wnd: &'a Window) -> Self { - let painter = wnd.painter.borrow_mut(); - Self { id, wnd, painter } + pub fn new(id: WidgetId, wnd_id: WindowId, painter: &'a mut Painter) -> Self { + Self { id, wnd_id, painter } } /// Return the 2d painter to draw 2d things. #[inline] - pub fn painter(&mut self) -> &mut Painter { &mut self.painter } + pub fn painter(&mut self) -> &mut Painter { self.painter } } diff --git a/core/src/context/widget_ctx.rs b/core/src/context/widget_ctx.rs index 55a5c9cc6..591d37ec4 100644 --- a/core/src/context/widget_ctx.rs +++ b/core/src/context/widget_ctx.rs @@ -1,10 +1,11 @@ use crate::{ - prelude::QueryOrder, + prelude::{AppCtx, QueryOrder}, widget::{BoxClamp, WidgetTree}, widget_tree::WidgetId, - window::Window, + window::{Window, WindowId}, }; use ribir_geom::{Point, Rect, Size}; +use std::rc::Rc; /// common action for all context of widget. pub trait WidgetCtx { @@ -63,14 +64,15 @@ pub trait WidgetCtx { id: WidgetId, callback: impl FnOnce(&W) -> R, ) -> Option; - /// Get the window of this context in the callback. - fn window(&self) -> &Window; + /// Get the window of this context, yous should not store the window, store + /// its id instead. + fn window(&self) -> Rc; } pub(crate) trait WidgetCtxImpl { fn id(&self) -> WidgetId; - fn current_wnd(&self) -> &Window; + fn current_wnd(&self) -> Rc; #[inline] fn with_tree R, R>(&self, f: F) -> R { @@ -171,7 +173,7 @@ impl WidgetCtx for T { }) } - fn window(&self) -> &Window { self.current_wnd() } + fn window(&self) -> Rc { self.current_wnd() } } macro_rules! define_widget_context { @@ -180,19 +182,18 @@ macro_rules! define_widget_context { $name: ident $(, $extra_name: ident: $extra_ty: ty)* ) => { $(#[$outer])* - pub struct $name<'a> { + pub struct $name { pub(crate) id: WidgetId, - /// use a reference to avoid user hold the `Window`. - pub(crate) wnd: &'a Window, + pub(crate) wnd_id: WindowId, $(pub(crate) $extra_name: $extra_ty,)* } - impl<'a> WidgetCtxImpl for $name<'a> { + impl WidgetCtxImpl for $name { #[inline] fn id(&self) -> WidgetId { self.id } #[inline] - fn current_wnd(&self) -> &Window { self.wnd } + fn current_wnd(&self) -> Rc { AppCtx::get_window_assert(self.wnd_id) } } }; } @@ -229,7 +230,7 @@ mod tests { let pos = Point::zero(); let child = root.single_child(&tree.arena).unwrap(); - let w_ctx = TestCtx { id: child, wnd: &wnd }; + let w_ctx = TestCtx { id: child, wnd_id: wnd.id() }; assert_eq!(w_ctx.map_from(pos, child), pos); assert_eq!(w_ctx.map_to(pos, child), pos); } @@ -255,7 +256,7 @@ mod tests { let root = wnd.widget_tree.borrow().root(); let child = get_single_child_by_depth(root, &wnd.widget_tree.borrow().arena, 4); - let w_ctx = TestCtx { id: root, wnd: &wnd }; + let w_ctx = TestCtx { id: root, wnd_id: wnd.id() }; let from_pos = Point::new(30., 30.); assert_eq!(w_ctx.map_from(from_pos, child), Point::new(45., 45.)); let to_pos = Point::new(50., 50.); diff --git a/core/src/data_widget.rs b/core/src/data_widget.rs index d2b619412..523ed8625 100644 --- a/core/src/data_widget.rs +++ b/core/src/data_widget.rs @@ -16,7 +16,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| { + id.wrap_node(&mut ctx.tree.borrow_mut().arena, |child| { Box::new(Self::new(child, data)) }); id @@ -25,30 +25,21 @@ 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( - id: WidgetId, - ctx: &mut BuildCtx, - 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 mut attached = attach_data(tmp); - std::mem::swap(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 56f97b516..df23d01a9 100644 --- a/core/src/declare.rs +++ b/core/src/declare.rs @@ -1,15 +1,79 @@ -use crate::context::BuildCtx; +use crate::{context::BuildCtx, prelude::Pipe, state::ModifyScope}; +use rxrust::ops::box_it::BoxOp; +use std::convert::Infallible; pub trait Declare { type Builder: DeclareBuilder; fn declare_builder() -> Self::Builder; } +/// The next version of `Declare` trait. It will replace the `Declare` trait +/// after it is stable. +pub trait Declare2 { + type Builder: DeclareBuilder; + fn declare2_builder() -> Self::Builder; +} + /// widget builder use to construct a widget in `widget!`. See the [mod level /// 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. +pub enum DeclareInit { + Value(V), + Pipe(Pipe), +} + +type ValueStream = BoxOp<'static, (ModifyScope, V), Infallible>; + +impl DeclareInit { + pub fn unzip(self) -> (V, Option>) { + match self { + Self::Value(v) => (v, None), + Self::Pipe(v) => { + let (v, pipe) = v.unzip(); + (v, Some(pipe)) + } + } + } + + pub fn value(&self) -> &V { + match self { + Self::Value(v) => v, + Self::Pipe(v) => v.value(), + } + } + + pub fn value_mut(&mut self) -> &mut V { + match self { + Self::Value(v) => v, + Self::Pipe(v) => v.value_mut(), + } + } +} + +impl Default for DeclareInit { + #[inline] + fn default() -> Self { Self::Value(T::default()) } +} + +pub trait DeclareFrom { + fn declare_from(value: V) -> Self; +} + +impl> DeclareFrom for DeclareInit { + #[inline] + fn declare_from(value: V) -> Self { Self::Value(value.into()) } +} + +impl + 'static> DeclareFrom, Pipe<()>> for DeclareInit { + #[inline] + fn declare_from(value: Pipe) -> Self { Self::Pipe(value.map(U::from)) } } #[derive(Debug, PartialEq, Hash)] diff --git a/core/src/dynamic_widget.rs b/core/src/dynamic_widget.rs deleted file mode 100644 index 69da355c7..000000000 --- a/core/src/dynamic_widget.rs +++ /dev/null @@ -1,905 +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(); - let arena = &mut tree.arena; - wid.detach(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) { - wid.remove_subtree(&mut wnd.widget_tree.borrow_mut()); - } - 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 { - wid.detach(&mut tree.arena); - 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_before_value(old_key_widget); - }); - old_key_list.remove(key); - } else { - new_key_widget.mounted(); - } - }); - }); - - old_key_list - .values() - .for_each(|wid| inspect_key(wid, arena, |old_key_widget| old_key_widget.disposed())); - } -} - -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: Some(c), - - MockBox { - size: Size::zero(), - on_mounted: move |_| { - if key.is_enter() { - (*enter_list).push(key.value.unwrap()); - } - - if key.is_changed() { - (*update_list).push(key.value.unwrap()); - *key_change = key.get_change(); - } - }, - on_disposed: move |_| { - if key.is_disposed() { - (*leave_list).push(key.value.unwrap()); - } - } - } - } - } - })) - } - }; - - // 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'), Some('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'), Some('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.rs b/core/src/events.rs index 646f5d3f0..0afefabfc 100644 --- a/core/src/events.rs +++ b/core/src/events.rs @@ -1,15 +1,11 @@ +use self::dispatcher::DispatchInfo; use crate::{ context::{define_widget_context, WidgetCtx, WidgetCtxImpl}, + prelude::AppCtx, widget_tree::WidgetId, - window::Window, -}; - -use rxrust::{ - prelude::*, - rc::{MutRc, RcDeref, RcDerefMut}, + window::{Window, WindowId}, }; -use smallvec::SmallVec; -use std::convert::Infallible; +use std::rc::Rc; pub(crate) mod dispatcher; mod pointers; @@ -34,7 +30,7 @@ define_widget_context!( prevent_default: bool ); -impl<'a> CommonEvent<'a> { +impl CommonEvent { /// The target property of the Event interface is a reference to the object /// onto which the event was dispatched. It is different from /// Event::current_target when the event handler is called during the bubbling @@ -107,11 +103,11 @@ impl<'a> CommonEvent<'a> { } pub trait EventListener { - type Event<'a>; - fn dispatch(&self, event: &mut Self::Event<'_>); + type Event; + fn dispatch(&self, event: &mut Self::Event); } -impl<'a> std::fmt::Debug for CommonEvent<'a> { +impl std::fmt::Debug for CommonEvent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("CommonEvent") .field("target", &self.id) @@ -121,16 +117,16 @@ impl<'a> std::fmt::Debug for CommonEvent<'a> { } } -impl<'a> CommonEvent<'a> { +impl CommonEvent { /// Create a new common event. /// /// Although the `dispatcher` is contained in the `wnd`, we still need to pass /// it because in most case the event create in a environment that the /// `Dispatcher` already borrowed. - pub(crate) fn new(target: WidgetId, wnd: &'a Window) -> Self { + pub(crate) fn new(target: WidgetId, wnd_id: WindowId) -> Self { Self { target, - wnd, + wnd_id, id: target, propagation: true, prevent_default: false, @@ -140,102 +136,6 @@ impl<'a> CommonEvent<'a> { pub(crate) fn set_current_target(&mut self, id: WidgetId) { self.id = id; } fn pick_info(&self, f: impl FnOnce(&DispatchInfo) -> R) -> R { - f(&self.wnd.dispatcher.borrow().info) + f(&self.current_wnd().dispatcher.borrow().info) } } - -macro_rules! impl_event_subject { - ($name: ident) => { - paste::paste! { - impl_event_subject!($name, event_name = [<$name Event>]); - } - }; - ($name: ident, event_name = $event: ident) => { - paste::paste! { - pub(crate) type [<$name Publisher>] = - MutRc Publisher<&'a mut $event<'b>, Infallible>>; 1] - >>>; - - #[derive(Clone)] - pub struct [<$name Subject>] { - observers: [<$name Publisher>], - chamber: [<$name Publisher>], - } - - impl<'a, 'b, O> Observable<&'a mut $event<'b>, Infallible, O> for [<$name Subject>] - where - O: for<'r1, 'r2> Observer<&'r1 mut $event<'r2>, Infallible> + 'static, - { - type Unsub = Subscriber; - - fn actual_subscribe(self, observer: O) -> Self::Unsub { - if let Some(chamber) = self.chamber.rc_deref_mut().as_mut() { - self - .observers - .rc_deref_mut() - .as_mut() - .unwrap() - .retain(|p| !p.p_is_closed()); - - let subscriber = Subscriber::new(Some(observer)); - chamber.push(Box::new(subscriber.clone())); - subscriber - } else { - Subscriber::new(None) - } - } - } - impl<'a, 'b> ObservableExt<&'a mut $event<'b>, Infallible> for [<$name Subject>] {} - - impl<'a, 'b> Observer<&'a mut $event<'b>, Infallible> for [<$name Subject>] { - fn next(&mut self, value: &'a mut $event<'b>) { - self.load(); - if let Some(observers) = self.observers.rc_deref_mut().as_mut() { - for p in observers.iter_mut() { - p.p_next(value); - } - } - } - - fn error(self, _: Infallible) {} - - fn complete(mut self) { - self.load(); - if let Some(observers) = self.observers.rc_deref_mut().take() { - observers - .into_iter() - .filter(|o| !o.p_is_closed()) - .for_each(|subscriber| subscriber.p_complete()); - } - } - - #[inline] - fn is_finished(&self) -> bool { self.observers.rc_deref().is_none() } - } - - impl [<$name Subject>] { - fn load(&mut self) { - if let Some(observers) = self.observers.rc_deref_mut().as_mut() { - observers.append(self.chamber.rc_deref_mut().as_mut().unwrap()); - } - } - } - - impl Default for [<$name Subject>] { - fn default() -> Self { - Self { - observers: MutRc::own(Some(<_>::default())), - chamber: MutRc::own(Some(<_>::default())), - } - } - } - } - }; -} - -pub(crate) use impl_event_subject; - -use self::dispatcher::DispatchInfo; - -impl_event_subject!(Common); diff --git a/core/src/events/character.rs b/core/src/events/character.rs index 1b73cf53f..d8b9f5c17 100644 --- a/core/src/events/character.rs +++ b/core/src/events/character.rs @@ -1,21 +1,17 @@ use crate::{ impl_all_event, impl_common_event_deref, impl_compose_child_with_focus_for_listener, - impl_listener, impl_multi_event_listener, impl_query_self_only, prelude::*, + impl_listener, impl_multi_event_listener, impl_query_self_only, prelude::*, window::WindowId, }; -use rxrust::{ - prelude::*, - rc::{MutRc, RcDeref, RcDerefMut}, -}; -use smallvec::SmallVec; +use rxrust::prelude::*; use std::convert::Infallible; #[derive(Debug)] -pub struct CharsEvent<'a> { +pub struct CharsEvent { pub chars: String, - pub common: CommonEvent<'a>, + pub common: CommonEvent, } -impl_event_subject!(Chars, event_name = AllChars); +pub type CharsSubject = MutRefItemSubject<'static, AllChars, Infallible>; impl_multi_event_listener! { "The listener use to fire and listen chars events.", @@ -28,12 +24,12 @@ impl_compose_child_with_focus_for_listener!(CharsListener); impl_common_event_deref!(CharsEvent); -impl<'a> CharsEvent<'a> { +impl CharsEvent { #[inline] - pub fn new(chars: String, id: WidgetId, wnd: &'a Window) -> Self { + pub fn new(chars: String, id: WidgetId, wnd_id: WindowId) -> Self { Self { chars, - common: CommonEvent::new(id, wnd), + common: CommonEvent::new(id, wnd_id), } } } @@ -41,14 +37,14 @@ impl<'a> CharsEvent<'a> { #[cfg(test)] mod tests { use super::*; - use crate::test_helper::*; + use crate::{reset_test_env, test_helper::*}; use std::{cell::RefCell, rc::Rc}; use winit::event::WindowEvent; #[test] fn smoke() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); let receive = Rc::new(RefCell::new("".to_string())); let c_receive = receive.clone(); @@ -67,13 +63,14 @@ mod tests { test_text_case .chars() .for_each(|c| wnd.processes_native_event(WindowEvent::ReceivedCharacter(c))); + wnd.run_frame_tasks(); assert_eq!(&*receive.borrow(), test_text_case); } #[test] fn chars_capture() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); let receive = Rc::new(RefCell::new("".to_string())); let chars_receive = receive.clone(); let capture_receive = receive.clone(); @@ -102,7 +99,7 @@ mod tests { test_text_case .chars() .for_each(|c| wnd.processes_native_event(WindowEvent::ReceivedCharacter(c))); - + wnd.run_frame_tasks(); assert_eq!(&*receive.borrow(), "214263"); } } diff --git a/core/src/events/dispatcher.rs b/core/src/events/dispatcher.rs index 5909fca81..95be391c2 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 }); } @@ -216,7 +216,7 @@ impl Dispatcher { let mut pos = self.info.cursor_pos; while let Some(id) = w { let r = id.assert_get(arena); - let ctx = HitTestCtx { id, wnd: &wnd }; + let ctx = HitTestCtx { id, wnd_id: wnd.id() }; let HitTest { hit, can_hit_child } = r.hit_test(&ctx, pos); pos = tree.store.map_from_parent(id, pos, arena); @@ -272,7 +272,7 @@ impl DispatchInfo { #[cfg(test)] mod tests { use super::*; - use crate::test_helper::*; + use crate::{reset_test_env, test_helper::*}; use std::{cell::RefCell, rc::Rc}; use winit::event::WindowEvent; use winit::event::{DeviceId, ElementState, ModifiersState, MouseButton}; @@ -293,9 +293,8 @@ mod tests { }) } }; - widget! { - DynWidget { - dyns: widget, + fn_widget! { + @$widget { on_pointer_down : handler_ctor(), on_pointer_move: handler_ctor(), on_pointer_up: handler_ctor(), @@ -307,7 +306,7 @@ mod tests { #[test] fn mouse_pointer_bubble() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); let event_record = Rc::new(RefCell::new(vec![])); let record = record_pointer( @@ -316,7 +315,7 @@ mod tests { ); let root = record_pointer( event_record.clone(), - widget! { MockMulti { DynWidget { dyns: record } } }.into(), + fn_widget! { @MockMulti { @ { record } } }.into(), ); let mut wnd = TestWindow::new(root); wnd.draw_frame(); @@ -328,14 +327,14 @@ mod tests { position: (1., 1.).into(), modifiers: ModifiersState::default(), }); - + wnd.run_frame_tasks(); { let mut records = event_record.borrow_mut(); assert_eq!(records.len(), 2); assert_eq!(records[0].btns.bits().count_ones(), 0); records.clear(); } - + wnd.run_frame_tasks(); #[allow(deprecated)] wnd.processes_native_event(WindowEvent::MouseInput { device_id, @@ -343,7 +342,7 @@ mod tests { button: MouseButton::Left, modifiers: ModifiersState::default(), }); - + wnd.run_frame_tasks(); let mut records = event_record.borrow_mut(); assert_eq!(records[0].btns.bits().count_ones(), 1); assert_eq!(records[0].pos, (1., 1.).into()); @@ -352,12 +351,12 @@ mod tests { #[test] fn mouse_buttons() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); let event_record = Rc::new(RefCell::new(vec![])); let root = record_pointer( event_record.clone(), - widget! { MockBox { size: Size::new(100., 30.) } }.into(), + fn_widget! { @MockBox { size: Size::new(100., 30.) } }.into(), ); let mut wnd = TestWindow::new(root); wnd.draw_frame(); @@ -370,6 +369,7 @@ mod tests { button: MouseButton::Left, modifiers: ModifiersState::default(), }); + wnd.run_frame_tasks(); #[allow(deprecated)] wnd.processes_native_event(WindowEvent::MouseInput { @@ -378,6 +378,7 @@ mod tests { button: MouseButton::Right, modifiers: ModifiersState::default(), }); + wnd.run_frame_tasks(); #[allow(deprecated)] wnd.processes_native_event(WindowEvent::CursorMoved { @@ -385,6 +386,7 @@ mod tests { position: (1, 1).into(), modifiers: ModifiersState::default(), }); + wnd.run_frame_tasks(); #[allow(deprecated)] wnd.processes_native_event(WindowEvent::MouseInput { @@ -393,6 +395,7 @@ mod tests { button: MouseButton::Left, modifiers: ModifiersState::default(), }); + wnd.run_frame_tasks(); #[allow(deprecated)] wnd.processes_native_event(WindowEvent::MouseInput { @@ -401,7 +404,7 @@ mod tests { button: MouseButton::Right, modifiers: ModifiersState::default(), }); - + wnd.run_frame_tasks(); let records = event_record.borrow(); assert_eq!(records.len(), 3); @@ -417,7 +420,7 @@ mod tests { #[cfg(not(target_os = "macos"))] #[test] fn different_device_mouse() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); let event_record = Rc::new(RefCell::new(vec![])); let root = record_pointer( @@ -435,7 +438,7 @@ mod tests { button: MouseButton::Left, modifiers: ModifiersState::default(), }); - + wnd.run_frame_tasks(); assert_eq!(event_record.borrow().len(), 1); // A mouse press/release emit during another mouse's press will be ignored. @@ -459,6 +462,7 @@ mod tests { button: MouseButton::Left, modifiers: ModifiersState::default(), }); + wnd.run_frame_tasks(); assert_eq!(event_record.borrow().len(), 1); #[allow(deprecated)] @@ -467,7 +471,7 @@ mod tests { position: (1, 1).into(), modifiers: ModifiersState::default(), }); - + wnd.run_frame_tasks(); // but cursor move processed. assert_eq!(event_record.borrow().len(), 2); assert_eq!(event_record.borrow().len(), 2); @@ -480,27 +484,26 @@ mod tests { button: MouseButton::Left, modifiers: ModifiersState::default(), }); - + wnd.run_frame_tasks(); assert_eq!(event_record.borrow().len(), 3); } #[test] fn cancel_bubble() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); #[derive(Default)] struct EventRecord(Rc>>); impl Compose for EventRecord { fn compose(this: State) -> Widget { - widget! { - states { this: this.into_writable() } - MockBox { + fn_widget! { + @MockBox { size: INFINITY_SIZE, - on_pointer_down: move |e| { this.0.borrow_mut().push(e.current_target()); }, + on_pointer_down: move |e| { $this.0.borrow_mut().push(e.current_target()); }, - MockBox { + @MockBox { size: Size::new(100., 30.), on_pointer_down: move |e| { - this.0.borrow_mut().push(e.current_target()); + $this.0.borrow_mut().push(e.current_target()); e.stop_propagation(); } } @@ -523,13 +526,13 @@ mod tests { button: MouseButton::Left, modifiers: ModifiersState::default(), }); - + wnd.run_frame_tasks(); assert_eq!(event_record.borrow().len(), 1); } #[test] fn enter_leave() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); #[derive(Default)] struct EnterLeave { @@ -539,17 +542,16 @@ mod tests { impl Compose for EnterLeave { fn compose(this: State) -> Widget { - widget! { - states { this: this.into_writable() } - MockBox { + fn_widget! { + @MockBox { size: INFINITY_SIZE, - on_pointer_enter: move |_| { this.enter.borrow_mut().push(2); }, - on_pointer_leave: move |_| { this.leave.borrow_mut().push(2); }, - MockBox { + on_pointer_enter: move |_| { $this.enter.borrow_mut().push(2); }, + on_pointer_leave: move |_| { $this.leave.borrow_mut().push(2); }, + @MockBox { margin: EdgeInsets::all(4.), size: INFINITY_SIZE, - on_pointer_enter: move |_| { this.enter.borrow_mut().push(1); }, - on_pointer_leave: move |_| { this.leave.borrow_mut().push(1); } + on_pointer_enter: move |_| { $this.enter.borrow_mut().push(1); }, + on_pointer_leave: move |_| { $this.leave.borrow_mut().push(1); } } } } @@ -572,6 +574,7 @@ mod tests { position: (10, 10).into(), modifiers: ModifiersState::default(), }); + wnd.run_frame_tasks(); assert_eq!(&*enter_event.borrow(), &[2, 1]); // leave to parent @@ -581,6 +584,7 @@ mod tests { position: (99, 99).into(), modifiers: ModifiersState::default(), }); + wnd.run_frame_tasks(); assert_eq!(&*leave_event.borrow(), &[1]); // move in same widget, @@ -591,6 +595,7 @@ mod tests { position: (99, 99).into(), modifiers: ModifiersState::default(), }); + wnd.run_frame_tasks(); assert_eq!(&*enter_event.borrow(), &[2, 1]); assert_eq!(&*leave_event.borrow(), &[1]); @@ -601,7 +606,7 @@ mod tests { position: (999, 999).into(), modifiers: ModifiersState::default(), }); - + wnd.run_frame_tasks(); assert_eq!(&*leave_event.borrow(), &[1, 2]); // leave event trigger by window left. @@ -614,16 +619,17 @@ mod tests { }); #[allow(deprecated)] wnd.processes_native_event(WindowEvent::CursorLeft { device_id }); + wnd.run_frame_tasks(); assert_eq!(&*leave_event.borrow(), &[1, 2]); } #[test] fn capture_click() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); let click_path = Stateful::new(vec![]) as Stateful>; let w = widget! { - states { click_path: click_path.clone() } + states { click_path: click_path.clone_stateful() } MockBox { size: Size::new(100., 100.), on_tap: move |_| (*click_path).push(4), @@ -663,7 +669,7 @@ mod tests { button: MouseButton::Left, modifiers, }); - + wnd.run_frame_tasks(); { let clicked = click_path.state_ref(); assert_eq!(*clicked, [1, 2, 3, 4]); @@ -672,11 +678,11 @@ mod tests { #[test] fn click() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); let click_path = Stateful::new(0); let w = widget! { - states { click_path: click_path.clone() } + states { click_path: click_path.clone_stateful() } MockMulti { on_tap: move |_| *click_path += 1, MockBox { @@ -715,6 +721,7 @@ mod tests { modifiers, }); + wnd.run_frame_tasks(); { let mut clicked = click_path.state_ref(); assert_eq!(*clicked, 2); @@ -747,7 +754,7 @@ mod tests { button: MouseButton::Left, modifiers, }); - + wnd.run_frame_tasks(); { let clicked = click_path.state_ref(); assert_eq!(*clicked, 1); @@ -756,7 +763,7 @@ mod tests { #[test] fn focus_change_by_event() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); let w = widget! { MockMulti { @@ -817,7 +824,7 @@ mod tests { #[test] fn fix_hit_out_window() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); let w = MockBox { size: INFINITY_SIZE }; let mut wnd = TestWindow::new(w); @@ -831,7 +838,7 @@ mod tests { #[test] fn hit_test_case() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); fn normal_mode_search() { struct T { diff --git a/core/src/events/focus.rs b/core/src/events/focus.rs index 97d376e48..518e9f93f 100644 --- a/core/src/events/focus.rs +++ b/core/src/events/focus.rs @@ -1,8 +1,4 @@ -use rxrust::{ - prelude::*, - rc::{MutRc, RcDeref, RcDerefMut}, -}; -use smallvec::SmallVec; +use rxrust::prelude::*; use std::convert::Infallible; use crate::{ @@ -10,8 +6,9 @@ use crate::{ impl_listener, impl_multi_event_listener, impl_query_self_only, prelude::*, }; -pub type FocusEvent<'a> = CommonEvent<'a>; -impl_event_subject!(Focus, event_name = AllFocus); +pub type FocusEvent = CommonEvent; +pub type FocusSubject = MutRefItemSubject<'static, AllFocus, Infallible>; + impl_multi_event_listener! { "The listener use to fire and listen focus events.", Focus, @@ -25,8 +22,9 @@ impl_multi_event_listener! { } impl_compose_child_with_focus_for_listener!(FocusListener); -type FocusBubbleEvent<'a> = CommonEvent<'a>; -impl_event_subject!(FocusBubble, event_name = AllFocusBubble); +pub type FocusBubbleEvent = CommonEvent; +pub type FocusBubbleSubject = MutRefItemSubject<'static, AllFocusBubble, Infallible>; + impl_multi_event_listener! { "The listener use to fire and listen focusin and focusout events.", FocusBubble, diff --git a/core/src/events/focus_mgr.rs b/core/src/events/focus_mgr.rs index 9d97b8fd4..cd76f338e 100644 --- a/core/src/events/focus_mgr.rs +++ b/core/src/events/focus_mgr.rs @@ -531,12 +531,12 @@ impl FocusManager { #[cfg(test)] mod tests { use super::*; - use crate::test_helper::*; + use crate::{reset_test_env, test_helper::*}; use std::{cell::RefCell, rc::Rc}; #[test] fn two_auto_focus() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); // two auto focus widget let size = Size::zero(); @@ -560,7 +560,7 @@ mod tests { #[test] fn on_auto_focus() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); // one auto focus widget let size = Size::zero(); let widget = widget! { @@ -585,7 +585,7 @@ mod tests { #[test] fn tab_index() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); let size = Size::zero(); let widget = widget! { @@ -648,7 +648,7 @@ mod tests { #[test] fn focus_event() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); #[derive(Debug, Default)] struct EmbedFocus { @@ -657,20 +657,35 @@ mod tests { impl Compose for EmbedFocus { fn compose(this: State) -> Widget { - widget! { - states { this: this.into_writable() } - MockBox { + fn_widget! { + @MockBox { size: INFINITY_SIZE, - on_focus: move |_| { this.log.borrow_mut().push("focus parent"); }, - on_blur: move |_| { this.log.borrow_mut().push("blur parent"); }, - on_focus_in: move |_| { this.log.borrow_mut().push("focusin parent"); }, - on_focus_out: move |_| { this.log.borrow_mut().push("focusout parent"); }, - MockBox { + on_focus: move |_| { + $this.log.borrow_mut().push("focus parent"); + }, + on_blur: move |_| { + $this.log.borrow_mut().push("blur parent"); + }, + on_focus_in: move |_| { + $this.log.borrow_mut().push("focusin parent"); + }, + on_focus_out: move |_| { + $this.log.borrow_mut().push("focusout parent"); + }, + @MockBox { size: Size::zero(), - on_focus: move |_| { this.log.borrow_mut().push("focus child"); }, - on_blur: move |_| { this.log.borrow_mut().push("blur child"); }, - on_focus_in: move |_| { this.log.borrow_mut().push("focusin child"); }, - on_focus_out: move |_| { this.log.borrow_mut().push("focusout child"); }, + on_focus: move |_| { + $this.log.borrow_mut().push("focus child"); + }, + on_blur: move |_| { + $this.log.borrow_mut().push("blur child"); + }, + on_focus_in: move |_| { + $this.log.borrow_mut().push("focusin child"); + }, + on_focus_out: move |_| { + $this.log.borrow_mut().push("focusout child"); + }, } } } @@ -696,7 +711,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,24 +719,24 @@ 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",]); } #[test] fn dynamic_focus() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); let visible = Stateful::new(Some(())); - let w = widget! { - states { visible: visible.clone_stateful() } - MockMulti{ - Option::map(*visible, |_| widget!{ - MockBox { + let c_visible = visible.clone_writer(); + let w = fn_widget! { + @MockMulti{ + @ { pipe! { + $visible.map(|_| @MockBox { size: Size::default(), on_tap: move |_| {}, - } - }) + })} + } } }; @@ -730,10 +745,10 @@ mod tests { wnd.draw_frame(); - *visible.state_ref() = None; + *c_visible.write() = None; wnd.draw_frame(); - *visible.state_ref() = Some(()); + *c_visible.write() = Some(()); wnd.draw_frame(); assert_eq!(wnd.focus_mgr.borrow().focusing(), focus_id); @@ -741,7 +756,7 @@ mod tests { #[test] fn scope_node_request_focus() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); let w = widget! { MockMulti{ diff --git a/core/src/events/keyboard.rs b/core/src/events/keyboard.rs index 09f842fe8..4f8e0220f 100644 --- a/core/src/events/keyboard.rs +++ b/core/src/events/keyboard.rs @@ -1,23 +1,19 @@ -use rxrust::{ - prelude::*, - rc::{MutRc, RcDeref, RcDerefMut}, -}; -use smallvec::SmallVec; +use rxrust::prelude::*; use std::convert::Infallible; use crate::{ impl_all_event, impl_common_event_deref, impl_compose_child_with_focus_for_listener, - impl_listener, impl_multi_event_listener, impl_query_self_only, prelude::*, + impl_listener, impl_multi_event_listener, impl_query_self_only, prelude::*, window::WindowId, }; #[derive(Debug)] -pub struct KeyboardEvent<'a> { +pub struct KeyboardEvent { pub scan_code: ScanCode, pub key: VirtualKeyCode, - common: CommonEvent<'a>, + common: CommonEvent, } -impl_event_subject!(Keyboard, event_name = AllKeyboard); +pub type KeyboardSubject = MutRefItemSubject<'static, AllKeyboard, Infallible>; impl_multi_event_listener! { "The listener use to fire and listen keyboard events.", @@ -36,13 +32,13 @@ impl_common_event_deref!(KeyboardEvent); impl_compose_child_with_focus_for_listener!(KeyboardListener); -impl<'a> KeyboardEvent<'a> { +impl KeyboardEvent { #[inline] - pub fn new(scan_code: ScanCode, key: VirtualKeyCode, id: WidgetId, wnd: &'a Window) -> Self { + pub fn new(scan_code: ScanCode, key: VirtualKeyCode, id: WidgetId, wnd_id: WindowId) -> Self { Self { scan_code, key, - common: CommonEvent::new(id, wnd), + common: CommonEvent::new(id, wnd_id), } } } @@ -77,26 +73,25 @@ mod tests { impl Compose for Keys { fn compose(this: State) -> Widget { - widget! { - states { this: this.into_writable() } - MockBox { + fn_widget! { + @MockBox { size: Size::zero(), on_key_down_capture: move |key| { - this.0.borrow_mut().push(format!("key down capture {:?}", key.key)); + $this.0.borrow_mut().push(format!("key down capture {:?}", key.key)); }, on_key_up_capture: move |key| { - this.0.borrow_mut().push(format!("key up capture {:?}", key.key)); + $this.0.borrow_mut().push(format!("key up capture {:?}", key.key)); }, - MockBox { + @MockBox { size: Size::zero(), auto_focus: true, on_key_down: move |key| { - this.0 + $this.0 .borrow_mut() .push(format!("key down {:?}", key.key)); }, on_key_up: move |key| { - this.0.borrow_mut().push(format!("key up {:?}", key.key)); + $this.0.borrow_mut().push(format!("key up {:?}", key.key)); } } } @@ -120,6 +115,8 @@ mod tests { #[allow(deprecated)] wnd.processes_native_event(new_key_event(VirtualKeyCode::Key1, ElementState::Released)); + wnd.run_frame_tasks(); + assert_eq!( &*keys.borrow(), &[ diff --git a/core/src/events/listener_impl_helper.rs b/core/src/events/listener_impl_helper.rs index e6e856dab..042fd9d5c 100644 --- a/core/src/events/listener_impl_helper.rs +++ b/core/src/events/listener_impl_helper.rs @@ -3,15 +3,15 @@ macro_rules! impl_all_event { ($name: ident, $($on_doc: literal, $event_ty: ident),+) => { paste::paste! { #[doc="All `" $name:snake "` related events"] - pub enum []<'a> { + pub enum [] { $( #[doc = $on_doc] - $event_ty([<$name Event>]<'a>), + $event_ty([<$name Event>]), )+ } - impl<'a> std::ops::Deref for []<'a> { - type Target = [<$name Event>]<'a>; + impl std::ops::Deref for [] { + type Target = [<$name Event>]; fn deref(&self) -> &Self::Target { match self { $([]::$event_ty(e)) |+ => e @@ -19,7 +19,7 @@ macro_rules! impl_all_event { } } - impl<'a> std::ops::DerefMut for []<'a> { + impl std::ops::DerefMut for [] { fn deref_mut(&mut self) -> &mut Self::Target { match self { $([]::$event_ty(e)) |+ => e @@ -27,8 +27,8 @@ macro_rules! impl_all_event { } } - impl<'a> []<'a> { - pub fn into_inner(self) -> [<$name Event>]<'a> { + impl [] { + pub fn into_inner(self) -> [<$name Event>] { match self { $([]::$event_ty(e)) |+ => e } @@ -43,7 +43,7 @@ macro_rules! impl_listener { ($doc: literal, $name: ident, $event_ty: ident) => { paste::paste! { #[doc= $doc] - #[derive(Declare)] + #[derive(Declare, Declare2)] pub struct [<$name Listener>]{ #[declare(skip)] [<$name:snake _subject>]: [<$name Subject>] @@ -58,6 +58,16 @@ macro_rules! impl_listener { } } + impl [<$name ListenerDeclarer2>] { + fn subject(&mut self) -> [<$name Subject>] { + self + .[<$name:snake _subject>] + .get_or_insert_with(DeclareInit::default) + .value() + .clone() + } + } + impl [<$name Listener>] { /// Convert a observable stream of this event. pub fn [<$name:snake _stream>](&self) -> [<$name Subject>] { @@ -66,9 +76,9 @@ macro_rules! impl_listener { } impl EventListener for [<$name Listener>] { - type Event<'a> = $event_ty<'a>; + type Event = $event_ty; #[inline] - fn dispatch(&self, event: &mut Self::Event<'_>) { + fn dispatch(&self, event: &mut Self::Event) { self.[<$name:snake _subject>].clone().next(event) } } @@ -88,13 +98,35 @@ macro_rules! impl_multi_event_listener { impl_all_event!($name, $($on_doc, $event_ty),+); impl_listener!($doc, $name, []); + impl [<$name ListenerDeclarer2>] { + $( + #[doc = "Sets up a function that will be called whenever the `" $event_ty "` is delivered"] + pub fn []( + mut self, + handler: impl FnMut(&mut [<$name Event>]) + 'static + ) -> Self + { + self + .subject() + .filter_map( + (|e| match e { + []::$event_ty(e) => Some(e), + _ => None, + }) as fn(&mut []) -> Option<&mut [<$name Event>]> + ) + .subscribe(handler); + self + } + )+ + } + impl [<$name ListenerDeclarer>] { $( #[doc = "Sets up a function that will be called \ whenever the `" $event_ty "` is delivered"] pub fn []( mut self, - handler: impl for<'r> FnMut(&'r mut [<$name Event>]<'_>) + 'static + handler: impl FnMut(&mut [<$name Event>]) + 'static ) -> Self { self .subject() @@ -102,8 +134,7 @@ macro_rules! impl_multi_event_listener { (|e| match e { []::$event_ty(e) => Some(e), _ => None, - }) as for<'a, 'b> fn(&'a mut []::<'b>) - -> Option<&'a mut [<$name Event>]<'b>>, + }) as fn(&mut []) -> Option<&mut [<$name Event>]> ) .subscribe(handler); self @@ -120,11 +151,24 @@ macro_rules! impl_single_event_listener { paste::paste! { impl_listener!($doc, $name); + impl [<$name ListenerDeclarer2>] { + #[doc = "Sets up a function that will be called whenever the `" [<$name Event>] "` is delivered"] + pub fn []( + self, + handler: impl FnMut(&'_ mut [<$name Event>]<'_>) + 'static + ) -> Self { + self + .subject() + .subscribe(handler); + self + } + } + impl [<$name ListenerDeclarer>] { #[doc = "Sets up a function that will be called whenever the `" [<$name Event>] "` is delivered"] pub fn []( self, - handler: impl for<'r> FnMut(&'r mut [<$name Event>]<'_>) + 'static + handler: impl FnMut(&'_ mut [<$name Event>]<'_>) + 'static ) -> Self { self .subject() @@ -139,26 +183,26 @@ macro_rules! impl_single_event_listener { #[macro_export] macro_rules! impl_common_event_deref { ($event_name: ident) => { - impl<'a> std::ops::Deref for $event_name<'a> { - type Target = CommonEvent<'a>; + impl std::ops::Deref for $event_name { + type Target = CommonEvent; #[inline] fn deref(&self) -> &Self::Target { &self.common } } - impl<'a> std::ops::DerefMut for $event_name<'a> { + impl std::ops::DerefMut for $event_name { #[inline] fn deref_mut(&mut self) -> &mut Self::Target { &mut self.common } } - impl<'a> std::borrow::Borrow> for $event_name<'a> { + impl std::borrow::Borrow for $event_name { #[inline] - fn borrow(&self) -> &CommonEvent<'a> { &self.common } + fn borrow(&self) -> &CommonEvent { &self.common } } - impl<'a> std::borrow::BorrowMut> for $event_name<'a> { + impl std::borrow::BorrowMut for $event_name { #[inline] - fn borrow_mut(&mut self) -> &mut CommonEvent<'a> { &mut self.common } + fn borrow_mut(&mut self) -> &mut CommonEvent { &mut self.common } } }; } diff --git a/core/src/events/pointers.rs b/core/src/events/pointers.rs index 8910ecd23..5751dce4e 100644 --- a/core/src/events/pointers.rs +++ b/core/src/events/pointers.rs @@ -3,11 +3,7 @@ use crate::{ impl_all_event, impl_common_event_deref, impl_compose_child_for_listener, impl_listener, impl_multi_event_listener, impl_query_self_only, prelude::*, }; -use rxrust::{ - prelude::*, - rc::{MutRc, RcDeref, RcDerefMut}, -}; -use smallvec::SmallVec; +use rxrust::prelude::*; use std::{ convert::Infallible, time::{Duration, Instant}, @@ -25,7 +21,7 @@ pub struct PointerId(usize); /// property to inspect the device type which produced the event. /// Reference: #[derive(Debug)] -pub struct PointerEvent<'a> { +pub struct PointerEvent { /// A unique identifier for the pointer causing the event. pub id: PointerId, /// The width (magnitude on the X axis), in pixels, of the contact geometry of @@ -58,7 +54,7 @@ pub struct PointerEvent<'a> { /// type. pub is_primary: bool, - pub common: CommonEvent<'a>, + pub common: CommonEvent, } bitflags! { @@ -116,7 +112,7 @@ impl_multi_event_listener!( impl_common_event_deref!(PointerEvent); -impl_event_subject!(Pointer, event_name = AllPointer); +pub type PointerSubject = MutRefItemSubject<'static, AllPointer, Infallible>; impl_compose_child_for_listener!(PointerListener); @@ -124,7 +120,7 @@ fn x_times_tap_map_filter( x: usize, dur: Duration, capture: bool, -) -> impl for<'a, 'b> FnMut(&'a mut AllPointer<'b>) -> Option<&'a mut PointerEvent<'b>> { +) -> impl FnMut(&mut AllPointer) -> Option<&mut PointerEvent> { assert!(x > 0); struct TapInfo { pointer_id: PointerId, @@ -166,119 +162,123 @@ fn x_times_tap_map_filter( } impl PointerListenerDeclarer { - pub fn on_double_tap( - self, - handler: impl for<'a, 'b> FnMut(&'a mut PointerEvent<'b>) + 'static, - ) -> Self { + pub fn on_double_tap(self, handler: impl FnMut(&mut PointerEvent) + 'static) -> Self { self.on_x_times_tap((2, handler)) } - pub fn on_double_tap_capture( - self, - handler: impl for<'a, 'b> FnMut(&'a mut PointerEvent<'b>) + 'static, - ) -> Self { + pub fn on_double_tap_capture(self, handler: impl FnMut(&mut PointerEvent) + 'static) -> Self { self.on_x_times_tap_capture((2, handler)) } - pub fn on_triple_tap( + pub fn on_triple_tap(self, handler: impl FnMut(&mut PointerEvent) + 'static) -> Self { + self.on_x_times_tap((3, handler)) + } + + pub fn on_triple_tap_capture(self, handler: impl FnMut(&mut PointerEvent) + 'static) -> Self { + self.on_x_times_tap_capture((3, handler)) + } + + pub fn on_x_times_tap( self, - handler: impl for<'a, 'b> FnMut(&'a mut PointerEvent<'b>) + 'static, + (times, handler): (usize, impl FnMut(&mut PointerEvent) + 'static), ) -> Self { - self.on_x_times_tap((3, handler)) + self.on_x_times_tap_impl(times, MULTI_TAP_DURATION, false, handler) } - pub fn on_triple_tap_capture( + pub fn on_x_times_tap_capture( self, - handler: impl for<'a, 'b> FnMut(&'a mut PointerEvent<'b>) + 'static, + (times, handler): (usize, impl FnMut(&mut PointerEvent) + 'static), ) -> Self { - self.on_x_times_tap_capture((3, handler)) + self.on_x_times_tap_impl(times, MULTI_TAP_DURATION, true, handler) } - pub fn on_x_times_tap( + fn on_x_times_tap_impl( mut self, - (times, handler): ( - usize, - impl for<'a, 'b> FnMut(&'a mut PointerEvent<'b>) + 'static, - ), + times: usize, + dur: Duration, + capture: bool, + handler: impl FnMut(&mut PointerEvent) + 'static, ) -> Self { - self.on_x_times_tap_impl(times, MULTI_TAP_DURATION, false, handler); self + .subject() + .filter_map(x_times_tap_map_filter(times, dur, capture)) + .subscribe(handler); + self + } +} + +impl PointerListenerDeclarer2 { + pub fn on_double_tap(self, handler: impl FnMut(&mut PointerEvent) + 'static) -> Self { + self.on_x_times_tap((2, handler)) + } + + pub fn on_double_tap_capture(self, handler: impl FnMut(&mut PointerEvent) + 'static) -> Self { + self.on_x_times_tap_capture((2, handler)) + } + + pub fn on_triple_tap(self, handler: impl FnMut(&mut PointerEvent) + 'static) -> Self { + self.on_x_times_tap((3, handler)) + } + + pub fn on_triple_tap_capture(self, handler: impl FnMut(&mut PointerEvent) + 'static) -> Self { + self.on_x_times_tap_capture((3, handler)) + } + + pub fn on_x_times_tap( + self, + (times, handler): (usize, impl FnMut(&mut PointerEvent) + 'static), + ) -> Self { + self.on_x_times_tap_impl(times, MULTI_TAP_DURATION, false, handler) } pub fn on_x_times_tap_capture( - mut self, - (times, handler): ( - usize, - impl for<'a, 'b> FnMut(&'a mut PointerEvent<'b>) + 'static, - ), + self, + (times, handler): (usize, impl FnMut(&mut PointerEvent) + 'static), ) -> Self { - self.on_x_times_tap_impl(times, MULTI_TAP_DURATION, true, handler); - self + self.on_x_times_tap_impl(times, MULTI_TAP_DURATION, true, handler) } fn on_x_times_tap_impl( - &mut self, + mut self, times: usize, dur: Duration, capture: bool, - handler: impl for<'a, 'b> FnMut(&'a mut PointerEvent<'b>) + 'static, - ) { + handler: impl FnMut(&mut PointerEvent) + 'static, + ) -> Self { self .subject() .filter_map(x_times_tap_map_filter(times, dur, capture)) .subscribe(handler); + self } } impl PointerListener { - pub fn on_double_tap( - self, - handler: impl for<'a, 'b> FnMut(&'a mut PointerEvent<'b>) + 'static, - ) -> Self { + pub fn on_double_tap(self, handler: impl FnMut(&mut PointerEvent) + 'static) { self.on_x_times_tap((2, handler)) } - pub fn on_double_tap_capture( - self, - handler: impl for<'a, 'b> FnMut(&'a mut PointerEvent<'b>) + 'static, - ) -> Self { + pub fn on_double_tap_capture(self, handler: impl FnMut(&mut PointerEvent) + 'static) { self.on_x_times_tap_capture((2, handler)) } - pub fn on_triple_tap( - self, - handler: impl for<'a, 'b> FnMut(&'a mut PointerEvent<'b>) + 'static, - ) -> Self { + pub fn on_triple_tap(self, handler: impl FnMut(&mut PointerEvent) + 'static) { self.on_x_times_tap((3, handler)) } - pub fn on_triple_tap_capture( - self, - handler: impl for<'a, 'b> FnMut(&'a mut PointerEvent<'b>) + 'static, - ) -> Self { + pub fn on_triple_tap_capture(self, handler: impl FnMut(&mut PointerEvent) + 'static) { self.on_x_times_tap_capture((3, handler)) } - pub fn on_x_times_tap( - self, - (times, handler): ( - usize, - impl for<'a, 'b> FnMut(&'a mut PointerEvent<'b>) + 'static, - ), - ) -> Self { + pub fn on_x_times_tap(self, (times, handler): (usize, impl FnMut(&mut PointerEvent) + 'static)) { self.on_x_times_tap_impl(times, MULTI_TAP_DURATION, false, handler); - self } pub fn on_x_times_tap_capture( self, - (times, handler): ( - usize, - impl for<'a, 'b> FnMut(&'a mut PointerEvent<'b>) + 'static, - ), - ) -> Self { + (times, handler): (usize, impl FnMut(&mut PointerEvent) + 'static), + ) { self.on_x_times_tap_impl(times, MULTI_TAP_DURATION, true, handler); - self } fn on_x_times_tap_impl( @@ -286,7 +286,7 @@ impl PointerListener { x: usize, dur: Duration, capture: bool, - handler: impl for<'a, 'b> FnMut(&'a mut PointerEvent<'b>) + 'static, + handler: impl FnMut(&mut PointerEvent) + 'static, ) { self .pointer_stream() @@ -298,7 +298,10 @@ impl PointerListener { #[cfg(test)] mod tests { use super::*; - use crate::test_helper::{MockBox, MockMulti, TestWindow}; + use crate::{ + reset_test_env, + test_helper::{MockBox, MockMulti, TestWindow}, + }; use std::{cell::RefCell, rc::Rc}; use winit::{ dpi::LogicalPosition, @@ -332,31 +335,30 @@ mod tests { #[test] fn tap_focus() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); let tap_cnt = Rc::new(RefCell::new(0)); let is_focused = Rc::new(RefCell::new(false)); let tap_cnt1 = tap_cnt.clone(); let tap_cnt2 = tap_cnt.clone(); - let is_focused1 = is_focused.clone(); - let w = widget! { - MockMulti { - id: host, - MockBox { + let is_focused2 = is_focused.clone(); + let w = fn_widget! { + let mut host = @MockMulti {}; + watch!($host.has_focus()) + .subscribe(move |v| *is_focused2.borrow_mut() = v); + + @$host { + @MockBox { size: Size::new(50., 50.,), on_tap: move |_| *tap_cnt1.borrow_mut() += 1, } - MockBox { + @MockBox { size: Size::new(50., 50.,), on_tap: move |_| *tap_cnt2.borrow_mut() += 1, on_key_down: move |_| println!("dummy code"), } } - finally { - let_watch!(host.has_focus()) - .subscribe(move |v| *is_focused1.borrow_mut() = v); - } }; let mut wnd = TestWindow::new_with_size(w, Size::new(100., 100.)); wnd.draw_frame(); diff --git a/core/src/events/pointers/from_mouse.rs b/core/src/events/pointers/from_mouse.rs index 5f137ffaf..59b5107f3 100644 --- a/core/src/events/pointers/from_mouse.rs +++ b/core/src/events/pointers/from_mouse.rs @@ -2,8 +2,8 @@ use super::PointerId; use crate::prelude::*; use winit::event::MouseButton; -impl<'a> PointerEvent<'a> { - pub(crate) fn from_mouse(target: WidgetId, wnd: &'a Window) -> Self { +impl PointerEvent { + pub(crate) fn from_mouse(target: WidgetId, wnd: &Window) -> Self { let no_button = wnd.dispatcher.borrow().info.mouse_buttons().is_empty(); PointerEvent { // todo: we need to trace the pressed pointer, how to generate pointer id, by device + button? @@ -16,7 +16,7 @@ impl<'a> PointerEvent<'a> { twist: 0., point_type: PointerType::Mouse, is_primary: true, - common: CommonEvent::new(target, wnd), + common: CommonEvent::new(target, wnd.id()), } } } diff --git a/core/src/events/wheel.rs b/core/src/events/wheel.rs index 0b4ffcde6..6a1c09571 100644 --- a/core/src/events/wheel.rs +++ b/core/src/events/wheel.rs @@ -1,22 +1,18 @@ use crate::{ impl_all_event, impl_common_event_deref, impl_compose_child_for_listener, impl_listener, - impl_multi_event_listener, impl_query_self_only, prelude::*, + impl_multi_event_listener, impl_query_self_only, prelude::*, window::WindowId, }; -use rxrust::{ - prelude::*, - rc::{MutRc, RcDeref, RcDerefMut}, -}; -use smallvec::SmallVec; +use rxrust::prelude::*; use std::convert::Infallible; #[derive(Debug)] -pub struct WheelEvent<'a> { +pub struct WheelEvent { pub delta_x: f32, pub delta_y: f32, - pub common: CommonEvent<'a>, + pub common: CommonEvent, } -impl_event_subject!(Wheel, event_name = AllWheel); +pub type WheelSubject = MutRefItemSubject<'static, AllWheel, Infallible>; impl_multi_event_listener! { "The listener use to fire and listen wheel events.", @@ -30,13 +26,13 @@ impl_multi_event_listener! { impl_compose_child_for_listener!(WheelListener); impl_common_event_deref!(WheelEvent); -impl<'a> WheelEvent<'a> { +impl WheelEvent { #[inline] - pub fn new(delta_x: f32, delta_y: f32, id: WidgetId, wnd: &'a Window) -> Self { + pub fn new(delta_x: f32, delta_y: f32, id: WidgetId, wnd_id: WindowId) -> Self { Self { delta_x, delta_y, - common: CommonEvent::new(id, wnd), + common: CommonEvent::new(id, wnd_id), } } } @@ -89,6 +85,7 @@ mod tests { phase: TouchPhase::Started, modifiers: ModifiersState::default(), }); + wnd.run_frame_tasks(); assert_eq!(*source_receive_for_bubble.borrow(), (1., 1.)); assert_eq!(*source_receive_for_capture.borrow(), (1., 1.)); diff --git a/core/src/lib.rs b/core/src/lib.rs index cb39b7f46..283a537d9 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,4 +1,5 @@ -#![feature(test, decl_macro)] +#![feature(decl_macro)] +#![cfg_attr(test, feature(mutex_unpoison, test))] #[macro_use] extern crate bitflags; @@ -11,22 +12,20 @@ pub mod data_widget; mod state; pub(crate) mod widget_tree; -pub mod assign_observable; pub mod clipboard; pub mod declare; -pub mod dynamic_widget; pub mod events; +pub mod pipe; pub mod ticker; pub mod timer; pub mod widget; pub mod widget_children; pub mod window; +pub use rxrust; pub mod prelude { pub use crate::animation::*; #[doc(no_inline)] - pub use crate::assign_observable::AssignObservable; - #[doc(no_inline)] pub use crate::builtin_widgets::*; #[doc(no_inline)] pub use crate::context::*; @@ -35,10 +34,10 @@ 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::*; #[doc(no_inline)] + pub use crate::pipe::Pipe; + #[doc(no_inline)] pub use crate::state::*; #[doc(no_inline)] pub use crate::widget; @@ -57,7 +56,10 @@ pub mod prelude { pub use ribir_algo::CowArc; pub use ribir_geom::*; #[doc(no_inline)] - pub use ribir_macros::{include_svg, widget, Declare, Lerp, MultiChild, SingleChild, Template}; + pub use ribir_macros::{ + 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::*; #[doc(no_inline)] diff --git a/core/src/pipe.rs b/core/src/pipe.rs new file mode 100644 index 000000000..8f09d83b9 --- /dev/null +++ b/core/src/pipe.rs @@ -0,0 +1,1041 @@ +use ribir_algo::Sc; +use rxrust::{ + ops::box_it::BoxOp, + prelude::{BoxIt, ObservableExt, ObservableItem}, + subscription::Subscription, +}; +use std::{ + cell::{Cell, RefCell, UnsafeCell}, + convert::Infallible, + ops::{Deref, Range}, +}; + +use crate::{ + builtin_widgets::{key::AnyKey, Void}, + context::{AppCtx, BuildCtx}, + impl_proxy_render, + prelude::*, + ticker::FrameMsg, + widget::{Query, QueryOrder, Render, 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, (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, (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, (ModifyScope, V), Infallible>) -> R, + ) -> Pipe + where + R: BoxIt>, + { + let Self { value, observable } = self; + let observable = f(observable).box_it(); + Pipe { value, observable } + } + + /// Creates a new `Pipe` which calls a closure on each element and + /// uses its return as the value. + pub fn map(self, mut f: F) -> Pipe + where + F: FnMut(V) -> R + 'static, + V: 'static, + { + let Self { value, observable } = self; + Pipe { + value: f(value), + 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, (ModifyScope, V), Infallible>) { + (self.value, self.observable) + } + + #[inline] + pub fn value(&self) -> &V { &self.value } + + #[inline] + pub fn value_mut(&mut self) -> &mut V { &mut self.value } +} + +impl + 'static> WidgetBuilder for Pipe { + fn build(self, ctx: &BuildCtx) -> WidgetId { + let (v, modifies) = self.unzip(); + let id = v.into().build(ctx); + let id_share = Sc::new(Cell::new(id)); + + let mut pipe_node = None; + id.wrap_node(&mut ctx.tree.borrow_mut().arena, |r| { + let p = PipeNode::new(r); + pipe_node = Some(p.clone()); + Box::new(p) + }); + + let id_share2 = id_share.clone(); + let handle = ctx.handle(); + 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(); + if !ctx.window().is_in_another_regenerating(id) { + let new_id = v.into().build(ctx); + + // We use a `PipeNode` wrap the initial widget node, so if the widget node is + // not a `PipeNode` means the node is attached some data, we need to keep the + // data attached when it replaced by the new widget, because the data is the + // static stuff. + // + // Only pipe widget as a normal widget need to do this, because compose child + // widget not support pipe object as its child if it's not a normal widget. Only + // compose child widget has the ability to apply new logic on a widget that + // built. + if let Some(pn) = pipe_node.as_mut() { + let mut tree = ctx.tree.borrow_mut(); + if !id.assert_get(&tree.arena).is::() { + let [old_node, new_node] = tree.get_many_mut(&[id, new_id]); + + std::mem::swap(pn.as_mut(), new_node.as_widget_mut()); + std::mem::swap(old_node, new_node); + } else { + // we know widget node not attached data, we can not care about it now. + pipe_node.take(); + } + } + + 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) + } + }); + }); + attach_unsubscribe_guard(id, ctx.window().id(), unsub); + + id + } +} + +impl + 'static> WidgetBuilder for Pipe> { + fn build(self, ctx: &crate::context::BuildCtx) -> WidgetId { + self + .map(|w| w.map_or_else(|| Widget::from(Void), |w| w.into())) + .build(ctx) + } +} + +impl 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 + } +} + +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 + } + } +} + +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) + } + }) + .append_child(child, ctx) + } +} + +fn half_to_close_interval(rg: Range, tree: &WidgetTree) -> Range { + rg.start..rg.end.parent(&tree.arena).unwrap() +} + +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> { + pub(crate) fn build_multi(self, vec: &mut Vec, ctx: &mut BuildCtx) + where + W: IntoIterator, + W::Item: Into, + { + fn build_multi( + v: Multi>>, + ctx: &mut BuildCtx, + ) -> Vec { + let mut ids = v + .into_inner() + .into_iter() + .map(|w| w.into().build(ctx)) + .collect::>(); + + if ids.is_empty() { + ids.push(Void.build(ctx)); + } + + ids + } + + let (m, modifies) = self.unzip(); + + 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 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 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); + } +} + +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)); + p.wrap_node(&mut tree.arena, |d| Box::new(DataWidget::new(d, guard))) + } + } + }) + .unwrap(); +} + +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) + }); + }); + } + (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(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() { + update_key_states(old_key, old, new_key, new, ctx) + } + }) + }); +} + +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| { + old_key_list.insert(old_key.key(), o); + }); + } + + 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) -> R) -> Option { + ctx + .assert_get(id) + .query_on_first_type::, _>(QueryOrder::OutsideFirst, |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 {} + +/// `PipeNode` just use to wrap a `Box`, and provide a choice to +/// change the inner `Box` by `UnsafeCell` at a safe time. It's +/// transparent except the `Pipe` widget. +#[derive(Clone)] +struct PipeNode(Sc>>); + +impl PipeNode { + fn new(value: Box) -> Self { Self(Sc::new(UnsafeCell::new(value))) } + + fn as_ref(&self) -> &dyn Render { + // safety: see the `PipeNode` document. + unsafe { &**self.0.get() } + } + + fn as_mut(&mut self) -> &mut Box { + // safety: see the `PipeNode` document. + unsafe { &mut *self.0.get() } + } +} + +impl Query for PipeNode { + fn query_all( + &self, + type_id: std::any::TypeId, + callback: &mut dyn FnMut(&dyn std::any::Any) -> bool, + order: QueryOrder, + ) { + self.as_ref().query_all(type_id, callback, order) + } +} + +impl_proxy_render!(proxy as_ref(), PipeNode); + +#[cfg(test)] +mod tests { + use std::{ + cell::{Cell, Ref}, + rc::Rc, + }; + + use crate::{ + builtin_widgets::key::{AnyKey, 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 attach_data_to_pipe_widget() { + reset_test_env!(); + let trigger = Stateful::new(false); + let c_trigger = trigger.clone_reader(); + let w = fn_widget! { + let p = pipe! { + // just use to force update the widget, when trigger modified. + $c_trigger; + MockBox { size: Size::zero() } + }; + @KeyWidget { + key: 0, + value: (), + @ { p } + } + }; + + let mut wnd = TestWindow::new(w); + wnd.draw_frame(); + { + *trigger.write() = true; + } + wnd.draw_frame(); + let tree = wnd.widget_tree.borrow(); + + // the key should still in the root widget after pipe widget updated. + assert!( + tree + .root() + .assert_get(&tree.arena) + .contain_type::>() + ); + } + + #[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 c95772618..df30fae65 100644 --- a/core/src/state.rs +++ b/core/src/state.rs @@ -1,46 +1,254 @@ -mod readonly; +mod map_state; +mod splitted_state; mod stateful; -use std::rc::Rc; -pub use readonly::*; -use rxrust::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, - widget::{Compose, 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. -#[derive(Clone)] -pub enum State { - Stateless(W), +pub struct State(pub(crate) UnsafeCell>); + +pub(crate) enum InnerState { + Data(StateData), Stateful(Stateful), } +impl StateReader for State { + type Value = T; + type OriginReader = Self; + type Reader = Reader; + type Ref<'a> = ReadRef<'a,T> + where + Self: 'a; + + 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() } + + #[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 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.0.into_inner() { + InnerState::Data(w) => w.into_inner().into(), + InnerState::Stateful(w) => w.into(), + } + } +} + impl WidgetBuilder for State { #[inline] fn build(self, ctx: &BuildCtx) -> WidgetId { Compose::compose(self).build(ctx) } } 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 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 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 into_writable(self) -> Stateful { self.as_stateful().clone_stateful() } + + pub fn clone_stateful(&mut self) -> Stateful { self.as_stateful().clone_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); + }; + + match self.inner_ref() { + InnerState::Stateful(w) => w, + _ => unreachable!(), + } + } + InnerState::Stateful(w) => w, } } + + 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() } + } } pub(crate) trait StateFrom { @@ -49,38 +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) - } + fn state_from(value: Stateful) -> State { State::stateful(value) } } impl From for State @@ -92,15 +274,67 @@ where #[cfg(test)] mod tests { + use std::cell::Cell; + + use ribir_algo::Sc; + use super::*; + use crate::{context::AppCtx, prelude::*, reset_test_env, timer::Timer}; + + struct Origin { + a: i32, + b: i32, + } #[test] - fn fix_dyn_widget_to_state_circular_mut_borrow_panic() { - let dyn_widget = Stateful::new(DynWidget { dyns: Some(1) }); - let c_dyns = dyn_widget.clone(); - let _: State = dyn_widget.into(); - { - c_dyns.state_ref().dyns = Some(2); - } + fn path_state_router_test() { + reset_test_env!(); + + let origin = State::value(Origin { a: 0, b: 0 }); + let a = split_writer!($origin.a); + let b = map_writer!($origin.b); + + let track_origin = Sc::new(Cell::new(0)); + let track_a = Sc::new(Cell::new(0)); + let track_b = Sc::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.write().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.write() = 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.write() = 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 ebcb77048..000000000 --- a/core/src/state/readonly.rs +++ /dev/null @@ -1,103 +0,0 @@ -use super::{ModifyScope, StateRef, Stateful}; -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(StateRef<'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 3198d462e..9032da3d4 100644 --- a/core/src/state/stateful.rs +++ b/core/src/state/stateful.rs @@ -1,54 +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}, + 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)] -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 StateRef<'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. @@ -56,304 +44,360 @@ 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() } - pub(crate) fn inner_ref(&self) -> &'a Stateful { self.0 } - } + #[inline] + fn clone_reader(&self) -> Self::Reader { Reader::from_stateful(self) } - impl<'a, W> Drop for ModifyGuard<'a, W> { - fn drop(&mut self) { - let guards = &self.0.inner.guard_cnt; - guards.set(guards.get() - 1); - - if guards.get() == 0 { - let inner = &self.0.inner; - assert_eq!(UNUSED, inner.borrow_flag.get()); - let scope = inner.modify_scope.take(); - if let Some(scope) = scope { - self.0.raw_modifies().next(scope); - } - } - } - } + #[inline] + fn origin_reader(&self) -> &Self::OriginReader { self } - impl<'a, W> std::ops::Deref for ModifyGuard<'a, W> { - type Target = Stateful; + #[inline] + fn modifies(&self) -> BoxOp<'static, ModifyScope, Infallible> { self.notifier.modifies() } - #[inline] - fn deref(&self) -> &Self::Target { self.0 } + #[inline] + fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { + self.notifier.raw_modifies() } } -type BorrowFlag = isize; -const UNUSED: BorrowFlag = 0; +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) } -#[inline(always)] -fn is_reading(x: BorrowFlag) -> bool { x > UNUSED } + fn shallow(&'_ self) -> Self::RefWrite<'_> { self.write_ref(ModifyScope::FRAMEWORK) } -struct InnerStateful { - borrow_flag: Cell, - modify_scope: Cell>, - #[cfg(debug_assertions)] - borrowed_at: Cell>>, - guard_cnt: Cell, - data: UnsafeCell, + fn clone_writer(&self) -> Self::Writer { Writer::from_stateful(self) } + + fn origin_writer(&self) -> &Self::OriginWriter { self } } -impl Clone for Stateful { +impl StateReader for Reader { + type Value = W; + type OriginReader = Self; + type Reader = Self; + type Ref<'a> = ReadRef<'a, W> where W:'a; + #[inline] - fn clone(&self) -> Self { - Self { - inner: self.inner.clone(), - modify_notifier: self.modify_notifier.clone(), - } - } + 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 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), - }), - modify_notifier: <_>::default(), - } - } +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() } - /// Return a guard that batch the modify event. #[inline] - pub fn modify_guard(&self) -> ModifyGuard { ModifyGuard::new(self) } + fn clone_reader(&self) -> Self::Reader { self.0.clone_reader() } - /// Return a reference of `Stateful`, modify across this reference will notify - /// data and framework. #[inline] - pub fn state_ref(&self) -> StateRef { StateRef::new(self, ModifyScope::BOTH) } + fn origin_reader(&self) -> &Self::OriginReader { self } - /// 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) -> StateRef { StateRef::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. + fn modifies(&self) -> BoxOp<'static, ModifyScope, Infallible> { self.0.modifies() } + #[inline] - pub(crate) fn shallow_ref(&self) -> StateRef { StateRef::new(self, ModifyScope::FRAMEWORK) } + fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { self.0.raw_modifies() } +} - pub fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { - self.modify_notifier.raw_modifies() - } +impl StateWriter for Writer { + type Writer = Self; + type OriginWriter = Self; + type RefWrite<'a> = WriteRef<'a, W> where W:'a; - /// Notify when this widget be mutable accessed, no mather if the widget - /// really be modified, the value is hint if it's only access by silent ref. #[inline] - pub fn modifies(&self) -> BoxOp<'static, (), Infallible> { - self - .raw_modifies() - .filter_map(|s: ModifyScope| s.contains(ModifyScope::DATA).then_some(())) - .box_it() - } + fn write(&'_ self) -> Self::RefWrite<'_> { self.0.write() } - /// Clone the stateful widget of which the reference point to. Require mutable - /// reference because we try to early release inner borrow when clone occur. #[inline] - pub fn clone_stateful(&self) -> Stateful { self.clone() } + fn silent(&'_ self) -> Self::RefWrite<'_> { self.0.silent() } - 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!()); - Ok(inner.data.into_inner()) - } else { - Err(self) - } + #[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(); } } -macro_rules! debug_borrow_location { - ($this: ident) => { - #[cfg(debug_assertions)] - { - let caller = std::panic::Location::caller(); - $this.value.inner.borrowed_at.set(Some(caller)); +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(); } - }; -} -macro_rules! already_borrow_panic { - ($this: ident) => { - #[cfg(debug_assertions)] - { - let location = $this.value.inner.borrowed_at.get().unwrap(); - panic!("already borrowed at {}", location); + // 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 ()); } - #[cfg(not(debug_assertions))] - panic!("already borrowed"); - }; + } } -impl<'a, W> Deref for StateRef<'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()) {} - } +impl Reader { + fn from_stateful(stateful: &Stateful) -> Self { + Reader(Stateful { + inner: stateful.inner.clone(), + notifier: stateful.notifier.clone(), + }) + } +} - // SAFETY: `BorrowFlag` guarantees unique access. - let ptr = self.value.inner.data.get(); - unsafe { &*ptr } +impl Writer { + #[inline] + 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(), + }) } } -impl<'a, W> DerefMut for StateRef<'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}"); - } +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, +} - 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); +#[repr(transparent)] +pub(crate) struct RenderFul(pub(crate) Stateful); - // 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. - } - } +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>); - if b.get() != -1 { - already_borrow_panic!(self); +impl Stateful { + pub fn new(data: W) -> Self { + Stateful { + inner: Sc::new(StateData::new(data)), + notifier: <_>::default(), } - - // SAFETY: `BorrowFlag` guarantees unique access. - let ptr = self.value.inner.data.get(); - unsafe { &mut *ptr } } -} -impl<'a, W> StateRef<'a, W> { - /// Fork a silent reference - pub fn silent(&self) -> StateRef<'a, W> { - self.release_borrow(); - StateRef::new(self.value.inner_ref(), ModifyScope::DATA) - } + /// Clone the stateful widget of which the reference point to. Require mutable + /// reference because we try to early release inner borrow when clone occur. + #[inline] + pub fn clone_stateful(&self) -> Stateful { self.clone_writer().0 } - /// 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); + /// just for compatibility with `Stateful` in old syntax. + #[inline] + pub fn state_ref(&self) -> WriteRef { self.write() } + + /// Run the `task` when the inner state data will drop. + #[inline] + pub fn on_state_drop(&self, task: impl FnOnce() + 'static) { + AppCtx::add_trigger_task(self.heap_ptr() as *const _, Box::new(task)) } + // unsubscribe the `subscription` when the inner state data will drop. #[inline] - pub fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { - self.value.raw_modifies() + pub fn unsubscribe_on_drop(&self, subscription: impl Subscription + 'static) { + self.on_state_drop(move || subscription.unsubscribe()) } + /// return the heap pointer of the data. #[inline] - pub fn modifies(&self) -> BoxOp<'static, (), Infallible> { self.value.modifies() } + fn heap_ptr(&self) -> *const W { self.inner.data.as_ptr() } - fn new(value: &'a Stateful, modify_scope: ModifyScope) -> Self { + pub(crate) fn from_state_data(data: StateData) -> Self { Self { - mut_accessed_flag: Cell::new(None), - modify_scope, - value: ModifyGuard::new(value), + inner: Sc::new(data), + notifier: <_>::default(), + } + } + + pub(crate) fn try_into_inner(self) -> Result { + 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) } } + 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); } +} + +impl StateData { + /// Assert the state data is not used by any reader and writer. #[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 => {} + #[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(), } - 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. + 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 ReadRef<'a, W> { + type Target = W; + + #[track_caller] + fn deref(&self) -> &Self::Target { &self.0 } +} + +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 WriteRef<'a, W> { + #[track_caller] + fn deref_mut(&mut self) -> &mut Self::Target { + self.modified = true; + &mut self.value + } +} + +impl<'a, W> RefWrite for WriteRef<'a, W> { #[inline] - pub fn clone_stateful(&self) -> Stateful { self.value.clone() } + fn forget_modifies(&mut self) -> bool { std::mem::replace(&mut self.modified, false) } +} + +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 { + modified: false, + modify_scope: scope, + value, + batched_modify: batch_scope, + notifier: modifier, + } + } } impl SingleChild for Stateful {} impl MultiChild for Stateful {} -impl_proxy_query!(paths [modify_notifier, state_ref()], Stateful, , where R: Query + 'static ); -impl_query_self_only!(StateChangeNotifier); +impl_proxy_query!( + paths [notifier, read()], + Stateful, , where R: Query + 'static +); +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 StateRef<'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() } +} + +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)] @@ -361,11 +405,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. @@ -378,7 +422,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)); @@ -391,12 +435,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); @@ -404,8 +448,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); @@ -414,7 +460,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(); @@ -424,9 +470,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() @@ -435,24 +481,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 238ff4997..e8619ba22 100644 --- a/core/src/test_helper.rs +++ b/core/src/test_helper.rs @@ -1,4 +1,4 @@ -use crate::timer::Timer; +pub use crate::timer::Timer; use std::{ rc::Rc, sync::atomic::{AtomicU64, Ordering}, @@ -15,23 +15,31 @@ pub struct Frame { pub viewport: Rect, pub surface: Color, } + +#[derive(Clone)] pub struct TestWindow(pub Rc); +#[macro_export] +macro_rules! reset_test_env { + () => { + let _ = $crate::prelude::NEW_TIMER_FN.set($crate::timer::Timer::new_timer_future); + let _guard = unsafe { $crate::prelude::AppCtx::new_lock_scope() }; + }; +} + impl TestWindow { /// Create a 1024x1024 window for test - pub fn new(root: impl Into) -> Self { - let _ = NEW_TIMER_FN.set(Timer::new_timer_future); - let wnd = Window::new(root.into(), Box::new(TestShellWindow::new(None))); - AppCtx::windows().borrow_mut().insert(wnd.id(), wnd.clone()); - Self(wnd) - } + pub fn new(root: impl Into) -> Self { Self::new_wnd(root, None) } pub fn new_with_size(root: impl Into, size: Size) -> Self { + Self::new_wnd(root, Some(size)) + } + + fn new_wnd(root: impl Into, size: Option) -> Self { let _ = NEW_TIMER_FN.set(Timer::new_timer_future); - Self(Window::new( - root.into(), - Box::new(TestShellWindow::new(Some(size))), - )) + let wnd = AppCtx::new_window(Box::new(TestShellWindow::new(size)), root.into()); + wnd.run_frame_tasks(); + Self(wnd) } /// Ues a index path to access widget tree and return the layout info, @@ -68,6 +76,7 @@ impl TestWindow { pub fn draw_frame(&mut self) { // Test window not have a eventloop, manually wake-up every frame. Timer::wake_timeout_futures(); + self.run_frame_tasks(); self.0.draw_frame(); } @@ -166,10 +175,10 @@ impl Render for MockStack { impl_query_self_only!(MockStack); -#[derive(Declare, MultiChild)] +#[derive(Declare, Declare2, MultiChild)] pub struct MockMulti; -#[derive(Declare, Clone, SingleChild)] +#[derive(Declare, Declare2, Clone, SingleChild)] pub struct MockBox { pub size: Size, } 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 676eb32fa..0bfc63387 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -51,16 +51,12 @@ pub trait Render: Query { fn get_transform(&self) -> Option { None } } -pub(crate) fn hit_test_impl(ctx: &HitTestCtx, pos: Point) -> bool { - ctx.box_rect().map_or(false, |rect| rect.contains(pos)) -} - /// The common type of all widget can convert to. pub struct Widget(Box WidgetId>); /// A trait to query dynamic type and its inner type on runtime, use this trait /// to provide type information you want framework know. -pub trait Query { +pub trait Query: QueryFiler { /// A type can composed by others, this method query all type(include self) /// match the type id, and call the callback one by one. The callback accept /// an `& dyn Any` of the target type, and return if it want to continue. @@ -105,7 +101,7 @@ impl<'a> dyn Render + 'a { pub fn query_all_type(&self, mut callback: impl FnMut(&T) -> bool, order: QueryOrder) { self.query_all( TypeId::of::(), - &mut |a: &dyn Any| a.downcast_ref().map_or(true, &mut callback), + &mut |a| a.downcast_ref().map_or(true, &mut callback), order, ) } @@ -141,6 +137,8 @@ impl<'a> dyn Render + 'a { ); hit } + + pub fn is(&self) -> bool { self.query_filter(TypeId::of::()).is_some() } } pub struct FnWidget(F); @@ -148,29 +146,17 @@ pub struct FnWidget(F); pub(crate) trait WidgetBuilder { fn build(self, ctx: &BuildCtx) -> WidgetId; } -pub(crate) trait WidgetOrId { - fn build_id(self, ctx: &BuildCtx) -> WidgetId; -} -impl WidgetOrId for T { +impl From for Widget { #[inline] - fn build_id(self, ctx: &BuildCtx) -> WidgetId { self.build(ctx) } -} - -impl WidgetOrId for WidgetId { - #[inline] - fn build_id(self, _: &BuildCtx) -> WidgetId { self } -} - -impl WidgetOrId for Widget { - #[inline] - fn build_id(self, ctx: &BuildCtx) -> WidgetId { self.build(ctx) } + fn from(id: WidgetId) -> Self { Widget(Box::new(move |_| id)) } } impl FnWidget { + #[inline] pub fn new(f: F) -> Self where - F: FnOnce(&BuildCtx) -> R, + F: FnOnce(&BuildCtx) -> R + Into>, { FnWidget(f) } @@ -182,9 +168,9 @@ impl FnWidget { impl WidgetBuilder for FnWidget where F: FnOnce(&BuildCtx) -> R + 'static, - R: WidgetOrId, + R: Into, { - fn build(self, ctx: &BuildCtx) -> WidgetId { (self.0)(ctx).build_id(ctx) } + fn build(self, ctx: &BuildCtx) -> WidgetId { (self.0)(ctx).into().build(ctx) } } #[macro_export] @@ -314,30 +300,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 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() } } @@ -346,11 +319,30 @@ impl From for Widget { fn from(value: W) -> Self { Self(Box::new(|ctx| value.build(ctx))) } } +impl From for FnWidget +where + F: FnOnce(&BuildCtx) -> R, + R: Into, +{ + #[inline] + fn from(value: F) -> Self { Self(value) } +} + impl Widget { #[inline] pub fn build(self, ctx: &BuildCtx) -> WidgetId { (self.0)(ctx) } } +impl From for Box { + #[inline] + fn from(value: R) -> Self { Box::new(value) } +} + +impl From> for Box { + #[inline] + fn from(value: Stateful) -> Self { Box::new(RenderFul(value)) } +} + impl_proxy_query!(paths [deref()], ShareResource, , where T: Render + 'static); impl_proxy_render!(proxy deref(), ShareResource, , where T: Render + 'static); impl_proxy_query!(paths [deref()], Rc, , where W: Query + 'static); @@ -371,3 +363,7 @@ pub fn then(b: bool, f: impl FnOnce() -> W) -> Option { b.then(f) } /// calls the closure on `value` and returns #[inline] pub fn map(value: T, f: impl FnOnce(T) -> W) -> W { f(value) } + +pub(crate) fn hit_test_impl(ctx: &HitTestCtx, pos: Point) -> bool { + ctx.box_rect().map_or(false, |rect| rect.contains(pos)) +} diff --git a/core/src/widget_children.rs b/core/src/widget_children.rs index dad605087..9f6c3079e 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; @@ -38,20 +38,26 @@ pub use multi_child_impl::*; pub use single_child_impl::*; pub mod child_convert; pub use child_convert::{ChildFrom, FromAnother}; -/// Trait to tell Ribir a widget can have one child. +/// Trait to tell Ribir a object can have one child. pub trait SingleChild {} -/// A render widget that has one child. -pub trait RenderSingleChild: Render + SingleChild { - fn into_render(self: Box) -> Box; +/// A boxed render widget that support accept one child. +pub trait BoxedSingleParent { + 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 widget that has multi children. +/// Trait to tell Ribir a object that has multi children. pub trait MultiChild {} -/// A render widget that has multi children. -pub trait RenderMultiChild: Render + MultiChild { - fn into_render(self: Box) -> Box; +/// A boxed render widget that support accept multi children. +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 @@ -63,26 +69,96 @@ pub trait ComposeChild: Sized { /// A alias of `WidgetPair`, means `Widget` is the /// child of the generic type. -pub type WidgetOf = WidgetPair; +pub type WidgetOf = SinglePair; -impl RenderSingleChild for T { - fn into_render(self: Box) -> Box { Box::new(*self) } +impl SingleChild for Box {} +impl MultiChild for Box {} + +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 WidgetBuilder for Box { + #[inline] + fn build(self, ctx: &BuildCtx) -> WidgetId { self.boxed_build(ctx.force_as_mut()) } +} + +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> + 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 RenderMultiChild for T { - fn into_render(self: Box) -> Box { Box::new(*self) } +impl BoxedMultiParent for W { + #[inline] + fn boxed_append_children( + self: Box, + children: Vec, + ctx: &mut BuildCtx, + ) -> WidgetId { + (*self).append_children(children, ctx) + } + + #[inline] + fn boxed_build(self: Box, ctx: &mut BuildCtx) -> WidgetId { (*self).build(ctx) } } #[cfg(test)] mod tests { use super::*; - use crate::test_helper::*; use crate::widget::WidgetBuilder; + use crate::{reset_test_env, test_helper::*}; use ribir_dev_helper::*; - use std::{cell::RefCell, rc::Rc}; #[test] fn compose_template_child() { + reset_test_env!(); #[derive(Declare)] struct Page; #[derive(Declare, SingleChild)] @@ -118,13 +194,15 @@ mod tests { #[test] fn compose_option_child() { + reset_test_env!(); + #[derive(Declare)] struct Parent; #[derive(Declare, SingleChild)] struct Child; impl ComposeChild for Parent { - type Child = Option>; + type Child = Option>; fn compose_child(_: State, _: Self::Child) -> Widget { unreachable!("Only for syntax support check"); @@ -140,16 +218,18 @@ mod tests { #[test] fn compose_option_dyn_parent() { - widget! { - DynWidget { - dyns: widget::then(true, || MockBox { size: Size::zero() }), - Void {} - } + reset_test_env!(); + + fn_widget! { + let p = Some(MockBox { size: Size::zero() }); + @$p { @{ Void } } }; } #[test] fn tuple_as_vec() { + reset_test_env!(); + #[derive(Declare)] struct A; #[derive(Declare)] @@ -172,60 +252,61 @@ mod tests { #[test] fn expr_with_child() { + reset_test_env!(); + let size = Stateful::new(Size::zero()); + let c_size = size.clone_reader(); // with single child - let _e = widget! { - states { size: size.clone() } - DynWidget { - dyns: if size.area() > 0. { - MockBox { size: *size } + let _e = fn_widget! { + let p = pipe!{ + if $c_size.area() > 0. { + MockBox { size: *$c_size } } else { MockBox { size: Size::new(1., 1.) } - }, - MockBox { size: *size } - } + } + }; + @$p { @MockBox { size: pipe!(*$c_size) } } }; + // with multi child - let _e = widget! { - DynWidget { - dyns: MockMulti {}, - MockBox { size: Size::zero() } - MockBox { size: Size::zero() } - MockBox { size: Size::zero() } + let _e = fn_widget! { + @MockMulti { + @MockBox { size: Size::zero() } + @MockBox { size: Size::zero() } + @MockBox { size: Size::zero() } } }; + let c_size = size.clone_reader(); // option with single child - let _e = widget! { - states { size: size.clone() } - DynWidget { - dyns: widget::then(size.area() > 0., || MockBox { size: Size::zero() }), - MockBox { size: Size::zero() } - } + let _e = fn_widget! { + let p = pipe!(($c_size.area() > 0.).then(|| @MockBox { size: Size::zero() })); + @$p { @MockBox { size: Size::zero() } } }; // option with `Widget` - let _e = widget! { - states { size: size } - DynWidget { - dyns: widget::then(size.area() > 0., || MockBox { size: Size::zero() }), - widget::from(Void) - } + let _e = fn_widget! { + let p = pipe!(($size.area() > 0.).then(|| @MockBox { size: Size::zero() })); + @$p { @ { Void }} }; } #[test] - fn compose_const_dyn_option_widget() { - let _ = widget! { - MockBox { + fn compose_expr_option_widget() { + reset_test_env!(); + + let _ = fn_widget! { + @MockBox { size: ZERO_SIZE, - widget::then(true, || MockBox { size: Size::zero() }) + @{ Some(@MockBox { size: Size::zero() })} } }; } #[test] fn pair_to_pair() { + reset_test_env!(); + #[derive(Declare)] struct P; @@ -241,6 +322,8 @@ mod tests { #[test] fn fix_multi_fill_for_pair() { + reset_test_env!(); + struct X; impl ComposeChild for X { type Child = WidgetOf; @@ -253,50 +336,6 @@ mod tests { }); } - fn dyns_compose_child() -> Widget { - #[derive(Declare)] - struct X; - - impl ComposeChild for X { - type Child = MockBox; - fn compose_child(_: State, child: Self::Child) -> Widget { child.into() } - } - - let dyns = Stateful::new(DynWidget { dyns: Some(X) }); - let size = Size::new(100., 200.); - - ComposeChild::compose_child(State::::from(dyns), MockBox { size }) - } - widget_layout_test!(dyns_compose_child, width == 100., height == 200.,); - - const COMPOSE_DYNS_CHILD_SIZE: Size = Size::new(100., 200.); - fn compose_dyns_child() -> Widget { - #[derive(Declare)] - struct X; - - impl ComposeChild for X { - type Child = State; - fn compose_child(_: State, child: Self::Child) -> Widget { child.into() } - } - - let trigger = Stateful::new(true); - - widget! { - states { trigger: trigger } - X { - DynWidget { - dyns: if *trigger { - MockBox { size: COMPOSE_DYNS_CHILD_SIZE } - } else { - MockBox { size: ZERO_SIZE } - } - } - } - } - .into() - } - widget_layout_test!(compose_dyns_child, size == COMPOSE_DYNS_CHILD_SIZE,); - const FIX_OPTION_TEMPLATE_EXPECT_SIZE: Size = Size::new(100., 200.); fn fix_option_template() -> impl Into { struct Field(String); @@ -305,60 +344,20 @@ mod tests { pub struct ConfigTml { _field: Option, } - #[derive(Declare)] + #[derive(Declare, Declare2)] struct Host {} impl ComposeChild for Host { type Child = Option; fn compose_child(_: State, _: Self::Child) -> Widget { - widget! { MockBox { size: FIX_OPTION_TEMPLATE_EXPECT_SIZE } }.into() + fn_widget! { @MockBox { size: FIX_OPTION_TEMPLATE_EXPECT_SIZE } }.into() } } - widget! { Host { Field("test".into()) }} + fn_widget! { @Host { @{ Field("test".into()) } }} } widget_layout_test!( fix_option_template, { path = [0], size == FIX_OPTION_TEMPLATE_EXPECT_SIZE, } ); - - #[test] - fn compose_dyn_multi_child() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - struct A; - - impl ComposeChild for A { - type Child = Vec; - - fn compose_child(_: State, child: Self::Child) -> Widget { - FnWidget::new(move |ctx| MockMulti.with_child(Multi::new(child), ctx).build(ctx)).into() - } - } - - let child = DynWidget { dyns: Some(Multi::new([Void])) }; - let child = Stateful::new(child); - let cnt = Rc::new(RefCell::new(0)); - let c_cnt = cnt.clone(); - child - .modifies() - .subscribe(move |_| *c_cnt.borrow_mut() += 1); - - let _ = TestWindow::new(FnWidget::new(|ctx| A.with_child(child, ctx).build(ctx))); - assert_eq!(*cnt.borrow(), 0); - } - - #[test] - fn compose_multi_with_stateful_option() { - struct M; - impl ComposeChild for M { - type Child = Vec; - fn compose_child(_: State, _: Self::Child) -> Widget { Void.into() } - } - - let _ = FnWidget::new(|ctx| { - let c = Stateful::new(DynWidget { dyns: Some(Some(Void)) }); - M.with_child(c, ctx).build(ctx) - }); - } } diff --git a/core/src/widget_children/child_convert.rs b/core/src/widget_children/child_convert.rs index c5a2cc174..76832678e 100644 --- a/core/src/widget_children/child_convert.rs +++ b/core/src/widget_children/child_convert.rs @@ -1,10 +1,10 @@ use super::{ - decorate_tml_impl::IntoDecorateTml, ComposeChild, ComposePair, DecorateTml, Multi, TmlFlag, - WidgetPair, + decorate_tml_impl::IntoDecorateTml, ComposeChild, ComposePair, DecorateTml, Multi, SinglePair, + TmlFlag, }; use crate::{ - dynamic_widget::{DynRender, DynWidget}, - state::{State, StateFrom, Stateful}, + builtin_widgets::FatObj, + state::{State, StateFrom}, widget::*, }; @@ -65,31 +65,41 @@ where } // WidgetPair --> WidgetPair -impl FromAnother, [(M1, M2); 0]> for WidgetPair +impl FromAnother, [(M1, M2); 0]> for SinglePair where W: FromAnother, - C: ChildFrom, + C: FromAnother, { #[inline] - fn from_another(value: WidgetPair) -> Self { - let WidgetPair { widget, child } = value; - WidgetPair { + 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 WidgetPair +impl FromAnother, [M; 1]> for SinglePair where - W: ChildFrom, - C: FromAnother, + W: FromAnother, { #[inline] - fn from_another(value: WidgetPair) -> Self { - let WidgetPair { widget, child } = value; - WidgetPair { - widget: W::child_from(widget), + 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, 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 edfbdb80d..db660d400 100644 --- a/core/src/widget_children/compose_child_impl.rs +++ b/core/src/widget_children/compose_child_impl.rs @@ -1,12 +1,11 @@ use crate::{ context::BuildCtx, - dynamic_widget::DynWidget, prelude::ChildFrom, state::{State, Stateful}, widget::{Widget, WidgetBuilder, WidgetId}, }; -use super::{child_convert::FillVec, ComposeChild, WidgetPair}; +use super::{child_convert::FillVec, ComposeChild, SinglePair}; /// Trait specify what child a compose child widget can have, and the target /// type after widget compose its child. @@ -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,30 +62,19 @@ where { type Target = as ComposeWithChild>::Target; fn with_child(self, child: C, ctx: &BuildCtx) -> Self::Target { - State::Stateful(self).with_child(child, ctx) + 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) - } -} - -impl ComposeWithChild for WidgetPair +impl ComposeWithChild for SinglePair where C1: ComposeWithChild, { - type Target = WidgetPair; + type Target = SinglePair; fn with_child(self, c: C2, ctx: &BuildCtx) -> Self::Target { - let WidgetPair { widget, child } = self; - WidgetPair { + let SinglePair { widget, child } = self; + SinglePair { widget, child: child.with_child(c, ctx), } @@ -182,9 +170,9 @@ pub(crate) mod decorate_tml_impl { impl DecorateTmlMarker<()> for DecorateTml {} - impl DecorateTmlMarker<[(); 0]> for WidgetPair {} + impl DecorateTmlMarker<[(); 0]> for SinglePair {} - impl> DecorateTmlMarker<[M; 1]> for WidgetPair {} + impl> DecorateTmlMarker<[M; 1]> for SinglePair {} impl> DecorateTmlMarker for ComposePair {} @@ -193,7 +181,7 @@ pub(crate) mod decorate_tml_impl { fn into_decorate_tml(self) -> DecorateTml; } - impl IntoDecorateTml for WidgetPair + impl IntoDecorateTml for SinglePair where W: TmlFlag, C: ChildFrom, @@ -201,26 +189,26 @@ pub(crate) mod decorate_tml_impl { type Flag = W; fn into_decorate_tml(self) -> DecorateTml { - let WidgetPair { widget: tml_flag, child } = self; + let SinglePair { widget: tml_flag, child } = self; let decorator = Box::new(|w| w); let child = C::child_from(child); DecorateTml { decorator, tml_flag, child } } } - impl IntoDecorateTml for WidgetPair + impl IntoDecorateTml for SinglePair where W: 'static, C2: IntoDecorateTml, - WidgetPair: WidgetBuilder, + SinglePair: WidgetBuilder, { type Flag = C2::Flag; fn into_decorate_tml(self) -> DecorateTml { - let WidgetPair { widget, child } = self; + let SinglePair { widget, child } = self; let DecorateTml { decorator, tml_flag, child } = ChildFrom::child_from(child); DecorateTml { - decorator: Box::new(move |w| WidgetPair { widget, child: decorator(w) }.into()), + decorator: Box::new(move |w| SinglePair { widget, child: decorator(w) }.into()), tml_flag, child, } @@ -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 7d1549b6d..c69b66be1 100644 --- a/core/src/widget_children/multi_child_impl.rs +++ b/core/src/widget_children/multi_child_impl.rs @@ -1,5 +1,5 @@ use super::*; -use crate::widget::{RenderFul, WidgetBuilder}; +use crate::widget::WidgetBuilder; /// Trait specify what child a multi child widget can have, and the target type /// after widget compose its child. @@ -8,8 +8,8 @@ pub trait MultiWithChild { fn with_child(self, child: C, ctx: &BuildCtx) -> Self::Target; } -pub struct MultiChildWidget { - pub widget: Box, +pub struct MultiPair

{ + pub parent: P, pub children: Vec, } @@ -25,10 +25,6 @@ impl Multi { pub fn into_inner(self) -> W { self.0 } } -trait IntoMultiParent { - fn into_multi_parent(self) -> Box; -} - // Same with ChildConvert::FillVec, but only for MultiChild, // There are some duplicate implementations, but better compile error for // users @@ -61,44 +57,31 @@ where } } -impl FillVec for Stateful>> +impl FillVec for Pipe> where D: IntoIterator + 'static, - Widget: From, + D::Item: Into, { fn fill_vec(self, vec: &mut Vec, ctx: &BuildCtx) { - vec.push(DynRender::multi(self).build(ctx)) + self.build_multi(vec, ctx.force_as_mut()); } } -impl IntoMultiParent for R { - #[inline] - fn into_multi_parent(self) -> Box { Box::new(self) } -} - -impl IntoMultiParent for Stateful { - #[inline] - fn into_multi_parent(self) -> Box { Box::new(RenderFul(self)) } -} - -impl MultiWithChild for R +impl MultiWithChild for P where - R: IntoMultiParent, + P: MultiChild, C: FillVec, { - type Target = MultiChildWidget; + type Target = MultiPair

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

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

{ fn build(self, ctx: &BuildCtx) -> WidgetId { - let MultiChildWidget { widget, children } = self; - let p = ctx.alloc_widget(widget); - children - .into_iter() - .for_each(|child| ctx.append_child(p, child)); - p + let MultiPair { parent, children } = self; + 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 6102cba56..05d2d9306 100644 --- a/core/src/widget_children/single_child_impl.rs +++ b/core/src/widget_children/single_child_impl.rs @@ -1,6 +1,5 @@ -use crate::widget::{RenderFul, WidgetBuilder}; - use super::*; +use crate::widget::WidgetBuilder; /// Trait specify what child a widget can have, and the target type is the /// result of widget compose its child. @@ -10,120 +9,82 @@ pub trait SingleWithChild { } /// A node of widget with not compose its child. -pub struct WidgetPair { - pub widget: W, - pub child: C, +pub struct SinglePair { + 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 {} impl SingleWithChild for W { - type Target = WidgetPair; + type Target = SinglePair; #[inline] - fn with_child(self, child: C, _: &BuildCtx) -> Self::Target { WidgetPair { widget: self, child } } + fn with_child(self, child: C, _: &BuildCtx) -> Self::Target { SinglePair { widget: self, child } } } -impl SingleWithChild for WidgetPair { - type Target = WidgetPair>; +impl SingleWithChild for SinglePair { + type Target = SinglePair>; fn with_child(self, c: C2, ctx: &BuildCtx) -> Self::Target { - let WidgetPair { widget, child } = self; - WidgetPair { + let SinglePair { widget, child } = self; + SinglePair { widget, child: child.with_child(c, ctx), } } } -trait IntoSingleParent { - fn into_single_parent(self) -> Box; -} - -trait WidgetChild { - fn child_build(self, ctx: &BuildCtx) -> WidgetId; -} - -impl IntoSingleParent for Box { - fn into_single_parent(self) -> Box { self.into_render() } -} - -impl IntoSingleParent for W { - fn into_single_parent(self) -> Box { Box::new(self) } -} - -impl IntoSingleParent for Stateful { - #[inline] - fn into_single_parent(self) -> Box { Box::new(RenderFul(self)) } -} - -impl IntoSingleParent for Stateful> -where - D: RenderSingleChild + WidgetBuilder + 'static, -{ - #[inline] - fn into_single_parent(self) -> Box { Box::new(DynRender::single(self)) } -} - -impl IntoSingleParent for Stateful>> -where - D: RenderSingleChild + WidgetBuilder + 'static, -{ - #[inline] - fn into_single_parent(self) -> Box { Box::new(DynRender::option(self)) } -} - -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 WidgetPair +impl WidgetBuilder for SinglePair where - W: IntoSingleParent, - C: WidgetChild, + W: SingleParent, + C: Into, { fn build(self, ctx: &BuildCtx) -> WidgetId { let Self { widget, child } = self; - let p = ctx.alloc_widget(widget.into_single_parent()); - let child = child.child_build(ctx); - ctx.append_child(p, child); - p + let child = child.into().build(ctx); + widget.append_child(child, ctx.force_as_mut()) } } -impl WidgetBuilder for WidgetPair, C> +impl WidgetBuilder for SinglePair, C> where - W: IntoSingleParent, - C: WidgetChild, + W: SingleParent, + C: Into, { fn build(self, ctx: &BuildCtx) -> WidgetId { let Self { widget, child } = self; if let Some(widget) = widget { - WidgetPair { widget, child }.build(ctx) + SinglePair { widget, child }.build(ctx) } else { - child.child_build(ctx) + child.into().build(ctx) } } } -impl WidgetBuilder for WidgetPair> +impl WidgetBuilder for SinglePair> where - W: IntoSingleParent, - C: WidgetChild, + W: SingleParent, + SinglePair: WidgetBuilder, { fn build(self, ctx: &BuildCtx) -> WidgetId { let Self { widget, child } = self; if let Some(child) = child { - WidgetPair { widget, child }.build(ctx) + SinglePair { widget, child }.build(ctx) } else { - let node = widget.into_single_parent(); - ctx.alloc_widget(node) + widget.build(ctx) } } } diff --git a/core/src/widget_tree.rs b/core/src/widget_tree.rs index 2c39f5ae8..74abdd978 100644 --- a/core/src/widget_tree.rs +++ b/core/src/widget_tree.rs @@ -2,6 +2,7 @@ use std::{ cell::RefCell, cmp::Reverse, collections::HashSet, + mem::MaybeUninit, rc::{Rc, Weak}, }; @@ -12,11 +13,12 @@ mod layout_info; use crate::prelude::*; pub use layout_info::*; +use self::widget_id::{empty_node, RenderNode}; + 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,25 +26,21 @@ pub(crate) struct WidgetTree { } impl WidgetTree { - pub(crate) fn new() -> WidgetTree { Self::default() } + pub fn init(&mut self, wnd: Weak) { self.wnd = wnd; } - 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 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.expect("Try to access a not init `WidgetTree`") - } + pub(crate) fn root(&self) -> WidgetId { self.root } /// Draw current tree by painter. pub(crate) fn draw(&self) { let wnd = self.window(); - let mut ctx = PaintingCtx::new(self.root(), &wnd); + let mut painter = wnd.painter.borrow_mut(); + let mut ctx = PaintingCtx::new(self.root(), wnd.id(), &mut painter); self.root().paint_subtree(&mut ctx); } @@ -62,10 +60,7 @@ impl WidgetTree { .map(|info| info.clamp) .unwrap_or_else(|| BoxClamp { min: Size::zero(), max: win_size }); - let wnd = self.window(); - let wnd = &*wnd; - - let mut layouter = Layouter::new(wid, wnd, true, self); + let mut layouter = Layouter::new(wid, self.window().id(), true, self); layouter.perform_widget_layout(clamp); } } @@ -81,7 +76,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)] @@ -148,7 +143,7 @@ impl WidgetTree { info.size.take(); } - let r = self.arena.get(p.0).unwrap().get(); + let r = p.assert_get(&self.arena); if r.only_sized_by_parent() { break; } @@ -161,12 +156,72 @@ impl WidgetTree { needs_layout }) } + + pub fn detach(&mut self, id: WidgetId) { + if self.root() == id { + let root = self.root(); + let new_root = root + .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 = new_root; + } + + id.0.detach(&mut self.arena); + } + + pub(crate) fn remove_subtree(&mut self, id: WidgetId) { + assert_ne!( + id, + self.root(), + "You should detach the root widget before remove it." + ); + + id.descendants(&self.arena).for_each(|id| { + self.store.remove(id); + }); + id.0.remove_subtree(&mut self.arena); + } + + pub(crate) fn get_many_mut( + &mut self, + ids: &[WidgetId; N], + ) -> [&mut RenderNode; N] { + unsafe { + let mut outs: MaybeUninit<[&mut RenderNode; N]> = MaybeUninit::uninit(); + let outs_ptr = outs.as_mut_ptr(); + for (idx, wid) in ids.iter().enumerate() { + let arena = &mut *(&mut self.arena as *mut TreeArena); + let cur = wid.get_node_mut(arena).expect("Invalid widget id."); + + *(*outs_ptr).get_unchecked_mut(idx) = cur; + } + outs.assume_init() + } + } +} + +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; - use crate::test_helper::{MockBox, MockMulti, TestWindow}; + use crate::{ + reset_test_env, + test_helper::{MockBox, MockMulti, TestWindow}, + widget::widget_id::empty_node, + }; use super::*; use test::Bencher; @@ -179,23 +234,22 @@ mod tests { impl Compose for Recursive { fn compose(this: State) -> Widget { - widget! { - states { this: this.into_writable() } - MockMulti { - Multi::new((0..this.width) - .map(move |_| { - if this.depth > 1 { - Widget::from( - Recursive { - width: this.width, - depth: this.depth - 1, - } - ) - } else { - Widget::from(MockBox { size: Size::new(10., 10.)}) - } - })) - } + fn_widget! { + @MockMulti { + @{ + pipe!(($this.width, $this.depth)) + .map(|(width, depth)| { + let iter = (0..width).map(move |_| -> Widget { + if depth > 1 { + Recursive { width, depth: depth - 1 }.into() + } else { + MockBox { size: Size::new(10., 10.)}.into() + } + }); + Multi::new(iter) + }) + } + } } .into() } @@ -209,23 +263,21 @@ mod tests { impl Compose for Embed { fn compose(this: State) -> Widget { - widget! { - states { this: this.into_writable()} - MockMulti { - Multi::new((0..this.width - 1) - .map(move |_| { - MockBox { size: Size::new(10., 10.)} - })) - DynWidget { - dyns: if this.depth > 1 { - Widget::from(Embed { - width: this.width, - depth: this.depth - 1, - }) - } else { - Widget::from(MockBox { size: Size::new(10., 10.)}) - } - } + fn_widget! { + let recursive_child: Widget = if $this.depth > 1 { + let width = $this.width; + let depth = $this.depth - 1; + Embed { width, depth }.into() + } else { + MockBox { size: Size::new(10., 10.) }.into() + }; + @MockMulti { + @{ pipe!{ + let leafs = (0..$this.width - 1) + .map(|_| MockBox { size: Size::new(10., 10.)} ); + Multi::new(leafs) + }} + @{ recursive_child } } } .into() @@ -233,38 +285,36 @@ mod tests { } fn bench_recursive_inflate(width: usize, depth: usize, b: &mut Bencher) { - let wnd = TestWindow::new(Void {}); + let mut 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.draw_frame(); }); } fn bench_recursive_repair(width: usize, depth: usize, b: &mut Bencher) { let w = Stateful::new(Recursive { width, depth }); - let trigger = w.clone(); - let wnd = TestWindow::new(w); - let mut tree = wnd.widget_tree.borrow_mut(); + let trigger = w.clone_writer(); + let mut wnd = TestWindow::new(w); b.iter(|| { { - let _: &mut Recursive = &mut trigger.state_ref(); + let _ = trigger.write(); } - tree.layout(Size::new(512., 512.)); + wnd.draw_frame(); }); } #[test] fn fix_relayout_incorrect_clamp() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); let expect_size = Size::new(20., 20.); let no_boundary_size = Stateful::new(INFINITY_SIZE); - let w = widget! { - states { size: no_boundary_size.clone() } - MockBox { + let c_size = no_boundary_size.clone_writer(); + let w = fn_widget! { + @MockBox { size: expect_size, - MockBox { size: *size } + @MockBox { size: pipe!(*$no_boundary_size) } } }; let mut wnd = TestWindow::new_with_size(w, Size::new(200., 200.)); @@ -275,7 +325,7 @@ mod tests { // when relayout the inner `MockBox`, its clamp should same with its previous // layout, and clamp its size. { - *no_boundary_size.state_ref() = INFINITY_SIZE; + *c_size.write() = INFINITY_SIZE; } wnd.draw_frame(); let size = wnd.layout_info_by_path(&[0, 0]).unwrap().size.unwrap(); @@ -284,26 +334,29 @@ mod tests { #[test] fn fix_dropped_child_expr_widget() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); let parent = Stateful::new(true); let child = Stateful::new(true); - let w = widget! { - states { parent: parent.clone(), child: child.clone() } - widget::then(*parent, || widget!{ - MockBox { - size: Size::zero(), - widget::then(*child, || Void) - } - }) + let c_p = parent.clone_writer(); + let c_c = child.clone_writer(); + let w = fn_widget! { + @ { + pipe!(*$parent).map(move |p|{ + p.then(|| @MockBox { + size: Size::zero(), + @ { pipe!($child.then(|| Void)) } + }) + }) + } }; let mut wnd = TestWindow::new(w); wnd.draw_frame(); { - *child.state_ref() = false; - *parent.state_ref() = false; + *c_c.write() = false; + *c_p.write() = false; } // fix crash here. @@ -312,21 +365,17 @@ mod tests { #[test] fn fix_child_expr_widget_same_root_as_parent() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); let trigger = Stateful::new(true); - let w = widget! { - states { trigger: trigger.clone() } - widget::then(*trigger, || widget!{ - widget::then(*trigger, || Void) - }) - }; + let c_trigger = trigger.clone_writer(); + let w = fn_widget! { @ { pipe!($trigger.then(|| Void)) }}; let mut wnd = TestWindow::new(w); wnd.draw_frame(); { - *trigger.state_ref() = false; + *c_trigger.write() = false; } // fix crash here @@ -339,7 +388,7 @@ mod tests { } #[test] fn drop_info_clear() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); let post = Embed { width: 5, depth: 3 }; let wnd = TestWindow::new(post); @@ -349,101 +398,118 @@ mod tests { let root = tree.root(); tree.mark_dirty(root); + let new_root = empty_node(&mut tree.arena); + root.insert_after(new_root, &mut tree.arena); + tree.mark_dirty(new_root); + tree.detach(root); + tree.remove_subtree(root); - root.remove_subtree(&mut tree); - - assert_eq!(tree.layout_list(), None); - assert!(!tree.is_dirty()); + assert_eq!(tree.layout_list(), Some(vec![new_root])); } #[bench] - fn inflate_5_x_1000(b: &mut Bencher) { - let _guard = unsafe { AppCtx::new_lock_scope() }; + fn new_5_x_1000(b: &mut Bencher) { + reset_test_env!(); + let wnd = TestWindow::new(Void); 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()); }); } #[bench] - fn inflate_50_pow_2(b: &mut Bencher) { - let _guard = unsafe { AppCtx::new_lock_scope() }; + fn new_50_pow_2(b: &mut Bencher) { + reset_test_env!(); + bench_recursive_inflate(50, 2, b); } #[bench] - fn inflate_100_pow_2(b: &mut Bencher) { - let _guard = unsafe { AppCtx::new_lock_scope() }; + fn new_100_pow_2(b: &mut Bencher) { + reset_test_env!(); + bench_recursive_inflate(100, 2, b); } #[bench] - fn inflate_10_pow_4(b: &mut Bencher) { - let _guard = unsafe { AppCtx::new_lock_scope() }; + fn new_10_pow_4(b: &mut Bencher) { + reset_test_env!(); + bench_recursive_inflate(10, 4, b); } #[bench] - fn inflate_10_pow_5(b: &mut Bencher) { - let _guard = unsafe { AppCtx::new_lock_scope() }; + fn new_10_pow_5(b: &mut Bencher) { + reset_test_env!(); + bench_recursive_inflate(10, 5, b); } #[bench] - fn repair_5_x_1000(b: &mut Bencher) { - let _guard = unsafe { AppCtx::new_lock_scope() }; + fn regen_5_x_1000(b: &mut Bencher) { + reset_test_env!(); + let post = Stateful::new(Embed { width: 5, depth: 1000 }); - let trigger = post.clone(); + let trigger = post.clone_writer(); let wnd = TestWindow::new(post); let mut tree = wnd.widget_tree.borrow_mut(); b.iter(|| { { - let _: &mut Embed = &mut trigger.state_ref(); + let _ = trigger.write(); } tree.layout(Size::new(512., 512.)); }); } #[bench] - fn repair_50_pow_2(b: &mut Bencher) { - let _guard = unsafe { AppCtx::new_lock_scope() }; + fn regen_50_pow_2(b: &mut Bencher) { + reset_test_env!(); + bench_recursive_repair(50, 2, b); } #[bench] - fn repair_100_pow_2(b: &mut Bencher) { - let _guard = unsafe { AppCtx::new_lock_scope() }; + fn regen_100_pow_2(b: &mut Bencher) { + reset_test_env!(); + bench_recursive_repair(100, 2, b); } #[bench] - fn repair_10_pow_4(b: &mut Bencher) { - let _guard = unsafe { AppCtx::new_lock_scope() }; + fn regen_10_pow_4(b: &mut Bencher) { + reset_test_env!(); + bench_recursive_repair(10, 4, b); } #[bench] - fn repair_10_pow_5(b: &mut Bencher) { - let _guard = unsafe { AppCtx::new_lock_scope() }; + fn regen_10_pow_5(b: &mut Bencher) { + reset_test_env!(); + bench_recursive_repair(10, 5, b); } #[test] fn perf_silent_ref_should_not_dirty_expr_widget() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); let trigger = Stateful::new(1); - let widget = widget! { - states { trigger: trigger.clone() } - MockMulti { - Multi::new((0..3).map(move |_| if *trigger > 0 { - MockBox { size: Size::new(1., 1.)} - } else { - MockBox { size: Size::zero()} - })) + let c_trigger = trigger.clone_stateful(); + let widget = fn_widget! { + @ MockMulti { + @{ + pipe!(*$trigger).map(move |b| { + let size = if b > 0 { + Size::new(1., 1.) + } else { + Size::zero() + }; + Multi::new((0..3).map(move |_| MockBox { size })) + }) + } } }; @@ -451,37 +517,41 @@ mod tests { let mut tree = wnd.widget_tree.borrow_mut(); tree.layout(Size::new(100., 100.)); { - *trigger.silent_ref() = 2; + *c_trigger.write() = 2; } assert!(!tree.is_dirty()) } #[test] fn draw_clip() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - let win_size = Size::new(150., 50.); + reset_test_env!(); - let w1 = widget! { - MockMulti { - Multi::new((0..100).map(|_| - widget! { MockBox { - size: Size::new(150., 50.), - background: Color::BLUE, - }})) + let win_size = Size::new(150., 50.); + let w1 = fn_widget! { + @MockMulti { + @ { + Multi::new((0..100).map(|_| + widget! { MockBox { + size: Size::new(150., 50.), + background: Color::BLUE, + }})) + } }}; let mut wnd = TestWindow::new_with_size(w1, win_size); wnd.draw_frame(); let len_100_widget = wnd.painter.borrow_mut().finish().len(); - let w2 = widget! { - MockMulti { - Multi::new((0..1).map(|_| - widget! { MockBox { - size: Size::new(150., 50.), - background: Color::BLUE, - }})) + let w2 = fn_widget! { + @MockMulti { + @ { + Multi::new((0..1).map(|_| + widget! { MockBox { + size: Size::new(150., 50.), + background: Color::BLUE, + }})) + } }}; let mut wnd = TestWindow::new(w2); diff --git a/core/src/widget_tree/layout_info.rs b/core/src/widget_tree/layout_info.rs index 3c614bf6e..b98b6ffeb 100644 --- a/core/src/widget_tree/layout_info.rs +++ b/core/src/widget_tree/layout_info.rs @@ -2,12 +2,12 @@ use ribir_geom::ZERO_SIZE; use super::{WidgetId, WidgetTree}; use crate::{ - context::{LayoutCtx, WidgetCtx, WidgetCtxImpl}, + context::{AppCtx, LayoutCtx, WidgetCtx, WidgetCtxImpl}, prelude::{Point, Size, INFINITY_SIZE}, widget::TreeArena, - window::{DelayEvent, Window}, + window::{DelayEvent, Window, WindowId}, }; -use std::collections::HashMap; +use std::{collections::HashMap, rc::Rc}; /// boundary limit of the render object's layout #[derive(Debug, Clone, PartialEq, Copy)] @@ -102,14 +102,16 @@ pub(crate) struct LayoutStore { pub struct Layouter<'a> { pub(crate) id: WidgetId, - pub(crate) wnd: &'a Window, + pub(crate) wnd_id: WindowId, pub(crate) is_layout_root: bool, pub(crate) tree: &'a mut WidgetTree, } impl<'a> WidgetCtxImpl for Layouter<'a> { + #[inline] fn id(&self) -> WidgetId { self.id } - fn current_wnd(&self) -> &Window { self.wnd } + #[inline] + fn current_wnd(&self) -> Rc { AppCtx::get_window_assert(self.wnd_id) } fn with_tree R, R>(&self, f: F) -> R { f(self.tree) } } @@ -200,19 +202,21 @@ impl<'a> Layouter<'a> { // or modify it during perform layout. let tree2 = unsafe { &mut *(self.tree as *mut WidgetTree) }; - let Self { id, wnd, ref tree, .. } = *self; - let mut ctx = LayoutCtx { id, wnd, tree: tree2 }; + let Self { id, wnd_id, ref tree, .. } = *self; + let mut ctx = LayoutCtx { id, wnd_id, tree: tree2 }; let size = id.assert_get(&tree.arena).perform_layout(clamp, &mut ctx); // The dynamic widget maybe generate a new widget to instead of self. In that // way we needn't add a layout event because it perform layout in another widget // and added the event in that widget. if id == ctx.id { - wnd.add_delay_event(DelayEvent::PerformedLayout(id)); + self + .window() + .add_delay_event(DelayEvent::PerformedLayout(id)); } else { self.id = ctx.id; } - let mut info = tree2.store.layout_info_or_default(id); + let info = tree2.store.layout_info_or_default(id); let size = clamp.clamp(size); info.clamp = clamp; info.size = Some(size); @@ -270,11 +274,11 @@ impl<'a> Layouter<'a> { impl<'a> Layouter<'a> { pub(crate) fn new( id: WidgetId, - wnd: &'a Window, + wnd_id: WindowId, is_layout_root: bool, tree: &'a mut WidgetTree, ) -> Self { - Self { id, wnd, is_layout_root, tree } + Self { id, wnd_id, is_layout_root, tree } } } @@ -299,7 +303,7 @@ impl std::ops::DerefMut for LayoutStore { #[cfg(test)] mod tests { use super::*; - use crate::{impl_query_self_only, prelude::*, test_helper::*}; + use crate::{impl_query_self_only, prelude::*, reset_test_env, test_helper::*}; use ribir_dev_helper::*; use std::{cell::RefCell, rc::Rc}; @@ -328,50 +332,47 @@ mod tests { #[test] fn fix_incorrect_relayout_root() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); // Can't use layout info of dirty widget to detect if the ancestors path have // in relayout list. Because new widget insert by `DynWidget` not have layout // info, but its parent have. let child_box = Stateful::new(MockBox { size: Size::zero() }); let root_layout_cnt = Stateful::new(0); - let w = widget! { - states { - child_box: child_box.clone(), - root_layout_cnt: root_layout_cnt.clone(), - } - MockMulti { - on_performed_layout: move |_| *root_layout_cnt += 1, - DynWidget { - dyns: if child_box.size.is_empty() { + let c_child_box = child_box.clone_writer(); + let c_root_layout_cnt = root_layout_cnt.clone_reader(); + let w = fn_widget! { + @MockMulti { + on_performed_layout: move |_| *$root_layout_cnt.write() += 1, + @ { pipe!($child_box.size.is_empty()).map(move|b| if b { Widget::from(MockBox { size: Size::new(1., 1.) }) } else { - Widget::from(child_box.clone_stateful()) - } + Widget::from(child_box.clone_writer().into_inner()) + }) } } }; let mut wnd = TestWindow::new(w); wnd.draw_frame(); - assert_eq!(*root_layout_cnt.state_ref(), 1); + assert_eq!(*c_root_layout_cnt.read(), 1); { - child_box.state_ref().size = Size::new(2., 2.); + c_child_box.write().size = Size::new(2., 2.); } wnd.draw_frame(); - assert_eq!(*root_layout_cnt.state_ref(), 2); + assert_eq!(*c_root_layout_cnt.read(), 2); } #[test] fn layout_list_from_root_to_leaf() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); let layout_order = Stateful::new(vec![]); let trigger = Stateful::new(Size::zero()); let w = widget! { states { - layout_order: layout_order.clone(), - trigger: trigger.clone() + layout_order: layout_order.clone_stateful(), + trigger: trigger.clone_stateful() } MockBox { size: *trigger, @@ -399,11 +400,11 @@ mod tests { #[test] fn relayout_size() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); let trigger = Stateful::new(Size::zero()); let w = widget! { - states {trigger: trigger.clone()} + states {trigger: trigger.clone_stateful()} OffsetBox { size: Size::new(100., 100.), offset: Point::new(50., 50.), @@ -444,13 +445,13 @@ mod tests { #[test] fn relayout_from_parent() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); let trigger = Stateful::new(Size::zero()); let cnt = Rc::new(RefCell::new(0)); let cnt2 = cnt.clone(); let w = widget! { - states {trigger: trigger.clone()} + states {trigger: trigger.clone_stateful()} init { let cnt = cnt2; } MockBox { size: Size::new(50., 50.), @@ -474,7 +475,7 @@ mod tests { #[test] fn layout_visit_prev_position() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); #[derive(Declare)] struct MockWidget { @@ -500,7 +501,7 @@ mod tests { let pos2 = pos.clone(); let trigger = Stateful::new(Size::zero()); let w = widget! { - states {trigger: trigger.clone()} + states {trigger: trigger.clone_stateful()} init { let pos = pos2.clone(); } diff --git a/core/src/widget_tree/widget_id.rs b/core/src/widget_tree/widget_id.rs index 5250c5edf..a3997c05d 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, }; @@ -16,52 +16,20 @@ pub struct WidgetId(pub(crate) NodeId); pub(crate) type TreeArena = Arena; -bitflags! { - #[derive(Default, PartialEq, Eq, Clone, Copy, Hash, Debug)] - pub(crate) struct RenderNodeFlag: u8 { - const NONE = 0; - const DROPPED = 0b0001; - } -} -pub(crate) struct RenderNode { - flags: RenderNodeFlag, - data: Box, -} +pub(crate) struct RenderNode(Box); impl WidgetId { /// Returns a reference to the node data. - pub(crate) fn get(self, tree: &TreeArena) -> Option<&dyn Render> { - self.get_node(tree).map(|n| n.data.as_ref()) - } - - /// Returns a mutable reference to the node data. - pub(crate) fn get_mut(self, tree: &mut TreeArena) -> Option<&mut Box> { - self.get_node_mut(tree).map(|n| &mut n.data) - } - - pub(crate) fn get_node(self, tree: &TreeArena) -> Option<&RenderNode> { - tree.get(self.0).map(|n| n.get()) + pub(crate) fn get<'a, 'b>(self, tree: &'a TreeArena) -> Option<&'a (dyn Render + 'b)> { + tree.get(self.0).map(|n| &*n.get().0) } pub(crate) fn get_node_mut(self, tree: &mut TreeArena) -> Option<&mut RenderNode> { tree.get_mut(self.0).map(|n| n.get_mut()) } - /// Mark the widget dropped but not release the node, caller has the - /// responsibility to release it. - pub(crate) fn mark_drop(self, tree: &mut TreeArena) { - if let Some(node) = self.get_node_mut(tree) { - node.flags.insert(RenderNodeFlag::DROPPED); - } - } - /// detect if the widget of this id point to is dropped. - pub(crate) fn is_dropped(self, tree: &TreeArena) -> bool { - self.0.is_removed(tree) - || self - .get_node(tree) - .map_or(true, |n| n.flags.contains(RenderNodeFlag::DROPPED)) - } + pub(crate) fn is_dropped(self, tree: &TreeArena) -> bool { self.0.is_removed(tree) } #[allow(clippy::needless_collect)] pub(crate) fn lowest_common_ancestor( @@ -108,11 +76,18 @@ impl WidgetId { pub(crate) fn next_sibling(self, tree: &TreeArena) -> Option { self.node_feature(tree, |node| node.next_sibling()) } + pub(crate) fn prev_sibling(self, tree: &TreeArena) -> Option { + self.node_feature(tree, Node::previous_sibling) + } pub(crate) fn previous_sibling(self, tree: &TreeArena) -> Option { self.node_feature(tree, |node| node.previous_sibling()) } + pub(crate) fn ancestor_of(self, other: WidgetId, tree: &TreeArena) -> bool { + other.ancestors(tree).any(|p| self == p) + } + pub(crate) fn ancestors(self, tree: &TreeArena) -> impl Iterator + '_ { self.0.ancestors(tree).map(WidgetId) } @@ -126,15 +101,6 @@ impl WidgetId { self.0.descendants(tree).map(WidgetId) } - pub(crate) fn detach(self, tree: &mut TreeArena) { self.0.detach(tree) } - - pub(crate) fn remove_subtree(self, tree: &mut WidgetTree) { - self.descendants(&tree.arena).for_each(|id| { - tree.store.remove(id); - }); - self.0.remove_subtree(&mut tree.arena); - } - pub(crate) fn on_mounted_subtree(self, tree: &WidgetTree) { self .descendants(&tree.arena) @@ -143,7 +109,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() @@ -163,10 +129,6 @@ impl WidgetId { self.0.insert_after(next.0, tree); } - pub(crate) fn insert_before(self, before: WidgetId, tree: &mut TreeArena) { - self.0.insert_before(before.0, tree); - } - pub(crate) fn append(self, child: WidgetId, tree: &mut TreeArena) { self.0.append(child.0, tree); } @@ -189,12 +151,23 @@ 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`") } - pub(crate) fn assert_get_mut(self, tree: &mut TreeArena) -> &mut Box { - self.get_mut(tree).expect("Widget not exists in the `tree`") + /// We assume the `f` wrap the widget into a new widget, and keep the old + /// widget as part of the new widget, otherwise, undefined behavior. + pub(crate) fn wrap_node( + self, + tree: &mut TreeArena, + f: impl FnOnce(Box) -> Box, + ) { + let node = self.get_node_mut(tree).unwrap(); + unsafe { + let data = Box::from_raw(&mut *node.0 as *mut _); + let copied = node.replace(f(data)); + std::mem::forget(copied) + } } pub(crate) fn paint_subtree(self, ctx: &mut PaintingCtx) { @@ -202,12 +175,13 @@ impl WidgetId { while let Some(id) = w { ctx.id = id; ctx.painter.save(); + let wnd = ctx.window(); + let arena = &wnd.widget_tree.borrow().arena; let mut need_paint = false; if ctx.painter.alpha() != 0. { if let Some(layout_box) = ctx.box_rect() { - let tree = ctx.wnd.widget_tree.borrow(); - let render = id.assert_get(&tree.arena); + let render = id.assert_get(arena); ctx .painter .translate(layout_box.min_x(), layout_box.min_y()); @@ -216,7 +190,6 @@ impl WidgetId { } } - let arena = &ctx.wnd.widget_tree.borrow().arena; w = id.first_child(arena).filter(|_| need_paint).or_else(|| { let mut node = w; while let Some(p) = node { @@ -239,25 +212,16 @@ impl WidgetId { } } -pub(crate) unsafe fn split_arena(tree: &mut TreeArena) -> (&mut TreeArena, &mut TreeArena) { - let ptr = tree as *mut TreeArena; - (&mut *ptr, &mut *ptr) -} - pub(crate) fn new_node(arena: &mut TreeArena, node: Box) -> WidgetId { - WidgetId(arena.new_node(RenderNode { - flags: RenderNodeFlag::NONE, - data: node, - })) + WidgetId(arena.new_node(RenderNode(node))) } pub(crate) fn empty_node(arena: &mut TreeArena) -> WidgetId { new_node(arena, Box::new(Void)) } -impl std::ops::Deref for RenderNode { - type Target = dyn Render; - fn deref(&self) -> &Self::Target { self.data.as_ref() } -} +impl RenderNode { + pub(crate) fn as_widget_mut(&mut self) -> &mut Box { &mut self.0 } -impl std::ops::DerefMut for RenderNode { - fn deref_mut(&mut self) -> &mut Self::Target { self.data.as_mut() } + pub(crate) fn replace(&mut self, value: Box) -> Box { + std::mem::replace(&mut self.0, value) + } } diff --git a/core/src/window.rs b/core/src/window.rs index e3b531017..b031fb0d2 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,6 @@ 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(); } /// Request switch the focus to next widget. @@ -110,72 +116,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) { - 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() { - break; - } - } + self.widget_tree.borrow().draw(); + self.draw_delay_drop_widgets(); - self.focus_mgr.borrow_mut().refresh_focus(); - self.widget_tree.borrow().draw(); + let surface = match AppCtx::app_theme() { + Theme::Full(theme) => theme.palette.surface(), + Theme::Inherit(_) => unreachable!(), + }; - 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); - 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)); @@ -187,22 +204,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.emit_events(); 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() } @@ -264,55 +285,114 @@ 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}; match e { DelayEvent::Mounted(id) => { - let e = AllLifecycle::Mounted(LifecycleEvent { id, wnd: self }); + let e = AllLifecycle::Mounted(LifecycleEvent { id, wnd_id: self.id() }); self.emit::(id, e); } DelayEvent::PerformedLayout(id) => { - let e = AllLifecycle::PerformedLayout(LifecycleEvent { id, wnd: self }); + 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: self }); + let e = AllLifecycle::Disposed(LifecycleEvent { id, wnd_id: self.id() }); self.emit::(id, e); }); - if !delay_drop { - id.remove_subtree(&mut self.widget_tree.borrow_mut()) + 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); } } DelayEvent::Focus(id) => { - let e = AllFocus::Focus(FocusEvent::new(id, self)); + let e = AllFocus::Focus(FocusEvent::new(id, self.id())); self.emit::(id, e); } DelayEvent::FocusIn { bottom, up } => { - let mut e = AllFocusBubble::FocusInCapture(FocusEvent::new(bottom, self)); + let mut e = AllFocusBubble::FocusInCapture(FocusEvent::new(bottom, self.id())); self.bottom_down_emit::(&mut e, bottom, up); let mut e = AllFocusBubble::FocusIn(e.into_inner()); self.bottom_up_emit::(&mut e, bottom, up); } DelayEvent::Blur(id) => { - let e = AllFocus::Blur(FocusEvent::new(id, self)); + let e = AllFocus::Blur(FocusEvent::new(id, self.id())); self.emit::(id, e); } DelayEvent::FocusOut { bottom, up } => { - let mut e = AllFocusBubble::FocusOutCapture(FocusEvent::new(bottom, self)); + let mut e = AllFocusBubble::FocusOutCapture(FocusEvent::new(bottom, self.id())); self.bottom_down_emit::(&mut e, bottom, up); let mut e = AllFocusBubble::FocusOut(e.into_inner()); self.bottom_up_emit::(&mut e, bottom, up); } DelayEvent::KeyDown { id, scancode, key } => { - let mut e = AllKeyboard::KeyDownCapture(KeyboardEvent::new(scancode, key, id, self)); + let mut e = AllKeyboard::KeyDownCapture(KeyboardEvent::new(scancode, key, id, self.id())); self.bottom_down_emit::(&mut e, id, None); let mut e = AllKeyboard::KeyDown(e.into_inner()); self.bottom_up_emit::(&mut e, id, None); @@ -332,19 +412,19 @@ impl Window { } } DelayEvent::KeyUp { id, scancode, key } => { - let mut e = AllKeyboard::KeyUpCapture(KeyboardEvent::new(scancode, key, id, self)); + let mut e = AllKeyboard::KeyUpCapture(KeyboardEvent::new(scancode, key, id, self.id())); self.bottom_down_emit::(&mut e, id, None); let mut e = AllKeyboard::KeyUp(e.into_inner()); self.bottom_up_emit::(&mut e, id, None); } DelayEvent::Chars { id, chars } => { - let mut e = AllChars::CharsCapture(CharsEvent::new(chars, id, self)); + let mut e = AllChars::CharsCapture(CharsEvent::new(chars, id, self.id())); self.bottom_down_emit::(&mut e, id, None); let mut e = AllChars::Chars(e.into_inner()); self.bottom_up_emit::(&mut e, id, None); } DelayEvent::Wheel { id, delta_x, delta_y } => { - let mut e = AllWheel::WheelCapture(WheelEvent::new(delta_x, delta_y, id, self)); + let mut e = AllWheel::WheelCapture(WheelEvent::new(delta_x, delta_y, id, self.id())); self.bottom_down_emit::(&mut e, id, None); let mut e = AllWheel::Wheel(e.into_inner()); self.bottom_up_emit::(&mut e, id, None); @@ -390,25 +470,28 @@ impl Window { } } - fn emit(&self, id: WidgetId, mut e: L::Event<'_>) + fn emit(&self, id: WidgetId, mut e: L::Event) where L: EventListener + 'static, { - id.assert_get(&self.widget_tree.borrow().arena) - .query_all_type( - |m: &L| { - m.dispatch(&mut e); - true - }, - QueryOrder::InnerFirst, - ); + // Safety: we only use tree to query the inner data of a node and dispatch a + // event by it, and never read or write the node. And in the callback, there is + // no way to mut access the inner data of node or destroy the node. + let tree = unsafe { &*(&*self.widget_tree.borrow() as *const WidgetTree) }; + id.assert_get(&tree.arena).query_all_type( + |m: &L| { + m.dispatch(&mut e); + true + }, + QueryOrder::InnerFirst, + ); } - fn bottom_down_emit<'a, L>(&self, e: &mut L::Event<'a>, bottom: WidgetId, up: Option) + fn bottom_down_emit(&self, e: &mut L::Event, bottom: WidgetId, up: Option) where L: EventListener + 'static, - L::Event<'a>: DerefMut, - as Deref>::Target: std::borrow::BorrowMut>, + L::Event: DerefMut, + ::Target: std::borrow::BorrowMut, { use std::borrow::Borrow; @@ -431,11 +514,11 @@ impl Window { }); } - fn bottom_up_emit<'a, L>(&self, e: &mut L::Event<'a>, bottom: WidgetId, up: Option) + fn bottom_up_emit(&self, e: &mut L::Event, bottom: WidgetId, up: Option) where L: EventListener + 'static, - L::Event<'a>: DerefMut, - as Deref>::Target: std::borrow::BorrowMut>, + L::Event: DerefMut, + ::Target: std::borrow::BorrowMut, { use std::borrow::Borrow; if !(**e).borrow().is_propagation() { @@ -458,6 +541,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 @@ -467,8 +566,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 9370b579d..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(), 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/painter_backend_eq_image_test.rs b/dev-helper/src/painter_backend_eq_image_test.rs index 44e16caa4..06622d073 100644 --- a/dev-helper/src/painter_backend_eq_image_test.rs +++ b/dev-helper/src/painter_backend_eq_image_test.rs @@ -86,7 +86,7 @@ pub fn assert_texture_eq_png(img: PixelImage, file_path: &std::path::Path) { ) .unwrap(); - const TOLERANCE: f64 = 0.000002; + const TOLERANCE: f64 = 0.0000025; let (v, _) = dssim.compare(&expected, dissim_mig); let v: f64 = v.into(); 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/docs/essentials/widget_language.md b/docs/essentials/widget_language.md index 3d16ba9fa..e3f35bd18 100644 --- a/docs/essentials/widget_language.md +++ b/docs/essentials/widget_language.md @@ -213,7 +213,7 @@ DynWidget { We added three lines of code. -The first line is `dyns := assign_watch!(*counter > 0)`, we use operator `:=` instead of `:` to initialize the `dyns`. Unlike `:`, `:=` accepts an `AssignObservable` as its initialization value and explicitly subscribes to it to update the field. `AssignObservable` is a type that contain the initialization value and an observable stream of that value. The `assign_watch!` macro is used to convert a expression to an `AssignObservable`. +The first line is `dyns := assign_watch!(*counter > 0)`, we use operator `:=` instead of `:` to initialize the `dyns`. Unlike `:`, `:=` accepts an `Pipe` as its initialization value and explicitly subscribes to it to update the field. `Pipe` is a type that contain the initialization value and an observable stream of that value. The `assign_watch!` macro is used to convert a expression to an `Pipe`. In the second line we use `stream_map` to chain `distinct_until_changed` on the stream observable. So we accept the changes only when the result of `*counter > 0` changed. diff --git a/examples/counter/src/counter.rs b/examples/counter/src/counter.rs index 40ff2a0ad..584920694 100644 --- a/examples/counter/src/counter.rs +++ b/examples/counter/src/counter.rs @@ -1,15 +1,21 @@ use ribir::prelude::*; -pub fn counter() -> Widget { - widget! { - states { cnt: Stateful::new(0) } - Column { +pub fn counter() -> impl Into { + fn_widget! { + let mut cnt = Stateful::new(0); + + @Column { h_align: HAlign::Center, align_items: Align::Center, - FilledButton { on_tap: move |_| *cnt += 1, Label::new("Add") } - H1 { text: cnt.to_string() } - FilledButton { on_tap: move |_| *cnt += -1, Label::new("Sub") } + @FilledButton { + on_tap: move |_: &mut _| *$cnt.write() += 1, + @{ Label::new("Add") } + } + @H1 { text: pipe!($cnt.to_string()) } + @FilledButton { + on_tap: move |_: &mut _| *$cnt.write() += -1, + @{ Label::new("Sub") } + } } } - .into() } 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/messages/src/main.rs b/examples/messages/src/main.rs index 5b84e405a..2228c93af 100644 --- a/examples/messages/src/main.rs +++ b/examples/messages/src/main.rs @@ -5,4 +5,4 @@ use messages::messages; use ribir::prelude::*; use ribir_dev_helper::*; -example_framework!(messages, wnd_size = Size::new(480., 960.)); +example_framework!(messages); diff --git a/examples/messages/src/messages.rs b/examples/messages/src/messages.rs index d1498f092..d02212b3d 100644 --- a/examples/messages/src/messages.rs +++ b/examples/messages/src/messages.rs @@ -13,7 +13,7 @@ struct MessageList { messages: Vec, } -pub fn messages() -> Widget { +pub fn messages() -> impl Into { MessageList { messages: vec![ Message { @@ -38,95 +38,83 @@ pub fn messages() -> Widget { }, ], } - .into() } impl Compose for MessageList { fn compose(this: State) -> Widget { - widget! { - states { this: this.into_readonly() } - init ctx => { - let title_style = TypographyTheme::of(ctx).title_large.text.clone(); - let title_icon_size = IconSize::of(ctx).tiny; - let background = Palette::of(ctx).surface(); - let foreground = Palette::of(ctx).on_surface(); - } - Column { - background, - Row { + fn_widget! { + let palette = Palette::of(ctx!()); + + @Column { + background: palette.surface(), + @Row { justify_content: JustifyContent::SpaceBetween, padding: EdgeInsets::new(8., 16., 8., 16.), align_items: Align::Center, - Row { + @Row { item_gap: 10., - Icon { - size: title_icon_size, - svgs::MENU - } - Text { + @TinyIcon { @{ svgs::MENU } } + @Text { text: "Message", - foreground, - text_style: title_style.clone(), + foreground: palette.on_surface(), + text_style: TypographyTheme::of(ctx!()).title_large.text.clone(), } } - Row { + @Row { item_gap: 10., - Icon { - size: title_icon_size, - svgs::SEARCH - } - Icon { - size: title_icon_size, - svgs::MORE_VERT - } + @TinyIcon { @{ svgs::SEARCH } } + @TinyIcon { @{ svgs::MORE_VERT } } } } - Tabs { + @Tabs { pos: Position::Bottom, - Tab { - TabItem { - material_svgs::SMS - Label::new("Messages") + @Tab { + @TabItem { + @{ material_svgs::SMS } + @{ Label::new("Messages") } } - TabPane { - VScrollBar { - Lists { - Multi::new(this.messages.clone().into_iter().map(move |message| { - let name = message.avatar.to_string(); - let mut avatar = format!("{}/examples/attachments/3DDD-{name}.png", env!("CARGO_WORKSPACE_DIR")); - let img = PixelImage::from_png(&std::fs::read(avatar).unwrap()); - let img = ShareResource::new(img); - - widget! { - Column { - ListItem { - line_number: 1, - HeadlineText(Label::new(message.nick_name.clone())) - SupportingText(Label::new(message.content.clone())) - Leading { Avatar { widget::from(img) } } - Trailing { svgs::MORE_HORIZ } + @TabPane { + @VScrollBar { + @Lists { + @{ + let message_gen = |message: Message| { + @Column { + @ListItem { + line_number: 1usize, + @HeadlineText(Label::new(message.nick_name.clone())) + @SupportingText(Label::new(message.content.clone())) + @Leading { + @Avatar { + @{ + let name = message.avatar.to_string(); + let mut avatar = format!("{}/examples/attachments/3DDD-{name}.png", env!("CARGO_WORKSPACE_DIR")); + let img = PixelImage::from_png(&std::fs::read(avatar).unwrap()); + ShareResource::new(img) + } + } + } + @Trailing { @{ svgs::MORE_HORIZ } } } - Divider {} + @Divider {} } - } - })) + }; + + Multi::new($this.messages.clone().into_iter().map(message_gen)) + } } } } } - Tab { - TabItem { - material_svgs::ACCOUNT_CIRCLE - Label::new("Person") - } - TabPane { - Text { - text: "Person" - } + @Tab { + @TabItem { + @{ material_svgs::ACCOUNT_CIRCLE } + @{ Label::new("Person") } } + @TabPane { @Text { text: "Person" } } } } } - }.into() + } + .into() } } diff --git a/examples/storybook/src/storybook.rs b/examples/storybook/src/storybook.rs index fb9ae35bb..8c3682571 100644 --- a/examples/storybook/src/storybook.rs +++ b/examples/storybook/src/storybook.rs @@ -1,358 +1,377 @@ -use material::material_svgs; -use ribir::prelude::*; -use std::rc::Rc; +use ribir::{material::material_svgs, prelude::*}; -const NORMAL_BUTTON_SIZE: Size = Size::new(120., 40.); +static NORMAL_BUTTON_SIZE: Size = Size::new(120., 40.); -struct Storybook; - -impl Compose for Storybook { - fn compose(_: State) -> Widget { - widget! { - init ctx => { - let surface_container_low: Brush = Palette::of(ctx).surface_container_low().clone().into(); - let primary: Brush = Palette::of(ctx).primary().clone().into(); - let surface_container_lowest: Brush = Palette::of(ctx) - .surface_container_lowest().clone().into(); +fn header() -> Widget { + static HEADER_HEIGHT: f32 = 64.; + static TITLE: &str = "Material Design"; + fn_widget! { + @ConstrainedBox { + clamp: BoxClamp::fixed_height(HEADER_HEIGHT), + @Row { + v_align: VAlign::Center, + justify_content: JustifyContent::SpaceAround, + @Text { + text: TITLE, + } } - ThemeWidget { - id: theme, - theme: Rc::new(Theme::Full(material::purple::light())), - Column { - background: surface_container_low.clone(), - ConstrainedBox { - clamp: BoxClamp::fixed_height(48.), - Row { - v_align: VAlign::Center, - justify_content: JustifyContent::SpaceAround, - Text { - text: "Material 3", - } - Checkbox { - id: brightness, - on_tap: move |_| { - if brightness.checked { - theme.theme = Rc::new(Theme::Full(material::purple::dark())); - } else { - theme.theme = Rc::new(Theme::Full(material::purple::light())); - } - }, - Trailing { - Label::new("Dark") + } + } + .into() +} + +fn content() -> Widget { + fn actions_show() -> Widget { + fn_widget! { + @VScrollBar { + @Column { + @Column { + align_items: Align::Center, + @ConstrainedBox { + clamp: BoxClamp::fixed_height(30.), + @Row { + h_align: HAlign::Center, + @Text { text: "Common buttons" } + @Icon { + size: Size::splat(16.), + @ { material_svgs::INFO } } } } - } - Expanded { - flex: 1., - Tabs { - pos: Position::Bottom, - Tab { - TabItem { - Label::new("Actions") + @Column { + item_gap: 20., + padding: EdgeInsets::new(20., 40., 20., 40.), + background: Palette::of(ctx!()).surface_container_low(), + border_radius: Radius::all(4.), + border: Border::all(BorderSide { + color: Palette::of(ctx!()).primary().into(), + width: 1., + }), + @Row { + item_gap: 20., + @SizedBox { + size: NORMAL_BUTTON_SIZE, + @FilledButton { + @ { Label::new("Filled") } + } } - TabPane { - VScrollBar { - Column { - Column { - align_items: Align::Center, - ConstrainedBox { - clamp: BoxClamp::fixed_height(30.), - Row { - h_align: HAlign::Center, - Text { text: "Common buttons" } - Icon { - size: Size::splat(16.), - material_svgs::INFO - } - } - } - Column { - item_gap: 20., - padding: EdgeInsets::new(20., 40., 20., 40.), - background: surface_container_lowest.clone(), - border_radius: Radius::all(4.), - border: Border::all(BorderSide { - color: primary.clone(), - width: 1., - }), - Row { - item_gap: 20., - SizedBox { - size: NORMAL_BUTTON_SIZE, - FilledButton { - Label::new("Filled") - } - } - SizedBox { - size: NORMAL_BUTTON_SIZE, - FilledButton { - svgs::ADD - Label::new("Icon") - } - } - } - Row { - item_gap: 20., - SizedBox { - size: NORMAL_BUTTON_SIZE, - OutlinedButton { - Label::new("Outlined") - } - } - SizedBox { - size: NORMAL_BUTTON_SIZE, - OutlinedButton { - svgs::ADD - Label::new("Icon") - } - } - } - Row { - item_gap: 20., - SizedBox { - size: NORMAL_BUTTON_SIZE, - Button { - Label::new("Text") - } - } - SizedBox { - size: NORMAL_BUTTON_SIZE, - Button { - svgs::ADD - Label::new("Icon") - } - } - } - } - } - Column { - align_items: Align::Center, - ConstrainedBox { - clamp: BoxClamp::fixed_height(30.), - Row { - h_align: HAlign::Center, - Text { text: "Floating action buttons" } - Icon { - size: Size::splat(16.), - material_svgs::INFO - } - } - } - Column { - item_gap: 20., - padding: EdgeInsets::new(20., 40., 20., 40.), - background: surface_container_lowest.clone(), - border_radius: Radius::all(4.), - border: Border::all(BorderSide { - color: primary.clone(), - width: 1., - }), - Row { - item_gap: 20., - FabButton { - svgs::ADD - } - FabButton { - svgs::ADD - Label::new("Create") - } - } - } - } - Column { - align_items: Align::Center, - ConstrainedBox { - clamp: BoxClamp::fixed_height(30.), - Row { - h_align: HAlign::Center, - Text { text: "Icon buttons" } - Icon { - size: Size::splat(16.), - material_svgs::INFO - } - } - } - Column { - item_gap: 20., - padding: EdgeInsets::new(20., 40., 20., 40.), - background: surface_container_lowest.clone(), - border_radius: Radius::all(4.), - border: Border::all(BorderSide { - color: primary.clone(), - width: 1., - }), - Row { - item_gap: 20., - Button { - svgs::SETTINGS - } - FilledButton { - svgs::SETTINGS - } - OutlinedButton { - svgs::SETTINGS - } - } - } - } - } + @SizedBox { + size: NORMAL_BUTTON_SIZE, + @FilledButton { + @ { svgs::ADD } + @ { Label::new("Icon") } } } } - Tab { - TabItem { - Label::new("Tabs") + @Row { + item_gap: 20., + @SizedBox { + size: NORMAL_BUTTON_SIZE, + @OutlinedButton { + @ { Label::new("Outlined") } + } } - TabPane { - Tabs { - Tab { - TabItem { - svgs::HOME - Label::new("Video") - } - } - Tab { - TabItem { - svgs::HOME - Label::new("Photos") - } - } - Tab { - TabItem { - svgs::HOME - Label::new("Audio") - } - } + @SizedBox { + size: NORMAL_BUTTON_SIZE, + @OutlinedButton { + @ { svgs::ADD } + @ { Label::new("Icon") } } } } - Tab { - TabItem { - Label::new("Containment") + @Row { + item_gap: 20., + @SizedBox { + size: NORMAL_BUTTON_SIZE, + @Button { + @ { Label::new("Text") } + } } - TabPane { - Column { - ConstrainedBox { - clamp: BoxClamp::fixed_height(30.), - Row { - h_align: HAlign::Center, - Text { text: "Divider" } - Icon { - size: Size::splat(16.), - material_svgs::INFO - } - } - } - Divider {} + @SizedBox { + size: NORMAL_BUTTON_SIZE, + @Button { + @ { svgs::ADD } + @ { Label::new("Icon") } } } } - Tab { - TabItem { - Label::new("Lists") + } + } + @Column { + align_items: Align::Center, + @ConstrainedBox { + clamp: BoxClamp::fixed_height(30.), + @Row { + h_align: HAlign::Center, + @Text { text: "Floating action buttons" } + @Icon { + size: Size::splat(16.), + @ { material_svgs::INFO } } - TabPane { - Column { - margin: EdgeInsets::all(20.), - Lists { - margin: EdgeInsets::only_top(20.), - Link { - url: "https://ribir.org", - ListItem { - line_number: 1, - Leading { svgs::CHECK_BOX_OUTLINE_BLANK } - HeadlineText(Label::new("One line list item")) - SupportingText(Label::new("One line supporting text")) - } - } - Divider { indent: 16. } - ListItem { - Leading { svgs::MENU } - HeadlineText(Label::new("One line list item")) - Trailing { Label::new("100+") } - } - Divider { indent: 16. } - ListItem { - line_number: 2, - Leading { - Avatar { - ShareResource::new(PixelImage::from_png(include_bytes!("../../attachments/3DDD-1.png"))) - } - } - HeadlineText(Label::new("Two lines list item")) - SupportingText(Label::new("Two lines supporting text \rTwo lines supporting text")) - Trailing { Label::new("100+") } - } - Divider { indent: 16. } - ListItem { - line_number: 1, - Leading { - ShareResource::new(PixelImage::from_png(include_bytes!("../../attachments/3DDD-2.png"))) - } - HeadlineText(Label::new("One lines list item")) - SupportingText(Label::new("One lines supporting text")) - Trailing { svgs::CHECK_BOX_OUTLINE_BLANK } - } - Divider { indent: 16. } - ListItem { - line_number: 1, - Leading { - Avatar { - Label::new("A") - } - } - HeadlineText(Label::new("One lines list item")) - SupportingText(Label::new("One lines supporting text")) - Trailing { Label::new("100+") } - } - Divider { indent: 16. } - ListItem { - line_number: 1, - Leading { - Poster(ShareResource::new(PixelImage::from_png(include_bytes!("../../attachments/3DDD-3.png")))) - } - HeadlineText(Label::new("One lines list item")) - SupportingText(Label::new("One lines supporting text")) - Trailing { Label::new("100+") } - } - } - } + } + } + @Column { + item_gap: 20., + padding: EdgeInsets::new(20., 40., 20., 40.), + background: Palette::of(ctx!()).surface_container_lowest(), + border_radius: Radius::all(4.), + border: Border::all(BorderSide { + color: Palette::of(ctx!()).primary().into(), + width: 1., + }), + @Row { + item_gap: 20., + @FabButton { + @ { svgs::ADD } + } + @FabButton { + @ { svgs::ADD } + @ { Label::new("Create") } } } - Tab { - TabItem { - Label::new("Selections") + } + } + @Column { + align_items: Align::Center, + @ConstrainedBox { + clamp: BoxClamp::fixed_height(30.), + @Row { + h_align: HAlign::Center, + @Text { text: "Icon buttons" } + @Icon { + size: Size::splat(16.), + @ { material_svgs::INFO } } - TabPane { - Column { - margin: EdgeInsets::all(20.), - Lists { - Checkbox { - Leading { - Label::new("Option1") - } - } - Checkbox { - Leading { - Label::new("Option2") - } - } - Checkbox { - Leading { - Label::new("Option3") - } - } - } - } + } + } + @Column { + item_gap: 20., + padding: EdgeInsets::new(20., 40., 20., 40.), + background: Palette::of(ctx!()).surface_container_lowest(), + border_radius: Radius::all(4.), + border: Border::all(BorderSide { + color: Palette::of(ctx!()).primary().into(), + width: 1., + }), + @Row { + item_gap: 20., + @Button { + @ { svgs::SETTINGS } + } + @FilledButton { + @ { svgs::SETTINGS } } + @OutlinedButton { + @ { svgs::SETTINGS } + } + } + } + } + } + } + } + .into() + } + + fn tabs_show() -> Widget { + fn_widget! { + @Tabs { + @Tab { + @TabItem { + @ { svgs::HOME } + @ { Label::new("Video") } + } + } + @Tab { + @TabItem { + @ { svgs::HOME } + @ { Label::new("Photos") } + } + } + @Tab { + @TabItem { + @ { svgs::HOME } + @ { Label::new("Audio") } + } + } + } + } + .into() + } + + fn containment_show() -> Widget { + fn_widget! { + @Column { + @ConstrainedBox { + clamp: BoxClamp::fixed_height(30.), + @Row { + h_align: HAlign::Center, + @Text { text: "Divider" } + @Icon { + size: Size::splat(16.), + @ { material_svgs::INFO } + } + } + } + @Divider {} + } + } + .into() + } + + fn lists_show() -> Widget { + fn_widget! { + @Column { + margin: EdgeInsets::all(20.), + @Lists { + margin: EdgeInsets::only_top(20.), + @Link { + url: "https://ribir.org", + @ListItem { + @Leading { + @ { svgs::CHECK_BOX_OUTLINE_BLANK } + } + @ { HeadlineText(Label::new("One line list item")) } + @ { SupportingText(Label::new("One line supporting text")) } + } + } + @Divider { indent: 16. } + @ListItem { + @Leading { + @ { svgs::MENU } + } + @ { HeadlineText(Label::new("One line list item")) } + @Trailing { + @ { Label::new("100+") } + } + } + @Divider { indent: 16. } + @ListItem { + line_number: 2usize, + @Leading { + @Avatar { + @ { ShareResource::new(PixelImage::from_png(include_bytes!("../../attachments/3DDD-1.png"))) } + } + } + @ { HeadlineText(Label::new("Two lines list item")) } + @ { SupportingText(Label::new("Two lines supporting text \rTwo lines supporting text")) } + @Trailing { + @ { svgs::CHECK_BOX_OUTLINE_BLANK } + } + } + @Divider { indent: 16. } + @ListItem { + @Leading { + @Avatar { + @ { Label::new("A") } } } + @ { HeadlineText(Label::new("One lines list item")) } + @ { SupportingText(Label::new("One lines supporting text")) } + @Trailing { + @ { Label::new("100+") } + } + } + @Divider { indent: 16. } + @ListItem { + @Leading { + @ { Poster(ShareResource::new(PixelImage::from_png(include_bytes!("../../attachments/3DDD-3.png")))) } + } + @ { HeadlineText(Label::new("One lines list item")) } + @ { SupportingText(Label::new("One lines supporting text")) } + @Trailing { + @ { Label::new("100+") } + } } } } - }.into() + } + .into() } + + fn checkbox_show() -> Widget { + fn_widget! { + @Column { + margin: EdgeInsets::all(20.), + @Lists { + @Checkbox { + @Leading { + @ { Label::new("Option1") } + } + } + @Checkbox { + @Leading { + @ { Label::new("Option2") } + } + } + @Checkbox { + @Leading { + @ { Label::new("Option3") } + } + } + } + } + } + .into() + } + + fn_widget! { + @Tabs { + pos: Position::Bottom, + @Tab { + @TabItem { + @ { Label::new("Actions") } + } + @TabPane { + @ { actions_show() } + } + } + @Tab { + @TabItem { + @ { Label::new("Tabs") } + } + @TabPane { + @ { tabs_show() } + } + } + @Tab { + @TabItem { + @ { Label::new("Containment") } + } + @TabPane { + @ { containment_show() } + } + } + @Tab { + @TabItem { + @ { Label::new("Lists") } + } + @TabPane { + @ { lists_show() } + } + } + @Tab { + @TabItem { + @ { Label::new("Selections") } + } + @TabPane { + @ { checkbox_show() } + } + } + } + } + .into() } -pub fn storybook() -> Widget { Storybook {}.into() } +pub fn storybook() -> Widget { + fn_widget! { + @Column { + background: Palette::of(ctx!()).surface_container_low(), + @ { header() } + @Expanded { + @ { content() } + } + } + } + .into() +} diff --git a/examples/todos/src/todos.rs b/examples/todos/src/todos.rs index 4d0e7bfec..24c89721f 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 { +impl Compose for Todos { 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 { + 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.write().new_task($input.text().to_string()); + $input.write().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_writer(), |_| 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_writer(), |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_writer(), |task| task.finished) } } } } } @@ -75,95 +65,87 @@ impl Compose for TodoMVP { } } -impl TodoMVP { - fn pane(this: StateRef, 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(this: Writer, cond: fn(&Task) -> bool) -> Widget { + fn_widget! { + // todo: pipe only for list items, not lists + @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.write() = 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 task_widget_iter = $this + .tasks + .iter() + .enumerate() + .filter_map(move |(idx, task)| { cond(task).then_some(idx) }) + .map(move |idx| { + let mut task = split_writer!($this.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: map_writer!($key.transform), + from: Transform::translation(-400., 0. ), + }; + @ $key { + @ListItem { + on_mounted: move |_| if $key.is_enter() { + *$mount_idx.write() = *$mount_task_cnt; + *$mount_task_cnt.write() += 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) + .distinct_until_changed() + .subscribe(move |v| $task.write().finished = v); + CustomEdgeWidget(checkbox.into()) + } + } + @Trailing { + cursor: CursorIcon::Hand, + visible: $key.mouse_hover(), + on_tap: move |_| { $this.write().tasks.remove(idx); }, + @{ svgs::CLOSE } + } + } + } + }).collect::>(); + Multi::new(task_widget_iter) } + } - } + }}} } .into() } - fn task(this: StateRef, task: Task, idx: usize, mount_task_cnt: StateRef) -> 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, @@ -181,6 +163,7 @@ pub fn todos() -> Widget { label: "Support Virtual Scroll".to_string(), }, ], + id_gen: 3, } .into() } diff --git a/macros/Cargo.toml b/macros/Cargo.toml index b4f2e212e..f52f71cd3 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -22,10 +22,10 @@ bitflags.workspace = true lazy_static.workspace = true proc-macro2.workspace = true quote.workspace = true -ribir_builtin = {path = "./builtin", version = "0.0.1-alpha.4" } -ribir_painter = {path = "../painter", version = "0.0.1-alpha.4" } +ribir_builtin = {path = "./builtin", version = "0.0.1-alpha.4"} +ribir_painter = {path = "../painter", version = "0.0.1-alpha.4"} smallvec.workspace = true -syn = {workspace = true, features = ["visit-mut", "full", "extra-traits"]} +syn = {workspace = true, features = ["visit-mut", "fold", "full", "extra-traits"]} [build-dependencies] -ribir_builtin = {path = "./builtin", version = "0.0.1-alpha.4" } +ribir_builtin = {path = "./builtin", version = "0.0.1-alpha.4"} 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 5958d10fc..ca89b5294 100644 --- a/macros/src/child_template.rs +++ b/macros/src/child_template.rs @@ -62,10 +62,16 @@ pub(crate) fn derive_child_template(input: &mut syn::DeriveInput) -> syn::Result fn declare_builder() -> Self::Builder { #name::builder() } } + impl #g_impl Declare2 for #name #g_ty #g_where { + type Builder = #builder #g_ty; + #[inline] + fn declare2_builder() -> Self::Builder { #name::builder() } + } + 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 a1581a57e..b3a72dfaf 100644 --- a/macros/src/declare_derive.rs +++ b/macros/src/declare_derive.rs @@ -54,6 +54,7 @@ mod kw { custom_keyword!(into); custom_keyword!(custom); custom_keyword!(skip); + custom_keyword!(strict); custom_keyword!(strip_option); } @@ -129,6 +130,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) { + let _ = input.parse::(); } else { return Err(lookahead.error()); } @@ -253,7 +256,6 @@ pub(crate) fn declare_derive(input: &mut syn::DeriveInput) -> syn::Result 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 new file mode 100644 index 000000000..2fb962fe0 --- /dev/null +++ b/macros/src/declare_derive2.rs @@ -0,0 +1,395 @@ +use crate::util::data_struct_unwrap; +use proc_macro::{Diagnostic, Level}; +use proc_macro2::TokenStream; +use quote::{quote, quote_spanned, ToTokens}; +use syn::{ + parse::{discouraged::Speculative, Parse}, + parse_quote, + punctuated::Punctuated, + spanned::Spanned, + token, DataStruct, Fields, Ident, Result, +}; + +const DECLARE: &str = "Declare2"; +pub const DECLARER: &str = "Declarer2"; +const DECLARE_ATTR: &str = "declare"; + +struct DefaultMeta { + _default_kw: kw::default, + _eq_token: Option, + value: Option, +} + +#[derive(Default)] +struct DeclareAttr { + rename: Option, + builtin: Option, + default: Option, + custom: Option, + // field with `skip` attr, will not generate setter method and use default to init value. + skip: Option, + strict: Option, +} + +struct DeclareField<'a> { + attr: Option, + field: &'a syn::Field, +} +mod kw { + use syn::custom_keyword; + custom_keyword!(rename); + custom_keyword!(builtin); + custom_keyword!(default); + custom_keyword!(custom); + custom_keyword!(skip); + custom_keyword!(strict); + // todo: tmp code, only for compatibility. + custom_keyword!(convert); +} + +impl Parse for DefaultMeta { + fn parse(input: syn::parse::ParseStream) -> Result { + Ok(Self { + _default_kw: input.parse()?, + _eq_token: input.parse()?, + value: { + let ahead = input.fork(); + let expr = ahead.parse::(); + if expr.is_ok() { + input.advance_to(&ahead); + } + expr.ok() + }, + }) + } +} + +impl Parse for DeclareAttr { + fn parse(input: syn::parse::ParseStream) -> Result { + let mut attr = DeclareAttr::default(); + while !input.is_empty() { + let lookahead = input.lookahead1(); + + // use input instead of lookahead to peek builtin, because need't complicate in + // compile error. + if input.peek(kw::builtin) { + attr.builtin = Some(input.parse()?); + } else if lookahead.peek(kw::rename) { + input.parse::()?; + input.parse::()?; + attr.rename = Some(input.parse()?); + } else if lookahead.peek(kw::custom) { + attr.custom = Some(input.parse()?); + } else if lookahead.peek(kw::default) { + 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::()?; + input.parse::()?; + } else { + return Err(lookahead.error()); + } + if let (Some(rename), Some(builtin)) = (attr.rename.as_ref(), attr.builtin.as_ref()) { + let mut d = Diagnostic::new( + Level::Error, + "`rename` and `builtin` can not be used in same time.", + ); + 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::()?; + } + } + Ok(attr) + } +} + +pub(crate) fn declare_derive(input: &mut syn::DeriveInput) -> syn::Result { + let syn::DeriveInput { vis, ident: name, generics, data, .. } = input; + + let stt = data_struct_unwrap(data, DECLARE)?; + + if stt.fields.is_empty() { + let construct = match &stt.fields { + Fields::Named(_) => 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 { #construct } + } + + impl DeclareBuilder for #name { + type Target = #name; + #[inline] + fn build_declare(self, _: &BuildCtx) -> Self::Target { self } + } + }; + Ok(tokens) + } else { + struct_with_fields_gen(stt, vis, generics, name) + } +} + +fn struct_with_fields_gen( + stt: &mut DataStruct, + vis: &syn::Visibility, + generics: &syn::Generics, + name: &syn::Ident, +) -> syn::Result { + let mut builder_fields = collect_filed_and_attrs(stt)?; + + // reverse name check. + builder_fields + .iter_mut() + .for_each(DeclareField::check_reserve); + + let declarer = Ident::new(&format!("{name}{DECLARER}"), name.span()); + + let mut builder_methods = quote! {}; + builder_fields + .iter() + .filter(|f| f.need_set_method()) + .for_each(|f| { + let field_name = f.field.ident.as_ref().unwrap(); + let ty = &f.field.ty; + let set_method = f.set_method_name(); + 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 + let def_fields = builder_fields.pairs().map(|p| { + let (f, c) = p.into_tuple(); + let mut f = f.field.clone(); + let ty = &f.ty; + f.ty = parse_quote!(Option>); + syn::punctuated::Pair::new(f, c) + }); + + // implement declare trait + + let fill_default = builder_fields.iter().filter_map(|f| { + let attr = f.attr.as_ref()?; + let field_name = f.member(); + + let set_default_value = match (&attr.default, &attr.skip) { + (Some(df), None) if df.value.is_some() => { + let v = df.value.as_ref(); + let method = f.set_method_name(); + Some(quote! { self = self.#method(#v); }) + } + (Some(df), Some(_)) if df.value.is_some() => { + let v = df.value.as_ref(); + Some(quote! { self.#field_name = Some(DeclareInit::declare_from(#v)); }) + } + (Some(_), _) | (_, Some(_)) => { + Some(quote! { self.#field_name = Some(DeclareInit::default()); }) + } + (None, None) => None, + }; + set_default_value.map(|set_default_value| { + quote! { + if self.#field_name.is_none() { + #set_default_value + } + } + }) + }); + + let unzip_fields = builder_fields.iter().map(|df| { + let field_name = df.field.ident.as_ref().unwrap(); + let err = format!("Required field `{name}::{field_name}` not init"); + quote_spanned! { field_name.span() => + let #field_name = self.#field_name.expect(#err).unzip(); + } + }); + + let field_names = builder_fields.iter().map(|f| f.field.ident.as_ref()); + 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 #lt_token #params #gt_token #where_clause { + #(#def_fields)* + } + + impl #g_impl Declare2 for #name #g_ty #g_where { + type Builder = #declarer #g_ty; + + fn declare2_builder() -> Self::Builder { + #declarer { #(#field_names : None ),*} + } + } + + impl #g_impl #declarer #g_ty #g_where { + #builder_methods + } + + impl #g_impl DeclareBuilder for #declarer #g_ty #g_where { + type Target = State<#name #g_ty>; + + #[inline] + fn build_declare(mut self, ctx: &BuildCtx) -> Self::Target { + set_build_ctx!(ctx); + + #(#fill_default)* + #(#unzip_fields)* + 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_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) + }; + } + );* + + if let Some(unsub) = _unsub_ಠ_ಠ { + _ribir_ಠ_ಠ.as_stateful().unsubscribe_on_drop(unsub); + } + + _ribir_ಠ_ಠ + } + } + }; + + Ok(tokens) +} + +fn collect_filed_and_attrs(stt: &mut DataStruct) -> Result> { + let mut builder_fields = Punctuated::default(); + match &mut stt.fields { + Fields::Named(named) => { + named + .named + .pairs_mut() + .try_for_each::<_, syn::Result<()>>(|pair| { + let (field, comma) = pair.into_tuple(); + let idx = field + .attrs + .iter() + .position(|attr| attr.path.is_ident(DECLARE_ATTR)); + let builder_attr = if let Some(idx) = idx { + let attr = field.attrs.remove(idx); + let args: DeclareAttr = attr.parse_args()?; + Some(args) + } else { + None + }; + + builder_fields.push(DeclareField { attr: builder_attr, field }); + if let Some(c) = comma { + builder_fields.push_punct(*c); + } + + Ok(()) + })?; + } + Fields::Unit => <_>::default(), + Fields::Unnamed(unnamed) => { + let err = syn::Error::new( + unnamed.span(), + format!("`{DECLARE}` not be supported to derive for tuple struct"), + ); + return Err(err); + } + }; + Ok(builder_fields) +} + +impl<'a> DeclareField<'a> { + fn member(&self) -> &Ident { self.field.ident.as_ref().unwrap() } + + fn set_method_name(&self) -> &Ident { + self + .attr + .as_ref() + .and_then(|attr| attr.rename.as_ref()) + .or(self.field.ident.as_ref()) + .unwrap() + } + + fn need_set_method(&self) -> bool { + self + .attr + .as_ref() + .map_or(true, |attr| attr.custom.is_none() && attr.skip.is_none()) + } + + fn check_reserve(&mut self) { + // reverse name check. + let reserve_ident = &crate::widget_macro::RESERVE_IDENT; + + let not_builtin = self + .attr + .as_ref() + .map_or(true, |attr| attr.builtin.is_none()); + + if not_builtin { + let method_name = self.set_method_name(); + if let Some(r) = reserve_ident.get(method_name.to_string().as_str()) { + let msg = format!("the identify `{}` is reserved to {}", method_name, &r); + let mut field = self.field.clone(); + // not display the attrs in the help code. + + field.attrs.clear(); + Diagnostic::spanned(vec![method_name.span().unwrap()], Level::Error, msg) + .help(format! { + "use `rename` meta to avoid the name conflict in `widget!` macro.\n\n\ + #[declare(rename = xxx)] \n\ + {}", field.into_token_stream() + }) + .emit(); + } + } + } +} diff --git a/macros/src/declare_obj.rs b/macros/src/declare_obj.rs new file mode 100644 index 000000000..e02c38b62 --- /dev/null +++ b/macros/src/declare_obj.rs @@ -0,0 +1,190 @@ +use crate::{ + rdl_macro::{DeclareField, RdlParent, StructLiteral}, + widget_macro::{WIDGETS, WIDGET_OF_BUILTIN_FIELD}, +}; +use inflector::Inflector; +use proc_macro2::{Span, TokenStream}; +use quote::{quote, quote_spanned, ToTokens}; +use smallvec::smallvec; +use smallvec::SmallVec; +use syn::{parse_str, spanned::Spanned, token::Brace, Ident, Macro, Path}; + +pub struct DeclareObj<'a> { + span: Span, + /// 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> { + Obj { + span: Span, + ty: &'a Path, + fields: SmallVec<[&'a DeclareField; 1]>, + }, + Var(&'a Ident), +} + +impl<'a> DeclareObj<'a> { + pub fn from_literal(mac: &'a StructLiteral) -> Result { + let StructLiteral { parent, brace, fields, children } = mac; + let mut builtin: Vec<(&'static str, SmallVec<[&'a DeclareField; 1]>)> = vec![]; + + 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])); + } + } else { + self_fields.push(f); + } + } + + 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 { + quote_spanned! { f.member.span() => #err_msg }.to_tokens(&mut err_tokens) + } + Err(err_tokens) + } + } + + 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) { + 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. + + quote_spanned! { *span => let _ribir_ಠ_ಠ = #this; }.to_tokens(tokens); + let name = Ident::new("_ribir_ಠ_ಠ", self.span); + self.compose_builtin_and_children(&name, tokens) + } + 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) + } + } + }) + } +} + +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()); + + 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); + } + 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) + } + + /// 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 { + 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))* } + } + } + + 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> { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + 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_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 new file mode 100644 index 000000000..6a41786ab --- /dev/null +++ b/macros/src/fn_widget_macro.rs @@ -0,0 +1,34 @@ +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_macro_input, Stmt}; + +use crate::symbol_process::DollarRefsCtx; + +pub struct FnWidgetMacro(Vec); + +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; + quote! { + FnWidget::new(move |ctx: &BuildCtx| { + set_build_ctx!(ctx); + #[allow(unused_mut)] + { #(#stmts)* } + }) + } + .to_tokens(tokens) + } +} diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 11da7f222..615b9160a 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -1,18 +1,30 @@ #![feature(proc_macro_diagnostic, proc_macro_span)] extern crate proc_macro; -extern crate proc_macro2; mod declare_derive; +mod declare_derive2; mod error; -mod widget_macro; - mod lerp_derive; mod util; +mod widget_macro; +use fn_widget_macro::FnWidgetMacro; use proc_macro::TokenStream; use quote::quote; +use symbol_process::DollarRefsCtx; use syn::{parse_macro_input, DeriveInput}; use widget_macro::gen_widget_macro; mod child_template; +mod fn_widget_macro; +mod pipe_macro; +mod rdl_macro; +mod watch_macro; +mod writer_map_macro; +pub(crate) use rdl_macro::*; + +use crate::pipe_macro::PipeMacro; +use crate::watch_macro::WatchMacro; +pub(crate) mod declare_obj; +pub(crate) mod symbol_process; pub(crate) const WIDGET_MACRO_NAME: &str = "widget"; pub(crate) const MOVE_TO_WIDGET_MACRO_NAME: &str = "move_to_widget"; @@ -22,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() } @@ -39,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() } @@ -76,6 +98,14 @@ pub fn declare_trait_macro_derive(input: TokenStream) -> TokenStream { .into() } +#[proc_macro_derive(Declare2, attributes(declare))] +pub fn declare_trait_macro_derive2(input: TokenStream) -> TokenStream { + let mut input = parse_macro_input!(input as DeriveInput); + declare_derive2::declare_derive(&mut input) + .unwrap_or_else(|e| e.into_compile_error()) + .into() +} + #[proc_macro_derive(Template, attributes(template))] pub fn child_template_trait_derive(input: TokenStream) -> TokenStream { let mut input = parse_macro_input!(input as DeriveInput); @@ -87,6 +117,107 @@ pub fn child_template_trait_derive(input: TokenStream) -> TokenStream { #[proc_macro] pub fn widget(input: TokenStream) -> TokenStream { gen_widget_macro(input, None) } +/// The macro use to declare a object, this macro will use `ctx!()` to access +/// the `BuildCtx`, so it can only use in the `fn_widget!` macro, or any scope +/// that called `set_build_ctx!` macro. +/// +/// # The Syntax +/// +/// `rdl` accept 3 kind of syntax: +/// +/// - 1. use struct literal syntax to declare a object tree, like `rdl!{ Row { +/// wrap: true } }`, if the `Row` contain any child, its child can be embed in +/// the struct literal, but must be use `rdl!` or `@` to declare, like: +/// +/// ```ignore +/// rdl!{ Row { wrap: true, child: rdl!{ Text { text: "hello" } } } } +/// ``` +/// - 2. similar to the first, but use a variable as parent and not accept any +/// fields of the parent(the builtin fields allowed), like: +/// +/// ```ignore +/// let row = rdl!{ Row { wrap: true } }; +/// rdl!{ $row { rdl!{ Text { text: "hello" } } } } +/// ``` +/// - 3. use expression to declare a object and not allow declare children, +/// like: `let row = rdl!{ Widget::new(Void) };` +#[proc_macro] +pub fn rdl(input: TokenStream) -> TokenStream { + RdlMacro::gen_code(input.into(), &mut DollarRefsCtx::top_level()) +} + +/// The `fn_widget` is a macro that create a widget from a function widget from +/// a expression. Its syntax is extended from rust syntax, you can use `@` and +/// `$` in the expression, the `@` is a short hand of `rdl` macro, and `$name` +/// use to expression a state reference of `name`. +#[proc_macro] +pub fn fn_widget(input: TokenStream) -> TokenStream { + // 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] +pub fn set_build_ctx(input: TokenStream) -> TokenStream { + let input: proc_macro2::TokenStream = input.into(); + quote! { let _ctx_ಠ_ಠ = #input; }.into() +} + +/// get the `BuildCtx` set by `set_build_ctx!` macro, if no `BuildCtx` set. +#[proc_macro] +pub fn ctx(input: TokenStream) -> TokenStream { + let tokens = if !input.is_empty() { + quote!(compile_error!("ctx! macro does not accept any argument")) + } else { + quote! { _ctx_ಠ_ಠ } + }; + tokens.into() +} + +/// `pipe` macro use to create `Pipe` object that continuous trace the +/// expression modify. Use the `$` mark the state reference and auto subscribe +/// to its modify. +#[proc_macro] +pub fn pipe(input: TokenStream) -> TokenStream { + 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 { + 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()) +} + +/// 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 map_writer(input: TokenStream) -> TokenStream { + writer_map_macro::gen_map_path_writer(input.into(), &mut DollarRefsCtx::top_level()) +} + #[proc_macro] pub fn include_svg(input: TokenStream) -> TokenStream { let w = parse_macro_input! { input as syn::LitStr }; diff --git a/macros/src/pipe_macro.rs b/macros/src/pipe_macro.rs new file mode 100644 index 000000000..ebc59462b --- /dev/null +++ b/macros/src/pipe_macro.rs @@ -0,0 +1,77 @@ +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::{ + parse::{Parse, ParseStream}, + parse_macro_input, + spanned::Spanned, + Stmt, +}; + +pub(crate) struct BodyExpr(pub(crate) Vec); +pub(crate) struct PipeMacro { + refs: DollarRefsScope, + expr: Vec, +} + +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() + } + } +} + +impl Parse for BodyExpr { + fn parse(input: ParseStream) -> syn::Result { Ok(Self(syn::Block::parse_within(input)?)) } +} + +impl ToTokens for PipeMacro { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let Self { refs, expr } = self; + + let upstream = refs.upstream_tokens(); + + if refs.used_ctx() { + quote! {{ + #refs + let upstream = #upstream; + let _ctx_handle_ಠ_ಠ = ctx!().handle(); + let mut expr_value = move |ctx!(): &BuildCtx<'_>| { #(#expr)* }; + Pipe::new( + expr_value(ctx!()), + upstream + .filter_map(move |scope| _ctx_handle_ಠ_ಠ + .with_ctx(&mut expr_value) + .map(|v| (scope, v)) + ) + .box_it() + ) + }} + .to_tokens(tokens) + } else { + quote! {{ + #refs + let upstream = #upstream; + let mut expr_value = move || { #(#expr)* }; + Pipe::new( + expr_value(), + 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 new file mode 100644 index 000000000..fade30cef --- /dev/null +++ b/macros/src/rdl_macro.rs @@ -0,0 +1,188 @@ +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; +use syn::{ + braced, + fold::Fold, + parse::{Parse, ParseBuffer, ParseStream}, + parse_macro_input, parse_quote, + punctuated::Punctuated, + spanned::Spanned, + token::{At, Bang, Brace, Colon, Comma, Dollar}, + Expr, Ident, Macro, Path, Result as SynResult, Stmt, +}; + +pub enum RdlMacro { + Literal(StructLiteral), + /// Declare an expression as a object, like `rdl! { Widget::new(...) }` + ExprObj { + span: Span, + stmts: Vec, + }, +} + +/// Declare a object use struct literal, like `rdl! { Row { ... } }` or +/// `@parent { ... }` +pub struct StructLiteral { + pub parent: RdlParent, + pub brace: Brace, + pub fields: Punctuated, + /// Declare a child in `rdl!` can use `rdl!` macro or `@` symbol. + /// `rdl! { Row { rdl! { SizedBox {...} } } }` + /// or + /// `rdl! { Row { @ SizedBox{ ... } } }` + /// but will be all processed as `rdl! { ... }` + pub children: Vec, +} + +pub enum RdlParent { + /// Declare parent use a type `Row { ... }` + Type(Path), + /// Declare parent use a variable prefixed with ` @parent { ... }` + Var(Ident), +} + +/// Declare a field of a widget. +pub struct DeclareField { + /// field member name. + pub member: Ident, + pub colon_tk: Option, + pub value: Expr, +} + +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(RdlMacro::Literal(input.parse()?)) + } else { + Ok(RdlMacro::ExprObj { + span: input.span(), + stmts: syn::Block::parse_within(input)?, + }) + } + } +} + +impl Parse for StructLiteral { + fn parse(input: ParseStream) -> SynResult { + let parent = input.parse()?; + let content; + let brace = braced!(content in input); + let mut children = vec![]; + let mut fields = Punctuated::default(); + loop { + if content.is_empty() { + break; + } + + if content.peek(At) || content.peek(kw::rdl) && content.peek2(Bang) { + children.push(content.parse()?); + } else if content.peek(Ident) { + let f: DeclareField = content.parse()?; + if !children.is_empty() { + let err_msg = "Field should always declare before children."; + return Err(syn::Error::new(f.span(), err_msg)); + } + fields.push(f); + if !content.is_empty() { + fields.push_punct(content.parse()?); + } + } else { + return Err(syn::Error::new( + content.span(), + "expected a field or a child.", + )); + } + } + + check_duplicate_field(&fields)?; + Ok(StructLiteral { parent, brace, fields, children }) + } +} + +impl Parse for RdlParent { + fn parse(input: ParseStream) -> SynResult { + if input.peek(kw::_dollar_ಠ_ಠ) && input.peek2(Bang) { + let mac: Macro = input.parse()?; + + Ok(RdlParent::Var(mac.parse_body_with( + |input: &ParseBuffer| { + input.parse::()?; + input.parse() + }, + )?)) + } else { + Ok(RdlParent::Type(input.parse()?)) + } + } +} + +impl Parse for DeclareField { + fn parse(input: ParseStream) -> SynResult { + let member: Ident = input.parse()?; + let colon_tk: Option<_> = input.parse()?; + let value = if colon_tk.is_none() { + parse_quote!(#member) + } else { + input.parse()? + }; + + Ok(DeclareField { member, colon_tk, value }) + } +} + +impl ToTokens for DeclareField { + fn to_tokens(&self, tokens: &mut TokenStream) { + let DeclareField { member, value, .. } = self; + quote_spanned! {value.span()=> .#member(#value)}.to_tokens(tokens); + } +} + +/// Check if a field is declared more than once. +fn check_duplicate_field(fields: &Punctuated) -> syn::Result<()> { + let mut sets = HashSet::<&Ident, ahash::RandomState>::default(); + for f in fields { + if !sets.insert(&f.member) { + return Err(syn::Error::new( + f.member.span(), + format!("`{}` declare more than once", f.member).as_str(), + )); + } + } + Ok(()) +} diff --git a/macros/src/symbol_process.rs b/macros/src/symbol_process.rs new file mode 100644 index 000000000..0dc82e890 --- /dev/null +++ b/macros/src/symbol_process.rs @@ -0,0 +1,649 @@ +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, Span, TokenStream}; +use quote::{quote, quote_spanned, ToTokens}; +use smallvec::{smallvec, SmallVec}; +use syn::{ + fold::Fold, + parse::{Parse, ParseStream}, + 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 { + syn::custom_keyword!(_dollar_ಠ_ಠ); + 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: Option, + pub write: bool, +} +#[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]>, + used_ctx: bool, +} + +pub struct StackGuard<'a>(&'a mut DollarRefsCtx); + +mod tokens_pre_process { + + use proc_macro::{TokenTree, *}; + use quote::quote_spanned; + + use super::KW_DOLLAR_STR; + use crate::symbol_process::KW_RDL; + + fn rdl_syntax_err(span: Span) -> Result { + let err_token = quote_spanned! { span.into() => + compile_error!("Syntax Error: use `@` to declare object, must be: \n \ + 1. `@ XXX { ... }`, declare a new `XXX` type object;\n \ + 2. `@ $parent { ... }`, declare a variable as parent;\n \ + 3. `@ { ... } `, declare an object by an expression.") + }; + Err(err_token.into()) + } + + fn dollar_err(span: Span) -> Result { + let err_token = quote_spanned! { span.into() => + compile_error!("Syntax error: expected an identifier after `$`") + }; + Err(err_token.into()) + } + + /// Convert `@` and `$` symbol to a `rdl!` or `_dollar_ಠ_ಠ!` macro, make it + /// conform to Rust syntax + pub fn symbol_to_macro( + input: impl IntoIterator, + ) -> Result { + let mut iter = input.into_iter(); + let mut tokens = vec![]; + + loop { + match iter.next() { + Some(TokenTree::Punct(at)) + // maybe rust identify bind syntax, `identify @` + if at.as_char() == '@' && !matches!(tokens.last(), Some(TokenTree::Ident(_))) => + { + tokens.push(TokenTree::Ident(Ident::new(KW_RDL, at.span()))); + tokens.push(TokenTree::Punct(Punct::new('!', Spacing::Alone))); + + let body = match iter.next() { + // declare a new widget: `@ SizedBox { ... }` + Some(TokenTree::Ident(name)) => { + let Some(TokenTree::Group(body)) = iter.next() else { + return rdl_syntax_err(at.span().join(name.span()).unwrap()) + }; + let tokens = TokenStream::from_iter([TokenTree::Ident(name), TokenTree::Group(body)]); + Group::new(Delimiter::Brace, tokens) + } + // declare a variable widget as parent, `@ $var { ... }` + Some(TokenTree::Punct(dollar)) if dollar.as_char() == '$' => { + if let Some(TokenTree::Ident(var)) = iter.next() { + let Some(TokenTree::Group(body)) = iter.next() else { + let span = at.span().join(dollar.span()).unwrap().join(var.span()).unwrap(); + return rdl_syntax_err(span) + }; + let tokens = TokenStream::from_iter([ + TokenTree::Punct(dollar), + TokenTree::Ident(var), + TokenTree::Group(body), + ]); + Group::new(Delimiter::Brace, tokens) + } else { + return dollar_err(dollar.span()); + } + } + // declare a expression widget `@ { ... }` + Some(TokenTree::Group(g)) => g, + n => { + let mut span = at.span(); + if let Some(n) = n { + span = span.join(n.span()).unwrap() + } + return rdl_syntax_err(span); + } + }; + tokens.push(TokenTree::Group(body)); + } + Some(TokenTree::Punct(p)) if p.as_char() == '$' => { + match iter.next() { + Some(TokenTree::Ident(name)) => { + tokens.push(TokenTree::Ident(Ident::new(KW_DOLLAR_STR, p.span()))); + tokens.push(TokenTree::Punct(Punct::new('!', Spacing::Alone))); + let span = name.span(); + let mut g = Group::new( + Delimiter::Parenthesis, + [TokenTree::Punct(p), TokenTree::Ident(name)].into_iter().collect() + ); + g.set_span(span); + tokens.push(TokenTree::Group(g)); + } + Some(t) => return dollar_err(t.span()), + 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) { + let mut n = Group::new(g.delimiter(), symbol_to_macro(g.stream())?); + n.set_span(g.span()); + g = n; + } + + tokens.push(TokenTree::Group(g)); + } + Some(t) => tokens.push(t), + None => break, + }; + } + Ok(tokens.into_iter().collect()) + } + + fn in_macro(tokens: &[TokenTree]) -> bool { + let [.., TokenTree::Ident(_), TokenTree::Punct(p)] = tokens else { + return false; + }; + p.as_char() == '!' + } +} + +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 { + 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; + } + } + + syn::fold::fold_expr_field(self, i) + } + + fn fold_expr_method_call(&mut self, mut i: ExprMethodCall) -> ExprMethodCall { + // 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_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 { + 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() => { + 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 = dollar_scope + .used_ctx() + .then(|| quote_spanned! { c.span() => let _ctx_handle = ctx!().handle(); }); + + Expr::Verbatim(quote_spanned!(c.span() => { + #dollar_scope + #handle + #c + })) + } else { + Expr::Closure(self.fold_expr_closure(c)) + } + } + _ => syn::fold::fold_expr(self, i), + } + } +} + +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) { + 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 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()); + } + + /// 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 push_code_stack(&mut self) -> StackGuard<'_> { + self.variable_stacks.push(vec![]); + StackGuard(self) + } + + 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 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_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 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 { + expand_read(state) + }; + 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) } + } + } + } +} + +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) { + Some(mac.parse_body::().unwrap()) + } else { + None + } +} + +impl std::ops::Deref for DollarRefsScope { + type Target = [DollarRef]; + fn deref(&self) -> &Self::Target { &self.refs } +} + +struct DollarMacro { + _dollar: Dollar, + name: Ident, +} + +impl Parse for DollarMacro { + fn parse(input: ParseStream) -> syn::Result { + Ok(Self { + _dollar: input.parse()?, + name: input.parse()?, + }) + } +} + +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 new file mode 100644 index 000000000..d3b48d359 --- /dev/null +++ b/macros/src/watch_macro.rs @@ -0,0 +1,54 @@ +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: DollarRefsScope, + expr: Vec, +} + +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 upstream = refs.upstream_tokens(); + + if refs.used_ctx() { + quote! {{ + #refs + let _ctx_handle_ಠ_ಠ = ctx!().handle(); + #upstream + .map(move |_| _ctx_handle_ಠ_ಠ.with_ctx(|ctx!(): &BuildCtx<'_>| { #(#expr)* })) + }} + .to_tokens(tokens) + } else { + quote! {{ + #refs + #upstream.map(move |_| { #(#expr)* }) + }} + .to_tokens(tokens) + } + } +} diff --git a/macros/src/widget_macro/code_gen.rs b/macros/src/widget_macro/code_gen.rs index 913a1fff4..7ab0225f4 100644 --- a/macros/src/widget_macro/code_gen.rs +++ b/macros/src/widget_macro/code_gen.rs @@ -86,7 +86,7 @@ pub(crate) fn gen_assign_watch(input: TokenStream, ctx: &mut VisitCtx) -> proc_m watch_expr.used_name_info.state_refs_tokens(tokens); watch_expr.to_tokens(tokens); }); - quote_spanned!(watch_expr.span() => AssignObservable::new(#value, #tokens)).into() + quote_spanned!(watch_expr.span() => Pipe::new(#value, #tokens.box_it())).into() } pub(crate) fn gen_prop_macro( @@ -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/widget_macro/visit_mut.rs b/macros/src/widget_macro/visit_mut.rs index 10e8257ea..062031e85 100644 --- a/macros/src/widget_macro/visit_mut.rs +++ b/macros/src/widget_macro/visit_mut.rs @@ -440,7 +440,7 @@ impl VisitCtx { let field_subject = ribir_suffix_variable(&f.member, "subject"); let field_value = ribir_suffix_variable(&f.member, "init"); let pre_def = quote_spanned! { expr.span() => - let (#field_value, #field_subject) = AssignObservable::unzip(#expr); + let (#field_value, #field_subject) = Pipe::unzip(#expr); }; expr.expr = parse_quote_spanned! { expr.span() => #field_value }; let guards = guard_vec_ident(); 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/Cargo.toml b/painter/Cargo.toml index 4fc011ff4..7a54e3fef 100644 --- a/painter/Cargo.toml +++ b/painter/Cargo.toml @@ -26,7 +26,7 @@ serde = {version = "1.0", features = ["rc", "derive"]} serde_json.workspace = true tiny-skia-path = {workspace = true} usvg.workspace = true -zerocopy = {workspace = true, optional = true} +zerocopy = {workspace = true, optional = true, features = ["derive"]} [features] png = ["image/png"] 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 78e5e75fe..9a19322a3 100644 --- a/ribir/src/app.rs +++ b/ribir/src/app.rs @@ -47,16 +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); + 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 @@ -124,20 +123,23 @@ impl App { wnd.processes_native_event(event); } } + wnd.run_frame_tasks(); } 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 => { @@ -215,3 +217,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/test_cases/messages/messages_with_default_by_wgpu.png b/test_cases/messages/messages_with_default_by_wgpu.png index ed7b29047..fb90c998b 100644 Binary files a/test_cases/messages/messages_with_default_by_wgpu.png and b/test_cases/messages/messages_with_default_by_wgpu.png differ diff --git a/test_cases/messages/messages_with_material_by_wgpu.png b/test_cases/messages/messages_with_material_by_wgpu.png index 3264c0626..18fa093df 100644 Binary files a/test_cases/messages/messages_with_material_by_wgpu.png and b/test_cases/messages/messages_with_material_by_wgpu.png differ diff --git a/test_cases/storybook/storybook_with_default_by_wgpu.png b/test_cases/storybook/storybook_with_default_by_wgpu.png index 1fc9da49b..ef383eb24 100644 Binary files a/test_cases/storybook/storybook_with_default_by_wgpu.png and b/test_cases/storybook/storybook_with_default_by_wgpu.png differ diff --git a/test_cases/storybook/storybook_with_material_by_wgpu.png b/test_cases/storybook/storybook_with_material_by_wgpu.png index dcd27a0d6..236373d4f 100644 Binary files a/test_cases/storybook/storybook_with_material_by_wgpu.png and b/test_cases/storybook/storybook_with_material_by_wgpu.png differ diff --git a/test_cases/todos/todos_with_default_by_wgpu.png b/test_cases/todos/todos_with_default_by_wgpu.png index 8c611463a..0c8a017de 100644 Binary files a/test_cases/todos/todos_with_default_by_wgpu.png and b/test_cases/todos/todos_with_default_by_wgpu.png differ diff --git a/test_cases/todos/todos_with_material_by_wgpu.png b/test_cases/todos/todos_with_material_by_wgpu.png index b134f3098..fce70df59 100644 Binary files a/test_cases/todos/todos_with_material_by_wgpu.png and b/test_cases/todos/todos_with_material_by_wgpu.png differ diff --git a/tests/Cargo.toml b/tests/Cargo.toml index b3079431c..fd37439f8 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -16,9 +16,8 @@ version.workspace = true [dev-dependencies] paste.workspace = true ribir = {path = "../ribir", features = ["material", "widgets"]} -ribir_geom = {path = "../geom"} - ribir_dev_helper = {path = "../dev-helper"} +ribir_geom = {path = "../geom"} trybuild.workspace = true winit.workspace = true @@ -27,12 +26,8 @@ name = "compile_message" path = "compile_message.rs" [[test]] -name = "code_gen_logic" -path = "code_gen_test.rs" - -[[test]] -name = "animations_syntax" -path = "animations_syntax_test.rs" +name = "rdl_macro_test" +path = "rdl_macro_test.rs" [[test]] name = "child_template_derive" 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/child_template_derive_test.rs b/tests/child_template_derive_test.rs index 9ce3d232b..50b244a7f 100644 --- a/tests/child_template_derive_test.rs +++ b/tests/child_template_derive_test.rs @@ -1,6 +1,6 @@ use ribir::prelude::*; -#[derive(Declare)] +#[derive(Declare, Declare2)] struct P; struct ChildA; @@ -20,7 +20,7 @@ impl ComposeChild for P { fn compose_child(_: State, _: Self::Child) -> Widget { Void.into() } } -#[derive(Declare)] +#[derive(Declare, Declare2)] struct P2; #[derive(Template)] @@ -32,7 +32,7 @@ impl ComposeChild for P2 { fn compose_child(_: State, _: Self::Child) -> Widget { Void.into() } } -#[derive(Declare)] +#[derive(Declare, Declare2)] struct P3; #[derive(Template)] @@ -50,43 +50,43 @@ impl ComposeChild for P3 { #[test] fn syntax_pass() { - let _no_care_order = widget! { - P { - self::ChildC - self::ChildA - self::ChildB + let _no_care_order = fn_widget! { + @P { + @{self::ChildC} + @{self::ChildA} + @{self::ChildB} } }; - let _omit_option = widget! { - P { - self::ChildA + let _omit_option = fn_widget! { + @P { + @{self::ChildA} } }; } #[test] fn tuple_struct_template_syntax_pass() { - let _no_care_order = widget! { - P2 { - self::ChildC - self::ChildA - self::ChildB + let _no_care_order = fn_widget! { + @P2 { + @{ self::ChildC } + @{ self::ChildA } + @{ self::ChildB } } }; - let _omit_option = widget! { - P2 { self::ChildA } + let _omit_option = fn_widget! { + @P2 { @{self::ChildA} } }; } #[test] fn enum_template() { - let _a = widget! { - P3 { self::ChildA } + let _a = fn_widget! { + @P3 { @{ self::ChildA } } }; - let _b = widget! { - P { self::ChildB } + let _b = fn_widget! { + @P { @{ self::ChildB } } }; - let _c = widget! { - P { self::ChildC } + let _c = fn_widget! { + @P { @{ self::ChildC } } }; } diff --git a/tests/code_gen_test.rs b/tests/code_gen_test.rs deleted file mode 100644 index a3da5c6dc..000000000 --- a/tests/code_gen_test.rs +++ /dev/null @@ -1,598 +0,0 @@ -use ribir::{core::test_helper::*, prelude::*}; -use ribir_dev_helper::*; -use std::{cell::Cell, rc::Rc, time::Duration}; -use winit::event::{DeviceId, ElementState, MouseButton, WindowEvent}; - -#[test] -fn declare_smoke() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - let _ = widget! { - SizedBox { - size: Size::new(500.,500.), - background: Color::RED, - } - }; -} - -#[test] -fn simple_ref_bind_work() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - let size = Size::new(100., 100.); - let w = widget! { - Flex { - SizedBox { - size: size2.size, - on_tap: move |_| size2.size *= 2., - } - SizedBox { id: size2, size, } - } - }; - - let flex_size = Size::new(200., 100.); - let mut wnd = TestWindow::new(w); - wnd.layout(); - assert_layout_result_by_path!(wnd, { path = [0], size == flex_size, }); - - tap_at(&mut wnd, (1, 1)); - - wnd.layout(); - assert_layout_result_by_path!(wnd, { path = [0], size == flex_size * 2., }); -} - -#[test] -fn event_attr_sugar_work() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - const BEFORE_SIZE: Size = Size::new(50., 50.); - const AFTER_TAP_SIZE: Size = Size::new(100., 100.); - let w = widget! { - SizedBox { - id: sized_box, - size: BEFORE_SIZE, - SizedBox { - size: sized_box.size, - on_tap: move |_| sized_box.size = AFTER_TAP_SIZE, - } - } - }; - - let mut wnd = TestWindow::new(w); - wnd.draw_frame(); - - assert_layout_result_by_path!(wnd, { path = [0], size == BEFORE_SIZE, }); - assert_layout_result_by_path!(wnd, { path = [0, 0], size == BEFORE_SIZE, }); - - tap_at(&mut wnd, (25, 25)); - - wnd.draw_frame(); - assert_layout_result_by_path!(wnd, { path = [0], size == AFTER_TAP_SIZE, }); - assert_layout_result_by_path!(wnd, { path = [0, 0], size == AFTER_TAP_SIZE, }); -} - -#[test] -fn widget_wrap_bind_work() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - let w = widget! { - Flex { - SizedBox { - id: sibling, - margin: EdgeInsets::all(1.0), - size: Size::new(50., 50.), - } - SizedBox { - margin: sibling.margin.clone(), - size: if sibling.margin.left > 1. { Size::zero() } else { sibling.size }, - on_tap: move |_| sibling.margin = EdgeInsets::all(5.), - } - } - }; - - let mut wnd = TestWindow::new(w); - wnd.draw_frame(); - assert_layout_result_by_path!(wnd, { path = [0], width == 104., height == 52.,}); - - tap_at(&mut wnd, (60, 1)); - - wnd.draw_frame(); - assert_layout_result_by_path!(wnd, { path = [0], width == 70., height == 60.,}); -} - -#[test] -fn expression_for_children() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - let size_one = Size::new(1., 1.); - let size_five = Size::new(5., 5.); - let embed_expr = widget! { - Flex { - on_tap: move |_| sized_box.size = size_five, - SizedBox { id: sized_box, size: size_one } - // todo: how should we hint user, he/she need wrap inner widget of `DynWidget` to track named widget change. - Multi::new((0..3).map(move |_| widget!{ SizedBox { size: sized_box.size } })) - DynWidget { - dyns: (sized_box.size.area() > 2.).then(|| widget!{ SizedBox { size: sized_box.size } }) - } - } - }; - - let mut wnd = TestWindow::new(embed_expr); - wnd.draw_frame(); - assert_layout_result_by_path!(wnd, { path = [0], width == 4., height == 1.,}); - assert_layout_result_by_path!(wnd, { path = [0, 0], size == size_one,}); - assert_layout_result_by_path!(wnd, { path = [0, 1], size == size_one,}); - assert_layout_result_by_path!(wnd, { path = [0, 2], size == size_one,}); - assert_layout_result_by_path!(wnd, { path = [0, 3], size == size_one,}); - assert_layout_result_by_path!(wnd, { path = [0, 4], size == ZERO_SIZE,}); - - tap_at(&mut wnd, (0, 0)); - wnd.layout(); - 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,}); - assert_layout_result_by_path!(wnd, { path = [0, 2], size == size_five,}); - assert_layout_result_by_path!(wnd, { path = [0, 3], size == size_five,}); - assert_layout_result_by_path!(wnd, { path = [0, 4], size == size_five,}); -} - -#[test] -fn embed_widget_ref_outside() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - let w = widget! { - Flex { - SizedBox { - id: first, - size: Size::new(1., 1.), - on_tap: move |_| first.size = Size::new(2., 2.) - } - Multi::new((0..3).map(move |_| widget!{ SizedBox { size: first.size } })) - } - }; - - let mut wnd = TestWindow::new(w); - wnd.draw_frame(); - assert_layout_result_by_path!(wnd, { path = [0], width == 4., height == 1.,}); - - tap_at(&mut wnd, (0, 0)); - wnd.draw_frame(); - assert_layout_result_by_path!(wnd, { path = [0], width == 8., height == 2.,}); -} - -#[test] -fn data_flow_macro() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - let size = Size::new(1., 1.); - let w = widget! { - Flex { - on_tap: move |_| a.size *= 2., - SizedBox { id: c, size } - SizedBox { id: a, size } - SizedBox { id: b, size: a.size } - } - finally { - watch!(a.size + b.size) - .subscribe(move |v| c.size = v); - } - }; - let mut wnd = TestWindow::new(w); - wnd.draw_frame(); - let size = wnd.layout_info_by_path(&[0]).unwrap().size.unwrap(); - // data flow not affect on init. - assert_eq!(size, Size::new(3., 1.)); - - tap_at(&mut wnd, (0, 0)); - wnd.draw_frame(); - - let size = wnd.layout_info_by_path(&[0]).unwrap().size.unwrap(); - assert_eq!(size, Size::new(8., 4.)); -} - -fn local_var_not_bind() -> Widget { - const EXPECT_SIZE: Size = Size::new(5., 5.); - const BE_CLIPPED_SIZE: Size = Size::new(500., 500.); - - widget! { - SizedBox { - size: { - let _size_box = EXPECT_SIZE; - let _size_box_def = EXPECT_SIZE; - _size_box + _size_box_def - }, - SizedBox { - id: _size_box, - size: BE_CLIPPED_SIZE, - } - } - } - .into() -} -widget_layout_test!( - local_var_not_bind, - { path = [0], width == 10., height == 10. ,} - { path = [0, 0], width == 10., height == 10. ,} -); - -#[test] - -fn builtin_ref() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - let icon_track = Rc::new(Cell::new(CursorIcon::default())); - let c_icon_track = icon_track.clone(); - - let w = widget! { - Flex { - cursor: tap_box.cursor.clone(), - SizedBox { - id: tap_box, - 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()); - } - } - } - }; - - let mut wnd = TestWindow::new(w); - wnd.draw_frame(); - - tap_at(&mut wnd, (1, 1)); - wnd.draw_frame(); - assert_eq!(icon_track.get(), CursorIcon::AllScroll); -} - -#[test] -fn builtin_bind_to_self() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - let icon_track = Rc::new(Cell::new(CursorIcon::default())); - let c_icon_track = icon_track.clone(); - let w = widget! { - SizedBox { - id: sized_box, - size: Size::new(5., 5.), - cursor: { - let icon = if sized_box.size.area() < 100. { - CursorIcon::Hand - } else { - CursorIcon::Help - }; - c_icon_track.set(icon); - icon - }, - on_tap: move |_| sized_box.size = Size::new(20.,20.), - } - }; - - let mut wnd = TestWindow::new(w); - wnd.draw_frame(); - tap_at(&mut wnd, (1, 1)); - wnd.draw_frame(); - assert_eq!(icon_track.get(), CursorIcon::Help); -} - -fn tap_at(wnd: &mut TestWindow, pos: (i32, i32)) { - let device_id = unsafe { DeviceId::dummy() }; - let modifiers = ModifiersState::default(); - - #[allow(deprecated)] - wnd.processes_native_event(WindowEvent::CursorMoved { - device_id, - position: pos.into(), - modifiers, - }); - #[allow(deprecated)] - wnd.processes_native_event(WindowEvent::MouseInput { - device_id, - state: ElementState::Pressed, - button: MouseButton::Left, - modifiers, - }); - #[allow(deprecated)] - wnd.processes_native_event(WindowEvent::MouseInput { - device_id, - state: ElementState::Released, - button: MouseButton::Left, - modifiers, - }); -} - -#[test] -fn builtin_method_support() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - let layout_size = Stateful::new(Size::zero()); - let w = widget! { - states { layout_size: layout_size.clone() } - SizedBox { - id: sized_box, - size: Size::new(100., 100.), - } - finally{ - watch!(sized_box.layout_size()) - .subscribe(move |v| *layout_size = v); - } - }; - - let mut wnd = TestWindow::new(w); - wnd.draw_frame(); - - assert_eq!(&*layout_size.state_ref(), &Size::new(100., 100.)); -} - -#[test] -fn fix_builtin_field_can_declare_as_widget() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - let w = widget! { - Margin { - margin: EdgeInsets::all(1.), - Void {} - } - }; - - let wnd = TestWindow::new(w); - assert_eq!(wnd.widget_count(), 2); -} - -#[test] -fn fix_use_builtin_field_of_builtin_widget_gen_duplicate() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - let w = widget! { - Margin { - id: margin, - margin: EdgeInsets::all(1.), - Void {} - } - finally { - watch!(margin.margin.clone()).subscribe(|_| {}); - } - }; - - let wnd = TestWindow::new(w); - assert_eq!(wnd.widget_count(), 2); -} - -#[test] -fn fix_access_builtin_with_gap() { - widget! { - Void { - id: this, - cursor: CursorIcon::Hand, - on_tap: move |_| { - // this access cursor across `silent` should compile pass. - let _ = this.silent().cursor; - } - } - }; -} - -#[test] -fn fix_subscribe_cancel_after_widget_drop() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - let notify_cnt = Stateful::new(0); - let trigger = Stateful::new(true); - let w = widget! { - states { cnt: notify_cnt.clone(), trigger: trigger.clone() } - SizedBox { - size: Size::zero(), - widget::then(*trigger, || widget! { - SizedBox { size: Size::zero() } - finally { - let_watch!(*trigger).subscribe(move |_| *cnt +=1 ); - } - }) - } - }; - - let mut wnd = TestWindow::new(w); - wnd.draw_frame(); - { - *trigger.state_ref() = true - } - wnd.draw_frame(); - assert_eq!(*notify_cnt.state_ref(), 1); - { - *trigger.state_ref() = true - } - wnd.draw_frame(); - assert_eq!(*notify_cnt.state_ref(), 2); - { - *trigger.state_ref() = true - } - wnd.draw_frame(); - assert_eq!(*notify_cnt.state_ref(), 3); -} - -fn fix_local_assign_tuple() -> Widget { - widget! { - Row { - SizedBox { - id: _sized, - size: Size::new(1., 1.,), - } - SizedBox { - size: { - let (x, _) = (_sized, 2); - x.size - } - } - } - } - .into() -} -widget_layout_test!( - fix_local_assign_tuple, - rect == ribir_geom::rect(0., 0., 2., 1.), -); - -#[test] -fn fix_silent_not_relayout_dyn_widget() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - let trigger_size = Stateful::new(ZERO_SIZE); - let w = widget! { - states { trigger_size: trigger_size.clone() } - DynWidget { - dyns: if trigger_size.area() > 0. { - SizedBox { size: *trigger_size } - } else { - SizedBox { size: ZERO_SIZE } - } - } - }; - - let mut wnd = TestWindow::new(w); - wnd.draw_frame(); - assert_layout_result_by_path!(wnd, { path = [0], size == ZERO_SIZE,}); - { - *trigger_size.state_ref().silent() = Size::new(100., 100.); - } - // after silent modified, dyn widget not rebuild. - wnd.draw_frame(); - assert_layout_result_by_path!(wnd, { path = [0], size == ZERO_SIZE,}); -} - -#[test] -fn no_watch() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - let size = Stateful::new(ZERO_SIZE); - let w = widget! { - states { size: size.clone() } - SizedBox { size: no_watch!(*size) } - }; - - let mut wnd = TestWindow::new(w); - wnd.draw_frame(); - assert_layout_result_by_path!(wnd, { path = [0], size == ZERO_SIZE,}); - - { - *size.state_ref() = Size::new(100., 100.) - } - wnd.draw_frame(); - assert_layout_result_by_path!(wnd, { path = [0], size == ZERO_SIZE,}); -} - -#[test] -fn embed_shadow_states() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - let _ = widget! { - // variable `_a` here - FnWidget::new(|_: &BuildCtx| widget! { - // states shadow `a` - states { _a: Stateful::new(ZERO_SIZE) } - // `_a` should be the state `_a` - SizedBox { size: *_a } - }) - }; -} - -#[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/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/compile_msg/declare/assign_watch_pass.rs b/tests/compile_msg/declare/assign_watch_pass.rs deleted file mode 100644 index 1621e91f6..000000000 --- a/tests/compile_msg/declare/assign_watch_pass.rs +++ /dev/null @@ -1,15 +0,0 @@ -use ribir::prelude::*; - -fn main() { - let _flow_simple = widget! { - Flex { - SizedBox { - id: a, - size: Size::zero(), - } - SizedBox { - size:= assign_watch!(a.size).stream_map(|o| o.distinct_until_changed()), - } - } - }; -} diff --git a/tests/compile_msg/declare/ctx_only_allow_in_init_and_finally_fail.rs b/tests/compile_msg/declare/ctx_only_allow_in_init_and_finally_fail.rs deleted file mode 100644 index 8ff715824..000000000 --- a/tests/compile_msg/declare/ctx_only_allow_in_init_and_finally_fail.rs +++ /dev/null @@ -1,34 +0,0 @@ -use ribir::prelude::*; - -fn main() { - let _ctx_use_in_init_pass = widget! { - init ctx => { - let primary = Palette::of(ctx).primary(); - } - SizedBox { size: ZERO_SIZE, background: primary } - }; - - let _ctx_use_in_finally_pass = widget! { - SizedBox { size: ZERO_SIZE } - finally ctx => { - let _primary = Palette::of(ctx).primary(); - } - }; - - let _no_ctx_conflict_pass = widget! { - init ctx => {} - DynWidget { - dyns: widget!{ - init ctx => {} - Void {} - } - } - }; - - let _ctx_not_allow_in_other_phase = widget! { - init ctx => {} - SizedBox { - size: ZERO_SIZE, background: Palette::of(ctx).primary() - } - }; -} diff --git a/tests/compile_msg/declare/ctx_only_allow_in_init_and_finally_fail.stderr b/tests/compile_msg/declare/ctx_only_allow_in_init_and_finally_fail.stderr deleted file mode 100644 index 9285dcbf2..000000000 --- a/tests/compile_msg/declare/ctx_only_allow_in_init_and_finally_fail.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error[E0425]: cannot find value `ctx` in this scope - --> compile_msg/declare/ctx_only_allow_in_init_and_finally_fail.rs:31:48 - | -31 | size: ZERO_SIZE, background: Palette::of(ctx).primary() - | ^^^ not found in this scope diff --git a/tests/compile_msg/declare/dollar_position_fail.rs b/tests/compile_msg/declare/dollar_position_fail.rs new file mode 100644 index 000000000..de662418b --- /dev/null +++ b/tests/compile_msg/declare/dollar_position_fail.rs @@ -0,0 +1,11 @@ +use ribir::prelude::*; + +fn main() { + let not_identify_after_dollar = fn_widget! { + rdl! { Row { x: $1 } } + }; + + let field_name_not_support_dollar = fn_widget! { + rdl! { Row { $x: 1} } + }; +} diff --git a/tests/compile_msg/declare/dollar_position_fail.stderr b/tests/compile_msg/declare/dollar_position_fail.stderr new file mode 100644 index 000000000..24b0e8a28 --- /dev/null +++ b/tests/compile_msg/declare/dollar_position_fail.stderr @@ -0,0 +1,16 @@ +error: Syntax error: expected an identifier after `$` + --> compile_msg/declare/dollar_position_fail.rs:5:22 + | +5 | rdl! { Row { x: $1 } } + | ^ + +error: expected `,` + --> compile_msg/declare/dollar_position_fail.rs:8:39 + | +8 | let field_name_not_support_dollar = fn_widget! { + | _______________________________________^ +9 | | rdl! { Row { $x: 1} } +10 | | }; + | |___^ + | + = note: this error originates in the macro `fn_widget` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/compile_msg/declare/duplicate_id_fail.rs b/tests/compile_msg/declare/duplicate_id_fail.rs deleted file mode 100644 index 89402c0a4..000000000 --- a/tests/compile_msg/declare/duplicate_id_fail.rs +++ /dev/null @@ -1,66 +0,0 @@ -use ribir::prelude::*; - -fn main() { - let _id_must_be_unique_err = widget! { - BoxDecoration { - id: same_id, - background: Some(Color::RED.into()), - SizedBox { - id: same_id, - size: Size::zero(), - } - } - }; - - let _id_conflict_with_states_err = widget! { - states { same_id: Stateful::new(0) } - BoxDecoration { - id: same_id, - background: Some(Color::RED.into()), - } - }; - - let _inner_id_conflict_outside_err = widget! { - BoxDecoration { - id: same_id, - background: Some(Color::RED.into()), - DynWidget { - dyns: widget!{ - SizedBox { id: same_id, size: Size::zero() } - } - } - } - }; - - let _inner_id_conflict_outside_states_err = widget! { - states { same_id: Stateful::new(0),} - DynWidget { - dyns: widget!{ - SizedBox { id: same_id, size: Size::zero(),} - } - } - }; - - let _inner_states_id_conflict_outside_states_err = widget! { - states { same_id: Stateful::new(0),} - DynWidget { - dyns: widget!{ - states { same_id: Stateful::new(0),} - SizedBox { size: Size::zero(),} - } - } - }; - - let _inner_states_id_conflict_outside_id_err = widget! { - BoxDecoration { - id: same_id, - background: Some(Color::RED.into()), - DynWidget { - dyns: widget!{ - states { same_id: Stateful::new(0),} - SizedBox { size: Size::zero(),} - } - } - } - }; -} diff --git a/tests/compile_msg/declare/duplicate_id_fail.stderr b/tests/compile_msg/declare/duplicate_id_fail.stderr deleted file mode 100644 index b3f865d0c..000000000 --- a/tests/compile_msg/declare/duplicate_id_fail.stderr +++ /dev/null @@ -1,64 +0,0 @@ -error: Same `id: same_id` assign to multiple objects, id must be unique. - --> compile_msg/declare/duplicate_id_fail.rs:6:11 - | -6 | id: same_id, - | ^^^^^^^ -... -9 | id: same_id, - | ^^^^^^^ - -error: Same `id: same_id` assign to multiple objects, id must be unique. - --> compile_msg/declare/duplicate_id_fail.rs:16:14 - | -16 | states { same_id: Stateful::new(0) } - | ^^^^^^^ -17 | BoxDecoration { -18 | id: same_id, - | ^^^^^^^ - -error: Same `id: same_id` assign to multiple objects, id must be unique. - --> compile_msg/declare/duplicate_id_fail.rs:25:11 - | -25 | id: same_id, - | ^^^^^^^ -... -29 | SizedBox { id: same_id, size: Size::zero() } - | ^^^^^^^ - -error: Same `id: same_id` assign to multiple objects, id must be unique. - --> compile_msg/declare/duplicate_id_fail.rs:36:14 - | -36 | states { same_id: Stateful::new(0),} - | ^^^^^^^ -... -39 | SizedBox { id: same_id, size: Size::zero(),} - | ^^^^^^^ - -error: Same `id: same_id` assign to multiple objects, id must be unique. - --> compile_msg/declare/duplicate_id_fail.rs:45:14 - | -45 | states { same_id: Stateful::new(0),} - | ^^^^^^^ -... -48 | states { same_id: Stateful::new(0),} - | ^^^^^^^ - -error: Same `id: same_id` assign to multiple objects, id must be unique. - --> compile_msg/declare/duplicate_id_fail.rs:56:11 - | -56 | id: same_id, - | ^^^^^^^ -... -60 | states { same_id: Stateful::new(0),} - | ^^^^^^^ - -error[E0282]: type annotations needed - --> compile_msg/declare/duplicate_id_fail.rs:4:7 - | -4 | let _id_must_be_unique_err = widget! { - | ^^^^^^^^^^^^^^^^^^^^^^ - | -help: consider giving `_id_must_be_unique_err` an explicit type - | -4 | let _id_must_be_unique_err: /* Type */ = widget! { - | ++++++++++++ diff --git a/tests/compile_msg/declare/expr_child_syntax_pass.rs b/tests/compile_msg/declare/expr_child_syntax_pass.rs deleted file mode 100644 index f1ae8537e..000000000 --- a/tests/compile_msg/declare/expr_child_syntax_pass.rs +++ /dev/null @@ -1,36 +0,0 @@ -use ribir::prelude::*; - -fn main() { - let size = Size::zero(); - let _child_always_declare_behind_field = widget! { - SizedBox { - size, - DynWidget { - dyns: if size.area() > 0. { - SizedBox { size } - } else { - SizedBox { size } - } - } - } - }; - - let _option_child = widget! { - SizedBox { - size, - background: Color::RED, - DynWidget { - dyns: (size.area() == 0.).then(|| SizedBox { size } ) - } - } - }; - - let _expr_child_use_named_widget = widget! { - Flex { - SizedBox { id: a, size } - DynWidget { - dyns: (a.size.area() > 0.).then(|| SizedBox { size }) - } - } - }; -} \ No newline at end of file diff --git a/tests/compile_msg/declare/fix_embed_used_name_warning_pass.rs b/tests/compile_msg/declare/fix_embed_used_name_warning_pass.rs deleted file mode 100644 index 644ebfcd3..000000000 --- a/tests/compile_msg/declare/fix_embed_used_name_warning_pass.rs +++ /dev/null @@ -1,18 +0,0 @@ -use ribir::prelude::*; - -fn main() { - let _ = widget! { - SizedBox { - id: outside, size: Size::zero(), - cursor: CursorIcon::Default, - DynWidget { - dyns: widget! { - SizedBox { - size: outside.size, - cursor: outside.cursor.clone(), - } - } - } - } - }; -} diff --git a/tests/compile_msg/declare/unused_warning_fail.rs b/tests/compile_msg/declare/unused_warning_fail.rs deleted file mode 100644 index 8a4a90050..000000000 --- a/tests/compile_msg/declare/unused_warning_fail.rs +++ /dev/null @@ -1,39 +0,0 @@ -use ribir::prelude::*; - -fn main() { - compile_error!("Test for declare syntax warning."); - let _unused_id_warning = widget! { - SizedBox { - id: test_id, - size: Size::zero() - } - }; - let _used_id_no_warning = widget! { - SizedBox { - id: id1, - size: Size::new(100., 100.), - SizedBox { - size: id1.size, - } - } - }; - - let _animate_used_builtin_no_warning = widget! { - SizedBox { - id: id1, - size: Size::zero(), - background: Color::RED, - } - transition prop!(id1.background) { - easing: easing::LINEAR - } - }; - - let _fix_use_no_declared_builtin_no_warning = widget! { - SizedBox { - id: sized_box, - size: Size::zero(), - SizedBox { size: Size::zero(), background: sized_box.background } - } - }; -} diff --git a/tests/compile_msg/declare/unused_warning_fail.stderr b/tests/compile_msg/declare/unused_warning_fail.stderr deleted file mode 100644 index 9901dbb40..000000000 --- a/tests/compile_msg/declare/unused_warning_fail.stderr +++ /dev/null @@ -1,17 +0,0 @@ -error: Test for declare syntax warning. - --> compile_msg/declare/unused_warning_fail.rs:4:3 - | -4 | compile_error!("Test for declare syntax warning."); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -warning: assigned id but not be used in anywhere. - --> compile_msg/declare/unused_warning_fail.rs:7:11 - | -7 | id: test_id, - | ^^^^^^^ - | -help: Remove this line. - --> compile_msg/declare/unused_warning_fail.rs:7:11 - | -7 | id: test_id, - | ^^^^^^^ diff --git a/tests/compile_msg/declare/watch_syntax_pass.rs b/tests/compile_msg/declare/watch_syntax_pass.rs deleted file mode 100644 index e47186447..000000000 --- a/tests/compile_msg/declare/watch_syntax_pass.rs +++ /dev/null @@ -1,89 +0,0 @@ -use ribir::prelude::*; - -fn main() { - let _flow_simple = widget! { - Flex { - SizedBox { - id: a, - size: Size::zero(), - } - SizedBox { - id: b, - size: Size::zero(), - } - } - finally { - watch!(a.size) - .subscribe(move |v| b.size = v ); - } - }; - - let _flow_handler = widget! { - Flex { - SizedBox { - id: a, - size: Size::zero(), - on_tap: move |_| {} - } - SizedBox { - id: b, - size: a.size, - } - SizedBox { - id: c, - size: Size::zero(), - } - } - finally { - let_watch!(a.size + b.size) - .subscribe(move |v| c.size = v); - } - - }; - - let _flow_embed = widget! { - Flex { - SizedBox { - id: a, - size: Size::zero(), - } - SizedBox { - id: b, - size: Size::zero(), - } - DynWidget { - dyns: true.then(||{ - widget!{ - SizedBox { - id: c, - size: Size::zero(), - } - finally { - watch!(a.size + b.size) - .subscribe(move |v| c.size = v); - } - } - }) - } - } - finally { - watch!(a.size) - .subscribe(move |v| b.size = v); - } - }; - - let _fix_named_obj_moved_in_flow = widget! { - Flex { - SizedBox { id: a, size: Size::zero() } - SizedBox { id: b, size: Size::zero() } - SizedBox { id: c, size: Size::zero() } - } - finally { - watch!(a.size) - .subscribe(move |v| { - b.size = v; - c.size = v; - }); - } - }; -} diff --git a/tests/declare_builder_test.rs b/tests/declare_builder_test.rs index 76c42357f..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,31 +31,31 @@ fn panic_if_miss_require_field() { _a: f32, } - let _ = ::declare_builder().build(dummy_ctx()); + let _ = ::declare_builder().build_declare(dummy_ctx()); } #[test] -fn empty_default_field() { +fn default_field() { #[derive(Declare)] - struct T { + struct DefaultDeclare { #[declare(default)] a: f32, } - let t = ::declare_builder().build(dummy_ctx()); + let t = ::declare_builder().build_declare(dummy_ctx()); assert_eq!(t.a, 0.); } #[test] -fn string_default_field() { +fn default_field_with_value() { #[derive(Declare)] - struct T { + struct DefaultWithValue { #[declare(default = "hi!")] 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/path_child_test.rs b/tests/path_child_test.rs index e56395a11..993d560ba 100644 --- a/tests/path_child_test.rs +++ b/tests/path_child_test.rs @@ -9,10 +9,9 @@ enum AB { const SIZE_ONE: Size = Size::new(1., 1.); impl Compose for AB { fn compose(this: State) -> Widget { - widget! { - states { this: this.into_writable() } - SizedBox { - size: match *this { + fn_widget! { + @SizedBox { + size: match *$this { AB::A => ZERO_SIZE, AB::B => SIZE_ONE } @@ -30,23 +29,24 @@ impl AB { #[test] fn path_widget() { - let _ = widget! { AB::A }; - let _ = widget! { AB::B }; - let _ = widget! { AB::a() }; - let _ = widget! { AB::b() }; + let _ = fn_widget! { AB::A }; + let _ = fn_widget! { AB::B }; + let _ = fn_widget! { AB::a() }; + let _ = fn_widget! { AB::b() }; } fn tuple_widget() -> Widget { struct TupleBox(Size); impl Compose for TupleBox { fn compose(this: State) -> Widget { - widget! { - states { this: this.into_readonly() } - SizedBox { size: this.0 } + fn_widget! { + @SizedBox { + size: pipe!($this.0), + } } .into() } } - widget! { TupleBox(Size::new(1., 1.)) }.into() + fn_widget! { TupleBox(Size::new(1., 1.)) }.into() } widget_layout_test!(tuple_widget, width == 1., height == 1.,); diff --git a/tests/rdl_macro_test.rs b/tests/rdl_macro_test.rs new file mode 100644 index 000000000..711ad0eaa --- /dev/null +++ b/tests/rdl_macro_test.rs @@ -0,0 +1,857 @@ +use ribir::{ + core::{reset_test_env, test_helper::*}, + prelude::*, +}; +use ribir_dev_helper::*; +use std::{cell::Cell, rc::Rc}; +use winit::event::{DeviceId, ElementState, MouseButton, WindowEvent}; + +fn simplest_leaf_rdl() -> impl Into { + fn_widget! { + rdl! { SizedBox { size: Size::new(500.,500.) } } + } +} +widget_layout_test!(simplest_leaf_rdl, width == 500., height == 500.,); + +fn with_child_rdl() -> impl Into { + fn_widget! { + rdl!{ + Row { + rdl!{ SizedBox { size: Size::new(500.,500.) } } + } + } + } +} +widget_layout_test!(with_child_rdl, width == 500., height == 500.,); + +fn with_builtin_child_rdl() -> impl Into { + fn_widget! { + rdl! { SizedBox { + size: Size::new(500.,500.), + margin: EdgeInsets::all(10.) + }} + } +} +widget_layout_test!(with_builtin_child_rdl, width == 520., height == 520.,); + +fn rdl_with_child() -> impl Into { + fn_widget! { + let single_p = rdl!{ SizedBox { size: Size::new(500.,500.) }}; + rdl! { $single_p { rdl! { Void } } } + } +} +widget_layout_test!(rdl_with_child, width == 500., height == 500.,); + +fn single_rdl_has_builtin_with_child() -> impl Into { + fn_widget! { + let single_p = rdl!{ SizedBox { + size: Size::new(500.,500.), + margin: EdgeInsets::all(10.) + }}; + rdl! { $single_p { rdl! { Void } } } + } +} +widget_layout_test!( + single_rdl_has_builtin_with_child, + width == 520., + height == 520., +); + +fn multi_child_rdl_has_builtin_with_child() -> impl Into { + fn_widget! { + let multi_p = rdl! { Flex { + margin: EdgeInsets::all(10.) + } }; + rdl! { $multi_p { rdl!{ Void } } } + } +} +widget_layout_test!( + multi_child_rdl_has_builtin_with_child, + width == 20., + height == 20., +); + +fn compose_child_rdl_has_builtin_with_child() -> impl Into { + fn_widget! { + let multi_p = rdl!{ Row { margin: EdgeInsets::all(10.) }}; + rdl! { $multi_p { rdl!{ Void {} }} } + } +} +widget_layout_test!( + compose_child_rdl_has_builtin_with_child, + width == 20., + height == 20., +); + +fn access_rdl_widget() -> impl Into { + fn_widget! { + let mut b = rdl! { SizedBox {size: Size::new(500.,500.)}}; + rdl! { Row { + rdl! { SizedBox { size: $b.size } } + rdl! { b } + }} + } +} +widget_layout_test!(access_rdl_widget, width == 1000., height == 500.,); + +fn access_builtin_rdl_widget() -> impl Into { + fn_widget! { + let mut b = rdl! { SizedBox { + size: Size::new(100.,100.), + margin: EdgeInsets::all(10.) + }}; + + rdl!{ + Row { + rdl! { + SizedBox { + size: $b.size, + margin: $b.margin, + } + } + rdl! { b } + } + } + } +} +widget_layout_test!(access_builtin_rdl_widget, width == 240., height == 120.,); + +fn dollar_as_rdl_parent() -> impl Into { + fn_widget! { + let b = rdl! {SizedBox { size: Size::new(500.,500.) }}; + rdl! { $b { rdl! { Void {}} } } + } +} +widget_layout_test!(dollar_as_rdl_parent, width == 500., height == 500.,); + +fn dollar_as_middle_parent() -> impl Into { + fn_widget! { + let b = rdl! { SizedBox { size: Size::new(500.,500.) }}; + rdl! { Row { rdl! { $b { rdl! { Void {} } } } } } + } +} +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_reader(); + let w = fn_widget! { + rdl! { SizedBox { size: pipe!(*$size2) }} + }; + *size.write() = Size::new(100., 100.); + w +} +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_reader(); + + let w = fn_widget! { + rdl! { SizedBox { + size: Size::zero(), + margin: pipe!(*$margin2) + }} + }; + *margin.write() = EdgeInsets::all(50.); + w +} +widget_layout_test!(pipe_as_builtin_field_value, width == 100., height == 100.,); + +fn pipe_with_ctx() -> impl Into { + let scale = Stateful::new(1.); + let scale2 = scale.clone_writer(); + let w = fn_widget! { + rdl! { SizedBox { + size: pipe!(IconSize::of(ctx!()).tiny * *$scale) + }} + }; + *scale2.write() = 2.; + w +} +widget_layout_test!(pipe_with_ctx, width == 36., height == 36.,); + +fn pipe_with_builtin_field() -> impl Into { + fn_widget! { + let mut box1 = @SizedBox { size: Size::zero(), margin: EdgeInsets::all(1.) }; + let mut box2 = @SizedBox { size: $box1.size, margin: pipe!($box1.margin) }; + @Row { + @{ box1 } + @{ box2 } + } + } +} +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.write().size = IconSize::of(ctx!()).tiny + } + } +} +widget_layout_test!(capture_closure_used_ctx, width == 18., height == 18.,); + +#[test] +fn pipe_single_parent() { + reset_test_env!(); + + let outside_blank = Stateful::new(true); + let outside_blank2 = outside_blank.clone_writer(); + let w = fn_widget! { + let edges = EdgeInsets::all(5.); + let blank = pipe! { + if *$outside_blank { + Box::new(Margin { margin: edges }) as Box + } else { + Box::new(Padding { padding: edges }) as Box + } + }; + rdl! { + $blank { + rdl!{ SizedBox { size: Size::new(100., 100.) } } + } + } + }; + + let mut wnd = TestWindow::new(w); + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], width == 110., height == 110., }); + + *outside_blank2.write() = false; + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], width == 100., height == 100., }); +} + +#[test] +fn pipe_multi_parent() { + reset_test_env!(); + + let stack_or_flex = Stateful::new(true); + let stack_or_flex2 = stack_or_flex.clone_writer(); + let w = fn_widget! { + let container = pipe! { + let c: Box = if *$stack_or_flex { + Box::new(rdl! { Stack { } }) + } else { + Box::new(rdl! { Flex { } }) + }; + c + }; + + rdl! { + $container { + rdl!{ SizedBox { size: Size::new(100., 100.) } } + rdl!{ SizedBox { size: Size::new(100., 100.) } } + } + } + }; + + let mut wnd = TestWindow::new(w); + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], width == 100., height == 100., }); + + *stack_or_flex2.write() = false; + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], width == 200., height == 100., }); +} + +#[test] +fn pipe_as_child() { + reset_test_env!(); + + let box_or_not = Stateful::new(true); + let box_or_not2 = box_or_not.clone_reader(); + let w = fn_widget! { + let blank: Pipe = pipe!{ + if *$box_or_not2 { + rdl! { SizedBox { size: Size::new(100., 100.) } }.into() + } else { + Void.into() + } + }; + rdl! { Stack { rdl! { blank } } } + }; + + let mut wnd = TestWindow::new(w); + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], width == 100., height == 100., }); + + *box_or_not.state_ref() = false; + + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], width == 0., height == 0., }); +} + +#[test] +fn pipe_as_multi_child() { + reset_test_env!(); + + let fix_box = SizedBox { size: Size::new(100., 100.) }; + let cnt = Stateful::new(0); + let cnt2 = cnt.clone_writer(); + let w = fn_widget! { + let boxes = pipe! { + Multi::new((0..*$cnt).map(|_| fix_box.clone()).collect::>()) + }; + rdl! { Flex { rdl!{ boxes } } } + }; + + let mut wnd = TestWindow::new(w); + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], width == 0., height == 0., }); + + *cnt2.write() = 3; + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], width == 300., height == 100., }); +} + +fn at_in_widget_macro() -> impl Into { + fn_widget! { + @SizedBox { size: Size::new(100., 100.) } + } +} +widget_layout_test!(at_in_widget_macro, width == 100., height == 100.,); + +fn at_as_variable_in_widget() -> impl Into { + fn_widget! { + let size = Size::new(100., 100.); + let row = @Row {}; + @ $row { + // @ in @ + @SizedBox { size } + // `rdl!` in @ + rdl! { SizedBox { size } } + } + } +} +widget_layout_test!(at_as_variable_in_widget, width == 200., height == 100.,); + +fn at_as_variable_in_rdl() -> impl Into { + fn_widget! { + let size = Size::new(100., 100.); + let row = @Row {}; + rdl! { + $row { + @SizedBox { size } + @SizedBox { size } + } + } + } +} +widget_layout_test!(at_as_variable_in_rdl, width == 200., height == 100.,); + +fn access_builtin_field_by_dollar() -> impl Into { + fn_widget! { + let size = Size::new(100., 100.); + let mut box1 = @SizedBox { size, margin: EdgeInsets::all(10.) }; + let box2 = @SizedBox { size, margin: $box1.margin }; + @Row { @ { box1 } @{ box2 } } + } +} +widget_layout_test!( + access_builtin_field_by_dollar, + width == 240., + height == 120., +); + +#[test] +fn closure_in_fn_widget_capture() { + reset_test_env!(); + + let hi_res = Stateful::new(CowArc::borrowed("")); + let hi_res2 = hi_res.clone_reader(); + let w = fn_widget! { + let mut text = @ Text { text: "hi" }; + let on_mounted = move |_: &mut _| *$hi_res.write() =$text.text.clone(); + @ $text { on_mounted } + }; + + let mut wnd = TestWindow::new(w); + wnd.draw_frame(); + + assert_eq!(&**hi_res2.read(), "hi"); +} + +fn at_embed_in_expression() -> impl Into { + fn_widget! { + @Row { + @{ Multi::new((0..3).map(|_| { + @SizedBox { size: Size::new(100., 100.) } + }))} + } + } +} +widget_layout_test!(at_embed_in_expression, width == 300., height == 100.,); + +#[test] +fn declare_smoke() { + reset_test_env!(); + + let _ = widget! { + SizedBox { + size: Size::new(500.,500.), + background: Color::RED, + } + }; +} + +#[test] +fn simple_ref_bind_work() { + reset_test_env!(); + + let size = Size::new(100., 100.); + let w = widget! { + Flex { + SizedBox { + size: size2.size, + on_tap: move |_| size2.size *= 2., + } + SizedBox { id: size2, size, } + } + }; + + let flex_size = Size::new(200., 100.); + let mut wnd = TestWindow::new(w); + wnd.layout(); + assert_layout_result_by_path!(wnd, { path = [0], size == flex_size, }); + + tap_at(&mut wnd, (1, 1)); + + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], size == flex_size * 2., }); +} + +#[test] +fn event_attr_sugar_work() { + reset_test_env!(); + const BEFORE_SIZE: Size = Size::new(50., 50.); + const AFTER_TAP_SIZE: Size = Size::new(100., 100.); + let w = widget! { + SizedBox { + id: sized_box, + size: BEFORE_SIZE, + SizedBox { + size: sized_box.size, + on_tap: move |_| sized_box.size = AFTER_TAP_SIZE, + } + } + }; + + let mut wnd = TestWindow::new(w); + wnd.draw_frame(); + + assert_layout_result_by_path!(wnd, { path = [0], size == BEFORE_SIZE, }); + assert_layout_result_by_path!(wnd, { path = [0, 0], size == BEFORE_SIZE, }); + + tap_at(&mut wnd, (25, 25)); + + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], size == AFTER_TAP_SIZE, }); + assert_layout_result_by_path!(wnd, { path = [0, 0], size == AFTER_TAP_SIZE, }); +} + +#[test] +fn widget_wrap_bind_work() { + reset_test_env!(); + + let w = widget! { + Flex { + SizedBox { + id: sibling, + margin: EdgeInsets::all(1.0), + size: Size::new(50., 50.), + } + SizedBox { + margin: sibling.margin.clone(), + size: if sibling.margin.left > 1. { Size::zero() } else { sibling.size }, + on_tap: move |_| sibling.margin = EdgeInsets::all(5.), + } + } + }; + + let mut wnd = TestWindow::new(w); + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], width == 104., height == 52.,}); + + tap_at(&mut wnd, (60, 1)); + + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], width == 70., height == 60.,}); +} + +#[test] +fn expression_for_children() { + reset_test_env!(); + + let size_one = Size::new(1., 1.); + let size_five = Size::new(5., 5.); + let embed_expr = fn_widget! { + let sized_box = @SizedBox { size: size_one }; + let multi_box = Multi::new((0..3).map(move |_| @SizedBox { size: pipe!($sized_box.size) } )) + ; + let pipe_box = pipe!($sized_box.size.area() > 2.) + .map(move |v| v.then(|| @SizedBox { size: pipe!($sized_box.size) })); + + @Flex { + on_tap: move |_| $sized_box.write().size = size_five, + @ { sized_box } + @ { multi_box } + @ { pipe_box } + } + }; + + let mut wnd = TestWindow::new(embed_expr); + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], width == 4., height == 1.,}); + assert_layout_result_by_path!(wnd, { path = [0, 0], size == size_one,}); + assert_layout_result_by_path!(wnd, { path = [0, 1], size == size_one,}); + assert_layout_result_by_path!(wnd, { path = [0, 2], size == size_one,}); + assert_layout_result_by_path!(wnd, { path = [0, 3], size == size_one,}); + assert_layout_result_by_path!(wnd, { path = [0, 4], size == ZERO_SIZE,}); + + tap_at(&mut wnd, (0, 0)); + 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,}); + assert_layout_result_by_path!(wnd, { path = [0, 2], size == size_five,}); + assert_layout_result_by_path!(wnd, { path = [0, 3], size == size_five,}); + assert_layout_result_by_path!(wnd, { path = [0, 4], size == size_five,}); +} + +#[test] +fn embed_widget_ref_outside() { + reset_test_env!(); + + let w = fn_widget! { + let first = @SizedBox { size: Size::new(1., 1.) }; + let three_box = @{ Multi::new((0..3).map(move |_| @ SizedBox { size: pipe!($first.size) } ))}; + @Flex { + @$first { on_tap: move |_| $first.write().size = Size::new(2., 2.)} + @{ three_box } + } + }; + + let mut wnd = TestWindow::new(w); + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], width == 4., height == 1.,}); + + tap_at(&mut wnd, (0, 0)); + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], width == 8., height == 2.,}); +} + +#[test] +fn bind_fields() { + reset_test_env!(); + + let size = Size::new(1., 1.); + let w = fn_widget! { + let a = @SizedBox { size }; + let b = @SizedBox { size: pipe!($a.size) }; + let c = @SizedBox { size }; + watch!($a.size + $b.size) + .subscribe(move |v| $c.write().size = v); + @Flex { + on_tap: move |_| $a.write().size *= 2., + @ { Multi::new([a, b, c]) } + } + }; + let mut wnd = TestWindow::new(w); + wnd.draw_frame(); + let size = wnd.layout_info_by_path(&[0]).unwrap().size.unwrap(); + // data flow not affect on init. + assert_eq!(size, Size::new(3., 1.)); + + tap_at(&mut wnd, (0, 0)); + wnd.draw_frame(); + + let size = wnd.layout_info_by_path(&[0]).unwrap().size.unwrap(); + assert_eq!(size, Size::new(8., 4.)); +} + +fn local_var_not_bind() -> Widget { + const EXPECT_SIZE: Size = Size::new(5., 5.); + const BE_CLIPPED_SIZE: Size = Size::new(500., 500.); + + widget! { + SizedBox { + size: { + let _size_box = EXPECT_SIZE; + let _size_box_def = EXPECT_SIZE; + _size_box + _size_box_def + }, + SizedBox { + id: _size_box, + size: BE_CLIPPED_SIZE, + } + } + } + .into() +} +widget_layout_test!( + local_var_not_bind, + { path = [0], width == 10., height == 10. ,} + { path = [0, 0], width == 10., height == 10. ,} +); + +#[test] + +fn builtin_ref() { + reset_test_env!(); + + let icon_track = Rc::new(Cell::new(CursorIcon::default())); + let c_icon_track = icon_track.clone(); + + let w = widget! { + Flex { + cursor: tap_box.cursor.clone(), + SizedBox { + id: tap_box, + size: Size::new(5., 5.), + cursor: CursorIcon::Hand, + on_tap: move |_| { + tap_box.cursor = CursorIcon::AllScroll; + c_icon_track.set(tap_box.cursor); + } + } + } + }; + + let mut wnd = TestWindow::new(w); + wnd.draw_frame(); + + tap_at(&mut wnd, (1, 1)); + wnd.draw_frame(); + assert_eq!(icon_track.get(), CursorIcon::AllScroll); +} + +#[test] +fn builtin_bind_to_self() { + reset_test_env!(); + + let icon_track = Rc::new(Cell::new(CursorIcon::default())); + let c_icon_track = icon_track.clone(); + let w = widget! { + SizedBox { + id: sized_box, + size: Size::new(5., 5.), + cursor: { + let icon = if sized_box.size.area() < 100. { + CursorIcon::Hand + } else { + CursorIcon::Help + }; + c_icon_track.set(icon); + icon + }, + on_tap: move |_| sized_box.size = Size::new(20.,20.), + } + }; + + let mut wnd = TestWindow::new(w); + wnd.draw_frame(); + tap_at(&mut wnd, (1, 1)); + wnd.draw_frame(); + assert_eq!(icon_track.get(), CursorIcon::Help); +} + +fn tap_at(wnd: &mut TestWindow, pos: (i32, i32)) { + let device_id = unsafe { DeviceId::dummy() }; + let modifiers = ModifiersState::default(); + + #[allow(deprecated)] + wnd.processes_native_event(WindowEvent::CursorMoved { + device_id, + position: pos.into(), + modifiers, + }); + #[allow(deprecated)] + wnd.processes_native_event(WindowEvent::MouseInput { + device_id, + state: ElementState::Pressed, + button: MouseButton::Left, + modifiers, + }); + #[allow(deprecated)] + wnd.processes_native_event(WindowEvent::MouseInput { + device_id, + state: ElementState::Released, + button: MouseButton::Left, + modifiers, + }); +} + +#[test] +fn builtin_method_support() { + reset_test_env!(); + + let layout_size = Stateful::new(Size::zero()); + let c_layout_size = layout_size.clone_reader(); + let w = fn_widget! { + let mut sized_box = @SizedBox { size: Size::new(100., 100.) }; + watch!($sized_box.layout_size()) + .subscribe(move |v| *$layout_size.write() = v); + sized_box + }; + + let mut wnd = TestWindow::new(w); + wnd.draw_frame(); + + assert_eq!(*c_layout_size.read(), Size::new(100., 100.)); +} + +#[test] +fn fix_builtin_field_can_declare_as_widget() { + reset_test_env!(); + + let w = fn_widget! { + @Margin { + margin: EdgeInsets::all(1.), + @Void {} + } + }; + + let wnd = TestWindow::new(w); + assert_eq!(wnd.widget_count(), 2); +} + +#[test] +fn fix_use_builtin_field_of_builtin_widget_gen_duplicate() { + reset_test_env!(); + + let w = fn_widget! { + let mut margin = @Margin { margin: EdgeInsets::all(1.) }; + watch!($margin.margin).subscribe(|_| {}); + @$margin { @Void {} } + }; + + let wnd = TestWindow::new(w); + assert_eq!(wnd.widget_count(), 2); +} + +#[test] +fn fix_access_builtin_with_gap() { + fn_widget! { + let mut this = @Void { cursor: CursorIcon::Hand }; + @$this { + on_tap: move |_| { + // this access cursor across `silent` should compile pass. + let _ = $this.silent().cursor; + } + } + }; +} + +#[test] +fn fix_subscribe_cancel_after_widget_drop() { + reset_test_env!(); + + let notify_cnt = Stateful::new(0); + let cnt: Writer = notify_cnt.clone_writer(); + let trigger = Stateful::new(true); + let c_trigger = trigger.clone_reader(); + let w = fn_widget! { + let mut container = @SizedBox { size: Size::zero() }; + let h = watch!(*$c_trigger).subscribe(move |_| *$cnt.write() +=1 ); + container.as_stateful().unsubscribe_on_drop(h); + + @$container { + @ { + pipe!{$c_trigger.then(|| { + @SizedBox { size: Size::zero() } + })} + } + } + }; + + let mut wnd = TestWindow::new(w); + wnd.draw_frame(); + { + *trigger.write() = true + } + wnd.draw_frame(); + assert_eq!(*notify_cnt.read(), 1); + { + *trigger.write() = true + } + wnd.draw_frame(); + assert_eq!(*notify_cnt.read(), 2); + { + *trigger.write() = true + } + wnd.draw_frame(); + assert_eq!(*notify_cnt.read(), 3); +} + +fn fix_local_assign_tuple() -> Widget { + widget! { + Row { + SizedBox { + id: _sized, + size: Size::new(1., 1.,), + } + SizedBox { + size: { + let (x, _) = (_sized, 2); + x.size + } + } + } + } + .into() +} +widget_layout_test!( + fix_local_assign_tuple, + rect == ribir_geom::rect(0., 0., 2., 1.), +); + +#[test] +fn fix_silent_not_relayout_dyn_widget() { + reset_test_env!(); + + let trigger_size = Stateful::new(ZERO_SIZE); + let c_trigger_size = trigger_size.clone_writer(); + let w = fn_widget! { + pipe! { + if $trigger_size.area() > 0. { + SizedBox { size: *$trigger_size } + } else { + SizedBox { size: ZERO_SIZE } + } + } + }; + + let mut wnd = TestWindow::new(w); + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], size == ZERO_SIZE,}); + { + *c_trigger_size.silent() = Size::new(100., 100.); + } + // after silent modified, dyn widget not rebuild. + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], size == ZERO_SIZE,}); +} + +#[test] +fn no_watch() { + reset_test_env!(); + + let size = Stateful::new(ZERO_SIZE); + let w = widget! { + states { size: size.clone_stateful() } + SizedBox { size: no_watch!(*size) } + }; + + let mut wnd = TestWindow::new(w); + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], size == ZERO_SIZE,}); + + { + *size.write() = Size::new(100., 100.) + } + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], size == ZERO_SIZE,}); +} 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..36d881fcb 100644 --- a/themes/material/src/ripple.rs +++ b/themes/material/src/ripple.rs @@ -4,7 +4,7 @@ use ribir_widgets::prelude::*; /// Widget use to do ripple animate as a visual feedback to user interactive. /// Usually for touch and mouse. -#[derive(Declare, Debug)] +#[derive(Declare, Debug, Declare2)] pub struct Ripple { /// The color of ripples. pub color: Color, @@ -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, @@ -39,86 +39,80 @@ 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_widget! { + let mut container = @Stack { fit: StackFit::Passthrough }; + let mut ripple_at = $this.ripple_at.clone_writer(); + + 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( + map_writer!($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.write().take(); + }); + + + let mut ripper_fade_out = map_writer!($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.write().opacity = 0., + on_mounted: move |_| { ripper_enter.run(); }, + @Container { + size: $container.layout_size(), + @$clipper { @ { ripple } } } - } - }) + }) + }); + + @ $container { + on_pointer_down: move |e| *$ripple_at.write() = 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.write() = Some(pos); } } diff --git a/themes/material/src/state_layer.rs b/themes/material/src/state_layer.rs index a5873fdc4..e3059835f 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, @@ -13,7 +13,7 @@ pub struct StateLayer { /// Widget that as visual indicator of material design used to communicate the /// status of interactive widget, its visual state will reactive to its child /// interactive state. -#[derive(Declare)] +#[derive(Declare, Declare2)] pub struct InteractiveLayer { /// the color of the state layer, will apply a fixed opacity in different /// state. @@ -24,13 +24,12 @@ 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_widget! { + @PathPaintKit { + path: pipe!($this.path.clone()), + brush: pipe!($this.role.calc_color($this.color)), } - ) + } .into() } } @@ -39,30 +38,32 @@ 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_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/avatar.rs b/widgets/src/avatar.rs index 0e0c053b5..fa82d1afb 100644 --- a/widgets/src/avatar.rs +++ b/widgets/src/avatar.rs @@ -9,20 +9,20 @@ use ribir_core::prelude::*; /// # use ribir_core::prelude::*; /// # use ribir_widgets::prelude::*; /// -/// widget! { -/// Avatar { -/// Label::new("A") +/// fn_widget! { +/// @ Avatar { +/// @ { Label::new("A") } /// } /// }; /// /// # #[cfg(feature="png")] -/// widget! { -/// Avatar { -/// ShallowImage::from_png(include_bytes!("../../gpu/examples/leaves.png")) +/// fn_widget! { +/// @ Avatar { +/// @ { ShallowImage::from_png(include_bytes!("../../gpu/examples/leaves.png")) } /// } /// }; /// ``` -#[derive(Declare, Default, Clone)] +#[derive(Declare, Declare2, Default, Clone)] pub struct Avatar { #[declare(default=Palette::of(ctx).primary())] pub color: Color, @@ -63,54 +63,51 @@ impl ComposeChild for Avatar { type Child = AvatarTemplate; fn compose_child(this: State, child: Self::Child) -> Widget { - widget! { - states { this: this.into_readonly() } - init ctx => { + fn_widget! { + @ { let AvatarStyle { size, radius, text_style, - } = AvatarStyle::of(ctx).clone(); - let palette1 = Palette::of(ctx).clone(); - let palette2 = Palette::of(ctx).clone(); - } - SizedBox { - size, - widget::from::(match child { - AvatarTemplate::Text(text) => widget! { - states { text: text.into_readonly() } - BoxDecoration { - background: Brush::from(palette1.base_of(&this.color)), + } = AvatarStyle::of(ctx!()); + let palette1 = Palette::of(ctx!()).clone(); + let palette2 = Palette::of(ctx!()).clone(); + let w: Widget = match child { + AvatarTemplate::Text(mut text) => { + @Container { + size, border_radius: radius.map(Radius::all), - Container { - size, - Text { - h_align: HAlign::Center, - v_align: VAlign::Center, - text: text.0.clone(), - text_style: text_style.clone(), - foreground: Brush::from(palette2.on_of(&palette2.base_of(&this.color))), - } + background: pipe!(Brush::from(palette1.base_of(&$this.color))), + @Text { + h_align: HAlign::Center, + v_align: VAlign::Center, + text: $text.0.clone(), + text_style, + foreground: pipe!(Brush::from(palette2.on_of(&palette2.base_of(&$this.color)))), } - } - }.into(), - AvatarTemplate::Image(image) => widget! { - DynWidget { - dyns: radius.map(|radius| { - let path = Path::rect_round( - &Rect::from_size(size), - &Radius::all(radius), - ); - Clip { clip: ClipType::Path(path) } - }), - Container { + }.into() + }, + AvatarTemplate::Image(image) => { + let clip = radius.map(|radius| { + let path = Path::rect_round( + &Rect::from_size(size), + &Radius::all(radius), + ); + Clip { clip: ClipType::Path(path) } + }); + @$clip { + @Container { size, - DynWidget{ + @$image { box_fit: BoxFit::Contain, - dyns: image, } } - } - }.into() - }) + }.into() + } + }; + + @SizedBox { + size, + @ { w } + } } } .into() diff --git a/widgets/src/buttons.rs b/widgets/src/buttons.rs index 8ca457a82..07c1f7ff1 100644 --- a/widgets/src/buttons.rs +++ b/widgets/src/buttons.rs @@ -1,7 +1,7 @@ use crate::prelude::*; use ribir_core::prelude::*; -#[derive(Declare)] +#[derive(Declare, Declare2)] pub struct ButtonImpl { #[declare(default = 48.)] pub min_width: f32, @@ -52,36 +52,34 @@ impl ComposeChild for ButtonImpl { fn compose_child(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/buttons/button.rs b/widgets/src/buttons/button.rs index e756fe0ea..a3538fee7 100644 --- a/widgets/src/buttons/button.rs +++ b/widgets/src/buttons/button.rs @@ -24,7 +24,7 @@ impl CustomStyle for ButtonStyle { } } -#[derive(Clone, Declare)] +#[derive(Clone, Declare, Declare2)] pub struct ButtonDecorator { #[allow(unused)] pub button_type: ButtonType, @@ -45,24 +45,24 @@ impl ComposeDecorator for ButtonDecorator { /// # use ribir_widgets::prelude::{Button, Label}; /// /// // only icon -/// let raw_icon_button = widget! { -/// Button { svgs::ADD } +/// let raw_icon_button = fn_widget! { +/// @Button { @{ svgs::ADD } } /// }; /// /// // only label -/// let raw_label_button = widget! { -/// Button { Label::new("raw button") } +/// let raw_label_button = fn_widget! { +/// @Button { @ { Label::new("raw button") } } /// }; /// /// // use custom color -/// let custom_color_button = widget! { -/// Button { +/// let custom_color_button = fn_widget! { +/// @Button { /// color: Color::RED, -/// Label::new("raw button") +/// @{ Label::new("raw button") } /// } /// }; /// ``` -#[derive(Declare, Default)] +#[derive(Declare, Default, Declare2)] pub struct Button { #[declare(default=Palette::of(ctx).primary())] color: Color, @@ -80,9 +80,8 @@ impl ComposeChild for Button { (None, None) => panic!("Button content cannot be empty!"), }; - widget! { - states { this: this.into_readonly() } - init ctx => { + fn_widget! { + @ { let ButtonStyle { height, icon_size, @@ -90,25 +89,26 @@ impl ComposeChild for Button { icon_pos, label_style, padding_style, - } = ButtonStyle::of(ctx).clone(); + } = ButtonStyle::of(ctx); let palette = Palette::of(ctx).clone(); - } - ButtonDecorator { - button_type, - color: this.color, - ButtonImpl { - height, - icon_size, - label_gap, - icon_pos, - label_style, - background_color: None, - foreground_color: Brush::from(palette.base_of(&this.color)), - radius: None, - border_style: None, - padding_style, - widget::from(child) + @ButtonDecorator { + button_type, + color: pipe!($this.color), + @ButtonImpl { + height, + icon_size, + label_gap, + icon_pos, + label_style, + background_color: None, + foreground_color: pipe!(Brush::from(palette.base_of(&$this.color))), + radius: None, + border_style: None, + padding_style, + + @ { child } + } } } } diff --git a/widgets/src/buttons/fab_button.rs b/widgets/src/buttons/fab_button.rs index d413fa6fb..35e43133e 100644 --- a/widgets/src/buttons/fab_button.rs +++ b/widgets/src/buttons/fab_button.rs @@ -26,7 +26,7 @@ impl CustomStyle for FabButtonStyle { } } -#[derive(Clone, Declare)] +#[derive(Clone, Declare, Declare2)] pub struct FabButtonDecorator { #[allow(unused)] pub button_type: ButtonType, @@ -47,33 +47,33 @@ impl ComposeDecorator for FabButtonDecorator { /// # use ribir_widgets::prelude::{FabButton, Label}; /// /// // only icon -/// let fab_icon_button = widget! { -/// FabButton { svgs::ADD } +/// let fab_icon_button = fn_widget! { +/// @FabButton { @{ svgs::ADD } } /// }; /// /// // only label -/// let fab_label_button = widget! { -/// FabButton { Label::new("fab button") } +/// let fab_label_button = fn_widget! { +/// @FabButton { @ { Label::new("fab button") } } /// }; /// /// // both icon and label -/// let fab_button = widget! { -/// FabButton { -/// svgs::ADD -/// Label::new("fab button") +/// let fab_button = fn_widget! { +/// @FabButton { +/// @ { svgs::ADD } +/// @ { Label::new("fab button") } /// } /// }; /// /// // use custom color -/// let custom_color_button = widget! { -/// FabButton { +/// let custom_color_button = fn_widget! { +/// @FabButton { /// color: Color::RED, -/// svgs::ADD -/// Label::new("fab button") +/// @ { svgs::ADD } +/// @ { Label::new("fab button") } /// } /// }; /// ``` -#[derive(Declare, Default)] +#[derive(Declare, Default, Declare2)] pub struct FabButton { #[declare(default=Palette::of(ctx).primary())] color: Color, @@ -91,9 +91,8 @@ impl ComposeChild for FabButton { (None, None) => panic!("Button content cannot be empty!"), }; - widget! { - states { this: this.into_readonly() } - init ctx => { + fn_widget! { + @ { let FabButtonStyle { height, icon_size, @@ -102,26 +101,27 @@ impl ComposeChild for FabButton { label_style, radius, padding_style, - } = FabButtonStyle::of(ctx).clone(); + } = FabButtonStyle::of(ctx); let palette1 = Palette::of(ctx).clone(); let palette2 = Palette::of(ctx).clone(); - } - FabButtonDecorator { - button_type, - color: this.color, - ButtonImpl { - height, - icon_size, - label_gap, - icon_pos, - label_style, - background_color: Brush::from(palette1.base_of(&this.color)), - foreground_color: Brush::from(palette2.on_of(&palette2.base_of(&this.color))), - radius, - border_style: None, - padding_style, - widget::from(child) + @FabButtonDecorator { + button_type, + color: pipe!($this.color), + @ButtonImpl { + height, + icon_size, + label_gap, + icon_pos, + label_style, + background_color: pipe!(Brush::from(palette1.base_of(&$this.color))), + foreground_color: pipe!(Brush::from(palette2.on_of(&palette2.base_of(&$this.color)))), + radius, + border_style: None, + padding_style, + + @ { child } + } } } } diff --git a/widgets/src/buttons/filled_button.rs b/widgets/src/buttons/filled_button.rs index 2d41698cc..16eb31bda 100644 --- a/widgets/src/buttons/filled_button.rs +++ b/widgets/src/buttons/filled_button.rs @@ -26,7 +26,7 @@ impl CustomStyle for FilledButtonStyle { } } -#[derive(Clone, Declare)] +#[derive(Clone, Declare, Declare2)] pub struct FilledButtonDecorator { #[allow(unused)] pub button_type: ButtonType, @@ -47,33 +47,33 @@ impl ComposeDecorator for FilledButtonDecorator { /// # use ribir_widgets::prelude::{FilledButton, Label}; /// /// // only icon -/// let filled_icon_button = widget! { -/// FilledButton { svgs::ADD } +/// let filled_icon_button = fn_widget! { +/// @FilledButton { @{ svgs::ADD } } /// }; /// /// // only label -/// let filled_label_button = widget! { -/// FilledButton { Label::new("filled button") } +/// let filled_label_button = fn_widget! { +/// @FilledButton { @{ Label::new("filled button") } } /// }; /// /// // both icon and label -/// let filled_button = widget! { -/// FilledButton { -/// svgs::ADD -/// Label::new("filled button") +/// let filled_button = fn_widget! { +/// @FilledButton { +/// @ { svgs::ADD } +/// @ { Label::new("filled button") } /// } /// }; /// /// // use custom color -/// let custom_color_button = widget! { -/// FilledButton { +/// let custom_color_button = fn_widget! { +/// @FilledButton { /// color: Color::RED, -/// svgs::ADD -/// Label::new("filled button") +/// @ { svgs::ADD } +/// @ { Label::new("filled button") } /// } /// }; /// ``` -#[derive(Declare, Default)] +#[derive(Declare, Declare2, Default)] pub struct FilledButton { #[declare(default=Palette::of(ctx).primary())] color: Color, @@ -91,9 +91,8 @@ impl ComposeChild for FilledButton { (None, None) => panic!("Button content cannot be empty!"), }; - widget! { - states { this: this.into_readonly() } - init ctx => { + fn_widget! { + @ { let FilledButtonStyle { height, icon_size, @@ -102,26 +101,27 @@ impl ComposeChild for FilledButton { label_style, radius, padding_style, - } = FilledButtonStyle::of(ctx).clone(); + } = FilledButtonStyle::of(ctx); let palette1 = Palette::of(ctx).clone(); let palette2 = Palette::of(ctx).clone(); - } - FilledButtonDecorator { - button_type, - color: this.color, - ButtonImpl { - height, - icon_size, - label_gap, - icon_pos, - label_style, - background_color: Brush::from(palette1.base_of(&this.color)), - foreground_color: Brush::from(palette2.on_of(&palette2.base_of(&this.color))), - radius, - border_style: None, - padding_style, - widget::from(child) + @FilledButtonDecorator { + button_type, + color: pipe!($this.color), + @ButtonImpl { + height, + icon_size, + label_gap, + icon_pos, + label_style, + background_color: pipe!(Brush::from(palette1.base_of(&$this.color))), + foreground_color: pipe!(Brush::from(palette2.on_of(&palette2.base_of(&$this.color)))), + radius, + border_style: None, + padding_style, + + @ { child } + } } } } diff --git a/widgets/src/buttons/outlined_button.rs b/widgets/src/buttons/outlined_button.rs index 34a7d7d0f..5d03c6e60 100644 --- a/widgets/src/buttons/outlined_button.rs +++ b/widgets/src/buttons/outlined_button.rs @@ -28,7 +28,7 @@ impl CustomStyle for OutlinedButtonStyle { } } -#[derive(Clone, Declare)] +#[derive(Clone, Declare, Declare2)] pub struct OutlinedButtonDecorator { #[allow(unused)] pub button_type: ButtonType, @@ -49,33 +49,33 @@ impl ComposeDecorator for OutlinedButtonDecorator { /// # use ribir_widgets::prelude::{OutlinedButton, Label}; /// /// // only icon -/// let outlined_icon_button = widget! { -/// OutlinedButton { svgs::ADD } +/// let outlined_icon_button = fn_widget! { +/// @OutlinedButton { @{ svgs::ADD } } /// }; /// /// // only label -/// let outlined_label_button = widget! { -/// OutlinedButton { Label::new("outlined button") } +/// let outlined_label_button = fn_widget! { +/// @OutlinedButton { @{ Label::new("outlined button") } } /// }; /// /// // both icon and label -/// let outlined_button = widget! { -/// OutlinedButton { -/// svgs::ADD -/// Label::new("outlined button") +/// let outlined_button = fn_widget! { +/// @OutlinedButton { +/// @ { svgs::ADD } +/// @ { Label::new("outlined button")} /// } /// }; /// /// // use custom color -/// let custom_color_button = widget! { -/// OutlinedButton { +/// let custom_color_button = fn_widget! { +/// @OutlinedButton { /// color: Color::RED, -/// svgs::ADD -/// Label::new("outlined button") +/// @ { svgs::ADD } +/// @ { Label::new("outlined button") } /// } /// }; /// ``` -#[derive(Declare, Default)] +#[derive(Declare, Default, Declare2)] pub struct OutlinedButton { #[declare(default=Palette::of(ctx).primary())] color: Color, @@ -93,9 +93,8 @@ impl ComposeChild for OutlinedButton { (None, None) => panic!("Button content cannot be empty!"), }; - widget! { - states { this: this.into_readonly() } - init ctx => { + fn_widget! { + @ { let OutlinedButtonStyle { height, icon_size, @@ -105,29 +104,30 @@ impl ComposeChild for OutlinedButton { radius, padding_style, border_width, - } = OutlinedButtonStyle::of(ctx).clone(); + } = OutlinedButtonStyle::of(ctx); let palette1 = Palette::of(ctx).clone(); let palette2 = Palette::of(ctx).clone(); - } - OutlinedButtonDecorator { - button_type, - color: this.color, - ButtonImpl { - height, - icon_size, - label_gap, - icon_pos, - label_style, - background_color: None, - foreground_color: Brush::from(palette1.base_of(&this.color)), - radius, - border_style: Border::all(BorderSide { - width: border_width, - color: Brush::from(palette2.base_of(&this.color)) - }), - padding_style, - widget::from(child) + @OutlinedButtonDecorator { + button_type, + color: pipe!($this.color), + @ButtonImpl { + height, + icon_size, + label_gap, + icon_pos, + label_style, + background_color: None, + foreground_color: pipe!(Brush::from(palette1.base_of(&$this.color))), + radius, + border_style: pipe!(Border::all(BorderSide { + width: border_width, + color: Brush::from(palette2.base_of(&$this.color)) + })), + padding_style, + + @ { child } + } } } } diff --git a/widgets/src/checkbox.rs b/widgets/src/checkbox.rs index 3fd5bf85f..d59e8d3fb 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, @@ -25,7 +25,7 @@ pub struct CheckBoxStyle { pub label_color: Brush, } -#[derive(Clone, Declare)] +#[derive(Clone, Declare, Declare2)] pub struct CheckBoxDecorator { #[declare(default=Palette::of(ctx).primary())] pub color: Color, @@ -44,8 +44,8 @@ impl Checkbox { #[derive(Template)] pub enum CheckboxTemplate { - Before(WidgetPair>), - After(WidgetPair>), + Before(SinglePair>), + After(SinglePair>), } impl ComposeDecorator for CheckBoxDecorator { @@ -54,82 +54,57 @@ impl ComposeDecorator for CheckBoxDecorator { fn compose_decorator(_: State, host: Self::Host) -> Widget { host } } -impl Checkbox { - fn icon(this: Stateful, size: Size) -> Widget { - widget! { - states { this } - CheckBoxDecorator { - color: this.color, - Icon { - size, - widget::from( - if this.indeterminate { +impl ComposeChild for Checkbox { + type Child = Option; + + fn compose_child(this: State, child: Self::Child) -> Widget { + fn_widget! { + let CheckBoxStyle { + icon_size, + label_style, + label_color, + } = CheckBoxStyle::of(ctx!()); + + let icon: Widget = @CheckBoxDecorator { + color: pipe!($this.color), + @Icon { size: icon_size, + @ { pipe!{ + if $this.indeterminate { svgs::INDETERMINATE_CHECK_BOX - } else if this.checked { + } else if $this.checked { svgs::CHECK_BOX } else { svgs::CHECK_BOX_OUTLINE_BLANK } - ) + }} } - } - } - .into() - } - - fn label(label: &State

EdgeWidget

-where - P: TmlFlag + Default, -{ +impl EdgeWidget { fn compose_with_style(self, config: EdgeWidgetStyle) -> Widget { let EdgeWidgetStyle { icon, @@ -200,72 +197,58 @@ where poster, custom, } = config; - match self { - EdgeWidget::Icon(w) => widget! { - DynWidget { - dyns: icon.gap.map(|margin| Margin { margin }), - Icon { - size: icon.size, - widget::from(w.decorate(|_, c| c.into())) - } - } - } - .into(), - EdgeWidget::Text(w) => widget! { - DynWidget { - dyns: text.gap.map(|margin| Margin { margin }), - widget::from(w.decorate(|_, label| widget!{ - states { label: label.into_readonly() } - Text { - text: label.0.clone(), + fn_widget! { + let w: Widget = match self { + EdgeWidget::Icon(w) => { + let margin = icon.gap.map(|margin| Margin { margin }); + @ $margin { + @Icon { + size: icon.size, + @ { w } + } + }.into() + }, + EdgeWidget::Text(mut label) => { + let margin = text.gap.map(|margin| Margin { margin }); + @ $margin { + @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()) - } - } - .into(), - EdgeWidget::Image(w) => widget! { - DynWidget { - dyns: image.gap.map(|margin| Margin { margin }), - SizedBox { - size: image.size, - DynWidget { - box_fit: BoxFit::None, - dyns: w.decorate(|_, c| c.into()) + }.into() + }, + EdgeWidget::Avatar(w) => { + let margin = avatar.gap.map(|margin| Margin { margin }); + @ $margin { @ { w }}.into() + }, + EdgeWidget::Image(w) => { + let margin = image.gap.map(|margin| Margin { margin }); + @ $margin { + @SizedBox { + size: image.size, + @ $w { box_fit: BoxFit::None } } - } - } - } - .into(), - EdgeWidget::Poster(w) => widget! { - DynWidget { - dyns: poster.gap.map(|margin| Margin { margin }), - SizedBox { - size: poster.size, - DynWidget { - box_fit: BoxFit::None, - dyns: w.decorate(|_, c| c.0.into()) + }.into() + }, + EdgeWidget::Poster(w) => { + let margin = poster.gap.map(|margin| Margin { margin }); + let w = w.0; + @ $margin { + @ SizedBox { + size: poster.size, + @ $w { box_fit: BoxFit::None } } - } - } - } - .into(), - EdgeWidget::Custom(w) => widget! { - DynWidget { - dyns: custom.gap.map(|margin| Margin { margin }), - widget::from(w.decorate(|_, c| c.0)) - } - } - .into(), + }.into() + }, + EdgeWidget::Custom(w) => { + let margin = custom.gap.map(|margin| Margin { margin }); + @$margin { @ { w.0 }}.into() + }, + }; + w } + .into() } } @@ -273,8 +256,8 @@ where pub struct ListItemTml { headline: State, supporting: Option>, - leading: Option>, - trailing: Option>, + leading: Option, EdgeWidget>>, + trailing: Option, EdgeWidget>>, } impl ComposeChild for ListItem { @@ -288,63 +271,68 @@ impl ComposeChild for ListItem { 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); + + 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 (leading, widget) = w.unzip(); + let (_, builtin) = leading.unzip(); + builtin.compose_with_host(widget.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(), + 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(), + text_style: supporting_style, } } - }) + })} } } } - Option::map(trailing, |w| w.compose_with_style(trailing_config)) + @{ trailing.map(|w| { + let (trailing, widget) = w.unzip(); + let (_, builtin) = trailing.unzip(); + builtin.compose_with_host(widget.compose_with_style(trailing_config), ctx!()) + })} } } } @@ -353,9 +341,9 @@ impl ComposeChild for ListItem { } } -#[derive(Declare)] +#[derive(Declare, Declare2)] pub struct ListItem { - #[declare(default = 1)] + #[declare(default = 1usize)] pub line_number: usize, #[declare(default = Palette::of(ctx).primary())] pub active_background: Color, @@ -451,7 +439,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/scrollbar.rs b/widgets/src/scrollbar.rs index 032de542f..c544796f9 100644 --- a/widgets/src/scrollbar.rs +++ b/widgets/src/scrollbar.rs @@ -1,11 +1,9 @@ -use std::time::Duration; - use crate::layout::{Container, Stack, StackFit}; use ribir_core::prelude::*; /// A control widget that enables the user to access horizontal parts child that /// is larger than the box rect. -#[derive(Declare, Clone)] +#[derive(Declare, Declare2, Clone)] pub struct HScrollBar { /// Scrolled pixels of child content. #[declare(default)] @@ -24,7 +22,7 @@ pub struct ScrollBarStyle { /// Compose style that use to decoration the thumb of horizontal scrollbar, /// overwrite it when init theme. -#[derive(Debug, Declare)] +#[derive(Debug, Declare, Declare2)] pub struct HScrollBarThumbDecorator { pub offset: f32, } @@ -33,17 +31,13 @@ impl ComposeDecorator for HScrollBarThumbDecorator { type Host = Widget; fn compose_decorator(this: State, host: Self::Host) -> Widget { - widget! { - states { this: this.into_readonly() } - DynWidget { left_anchor: this.offset, dyns: host } - } - .into() + fn_widget! { @$host { left_anchor: pipe!($this.offset) } }.into() } } /// Compose style that use to decoration the thumb of vertical scrollbar, /// overwrite it when init theme. -#[derive(Debug, Declare)] +#[derive(Debug, Declare, Declare2)] pub struct VScrollBarThumbDecorator { pub offset: f32, } @@ -52,47 +46,38 @@ impl ComposeDecorator for VScrollBarThumbDecorator { type Host = Widget; fn compose_decorator(this: State, host: Self::Host) -> Widget { - widget! { - states { this: this.into_readonly() } - DynWidget { - top_anchor: this.offset, - dyns: host - } - } - .into() + fn_widget! { @$host { top_anchor: pipe!($this.offset) } }.into() } } impl ComposeChild for HScrollBar { type Child = Widget; fn compose_child(this: State, child: Self::Child) -> Widget { - widget! { - states { this: this.into_writable() } - Stack { + fn_widget! { + let mut scrolling = @ScrollableWidget { + scrollable: Scrollable::X, + scroll_pos: Point::new($this.offset, 0.), + }; + let scrollbar = @HRawScrollbar { + scrolling: scrolling.get_builtin_scrollable_widget(ctx!()).clone_reader(), + v_align: VAlign::Bottom, + }; + + // `scrolling` and `this` have same lifetime, so we needn't unsubscribe. + watch!($scrolling.scroll_pos.x) + .distinct_until_changed() + .subscribe(move |v| $this.write().offset = v); + watch!($this.offset) + .distinct_until_changed() + .subscribe(move |v| { + let y = $scrolling.scroll_pos.y; + $scrolling.write().jump_to(Point::new(v, y)); + }); + + @Stack { fit: StackFit::Passthrough, - ScrollableWidget { - id: scrolling, - scrollable: Scrollable::X, - scroll_pos: Point::new(this.offset, 0.), - DynWidget { dyns: child } - } - HRawScrollbar { - scrolling: scrolling.clone_stateful(), - v_align: VAlign::Bottom, - } - } - finally ctx => { - let_watch!(scrolling.scroll_pos.x) - .distinct_until_changed() - .debounce(Duration::ZERO, ctx.window().frame_scheduler()) - .subscribe(move |v| this.offset = v); - let_watch!(this.offset) - .distinct_until_changed() - .debounce(Duration::ZERO, ctx.window().frame_scheduler()) - .subscribe(move |v| { - let y = scrolling.scroll_pos.y; - scrolling.jump_to(Point::new(v, y)); - }); + @ $scrolling { @{ child } } + @ { scrollbar } } } .into() @@ -101,7 +86,7 @@ impl ComposeChild for HScrollBar { /// A control widget that enables the user to access vertical parts child that /// is larger than the box rect. -#[derive(Declare, Clone)] +#[derive(Declare, Declare2, Clone)] pub struct VScrollBar { /// Scrolled pixels of child content. #[declare(default)] @@ -111,33 +96,32 @@ pub struct VScrollBar { impl ComposeChild for VScrollBar { type Child = Widget; fn compose_child(this: State, child: Self::Child) -> Widget { - widget! { - states { this: this.into_writable() } - Stack { + fn_widget! { + let mut scrolling = @ScrollableWidget { + scrollable: Scrollable::Y, + scroll_pos: Point::new(0., $this.offset), + }; + + let scrollbar = @VRawScrollbar { + scrolling: scrolling.get_builtin_scrollable_widget(ctx!()).clone_reader(), + h_align: HAlign::Right + }; + + // `scrolling` and `this` have same lifetime, so we needn't unsubscribe. + watch!($scrolling.scroll_pos.y) + .distinct_until_changed() + .subscribe(move |v| $this.write().offset = v); + watch!($this.offset) + .distinct_until_changed() + .subscribe(move |v| { + let x = $scrolling.scroll_pos.x; + $scrolling.write().jump_to(Point::new(x, v)); + }); + + @Stack { fit: StackFit::Passthrough, - ScrollableWidget { - id: scrolling, - scrollable: Scrollable::Y, - scroll_pos: Point::new(0., this.offset), - DynWidget { dyns: child } - } - VRawScrollbar { - scrolling: scrolling.clone_stateful(), - h_align: HAlign::Right - } - } - finally ctx => { - let_watch!(scrolling.scroll_pos.y) - .distinct_until_changed() - .debounce(Duration::ZERO, ctx.window().frame_scheduler()) - .subscribe(move |v| this.offset = v); - let_watch!(this.offset) - .distinct_until_changed() - .debounce(Duration::ZERO, ctx.window().frame_scheduler()) - .subscribe(move |v| { - let x = scrolling.scroll_pos.x; - scrolling.jump_to(Point::new(x, v)); - }); + @ $scrolling { @{ child } } + @ { scrollbar } } } .into() @@ -145,7 +129,7 @@ impl ComposeChild for VScrollBar { } /// A control widget that enables the user to access horizontal parts child that /// is larger than the box rect. -#[derive(Declare, Clone)] +#[derive(Declare, Declare2, Clone)] pub struct BothScrollbar { /// Scrolled pixels of child content. #[declare(default)] @@ -155,40 +139,34 @@ pub struct BothScrollbar { impl ComposeChild for BothScrollbar { type Child = Widget; fn compose_child(this: State, child: Self::Child) -> Widget { - widget! { - states { this: this.into_writable() } - Stack { + fn_widget! { + let mut scrolling = @ScrollableWidget { + scrollable: Scrollable::Both, + scroll_pos: $this.offset, + }; + let mut h_bar = @HRawScrollbar { + scrolling: scrolling.get_builtin_scrollable_widget(ctx!()).clone_reader(), + v_align: VAlign::Bottom, + }; + let mut v_bar = @VRawScrollbar { + scrolling: scrolling.get_builtin_scrollable_widget(ctx!()).clone_reader(), + h_align: HAlign::Right, + margin: EdgeInsets::only_bottom($h_bar.layout_height()) + }; + + // `scrolling` and `this` have same lifetime, so we needn't unsubscribe. + watch!($scrolling.scroll_pos) + .distinct_until_changed() + .subscribe(move |v| $this.write().offset = v); + watch!($this.offset) + .distinct_until_changed() + .subscribe(move |v| $scrolling.write().jump_to(v) ); + + @Stack{ fit: StackFit::Passthrough, - ScrollableWidget { - id: scrolling, - scrollable: Scrollable::Both, - scroll_pos: this.offset, - DynWidget { dyns: child } - } - HRawScrollbar { - id: h_bar, - scrolling: scrolling.clone_stateful(), - v_align: VAlign::Bottom, - margin: EdgeInsets::only_right(v_bar.layout_width()) - } - VRawScrollbar { - id: v_bar, - scrolling: scrolling.clone_stateful(), - h_align: HAlign::Right, - margin: EdgeInsets::only_bottom(h_bar.layout_height()) - } - } - finally ctx => { - let_watch!(scrolling.scroll_pos) - .distinct_until_changed() - .debounce(Duration::ZERO, ctx.window().frame_scheduler()) - .subscribe(move |v| this.offset = v); - let_watch!(this.offset) - .distinct_until_changed() - .debounce(Duration::ZERO, ctx.window().frame_scheduler()) - .subscribe(move |v| { - scrolling.jump_to(v); - }); + @ $scrolling { @{ child } } + @ $h_bar{ margin: EdgeInsets::only_right($v_bar.layout_width()) } + @ { v_bar } } } .into() @@ -197,51 +175,61 @@ impl ComposeChild for BothScrollbar { /// A widget that display the horizontal scrolling information of the /// `scrolling` widget. -#[derive(Declare)] +#[derive(Declare, Declare2)] pub struct HRawScrollbar { - scrolling: Stateful, + scrolling: Reader, } impl Compose for HRawScrollbar { fn compose(this: State) -> Widget { - let this = this.into_writable(); - let scrolling = this.state_ref().scrolling.clone(); - - widget! { - states { scrolling, this } - init ctx => { + fn_widget! { + @ { + let mut scrolling = $this.scrolling.clone_reader(); let ScrollBarStyle { thickness, thumb_min_size, track_brush, - } = ScrollBarStyle::of(ctx).clone(); - } + } = ScrollBarStyle::of(ctx); - Stack { - visible: scrolling.can_scroll(), - Container { - id: track_box, - size: Size::new(f32::MAX, thumb_outline.layout_height()), - background: track_brush.clone() - } - LayoutBox { - id: thumb_outline, - HScrollBarThumbDecorator{ - offset: { - let content_width = scrolling.scroll_content_size().width; - -scrolling.scroll_pos.x * safe_recip(content_width) * track_box.layout_width() - }, - Container { - size: { - let page_width = scrolling.scroll_view_size().width; - let content_width = scrolling.scroll_content_size().width; - let width = page_width / content_width * track_box.layout_width(); - Size::new(width.max(thumb_min_size), thickness) - }, - } - } + let mut track_box = @Container { + size: Size::new(f32::MAX, 0.), + background: track_brush + }; + + let thumb_outline = @HScrollBarThumbDecorator { + offset: pipe!{ + let scrolling = $scrolling; + let content_width = scrolling.scroll_content_size().width; + -scrolling.scroll_pos.x * safe_recip(content_width) * $track_box.layout_width() + } + }; + + let mut container = @Container { + size: { + let scrolling = $scrolling; + let page_width = scrolling.scroll_view_size().width; + let content_width = scrolling.scroll_content_size().width; + let width = page_width / content_width * $track_box.layout_width(); + Size::new(width.max(thumb_min_size), thickness) + }, + }; + + watch!($container.layout_height()) + .distinct_until_changed() + .subscribe(move |v| $track_box.write().size.height = v); + + @Stack { + visible: pipe! { + let scrolling = $scrolling; + scrolling.can_scroll() + }, + @ { track_box } + @$thumb_outline { + @ { container } + } } } + } .into() } @@ -249,48 +237,54 @@ impl Compose for HRawScrollbar { /// A widget that display the vertical scrolling information of the /// `scrolling` widget. -#[derive(Declare)] +#[derive(Declare, Declare2)] pub struct VRawScrollbar { - scrolling: Stateful, + scrolling: Reader, } impl Compose for VRawScrollbar { fn compose(this: State) -> Widget { - let this = this.into_writable(); - let scrolling = this.state_ref().scrolling.clone(); - - widget! { - states { scrolling, this } - init ctx => { + fn_widget! { + @ { + let mut scrolling = $this.scrolling.clone_reader(); let ScrollBarStyle { thickness, thumb_min_size, ref track_brush } = ScrollBarStyle::of(ctx); - } - Stack { - visible: scrolling.can_scroll(), - Container { - id: track_box, - size: Size::new(thumb_outline.layout_width() , f32::MAX), - background: track_brush.clone(), - } - LayoutBox { - id: thumb_outline, - VScrollBarThumbDecorator { - offset: { - let content_height = scrolling.scroll_content_size().height; - -scrolling.scroll_pos.y * safe_recip(content_height) * track_box.layout_height() - }, - Container { - size: { - let page_height = scrolling.scroll_view_size().height; - let content_height = scrolling.scroll_content_size().height; - let height = page_height / content_height * track_box.layout_height(); - Size::new(thickness, height.max(thumb_min_size)) - }, - } + let mut track_box = @Container { + size: Size::new(0., f32::MAX), + background: track_brush.clone() + }; + + let thumb_outline = @VScrollBarThumbDecorator { + offset: pipe! { + let scrolling = $scrolling; + let content_height = scrolling.scroll_content_size().height; + -scrolling.scroll_pos.y * safe_recip(content_height) * $track_box.layout_height() + } + }; + + let mut container = @Container { + size: pipe! { + let scrolling = $scrolling; + let page_height = scrolling.scroll_view_size().height; + let content_height = scrolling.scroll_content_size().height; + let height = page_height / content_height * $track_box.layout_height(); + Size::new(thickness, height.max(thumb_min_size)) + }, + }; + + watch!($container.layout_width()) + .distinct_until_changed() + .subscribe(move |v| $track_box.write().size.width = v); + + @Stack { + visible: pipe! { $scrolling.can_scroll() }, + @ { track_box } + @$thumb_outline { + @ { container } } } } @@ -316,10 +310,9 @@ impl CustomStyle for ScrollBarStyle { #[cfg(test)] mod test { - use crate::layout::{Column, ConstrainedBox}; - use super::*; - use ribir_core::test_helper::*; + use crate::layout::{Column, ConstrainedBox}; + use ribir_core::{reset_test_env, test_helper::*}; use ribir_dev_helper::*; fn content_expand_so_all_view_can_scroll() -> Widget { @@ -352,57 +345,51 @@ mod test { #[test] fn scrollable() { - let _guard = unsafe { AppCtx::new_lock_scope() }; + reset_test_env!(); let offset = Stateful::new(Point::zero()); let v_offset = Stateful::new(0.); let h_offset = Stateful::new(0.); - let w = widget! { - states { offset: offset.clone(), v_offset: v_offset.clone(), h_offset: h_offset.clone() } - Column { - Container { + let c_offset = offset.clone_writer(); + let c_v_offset = v_offset.clone_reader(); + let c_h_offset = h_offset.clone_reader(); + let w = fn_widget! { + let mut both_bar = @BothScrollbar { offset: pipe!(*$offset) }; + let mut h_bar = @HScrollBar { offset: pipe!($both_bar.offset.x) }; + let mut v_bar = @VScrollBar { offset: pipe!($both_bar.offset.y) }; + + watch!($v_bar.offset) + .subscribe(move|v| *$v_offset.write() = v); + watch!($h_bar.offset) + .subscribe(move|v| *$h_offset.write() = v); + + let container_size = Size::new(100., 100.); + @Column { + @Container { size: Size::new(30., 30.), - BothScrollbar { - id: both_bar, - offset: *offset, - Container { size: Size::new(100., 100.) } - } + @$both_bar { @Container { size: container_size } } } - Container { + @Container { size: Size::new(30., 30.), - HScrollBar { - id: h_bar, - offset: both_bar.offset.x, - Container { size: Size::new(100., 100.) } - } + @$h_bar { @Container { size: container_size } } } - Container { + @Container { size: Size::new(30., 30.), - VScrollBar { - id: v_bar, - offset: both_bar.offset.y, - Container { size: Size::new(100., 100.) } - } + @$v_bar { @Container { size: container_size } } } } - - finally { - let_watch!(v_bar.offset) - .subscribe(move|v| *v_offset = v); - let_watch!(h_bar.offset) - .subscribe(move|v| *h_offset = v); - } }; let mut wnd = TestWindow::new_with_size(w, Size::new(1024., 1024.)); + wnd.draw_frame(); { - *offset.state_ref() = Point::new(-10., -10.); + *c_offset.write() = Point::new(-10., -10.); } { - *offset.state_ref() = Point::new(-20., -20.); + *c_offset.write() = Point::new(-20., -20.); } wnd.draw_frame(); - assert_eq!(*v_offset.state_ref(), offset.state_ref().y); - assert_eq!(*h_offset.state_ref(), offset.state_ref().x); + assert_eq!(*c_v_offset.read(), c_offset.read().y); + assert_eq!(*c_h_offset.read(), c_offset.read().x); } } diff --git a/widgets/src/tabs.rs b/widgets/src/tabs.rs index 75c59e082..930c1d7e1 100644 --- a/widgets/src/tabs.rs +++ b/widgets/src/tabs.rs @@ -8,59 +8,59 @@ use ribir_core::prelude::*; /// # use ribir_core::prelude::*; /// # use ribir_widgets::prelude::*; /// -/// let tabs = widget! { -/// Tabs { -/// Tab { -/// TabItem { -/// svgs::HOME -/// Label::new("Home") +/// let tabs = fn_widget! { +/// @Tabs { +/// @Tab { +/// @TabItem { +/// @ { svgs::HOME } +/// @ { Label::new("Home") } /// } -/// TabPane { -/// Text { text: "content" } +/// @TabPane { +/// @{ Text { text: "content" } } /// } /// } -/// Tab { -/// TabItem { -/// svgs::HOME -/// Label::new("Home") +/// @Tab { +/// @TabItem { +/// @ { svgs::HOME } +/// @ { Label::new("Home") } /// } -/// TabPane { -/// Text { text: "content" } +/// @TabPane { +/// @Text { text: "content" } /// } /// } /// } /// }; /// /// // bottom tabs -/// let bottom_tabs = widget! { -/// Tabs { +/// let bottom_tabs = fn_widget! { +/// @Tabs { /// pos: Position::Bottom, -/// Tab { -/// TabItem { -/// svgs::HOME -/// Label::new("Home") +/// @Tab { +/// @TabItem { +/// @ { svgs::HOME } +/// @ { Label::new("Home") } /// } -/// TabPane { -/// Text { text: "content" } +/// @TabPane { +/// @Text { text: "content" } /// } /// } -/// Tab { -/// TabItem { -/// svgs::HOME -/// Label::new("Home") +/// @Tab { +/// @TabItem { +/// @ { svgs::HOME } +/// @ { Label::new("Home") } /// } -/// TabPane { -/// Text { text: "content" } +/// @TabPane { +/// @Text { text: "content" } /// } /// } /// } /// }; /// ``` -#[derive(Declare, Clone)] +#[derive(Declare, Declare2, Clone)] pub struct Tabs { #[declare(default = Position::Top)] pub pos: Position, - #[declare(default = 0)] + #[declare(default)] pub cur_idx: usize, } @@ -99,7 +99,7 @@ impl CustomStyle for TabsStyle { } } } -#[derive(Declare)] +#[derive(Declare, Declare2)] pub struct TabsDecorator {} impl ComposeDecorator for TabsDecorator { @@ -110,20 +110,20 @@ impl ComposeDecorator for TabsDecorator { #[derive(Template)] pub struct Tab { - header: TabItem, - pane: Option>, + label: TabItem, + child: Option>, } #[derive(Template)] pub struct TabItem { icon: Option, - label: Option>, + text: Option>, } -#[derive(Declare, SingleChild)] +#[derive(Declare, Declare2, SingleChild)] pub struct TabPane; -#[derive(Declare)] +#[derive(Declare, Declare2)] pub struct TabDecorator {} impl ComposeDecorator for TabDecorator { @@ -132,7 +132,7 @@ impl ComposeDecorator for TabDecorator { fn compose_decorator(_: State, host: Self::Host) -> Widget { host } } -#[derive(Declare)] +#[derive(Declare, Declare2)] pub struct IndicatorDecorator { pub pos: Position, pub rect: Rect, @@ -143,22 +143,26 @@ impl ComposeDecorator for IndicatorDecorator { type Host = Widget; fn compose_decorator(this: State, host: Self::Host) -> Widget { - widget! { - states { this: this.into_readonly() } - DynWidget { - left_anchor: match this.pos { - Position::Top | Position::Bottom => this.rect.origin.x - + (this.rect.size.width - 60.) / 2., - Position::Left => this.rect.size.width - this.extent, - Position::Right => 0., - }, - top_anchor: match this.pos { - Position::Left | Position::Right => this.rect.origin.y - + (this.rect.size.height - 60.) / 2., - Position::Top => this.rect.size.height - this.extent, - Position::Bottom => 0., + fn_widget! { + @ $host{ + left_anchor: pipe!{ + let this = $this; + match this.pos { + Position::Top | Position::Bottom => this.rect.origin.x + + (this.rect.size.width - 60.) / 2., + Position::Left => this.rect.size.width - this.extent, + Position::Right => 0., + } }, - dyns: host, + top_anchor: pipe! { + let this = $this; + match this.pos { + Position::Left | Position::Right => this.rect.origin.y + + (this.rect.size.height - 60.) / 2., + Position::Top => this.rect.size.height - this.extent, + Position::Bottom => 0., + } + } } } .into() @@ -169,8 +173,8 @@ impl Tabs { fn tab_header( headers: Vec<(Option, Option>)>, tabs_style: TabsStyle, - tabs: Stateful, - indicator: Stateful, + tabs: impl StateWriter + 'static, + indicator: impl StateWriter + 'static, ) -> impl Iterator { let TabsStyle { icon_size: size, @@ -184,63 +188,50 @@ impl Tabs { .into_iter() .enumerate() .map(move |(idx, (icon, label))| { - let icon_widget = icon.map(|icon| { - widget! { - Icon { - size, - widget::from(icon) - } - } - }); - + let tabs = tabs.clone_writer(); let active_color = active_color.clone(); let foreground = foreground.clone(); let label_style = label_style.clone(); - let label_widget = label.map(|label| { - widget! { - states { - tabs: tabs.clone(), - text: label.into_readonly(), - } - Text { - text: text.0.clone(), - foreground: match tabs.cur_idx == idx { + let indicator = indicator.clone_writer(); + fn_widget! { + let icon_widget = icon.map(|icon| @Icon { size, @ { icon }}); + let label_widget = label.map(|label| { + @Text { + text: pipe!($label.0.clone()), + foreground: pipe!(match $tabs.cur_idx == idx { true => active_color.clone(), false => foreground.clone(), - }, + }), text_style: label_style, } - } - }); - let indicator = indicator.clone(); - widget! { - states { tabs: tabs.clone() } - TabDecorator { - Expanded { - id: tab_header, - flex: 1., - on_tap: move |_| if tabs.cur_idx != idx { - tabs.cur_idx = idx; + }); + @ { + let mut tab_header = @Expanded { + on_tap: move |_| if $tabs.cur_idx != idx { + $tabs.write().cur_idx = idx; }, - Flex { - align_items: Align::Center, - justify_content: JustifyContent::Center, - direction: match icon_pos { - Position::Left | Position::Right => Direction::Horizontal, - Position::Top | Position::Bottom => Direction::Vertical, - }, - reverse: matches!(icon_pos, Position::Right | Position::Bottom), - widget::from(icon_widget) - // todo: insert `Spacer` - widget::from(label_widget) + }; + + watch!(($tabs.cur_idx == idx, $tab_header.layout_rect())) + .filter_map(|(active, rect)| active.then_some(rect)) + .subscribe(move |v| $indicator.write().rect = v); + + @TabDecorator { + @$tab_header { + @Flex { + align_items: Align::Center, + justify_content: JustifyContent::Center, + direction: match icon_pos { + Position::Left | Position::Right => Direction::Horizontal, + Position::Top | Position::Bottom => Direction::Vertical, + }, + reverse: matches!(icon_pos, Position::Right | Position::Bottom), + @ { icon_widget } + @ { label_widget } + } } } } - finally { - let_watch!((tabs.cur_idx == idx, tab_header.layout_rect())) - .filter_map(|(active, rect)| active.then_some(rect)) - .subscribe(move |v| indicator.silent_ref().rect = v); - } } .into() }) @@ -255,17 +246,15 @@ impl ComposeChild for Tabs { let mut panes = vec![]; for tab in child.into_iter() { - let Tab { header, pane } = tab; - headers.push((header.icon, header.label)); + let Tab { label: header, child: pane } = tab; + headers.push((header.icon, header.text)); if let Some(pane) = pane { - panes.push(pane.child) + panes.push(pane.child()) } } - widget! { - states { this: this.into_writable() } - init ctx => { - let tabs_style = TabsStyle::of(ctx); + fn_widget! { + let tabs_style = TabsStyle::of(ctx!()); let TabsStyle { extent_only_icon, extent_only_label, @@ -274,7 +263,6 @@ impl ComposeChild for Tabs { indicator, .. } = tabs_style.clone(); - let tabs_style = tabs_style.clone(); let has_icon = headers.iter().any(|item| item.0.is_some()); let has_label = headers.iter().any(|item| item.1.is_some()); let extent = match (has_icon, has_label) { @@ -285,84 +273,83 @@ impl ComposeChild for Tabs { }; let mut panes = panes.into_iter() .enumerate() - .map(move |(idx, pane)| { - widget! { - Expanded { - flex: 1., - DynWidget { - visible: this.cur_idx == idx, - dyns: pane, - } - } - } + .map(move |(idx, pane)| @Expanded { + flex: 1., + @ $pane { visible: pipe!($this.cur_idx == idx) } }); - let mut header = widget! { - Stack { - ConstrainedBox { - clamp: match this.pos { - Position::Top | Position::Bottom => BoxClamp::fixed_height(extent), - Position::Left | Position::Right => BoxClamp::fixed_width(extent), - }, - Flex { - id: flex, - direction: match this.pos { - Position::Top | Position::Bottom => Direction::Horizontal, - Position::Left | Position::Right => Direction::Vertical, - }, + let mut flex = @Flex { + direction: pipe!(match $this.pos { + Position::Top | Position::Bottom => Direction::Horizontal, + Position::Left | Position::Right => Direction::Vertical, + }) + }; + let divider = @Divider { + direction: pipe!(match $this.pos { + Position::Top | Position::Bottom => Direction::Horizontal, + Position::Left | Position::Right => Direction::Vertical, + }), + left_anchor: pipe!(match $this.pos { + Position::Left => $flex.layout_size().width - 1., + Position::Top | Position::Right | Position::Bottom => 0., + }), + top_anchor: pipe!(match $this.pos { + Position::Top => $flex.layout_size().height - 1., + Position::Bottom | Position::Right | Position::Left => 0., + }), + }; + let mut indicator_decorator = @IndicatorDecorator { + pos: pipe!($this.pos), + extent: indicator.extent, + rect: Rect::zero() + }; + let mut header = @Stack { + @ConstrainedBox { + clamp: pipe!(match $this.pos { + Position::Top | Position::Bottom => BoxClamp::fixed_height(extent), + Position::Left | Position::Right => BoxClamp::fixed_width(extent), + }), + @ $flex { + @ { Multi::new( Tabs::tab_header( - headers, tabs_style.clone(), - no_watch!(this.clone_stateful()), - no_watch!(indicator_decorator.clone_stateful()), + headers, tabs_style, + this.clone_writer(), + indicator_decorator.clone_writer() ) ) } } - Divider { - direction: match this.pos { - Position::Top | Position::Bottom => Direction::Horizontal, - Position::Left | Position::Right => Direction::Vertical, - }, - left_anchor: match this.pos { - Position::Left => flex.layout_size().width - 1., - Position::Top | Position::Right | Position::Bottom => 0., - }, - top_anchor: match this.pos { - Position::Top => flex.layout_size().height - 1., - Position::Bottom | Position::Right | Position::Left => 0., - }, - } - IndicatorDecorator { - id: indicator_decorator, - pos: this.pos, - extent: indicator.extent, - rect: Rect::zero(), - Container { - background: active_color.clone(), - size: match this.pos { - Position::Top | Position::Bottom => indicator.measure.map_or( - Size::new(indicator_decorator.rect.width(), indicator.extent), - |measure| Size::new(measure, indicator.extent) - ), - Position::Left | Position::Right => indicator.measure.map_or( - Size::new(indicator.extent, indicator_decorator.rect.height()), - |measure| Size::new(indicator.extent, measure) - ), - } - } + } + @ { divider } + @ $indicator_decorator { + @ Container { + background: active_color, + size: pipe!(match $this.pos { + Position::Top | Position::Bottom => indicator.measure.map_or( + Size::new($indicator_decorator.rect.width(), indicator.extent), + |measure| Size::new(measure, indicator.extent) + ), + Position::Left | Position::Right => indicator.measure.map_or( + Size::new(indicator.extent, $indicator_decorator.rect.height()), + |measure| Size::new(indicator.extent, measure) + ), + }) } } }; - } - TabsDecorator { - Flex { - direction: match this.pos { + + @TabsDecorator { + @Flex { + direction: pipe!(match $this.pos { Position::Left | Position::Right => Direction::Horizontal, Position::Top | Position::Bottom => Direction::Vertical, + }), + reverse: pipe!{ + let pos = $this.pos; + matches!(pos, Position::Right | Position::Bottom) }, - reverse: matches!(this.silent_ref().pos, Position::Right | Position::Bottom), - widget::from(header) - Multi::new(panes) + @ { header } + @ { Multi::new(panes) } } } } diff --git a/widgets/src/text.rs b/widgets/src/text.rs index a8cf3b4c1..38c7dfdad 100644 --- a/widgets/src/text.rs +++ b/widgets/src/text.rs @@ -7,7 +7,7 @@ use ribir_core::{ }; /// The text widget display text with a single style. -#[derive(Debug, Declare, Clone, PartialEq)] +#[derive(Debug, Declare, Declare2, Clone, PartialEq)] pub struct Text { #[declare(convert=into)] pub text: CowArc, @@ -24,7 +24,7 @@ pub struct Text { } impl Text { - pub fn text_layout(&self, t_store: &TypographyStore, bound: Size) -> VisualGlyphs { + pub fn text_layout(&self, bound: Size) -> VisualGlyphs { let TextStyle { font_size, letter_space, @@ -35,8 +35,7 @@ impl Text { let width: Em = Pixel(bound.width.into()).into(); let height: Em = Pixel(bound.height.into()).into(); - - t_store.typography( + AppCtx::typography_store().typography( self.text.substr(..), font_size, font_face, @@ -54,11 +53,7 @@ impl Text { impl Render for Text { fn perform_layout(&self, clamp: BoxClamp, _: &mut LayoutCtx) -> Size { - self - .text_layout(AppCtx::typography_store(), clamp.max) - .visual_rect() - .size - .cast_unit() + self.text_layout(clamp.max).visual_rect().size.cast_unit() } #[inline] @@ -135,11 +130,11 @@ impl_query_self_only!(Text); macro_rules! define_text_with_theme_style { ($name: ident, $style: ident) => { - #[derive(Declare)] + #[derive(Declare, Declare2)] pub struct $name { #[declare(convert=into)] pub text: CowArc, - #[declare(default = Brush::Color(Palette::of(ctx).on_surface_variant()), convert = into)] + #[declare(default = Palette::of(ctx).on_surface_variant(), convert = into)] pub foreground: Brush, #[declare(default)] pub overflow: Overflow, @@ -147,16 +142,12 @@ macro_rules! define_text_with_theme_style { impl Compose for $name { fn compose(this: State) -> Widget { - widget! { - init ctx => { - let text_style = TypographyTheme::of(ctx).$style.text.clone(); - } - states { this: this.into_readonly() } - Text { - text: this.text.clone(), - foreground: this.foreground.clone(), - text_style, - overflow: this.overflow, + fn_widget! { + @Text { + text: pipe!($this.text.clone()), + foreground: pipe!($this.foreground.clone()), + text_style: TypographyTheme::of(ctx).$style.text.clone(), + overflow: pipe!($this.overflow), } } .into() diff --git a/widgets/src/text_field.rs b/widgets/src/text_field.rs index 9cf63c194..04df03a2c 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(this: State, child: Self::Child) -> Widget { + fn_widget! { + @ $child { on_tap: move |_| { + let mut this = $this.write(); match this.state { TextFieldState::Enabled => this.state = TextFieldState::Focused, TextFieldState::Hovered => this.state = TextFieldState::Focused, _ => (), }; }, - on_pointer_move: move |_| { + let mut this = $this.write(); if this.state == TextFieldState::Enabled { this.state = TextFieldState::Hovered } }, - on_pointer_leave: move |_| { + let mut this = $this.write(); if this.state == TextFieldState::Hovered { this.state = TextFieldState::Enabled } }, on_focus_out: move |_| { + let mut this = $this.write(); if this.state == TextFieldState::Focused { this.state = TextFieldState::Enabled } }, } @@ -291,59 +281,48 @@ 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(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, - } + }.into_inner(); + let indicator_size = pipe!(Size::new(f32::MAX, $theme.indicator_height)); + let indicator_bg = pipe!($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, theme, config) } + } + @{ + trailing_icon.map(|t| @Icon { + size: IconSize::of(ctx!()).small, + @{ t.child() } + }) } + } + @Container { + v_align: VAlign::Bottom, + size: indicator_size, + background: indicator_bg, + } } } .into() @@ -351,72 +330,62 @@ impl ComposeChild for TextField { } fn build_input_area( - this: &mut StateRef, - theme: &mut StateRef, + this: State, + theme: State, prefix: Option, suffix: Option, placeholder: Option, ) -> Widget { - fn text_label(text: CowArc, theme: StateRef) -> 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 = map_writer!($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.write().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.write().set_text(val)); + + let h = watch!($theme.state) + .distinct_until_changed() + .filter(|state| state == &TextFieldState::Focused) + .subscribe(move |_| $input.request_focus()); + input.as_stateful().unsubscribe_on_drop(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, @@ -424,96 +393,48 @@ struct TextFieldLabel { 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_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, - } + map_writer!($this.style.font_size) + .transition(transitions::LINEAR.of(ctx!()), ctx!()); + + label } .into() } } fn build_content_area( - this: &mut StateRef, - theme: &mut StateRef, + this: State, + theme: State, 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())), + }; + + map_writer!($content_area.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, theme, 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() - } -}