diff --git a/core/src/animation/animate.rs b/core/src/animation/animate.rs index a92d37e63..19e7e1cd9 100644 --- a/core/src/animation/animate.rs +++ b/core/src/animation/animate.rs @@ -11,23 +11,23 @@ where pub transition: T, #[declare(strict)] pub state: S, - pub from: ::V, + pub from: ::Value, #[declare(skip)] - running_info: Option::V>>, + running_info: Option::Value>>, #[declare(skip, default = ctx.window().id())] window_id: WindowId, } pub trait AnimateState { - type State: Share; - fn state(&mut self) -> &mut Self::State; + type State: StateWriter; + fn state(&self) -> &Self::State; fn calc_lerp_value( &mut self, - from: &::V, - to: &::V, + from: &::Value, + to: &::Value, rate: f32, - ) -> ::V; + ) -> ::Value; } /// A state with a lerp function as an animation state that use the `lerp_fn` @@ -55,13 +55,13 @@ impl State> where T: Roc + 'static, S: AnimateState + 'static, - ::V: Clone, + ::Value: Clone, { pub fn run(&mut self) { - let mut animate_ref = self.state_ref(); + let mut animate_ref = self.write(); let this = &mut *animate_ref; let wnd_id = this.window_id; - let new_to = this.state.state().state_ref().clone(); + 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()); @@ -69,33 +69,36 @@ where } else if let Some(wnd) = AppCtx::get_window(wnd_id) { drop(animate_ref); - let animate = self.clone_state(); + 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.state_ref().shallow().rate_at_instant(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_state(); + let animate = animate.clone_writer(); wnd - .frame_spawn(async move { animate.state_ref().silent().stop() }) + .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 mut animate = animate.state_ref(); - let animate = &mut **animate.silent(); + let animate = &mut *animate.silent(); let info = animate.running_info.as_mut().unwrap(); - **animate.state.state().state_ref().shallow() = info.to.clone(); + *animate.state.state().shallow() = info.to.clone(); info.already_lerp = false; } } }); let guard = BoxSubscription::new(unsub).unsubscribe_when_dropped(); - let animate = &mut *self.state_ref(); + let animate = &mut *self.write(); animate.running_info = Some(AnimateInfo { from: animate.from.clone(), to: new_to, @@ -113,9 +116,9 @@ impl Animate where S: AnimateState + 'static, { - fn rate_at_instant(&mut self, now: Instant) -> AnimateProgress + fn lerp_by_instant(&mut self, now: Instant) -> AnimateProgress where - ::V: Clone, + ::Value: Clone, { let AnimateInfo { from, @@ -139,12 +142,12 @@ where match progress { AnimateProgress::Between(rate) => { let value = self.state.calc_lerp_value(from, to, rate); - let state = &mut self.state.state().state_ref(); + let state = &mut self.state.state().shallow(); // the state may change during animate. *to = state.clone(); - **state.shallow() = value; + **state = value; } - AnimateProgress::Dismissed => **self.state.state().state_ref().shallow() = from.clone(), + AnimateProgress::Dismissed => *self.state.state().shallow() = from.clone(), AnimateProgress::Finish => {} } @@ -180,48 +183,34 @@ where } } -impl AnimateState for S +impl AnimateState for S where - S: Share, - S::V: Lerp, + S: StateWriter, + V: Lerp, { type State = S; - fn state(&mut self) -> &mut Self::State { self } + fn state(&self) -> &Self::State { self } - fn calc_lerp_value( - &mut self, - from: &::V, - to: &::V, - rate: f32, - ) -> ::V { - from.lerp(to, rate) - } + fn calc_lerp_value(&mut self, from: &V, to: &V, rate: f32) -> V { from.lerp(to, rate) } } -impl AnimateState for LerpFnState +impl AnimateState for LerpFnState where - S: Share, - F: FnMut(&::V, &::V, f32) -> ::V, + S: StateWriter, + F: FnMut(&V, &V, f32) -> V, { type State = S; - fn state(&mut self) -> &mut Self::State { &mut self.state } + 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) - } + fn calc_lerp_value(&mut self, from: &V, to: &V, rate: f32) -> V { (self.lerp_fn)(from, to, rate) } } -impl LerpFnState +impl LerpFnState where - S: Share, - F: FnMut(&::V, &::V, f32) -> ::V, + S: StateReader, + F: FnMut(&V, &V, f32) -> V, { #[inline] pub fn new(state: S, lerp_fn: F) -> Self { Self { state, lerp_fn } } diff --git a/core/src/animation/transition.rs b/core/src/animation/transition.rs index 234ce07a6..ffb038ff9 100644 --- a/core/src/animation/transition.rs +++ b/core/src/animation/transition.rs @@ -18,35 +18,30 @@ pub struct Transition { /// Trait help to transition the state. pub trait TransitionState: Sized + 'static { /// Use an animate to transition the state after it modified. - fn transition( - mut self, - transition: T, - ctx: &BuildCtx, - ) -> State> + fn transition(self, transition: T, ctx: &BuildCtx) -> Writer> where Self: AnimateState, - ::V: Clone, + ::Value: Clone, { - let state = self.state().clone_state(); - - let from = state.state_ref().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_state(); - let h = state + let c_animate = animate.clone_writer(); + let init_value = observable::of(state.read().clone()); + state .modifies() - .map(move |_| state.state_ref().clone()) + .map(move |_| state.read().clone()) + .merge(init_value) .pairwise() .subscribe(move |(old, _)| { - animate.state_ref().from = old; + animate.write().from = old; animate.run(); - }) - .unsubscribe_when_dropped(); - c_animate.own_data(h); + }); c_animate } @@ -56,11 +51,11 @@ pub trait TransitionState: Sized + 'static { transition: T, lerp_fn: F, ctx: &BuildCtx, - ) -> State>> + ) -> Writer>> where - Self: Share, - Self::V: Clone, - F: FnMut(&Self::V, &Self::V, f32) -> Self::V + 'static, + 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); @@ -101,20 +96,19 @@ impl Roc for Transition { } } -impl Roc for T +impl Roc for T where - T::V: Roc, + T::Value: Roc, { - fn rate_of_change(&self, dur: Duration) -> AnimateProgress { - self.state_ref().rate_of_change(dur) - } + fn rate_of_change(&self, dur: Duration) -> AnimateProgress { self.read().rate_of_change(dur) } } -impl TransitionState for S {} -impl TransitionState for LerpFnState +impl TransitionState for S {} + +impl TransitionState for LerpFnState where - S: Share + 'static, - F: FnMut(&::V, &::V, f32) -> ::V + 'static, + S: StateWriter + 'static, + F: FnMut(&V, &V, f32) -> V + 'static, { } diff --git a/core/src/builtin_widgets.rs b/core/src/builtin_widgets.rs index b0a0a0716..abed9e403 100644 --- a/core/src/builtin_widgets.rs +++ b/core/src/builtin_widgets.rs @@ -7,8 +7,8 @@ pub mod key; pub use key::{Key, KeyWidget}; pub mod image_widget; pub use image_widget::*; -pub mod delay_drop_widget; -pub use delay_drop_widget::DelayDropWidget; +pub mod delay_drop; +pub use delay_drop::DelayDrop; mod theme; pub use theme::*; mod cursor; @@ -158,7 +158,7 @@ impl_builtin_obj!( Visibility, Opacity, LifecycleListener, - DelayDropWidget + DelayDrop ); /// A fat object that extend the `T` object with all builtin widgets ability. A @@ -198,7 +198,7 @@ impl ComposeChild for FatObj> { type Child = T::Child; fn compose_child(this: State, child: Self::Child) -> Widget { - let this = this.into_inner_value(); + let this = this.into_value(); let Self { host, builtin } = this; FnWidget::new(move |ctx| { let this = host.with_child(child, ctx); @@ -212,7 +212,7 @@ impl ComposeChild for FatObj { type Child = T::Child; fn compose_child(this: State, child: Self::Child) -> Widget { - let this = this.into_inner_value(); + let this = this.into_value(); let Self { host, builtin } = this; FnWidget::new(move |ctx| { let this = host.with_child(child, ctx); @@ -242,7 +242,7 @@ impl ComposeChild for BuiltinObj { type Child = Widget; fn compose_child(this: State, child: Self::Child) -> Widget { - let this = this.into_inner_value(); + let this = this.into_value(); fn_widget! { this.compose_with_host(child, ctx!()) }.into() } } diff --git a/core/src/builtin_widgets/cursor.rs b/core/src/builtin_widgets/cursor.rs index 3081b00d6..af0fad5ea 100644 --- a/core/src/builtin_widgets/cursor.rs +++ b/core/src/builtin_widgets/cursor.rs @@ -20,7 +20,7 @@ impl ComposeChild for Cursor { && e.mouse_buttons() == MouseButtons::empty() { let wnd = e.window(); - *$save_cursor = wnd.get_cursor(); + *$save_cursor.write() = wnd.get_cursor(); wnd.set_cursor($this.get_cursor()); } }, diff --git a/core/src/builtin_widgets/delay_drop.rs b/core/src/builtin_widgets/delay_drop.rs new file mode 100644 index 000000000..44d77cde8 --- /dev/null +++ b/core/src/builtin_widgets/delay_drop.rs @@ -0,0 +1,29 @@ +use crate::{impl_query_self_only, prelude::*}; + +/// A widget that can delay drop its child until the `delay_drop_until` field be +/// set to `true`. +/// +/// This widget not effect the widget lifecycle, if the widget is dispose but +/// the `delay_drop_until` is `false`, it's not part of the widget tree anymore +/// but not drop immediately, is disposed in `logic`, but not release resource. +/// It's be isolated from the widget tree and can layout and paint normally. +/// +/// Once the `delay_drop_until` field be set to `true`, the widget will be +/// dropped. +/// +/// It's useful when you need run a leave animation for a widget. +#[derive(Declare, Declare2)] +pub struct DelayDrop { + #[declare(builtin)] + pub delay_drop_until: bool, +} + +impl ComposeChild for DelayDrop { + type Child = Widget; + #[inline] + fn compose_child(this: State, child: Self::Child) -> Widget { + DataWidget::attach(child, this.into_writable()) + } +} + +impl_query_self_only!(DelayDrop); diff --git a/core/src/builtin_widgets/delay_drop_widget.rs b/core/src/builtin_widgets/delay_drop_widget.rs deleted file mode 100644 index 9c11ff019..000000000 --- a/core/src/builtin_widgets/delay_drop_widget.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::{impl_query_self_only, prelude::*}; - -#[derive(Declare, Declare2)] -pub struct DelayDropWidget { - #[declare(builtin)] - pub delay_drop_until: bool, -} - -impl ComposeChild for DelayDropWidget { - type Child = Widget; - #[inline] - fn compose_child(this: State, child: Self::Child) -> Widget { - DataWidget::attach(child, this.into_writable()) - } -} - -impl_query_self_only!(DelayDropWidget); diff --git a/core/src/builtin_widgets/fitted_box.rs b/core/src/builtin_widgets/fitted_box.rs index 72685b373..ecb364ae0 100644 --- a/core/src/builtin_widgets/fitted_box.rs +++ b/core/src/builtin_widgets/fitted_box.rs @@ -100,7 +100,7 @@ mod tests { expected_scale, } = self; let fit = Stateful::new(FittedBox { box_fit, scale_cache: <_>::default() }); - let c_fit = fit.clone(); + let c_fit = fit.clone_reader(); let w = fn_widget! { @$fit { @MockBox { size } } }; @@ -108,7 +108,7 @@ mod tests { wnd.draw_frame(); assert_layout_result_by_path!(wnd, {path = [0], size == expect,} ); - assert_eq!(c_fit.state_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 df20125b2..ba96f0543 100644 --- a/core/src/builtin_widgets/focus_node.rs +++ b/core/src/builtin_widgets/focus_node.rs @@ -59,10 +59,10 @@ impl ComposeChild for FocusNode { subject }); - fn subscribe_fn(this: Stateful) -> impl FnMut(&'_ mut AllLifecycle) + 'static { + fn subscribe_fn(this: Reader) -> impl FnMut(&'_ mut AllLifecycle) + 'static { move |e| match e { AllLifecycle::Mounted(e) => { - let auto_focus = this.state_ref().auto_focus; + let auto_focus = this.read().auto_focus; e.window().add_focus_node(e.id, auto_focus, FocusType::Node) } AllLifecycle::PerformedLayout(_) => {} @@ -70,7 +70,7 @@ 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, &mut *ctx.tree.borrow_mut(), |child| { @@ -104,7 +104,7 @@ pub struct RequestFocus { impl ComposeChild for RequestFocus { type Child = Widget; fn compose_child(this: State, child: Self::Child) -> Widget { - let this2 = this.clone_state(); + let this2 = this.clone_reader(); let w: Widget = fn_widget! { @$child { on_mounted: move |e| { @@ -114,7 +114,7 @@ impl ComposeChild for RequestFocus { } } .into(); - DataWidget::attach_state(w, this2) + DataWidget::attach(w, this2) } } impl RequestFocus { diff --git a/core/src/builtin_widgets/has_focus.rs b/core/src/builtin_widgets/has_focus.rs index 8320392da..39a21792b 100644 --- a/core/src/builtin_widgets/has_focus.rs +++ b/core/src/builtin_widgets/has_focus.rs @@ -14,8 +14,8 @@ impl ComposeChild for HasFocus { fn compose_child(this: State, child: Self::Child) -> Widget { fn_widget! { @ $child { - on_focus_in: move|_| $this.focused = true, - on_focus_out: move |_| $this.focused = false, + on_focus_in: move|_| $this.write().focused = true, + on_focus_out: move |_| $this.write().focused = false, } } .into() diff --git a/core/src/builtin_widgets/key.rs b/core/src/builtin_widgets/key.rs index 23b881e25..a8a6fe3cf 100644 --- a/core/src/builtin_widgets/key.rs +++ b/core/src/builtin_widgets/key.rs @@ -69,7 +69,7 @@ where V: Default + Clone + PartialEq, Self: Any, { - fn key(&self) -> Key { self.state_ref().key.clone() } + fn key(&self) -> Key { self.write().key.clone() } fn record_prev_key_widget(&self, key: &dyn AnyKey) { assert_eq!(self.key(), key.key()); @@ -77,12 +77,10 @@ where log::warn!("Different value type for same key."); return; }; - self - .state_ref() - .record_before_value(key.state_ref().value.clone()); + self.write().record_before_value(key.read().value.clone()); } - fn record_next_key_widget(&self, _: &dyn AnyKey) { self.state_ref().has_successor.set(true); } + fn record_next_key_widget(&self, _: &dyn AnyKey) { self.write().has_successor.set(true); } fn as_any(&self) -> &dyn Any { self } } diff --git a/core/src/builtin_widgets/lifecycle.rs b/core/src/builtin_widgets/lifecycle.rs index a39bbb09f..febe15eb6 100644 --- a/core/src/builtin_widgets/lifecycle.rs +++ b/core/src/builtin_widgets/lifecycle.rs @@ -143,23 +143,23 @@ mod tests { let trigger = Stateful::new(true); let lifecycle = Stateful::new(vec![]); - let c_lc = lifecycle.clone_state(); - let c_trigger = trigger.clone_state(); + let c_lc = lifecycle.clone_reader(); + let c_trigger = trigger.clone_writer(); let w = fn_widget! { @MockBox { size: Size::zero(), - on_mounted: move |_| $lifecycle.push("static mounted"), - on_performed_layout: move |_| $lifecycle.push("static performed layout"), - on_disposed: move |_| $lifecycle.push("static 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.push("dyn mounted"), - on_performed_layout: move |_| $lifecycle.push("dyn performed layout"), - on_disposed: move |_| $lifecycle.push("dyn disposed") + 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") } }) }) @@ -168,12 +168,12 @@ mod tests { }; let mut wnd = TestWindow::new_with_size(w, Size::new(100., 100.)); - assert_eq!(&**c_lc.state_ref(), ["static mounted", "dyn mounted",]); + assert_eq!(&**c_lc.read(), ["static mounted", "dyn mounted",]); wnd.draw_frame(); assert_eq!( - &**c_lc.state_ref(), + &**c_lc.read(), [ "static mounted", "dyn mounted", @@ -182,11 +182,11 @@ mod tests { ] ); { - *c_trigger.state_ref() = false; + *c_trigger.write() = false; } wnd.draw_frame(); assert_eq!( - &**c_lc.state_ref(), + &**c_lc.read(), [ "static mounted", "dyn mounted", @@ -206,17 +206,17 @@ mod tests { let mounted: Stateful> = Stateful::new(HashSet::default()); let disposed: Stateful> = Stateful::new(HashSet::default()); - let c_cnt = cnt.clone_state(); - let c_mounted = mounted.clone_state(); - let c_disposed = disposed.clone_state(); + 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.insert(e.id); }, - on_disposed: move |e| { $disposed.insert(e.id); }, + on_mounted: move |e| { $mounted.write().insert(e.id); }, + on_disposed: move |e| { $disposed.write().insert(e.id); }, }); Multi::new(iter) }) @@ -226,13 +226,13 @@ mod tests { let mut wnd = TestWindow::new_with_size(w, Size::new(100., 100.)); wnd.draw_frame(); - let mounted_ids = (*c_mounted.state_ref()).clone(); + let mounted_ids = c_mounted.read().clone(); - *c_cnt.state_ref() = 5; + *c_cnt.write() = 5; wnd.on_wnd_resize_event(Size::zero()); wnd.draw_frame(); assert_eq!(mounted_ids.len(), 3); - assert_eq!(&mounted_ids, &*c_disposed.state_ref()); + assert_eq!(&mounted_ids, &*c_disposed.read()); } } diff --git a/core/src/builtin_widgets/mouse_hover.rs b/core/src/builtin_widgets/mouse_hover.rs index 5d5531dff..29e7726e1 100644 --- a/core/src/builtin_widgets/mouse_hover.rs +++ b/core/src/builtin_widgets/mouse_hover.rs @@ -15,8 +15,8 @@ impl ComposeChild for MouseHover { fn compose_child(this: State, child: Self::Child) -> Widget { fn_widget! { @ $child { - on_pointer_enter: move |_| $this.hover = true, - on_pointer_leave: move |_| $this.hover = false, + on_pointer_enter: move |_| $this.write().hover = true, + on_pointer_leave: move |_| $this.write().hover = false, } } .into() diff --git a/core/src/builtin_widgets/pointer_pressed.rs b/core/src/builtin_widgets/pointer_pressed.rs index 71314f9ce..3a70daccd 100644 --- a/core/src/builtin_widgets/pointer_pressed.rs +++ b/core/src/builtin_widgets/pointer_pressed.rs @@ -19,8 +19,8 @@ impl ComposeChild for PointerPressed { fn compose_child(this: State, child: Self::Child) -> Widget { fn_widget! { @ $child { - on_pointer_down: move|_| $this.pointer_pressed = true, - on_pointer_up: move |_| $this.pointer_pressed = false, + 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 75d64e23b..a470617db 100644 --- a/core/src/builtin_widgets/scrollable.rs +++ b/core/src/builtin_widgets/scrollable.rs @@ -40,21 +40,21 @@ impl ComposeChild for ScrollableWidget { 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), - }; + 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.set_content_size(v)); - watch!($view.layout_size()) - .distinct_until_changed() - .subscribe(move |v| $this.set_page(v)); + 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.validate_scroll(Point::new(e.delta_x, e.delta_y)), + on_wheel: move |e| $this.write().validate_scroll(Point::new(e.delta_x, e.delta_y)), @ { child } } } diff --git a/core/src/builtin_widgets/theme.rs b/core/src/builtin_widgets/theme.rs index 17b1b9476..8de5418fd 100644 --- a/core/src/builtin_widgets/theme.rs +++ b/core/src/builtin_widgets/theme.rs @@ -78,7 +78,7 @@ impl ComposeChild for ThemeWidget { use crate::prelude::*; FnWidget::new(move |ctx| { let ctx = ctx.force_as_mut(); - let theme = this.state_ref().theme.clone(); + let theme = this.read().theme.clone(); AppCtx::load_font_from_theme(&theme); ctx.push_theme(theme.clone()); diff --git a/core/src/context/app_ctx.rs b/core/src/context/app_ctx.rs index 176ee2a1c..b502a5369 100644 --- a/core/src/context/app_ctx.rs +++ b/core/src/context/app_ctx.rs @@ -47,8 +47,11 @@ pub struct AppCtx { runtime_waker: Box, scheduler: FuturesLocalScheduler, executor: RefCell, + triggers: 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; @@ -128,6 +131,33 @@ 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 { + if let Some(task) = Self::shared().triggers.borrow_mut().remove(&trigger) { + 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] @@ -210,7 +240,7 @@ impl AppCtx { /// If your application want create multi `AppCtx` instances, hold a scope for /// every instance. Otherwise, the behavior is undefined. #[track_caller] - pub unsafe fn new_lock_scope() -> MutexGuard<'static, ()> { + pub unsafe fn new_lock_scope() -> AppCtxScopeGuard { static LOCK: Mutex<()> = Mutex::new(()); let locker = LOCK.lock().unwrap_or_else(|e| { @@ -220,14 +250,14 @@ impl AppCtx { e.into_inner() }); - APP_CTX_INIT = Once::new(); - locker + + AppCtxScopeGuard(locker) } #[track_caller] pub(crate) fn end_frame() { - // todo: frame cache is not a good choice? because not every text will relayout - // in every frame. + // todo: frame cache is not a good algorithm? because not every text will + // relayout in every frame. let ctx = unsafe { Self::shared_mut() }; ctx.shaper.end_frame(); ctx.reorder.end_frame(); @@ -260,6 +290,7 @@ impl AppCtx { 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()); @@ -385,6 +416,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 e22776bb6..0fd9ad573 100644 --- a/core/src/context/build_context.rs +++ b/core/src/context/build_context.rs @@ -93,7 +93,7 @@ impl<'a> BuildCtx<'a> { /// and fire mount events. pub(crate) fn on_widget_mounted(&self, id: WidgetId) { self.assert_get(id).query_all_type( - |notifier: &Modifier| { + |notifier: &Notifier| { let state_changed = self.tree.borrow().dirty_set.clone(); notifier .raw_modifies() @@ -186,7 +186,7 @@ mod tests { struct LightDarkThemes(Rc>>); let themes: Stateful>> = Stateful::new(vec![]); - let c_themes = themes.clone(); + let c_themes = themes.clone_writer(); let light_palette = Palette { brightness: Brightness::Light, ..Default::default() @@ -213,7 +213,7 @@ mod tests { size: ZERO_SIZE, @ { FnWidget::new(move |ctx: &BuildCtx| { - *$c_themes = ctx.themes().clone(); + *$c_themes.write() = ctx.themes().clone(); Void }) } diff --git a/core/src/data_widget.rs b/core/src/data_widget.rs index a36f25a11..e2449ce6a 100644 --- a/core/src/data_widget.rs +++ b/core/src/data_widget.rs @@ -29,12 +29,8 @@ impl DataWidget { pub fn attach_state(widget: Widget, data: State) -> Widget { match data.0.into_inner() { InnerState::Data(data) => { - let (data, slot) = data.into_inner(); - let mut widget = DataWidget::attach(widget, data); - if let Some(slot) = slot { - widget = DataWidget::attach(widget, AnonymousData::new(Box::new(slot))); - } - widget + let data = data.into_inner(); + DataWidget::attach(widget, data) } InnerState::Stateful(data) => DataWidget::attach(widget, data), } diff --git a/core/src/declare.rs b/core/src/declare.rs index bff6d31f0..c48f24157 100644 --- a/core/src/declare.rs +++ b/core/src/declare.rs @@ -18,6 +18,8 @@ pub trait Declare2 { /// document](declare) to know how to use it. pub trait DeclareBuilder { type 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; } diff --git a/core/src/events/dispatcher.rs b/core/src/events/dispatcher.rs index b5c10476f..95be391c2 100644 --- a/core/src/events/dispatcher.rs +++ b/core/src/events/dispatcher.rs @@ -629,7 +629,7 @@ mod tests { 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), @@ -682,7 +682,7 @@ mod tests { 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 { diff --git a/core/src/events/focus_mgr.rs b/core/src/events/focus_mgr.rs index d5a8ea67d..cd76f338e 100644 --- a/core/src/events/focus_mgr.rs +++ b/core/src/events/focus_mgr.rs @@ -728,7 +728,7 @@ mod tests { reset_test_env!(); let visible = Stateful::new(Some(())); - let c_visible = visible.clone_state(); + let c_visible = visible.clone_writer(); let w = fn_widget! { @MockMulti{ @ { pipe! { @@ -745,10 +745,10 @@ mod tests { wnd.draw_frame(); - *c_visible.state_ref() = None; + *c_visible.write() = None; wnd.draw_frame(); - *c_visible.state_ref() = Some(()); + *c_visible.write() = Some(()); wnd.draw_frame(); assert_eq!(wnd.focus_mgr.borrow().focusing(), focus_id); diff --git a/core/src/lib.rs b/core/src/lib.rs index ab669a2dd..283a537d9 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -21,6 +21,7 @@ pub mod timer; pub mod widget; pub mod widget_children; pub mod window; +pub use rxrust; pub mod prelude { pub use crate::animation::*; @@ -56,9 +57,8 @@ pub mod prelude { pub use ribir_geom::*; #[doc(no_inline)] pub use ribir_macros::{ - ctx, fn_widget, include_svg, partial_state, pipe, rdl, route_state, set_build_ctx, watch, - widget, Declare, Declare2, Lerp, MultiChild, SingleChild, Template, _dollar_ಠ_ಠ, - ribir_expanded_ಠ_ಠ, + ctx, fn_widget, include_svg, map_writer, pipe, rdl, ribir_expanded_ಠ_ಠ, set_build_ctx, + split_writer, watch, widget, Declare, Declare2, Lerp, MultiChild, SingleChild, Template, }; #[doc(no_inline)] pub use ribir_painter::*; diff --git a/core/src/pipe.rs b/core/src/pipe.rs index 8fe2129cb..3e7bb8a93 100644 --- a/core/src/pipe.rs +++ b/core/src/pipe.rs @@ -376,7 +376,7 @@ impl MultiChild for Pipe {} #[cfg(test)] mod tests { use std::{ - cell::{Ref, RefCell}, + cell::{Cell, Ref}, rc::Rc, }; @@ -390,7 +390,7 @@ mod tests { reset_test_env!(); let size = Stateful::new(Size::zero()); - let c_size = size.clone_state(); + let c_size = size.clone_writer(); let w = fn_widget! { let p = pipe! { MockBox { size: *$size }}; @$p { @Void {} } @@ -401,7 +401,7 @@ mod tests { let ids = tree.root().descendants(&tree.arena).collect::>(); assert_eq!(ids.len(), 2); { - *c_size.state_ref() = Size::new(1., 1.); + *c_size.write() = Size::new(1., 1.); } tree.layout(Size::zero()); let new_ids = tree.root().descendants(&tree.arena).collect::>(); @@ -415,7 +415,7 @@ mod tests { reset_test_env!(); let size = Stateful::new(Size::zero()); - let c_size = size.clone_state(); + let c_size = size.clone_writer(); let w = fn_widget! { @MockBox { size: Size::zero(), @@ -431,7 +431,7 @@ mod tests { let ids = tree.root().descendants(&tree.arena).collect::>(); assert_eq!(ids.len(), 3); { - *c_size.state_ref() = Size::new(1., 1.); + *c_size.write() = Size::new(1., 1.); } tree.layout(Size::zero()); let new_ids = tree.root().descendants(&tree.arena).collect::>(); @@ -449,9 +449,9 @@ mod tests { let new_cnt = Stateful::new(0); let drop_cnt = Stateful::new(0); - let c_v = v.clone_state(); - let c_new_cnt = new_cnt.clone_state(); - let c_drop_cnt = drop_cnt.clone_state(); + 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 { @ { @@ -459,8 +459,8 @@ mod tests { let iter = v.into_iter().map(move |_| { @MockBox{ size: Size::zero(), - on_mounted: move |_| *$new_cnt += 1, - on_disposed: move |_| *$drop_cnt += 1 + on_mounted: move |_| *$new_cnt.write() += 1, + on_disposed: move |_| *$drop_cnt.write() += 1 } }); Multi::new(iter) @@ -472,18 +472,18 @@ mod tests { let mut wnd = TestWindow::new(w); wnd.on_wnd_resize_event(Size::zero()); wnd.draw_frame(); - assert_eq!(*c_new_cnt.state_ref(), 3); - assert_eq!(*c_drop_cnt.state_ref(), 0); + assert_eq!(*c_new_cnt.read(), 3); + assert_eq!(*c_drop_cnt.read(), 0); - c_v.state_ref().push(4); + c_v.write().push(4); wnd.draw_frame(); - assert_eq!(*c_new_cnt.state_ref(), 7); - assert_eq!(*c_drop_cnt.state_ref(), 3); + assert_eq!(*c_new_cnt.read(), 7); + assert_eq!(*c_drop_cnt.read(), 3); - c_v.state_ref().pop(); + c_v.write().pop(); wnd.draw_frame(); - assert_eq!(*c_new_cnt.state_ref(), 10); - assert_eq!(*c_drop_cnt.state_ref(), 7); + assert_eq!(*c_new_cnt.read(), 10); + assert_eq!(*c_drop_cnt.read(), 7); } #[test] @@ -492,20 +492,20 @@ mod tests { 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_state(); - let c_c_trigger = c_trigger.clone_state(); - let mnt_cnt2 = mnt_cnt.clone_state(); + 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 +=1, + on_mounted: move |_| *$mnt_cnt.write() +=1, @{ pipe!(*$c_trigger).map(move |_| { @MockBox { size: Size::zero(), - on_mounted: move |_| *$mnt_cnt +=1, + on_mounted: move |_| *$mnt_cnt.write() +=1, } }) } @@ -515,18 +515,16 @@ mod tests { let mut wnd = TestWindow::new(w); wnd.draw_frame(); - assert_eq!(*mnt_cnt2.state_ref(), 2); + assert_eq!(*mnt_cnt2.read(), 2); // trigger the parent update - *c_p_trigger.state_ref() = true; + *c_p_trigger.write() = true; wnd.run_frame_tasks(); // then trigger the child update. - *c_c_trigger.silent_ref() = true; + *c_c_trigger.write() = true; wnd.draw_frame(); - // fixme: we always let child pipe subscribe tick first. - // only the parent update will be executed. - assert_eq!(*mnt_cnt2.state_ref(), 4); + assert_eq!(*mnt_cnt2.read(), 4); } #[test] @@ -539,11 +537,11 @@ mod tests { let leave_list: Stateful> = Stateful::new(vec![]); let key_change: Stateful> = Stateful::new(KeyChange::default()); - let c_v = v.clone_state(); - let c_enter_list = enter_list.clone_state(); - let c_update_list = update_list.clone_state(); - let c_leave_list = leave_list.clone_state(); - let c_key_change = key_change.clone_state(); + 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 { @ { @@ -555,16 +553,16 @@ mod tests { size: Size::zero(), on_mounted: move |_| { if $key.is_enter() { - (*($c_enter_list)).push($key.value); + $c_enter_list.write().push($key.value); } if $key.is_changed() { - (*($c_update_list)).push($key.value); - *($c_key_change) = $key.get_change(); + $c_update_list.write().push($key.value); + *$c_key_change.write() = $key.get_change(); } }, on_disposed: move |_| if $key.is_leave() { - (*($c_leave_list)).push($key.value); + $c_leave_list.write().push($key.value); } } } @@ -590,7 +588,7 @@ mod tests { (*enter_list.state_ref()).clear(); // 2. add 1 item - c_v.state_ref().push((4, '4')); + c_v.write().push((4, '4')); wnd.on_wnd_resize_event(ZERO_SIZE); wnd.draw_frame(); @@ -605,7 +603,7 @@ mod tests { (*enter_list.state_ref()).clear(); // 3. update the second item - c_v.state_ref()[1].1 = 'b'; + c_v.write()[1].1 = 'b'; wnd.draw_frame(); let expect_vec = vec![]; @@ -627,7 +625,7 @@ mod tests { (*update_list.state_ref()).clear(); // 4. remove the second item - c_v.state_ref().remove(1); + c_v.write().remove(1); wnd.draw_frame(); let expect_vec = vec!['b']; assert_eq!((*leave_list.state_ref()), expect_vec); @@ -640,7 +638,7 @@ mod tests { (*leave_list.state_ref()).clear(); // 5. update the first item - c_v.state_ref()[0].1 = 'a'; + c_v.write()[0].1 = 'a'; wnd.draw_frame(); assert_eq!((*enter_list.state_ref()).len(), 0); @@ -664,15 +662,16 @@ mod tests { struct Task { mounted: u32, pin: bool, - paint_cnt: Rc>, - layout_cnt: Rc>, + paint_cnt: Rc>, + layout_cnt: Rc>, trigger: u32, wid: Option, } - fn build(item: Stateful) -> Widget { + fn build(item: Writer) -> Widget { + let item = item.into_inner(); widget! { - states { task: item.clone() } + states { task: item.clone_stateful() } TaskWidget { delay_drop_until: !task.pin, layout_cnt: task.layout_cnt.clone(), @@ -694,17 +693,17 @@ mod tests { #[derive(Declare)] struct TaskWidget { trigger: u32, - paint_cnt: Rc>, - layout_cnt: Rc>, + paint_cnt: Rc>, + layout_cnt: Rc>, } impl Render for TaskWidget { fn perform_layout(&self, _: BoxClamp, _: &mut LayoutCtx) -> Size { - *self.layout_cnt.borrow_mut() += 1; + self.layout_cnt.set(self.layout_cnt.get() + 1); Size::new(1., 1.) } - fn paint(&self, _: &mut PaintingCtx) { *self.paint_cnt.borrow_mut() += 1; } + fn paint(&self, _: &mut PaintingCtx) { self.paint_cnt.set(self.paint_cnt.get() + 1); } } impl_query_self_only!(TaskWidget); @@ -719,10 +718,13 @@ mod tests { .map(|_| Stateful::new(Task::default())) .collect::>(); let tasks = Stateful::new(tasks); - let c_tasks = tasks.clone_state(); + let c_tasks = tasks.clone_reader(); let w = fn_widget! { @MockMulti { - @ { pipe!(Multi::new($c_tasks.clone().into_iter().map(build)))} + @ { pipe!{ + let iter = $c_tasks.iter().map(|t| build(t.clone_writer())).collect::>(); + Multi::new(iter) + }} } }; @@ -737,71 +739,59 @@ mod tests { 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); + 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.borrow(); + 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.borrow(), 3); - assert_eq!(*removed[1].state_ref().paint_cnt.borrow(), 3); - assert_eq!( - *removed[0].state_ref().layout_cnt.borrow(), - first_layout_cnt - ); + 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.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(); + 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.borrow(), + removed[0].state_ref().layout_cnt.get(), 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!(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.borrow(), + 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.borrow(), 4); - assert_eq!(*removed[1].state_ref().paint_cnt.borrow(), 5); + 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.borrow(); + 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.borrow(), 4); - assert_eq!(*removed[1].state_ref().paint_cnt.borrow(), 5); - assert_eq!( - *removed[0].state_ref().layout_cnt.borrow(), - first_layout_cnt - ); + 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.borrow(); - let second_layout_cnt = *removed[1].state_ref().layout_cnt.borrow(); + 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.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[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.borrow(), + removed[1].state_ref().layout_cnt.get(), second_layout_cnt + 1, ); } @@ -814,8 +804,8 @@ mod tests { let child_destroy_until = Stateful::new(false); let grandson = Stateful::new(Some(())); let grandson_destroy_until = Stateful::new(false); - let c_child = child.clone_state(); - let c_child_destroy_until = child_destroy_until.clone_state(); + let c_child = child.clone_writer(); + let c_child_destroy_until = child_destroy_until.clone_writer(); let w = fn_widget! { @MockMulti { @@ -853,11 +843,11 @@ mod tests { wnd.draw_frame(); assert!(!grandson_id.is_dropped(&tree_arena(&wnd))); - c_child.state_ref().take(); + c_child.write().take(); wnd.draw_frame(); assert!(!grandson_id.is_dropped(&tree_arena(&wnd))); - *c_child_destroy_until.state_ref() = true; + *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 7bbc6cb31..b97c8e954 100644 --- a/core/src/state.rs +++ b/core/src/state.rs @@ -1,122 +1,124 @@ -mod partial_state; -mod readonly; -mod route_state; +mod map_state; +mod splitted_state; mod stateful; use std::{ - any::Any, cell::UnsafeCell, convert::Infallible, mem::MaybeUninit, ops::DerefMut, rc::Rc, + cell::UnsafeCell, + convert::Infallible, + mem::MaybeUninit, + ops::{Deref, DerefMut}, }; -pub use partial_state::*; -pub use readonly::*; -pub use route_state::*; +pub use map_state::*; use rxrust::{ops::box_it::BoxOp, subject::Subject}; +pub use splitted_state::*; pub use stateful::*; use crate::{ context::BuildCtx, - prelude::{AnonymousData, DataWidget, MultiChild, SingleChild}, + prelude::{MultiChild, SingleChild}, widget::{Compose, Render, WidgetBuilder, WidgetId}, }; -/// A trait that a state should need to implement. -pub trait Share: Sized { +/// The `StateReader` trait allows for reading, clone and map the state. +pub trait StateReader: Sized { /// The value type of this state. - type V; - /// The origin state of this state. - type Origin: Share; - /// The a reference type that can track the modifies of the state. - type Ref<'a>: RefShare + 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 the reference of the state. - fn state_ref(&'_ self) -> Self::Ref<'_>; - - /// Clone the state and return a new one. The returned state should be the - /// same as the original one, if the original one is not shareable should - /// convert to a shareable one. - fn clone_state(&self) -> Self; - - /// Create a partial state of the state by what the `R` route to. This return - /// state let you can directly access the part of data what the `R` route to. - /// - /// The return state is a subset of the original state. When modifies the - /// origin state the return state will be notified. But when modifies the - /// return state the origin state will not be notified. - #[inline] - fn partial_state( - &self, - router: R1, - router_mut: R2, - ) -> PartialState + /// 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 - R1: FnOnce(&Self::V) -> &Target + Copy, - R2: FnOnce(&mut Self::V) -> &mut Target + Copy, + F: FnOnce(&Self::Value) -> &Target + Copy, { - PartialState::new(self.clone_state(), router, router_mut) + MapReader::new(self.clone_reader(), f) } - - /// Create a state that help you quick route to the part of the origin state - /// before you access it. The return state and the origin state are the same - /// state. So when one of them is modified, they will both be notified. - fn route_state( - &self, - router: R1, - router_mut: R2, - ) -> RouteState - where - R1: FnOnce(&Self::V) -> &Target + Copy, - R2: FnOnce(&mut Self::V) -> &mut Target + Copy, - { - RouteState::new(self.clone_state(), router, router_mut) - } - - /// Return the origin state of the state if this state is a partial state. - /// Otherwise return itself. - fn origin_state(&self) -> Self::Origin; - + /// 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, (), Infallible>; - fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible>; - - /// Store an anonymous data to the state, so we can keep the `data` live as - /// long as the state. - fn own_data(&self, data: impl Any); } -/// A trait that a reference of a share state should implement. -pub trait RefShare: DerefMut { - /// Convert this state reference to silent reference which notifies will be - /// ignored by the framework. - fn silent(&mut self) -> &mut Self; +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(&mut self) -> &mut Self; - /// Forget all modifies record in this reference. So all the modifies before - /// this call will not be notified. - fn forget_modifies(&self) -> ModifyScope; - - /// Makes a new reference of state for a component of the borrowed data. - fn map_state_ref( - self, - router: R1, - router_mut: R2, - ) -> RouteRef + 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 - Self: Sized, - R1: FnOnce(&Self::Target) -> &Target + Copy, - R2: FnOnce(&mut Self::Target) -> &mut Target + Copy, + R: FnOnce(&Self::Value) -> &Target + Copy, + W: FnOnce(&mut Self::Value) -> &mut Target + Copy, { - RouteRef::new(self, router, router_mut) + MapWriter::new(self.clone_writer(), read_map, writer_map) } } +pub trait RefWrite: DerefMut { + /// Forget all modifies of this reference. So all the modifies occurred on + /// this reference before this call will not be notified. Return true if there + /// is any modifies on this reference. + fn forget_modifies(&mut self) -> bool; +} + /// Enum to store both stateless and stateful object. pub struct State(pub(crate) UnsafeCell>); @@ -125,38 +127,54 @@ pub(crate) enum InnerState { Stateful(Stateful), } -impl Share for State { - type V = T; - type Origin = Self; - type Ref<'a> = RefState<'a, T> where Self: 'a; +impl StateReader for State { + type Value = T; + type OriginReader = Self; + type Reader = Reader; + type Ref<'a> = ReadRef<'a,T> + where + Self: 'a; - fn state_ref(&'_ self) -> RefState<'_, T> { + fn read(&'_ self) -> Self::Ref<'_> { match self.inner_ref() { - InnerState::Data(w) => RefState::new(w, ModifyScope::BOTH, None), - InnerState::Stateful(w) => w.state_ref(), + InnerState::Data(w) => w.read(), + InnerState::Stateful(w) => w.read(), } } - fn clone_state(&self) -> Self { Self::stateful(self.as_stateful().clone()) } + fn clone_reader(&self) -> Self::Reader { self.as_stateful().clone_reader() } #[inline] + fn origin_reader(&self) -> &Self::OriginReader { self } + fn modifies(&self) -> BoxOp<'static, (), Infallible> { self.as_stateful().modifies() } - #[inline] 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 origin_state(&self) -> Self::Origin { self.clone_state() } + fn write(&'_ self) -> Self::RefWrite<'_> { self.as_stateful().write() } #[inline] - fn own_data(&self, data: impl Any) { - match self.inner_ref() { - InnerState::Data(w) => w.own_data(data), - InnerState::Stateful(w) => w.own_data(data), - } - } + 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 {} @@ -166,14 +184,7 @@ impl From> for Box { #[inline] fn from(s: State) -> Self { match s.0.into_inner() { - InnerState::Data(w) => { - let (w, data) = w.into_inner(); - if let Some(d) = data { - DataWidget::new(w.into(), AnonymousData::new(Box::new(d))).into() - } else { - w.into() - } - } + InnerState::Data(w) => w.into_inner().into(), InnerState::Stateful(w) => w.into(), } } @@ -189,52 +200,29 @@ impl State { State(UnsafeCell::new(InnerState::Stateful(stateful))) } - pub fn from_value(value: W) -> Self { - State(UnsafeCell::new(InnerState::Data(StateData::new(value)))) - } + 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_inner_value(self) -> W { + pub fn into_value(self) -> W { match self.0.into_inner() { - InnerState::Data(w) => { - let (w, slot) = w.into_inner(); - assert!(slot.is_none(), "already attached data"); - w - } + 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.0.into_inner() { - InnerState::Data(w) => { - // ignore the slot, compatible with the old version. - let (w, _) = w.into_inner(); - Readonly::Stateless(Rc::new(w)) - } - InnerState::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() } + pub fn into_writable(self) -> Stateful { self.as_stateful().clone_stateful() } - pub fn clone_stateful(&mut self) -> Stateful { self.as_stateful().clone() } + pub fn clone_stateful(&mut self) -> Stateful { self.as_stateful().clone_stateful() } - fn as_stateful(&self) -> &Stateful { + pub fn as_stateful(&self) -> &Stateful { match self.inner_ref() { InnerState::Data(w) => { - // we can't convert a stateless value to stateful if others are using it. - if w.is_used() { - w.already_borrow_panic() - } + w.assert_is_not_used(); let mut uninit: MaybeUninit<_> = MaybeUninit::uninit(); // Safety: we already check there is no other reference to the state data. @@ -269,7 +257,7 @@ pub(crate) trait StateFrom { impl StateFrom for State { #[inline] - fn state_from(value: W) -> State { State::from_value(value) } + fn state_from(value: W) -> State { State::value(value) } } impl StateFrom> for State { @@ -288,6 +276,8 @@ where mod tests { use std::cell::Cell; + use ribir_algo::Sc; + use super::*; use crate::{context::AppCtx, prelude::*, reset_test_env, timer::Timer}; @@ -300,13 +290,13 @@ mod tests { fn path_state_router_test() { reset_test_env!(); - let origin = State::from_value(Origin { a: 0, b: 0 }); - let a = partial_state!($origin.a); - let b = route_state!($origin.b); + let origin = State::value(Origin { a: 0, b: 0 }); + let a = split_writer!($origin.a); + let b = map_writer!($origin.b); - let track_origin = Rc::new(Cell::new(0)); - let track_a = Rc::new(Cell::new(0)); - let track_b = Rc::new(Cell::new(0)); + let 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 |_| { @@ -323,7 +313,7 @@ mod tests { c_b.set(c_b.get() + 1); }); - origin.state_ref().a = 1; + origin.write().a = 1; Timer::wake_timeout_futures(); AppCtx::run_until_stalled(); @@ -331,7 +321,7 @@ mod tests { assert_eq!(track_a.get(), 1); assert_eq!(track_b.get(), 1); - *a.state_ref() = 1; + *a.write() = 1; Timer::wake_timeout_futures(); AppCtx::run_until_stalled(); @@ -339,7 +329,7 @@ mod tests { assert_eq!(track_a.get(), 2); assert_eq!(track_b.get(), 1); - *b.state_ref() = 1; + *b.write() = 1; Timer::wake_timeout_futures(); AppCtx::run_until_stalled(); diff --git a/core/src/state/map_state.rs b/core/src/state/map_state.rs new file mode 100644 index 000000000..4544c95e4 --- /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, (), 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, (), 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/partial_state.rs b/core/src/state/partial_state.rs deleted file mode 100644 index 416401c2f..000000000 --- a/core/src/state/partial_state.rs +++ /dev/null @@ -1,177 +0,0 @@ -use crate::context::AppCtx; - -use super::{Modifier, ModifyScope, RefShare, Share}; -use rxrust::{ - prelude::{ObservableItem, Observer}, - subject::Subject, - subscription::Subscription, -}; -use std::{any::Any, rc::Rc}; - -/// A partial state is a state that is a subset of another state. This state -/// share this part data with the original state. But its modifier is -/// independent with the original state. When modify this state, its original -/// state will not be notified. But when modify the original state, this state -/// will be notified. - -// Keep the `S` as the first generic, so the user know the actual state type -// when ide hints. -pub struct PartialState -where - T: Share, - R1: FnOnce(&T::V) -> &S + Copy, - R2: FnOnce(&mut T::V) -> &mut S + Copy, -{ - origin_state: T, - router: R1, - router_mut: R2, - modifier: Modifier, - connect_guard: Rc>, -} - -/// The reference of `OrphanState`. - -// Keep the `S` as the first generic, so the user know the actual state type -// when ide hints. -pub struct PartialRef -where - O: RefShare, - R1: FnOnce(&O::Target) -> &S + Copy, - R2: FnOnce(&mut O::Target) -> &mut S + Copy, -{ - origin_ref: O, - router: R1, - router_mut: R2, - modifier: Modifier, -} - -impl Share for PartialState -where - T: Share, - R1: FnOnce(&T::V) -> &S + Copy, - R2: FnOnce(&mut T::V) -> &mut S + Copy, -{ - type V = S; - type Origin = T; - type Ref<'a> = PartialRef, R1, R2> where Self: 'a; - - fn clone_state(&self) -> Self { - PartialState { - origin_state: self.origin_state.clone_state(), - router: self.router, - router_mut: self.router_mut, - modifier: self.modifier.clone(), - connect_guard: self.connect_guard.clone(), - } - } - - fn modifies(&self) -> rxrust::ops::box_it::BoxOp<'static, (), std::convert::Infallible> { - self.modifier.modifies() - } - - fn raw_modifies(&self) -> Subject<'static, ModifyScope, std::convert::Infallible> { - self.modifier.raw_modifies() - } - - #[inline] - fn origin_state(&self) -> Self::Origin { self.origin_state.clone_state() } - - #[inline] - fn state_ref(&'_ self) -> Self::Ref<'_> { - PartialRef { - origin_ref: self.origin_state.state_ref(), - router: self.router, - router_mut: self.router_mut, - modifier: self.modifier.clone(), - } - } - - #[inline] - fn own_data(&self, data: impl std::any::Any) { self.origin_state.own_data(data); } -} - -impl PartialState -where - T: Share, - R1: FnOnce(&T::V) -> &S + Copy, - R2: FnOnce(&mut T::V) -> &mut S + Copy, -{ - pub(super) fn new(state: T, router: R1, router_mut: R2) -> Self { - let modifier = Modifier::default(); - let c_modifier = modifier.clone(); - // todo: use connectable observable subscribe source after downstream - // subscribed. - let h = state - .raw_modifies() - .subscribe(move |v| c_modifier.raw_modifies().next(v)) - .unsubscribe_when_dropped(); - - Self { - origin_state: state, - router, - router_mut, - modifier, - connect_guard: Rc::new(Box::new(h)), - } - } -} - -impl RefShare for PartialRef -where - T: RefShare, - R1: FnOnce(&T::Target) -> &S + Copy, - R2: FnOnce(&mut T::Target) -> &mut S + Copy, -{ - #[inline] - fn silent(&mut self) -> &mut Self { - self.origin_ref.silent(); - self - } - - #[inline] - fn shallow(&mut self) -> &mut Self { - self.origin_ref.shallow(); - self - } - - #[inline] - fn forget_modifies(&self) -> ModifyScope { self.origin_ref.forget_modifies() } -} - -impl std::ops::Deref for PartialRef -where - T: RefShare, - R1: FnOnce(&T::Target) -> &S + Copy, - R2: FnOnce(&mut T::Target) -> &mut S + Copy, -{ - type Target = S; - #[inline] - #[track_caller] - fn deref(&self) -> &Self::Target { (self.router)(self.origin_ref.deref()) } -} - -impl std::ops::DerefMut for PartialRef -where - T: RefShare, - R1: FnOnce(&T::Target) -> &S + Copy, - R2: FnOnce(&mut T::Target) -> &mut S + Copy, -{ - #[inline] - #[track_caller] - fn deref_mut(&mut self) -> &mut Self::Target { (self.router_mut)(self.origin_ref.deref_mut()) } -} - -impl Drop for PartialRef -where - T: RefShare, - R1: FnOnce(&T::Target) -> &S + Copy, - R2: FnOnce(&mut T::Target) -> &mut S + Copy, -{ - fn drop(&mut self) { - let scope = self.origin_ref.forget_modifies(); - if !scope.is_empty() { - let mut subject = self.modifier.raw_modifies(); - AppCtx::spawn_local(async move { subject.next(scope) }).unwrap(); - } - } -} diff --git a/core/src/state/readonly.rs b/core/src/state/readonly.rs deleted file mode 100644 index 5b91f867c..000000000 --- a/core/src/state/readonly.rs +++ /dev/null @@ -1,96 +0,0 @@ -use super::{ModifyScope, RefState, Share, 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(RefState<'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.modifier.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.modifier.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(), - } - } -} - -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/route_state.rs b/core/src/state/route_state.rs deleted file mode 100644 index 72adfa1be..000000000 --- a/core/src/state/route_state.rs +++ /dev/null @@ -1,152 +0,0 @@ -use super::{ModifyScope, RefShare, Share}; -use rxrust::{ops::box_it::BoxOp, subject::Subject}; -use std::{convert::Infallible, ops::Deref}; - -/// A route state is shortcut state for the origin state. They are share the -/// same state, but the route state always route to the target of `P` before you -/// try to access. It's also have the same modifier with the origin state. -// Keep the `S` as the first generic, so the user know the actual state type -// when ide hints. -pub struct RouteState -where - T: Share, - R1: FnOnce(&T::V) -> &S + Copy, - R2: FnOnce(&mut T::V) -> &mut S + Copy, -{ - origin_state: T, - router: R1, - router_mut: R2, -} - -impl Share for RouteState -where - T: Share, - R1: FnOnce(&T::V) -> &S + Copy, - R2: FnOnce(&mut T::V) -> &mut S + Copy, -{ - type V = S; - type Origin = T; - type Ref<'a> = RouteRef, R1, R2> - where - Self: 'a; - - #[inline] - fn clone_state(&self) -> Self { - RouteState { - origin_state: self.origin_state.clone_state(), - router: self.router, - router_mut: self.router_mut, - } - } - - #[inline] - fn modifies(&self) -> BoxOp<'static, (), Infallible> { self.origin_state.modifies() } - - #[inline] - fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { - self.origin_state.raw_modifies() - } - - #[inline] - fn origin_state(&self) -> Self::Origin { self.origin_state.clone_state() } - - #[inline] - fn state_ref(&'_ self) -> Self::Ref<'_> { - RouteRef { - origin_ref: self.origin_state.state_ref(), - router: self.router, - router_mut: self.router_mut, - } - } - - #[inline] - fn own_data(&self, data: impl std::any::Any) { self.origin_state.own_data(data); } -} - -impl RouteState -where - T: Share, - R1: FnOnce(&T::V) -> &S + Copy, - R2: FnOnce(&mut T::V) -> &mut S + Copy, -{ - #[inline] - pub fn new(state: T, router: R1, router_mut: R2) -> Self { - Self { - origin_state: state, - router, - router_mut, - } - } -} - -/// The reference of `PartialState`. -// Keep the `S` as the first generic, so the user know the actual state type -// when ide hints. -pub struct RouteRef -where - T: RefShare, - R1: FnOnce(&T::Target) -> &S + Copy, - R2: FnOnce(&mut T::Target) -> &mut S + Copy, -{ - origin_ref: T, - router: R1, - router_mut: R2, -} - -impl RouteRef -where - T: RefShare, - R1: FnOnce(&T::Target) -> &S + Copy, - R2: FnOnce(&mut T::Target) -> &mut S + Copy, -{ - #[inline] - pub fn new(origin_ref: T, router: R1, router_mut: R2) -> Self { - Self { origin_ref, router, router_mut } - } -} - -impl Deref for RouteRef -where - T: RefShare, - R1: FnOnce(&T::Target) -> &S + Copy, - R2: FnOnce(&mut T::Target) -> &mut S + Copy, -{ - type Target = S; - - #[inline] - #[track_caller] - fn deref(&self) -> &Self::Target { (self.router)(self.origin_ref.deref()) } -} - -impl std::ops::DerefMut for RouteRef -where - T: RefShare, - R1: FnOnce(&T::Target) -> &S + Copy, - R2: FnOnce(&mut T::Target) -> &mut S + Copy, -{ - #[inline] - #[track_caller] - fn deref_mut(&mut self) -> &mut Self::Target { (self.router_mut)(self.origin_ref.deref_mut()) } -} - -impl RefShare for RouteRef -where - T: RefShare, - R1: FnOnce(&T::Target) -> &S + Copy, - R2: FnOnce(&mut T::Target) -> &mut S + Copy, -{ - #[inline] - fn silent(&mut self) -> &mut Self { - self.origin_ref.silent(); - self - } - - #[inline] - fn shallow(&mut self) -> &mut Self { - self.origin_ref.shallow(); - self - } - - #[inline] - fn forget_modifies(&self) -> ModifyScope { self.origin_ref.forget_modifies() } -} diff --git a/core/src/state/splitted_state.rs b/core/src/state/splitted_state.rs new file mode 100644 index 000000000..27e87bb0a --- /dev/null +++ b/core/src/state/splitted_state.rs @@ -0,0 +1,203 @@ +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, (), 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 5ead781da..e401481f4 100644 --- a/core/src/state/stateful.rs +++ b/core/src/state/stateful.rs @@ -1,39 +1,36 @@ 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, 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 { - pub(crate) inner: Rc>, - pub(crate) modifier: Modifier, + pub(crate) inner: Sc>, + pub(crate) notifier: Notifier, } -/// The modifier is a `RxRust` stream that emit notification when the state -/// changed. -#[derive(Default, Clone)] -pub struct Modifier(Subject<'static, ModifyScope, Infallible>); +pub struct Reader(Stateful); + +pub struct Writer(Stateful); -/// A reference of `Stateful which tracked the state change across if user -/// mutable deref this reference. -pub struct RefState<'a, W> { - ref_state: Cell, +pub struct ReadRef<'a, W>(Ref<'a, W>); + +pub struct WriteRef<'a, W> { + modified: bool, modify_scope: ModifyScope, - value: &'a StateData, - modifier: Option<&'a Modifier>, + value: RefMut<'a, W>, + batched_modify: &'a Sc>, + notifier: Option<&'a Notifier>, } -#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] -enum BorrowState { - #[default] - None, - Reading, - Writing, -} +/// 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, Default)] @@ -47,338 +44,305 @@ bitflags! { } } -impl Share for Stateful { - type V = W; - type Origin = Self; - type Ref<'a> = RefState<'a, W> - where - Self: 'a; +impl StateReader for Stateful { + type Value = W; + type OriginReader = Self; + type Reader = Reader; + type Ref<'a> = ReadRef<'a, W> where Self: 'a,; - /// Return a reference of `Stateful`, modify across this reference will notify - /// data and framework. #[inline] - fn state_ref(&self) -> RefState { - RefState::new(&self.inner, ModifyScope::BOTH, Some(&self.modifier)) - } + fn read(&self) -> ReadRef<'_, W> { self.inner.read() } #[inline] - fn clone_state(&self) -> Self { self.clone() } + fn clone_reader(&self) -> Self::Reader { Reader::from_stateful(self) } #[inline] - fn origin_state(&self) -> Self::Origin { self.clone() } + fn origin_reader(&self) -> &Self::OriginReader { self } #[inline] - fn modifies(&self) -> BoxOp<'static, (), Infallible> { self.modifier.modifies() } + fn modifies(&self) -> BoxOp<'static, (), Infallible> { self.notifier.modifies() } #[inline] fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { - self.modifier.raw_modifies() + self.notifier.raw_modifies() } +} + +impl StateWriter for Stateful { + type Writer = Writer; + type OriginWriter = Self; + type RefWrite<'a> = WriteRef<'a, W> where Self: 'a; + + fn write(&'_ self) -> Self::RefWrite<'_> { self.write_ref(ModifyScope::BOTH) } + + fn silent(&'_ self) -> Self::RefWrite<'_> { self.write_ref(ModifyScope::DATA) } + + fn shallow(&'_ self) -> Self::RefWrite<'_> { self.write_ref(ModifyScope::FRAMEWORK) } + + fn clone_writer(&self) -> Self::Writer { Writer::from_stateful(self) } + + fn origin_writer(&self) -> &Self::OriginWriter { self } +} + +impl StateReader for Reader { + type Value = W; + type OriginReader = Self; + type Reader = Self; + type Ref<'a> = ReadRef<'a, W> where W:'a; #[inline] - fn own_data(&self, data: impl Any) { self.inner.own_data(data); } + 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, (), Infallible> { self.0.modifies() } + + #[inline] + fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { self.0.raw_modifies() } } -impl<'a, W> RefShare for RefState<'a, W> { - /// Convert this state reference to silent which only notify the modifies - /// but be ignored by the framework. - fn silent(&mut self) -> &mut Self { - self.modify_scope.remove(ModifyScope::FRAMEWORK); - self - } +impl StateReader for Writer { + type Value = W; + type OriginReader = Self; + type Reader = Reader; + type Ref<'a> = ReadRef<'a, W> where W:'a; - fn shallow(&mut self) -> &mut Self { - self.modify_scope.remove(ModifyScope::DATA); - self + #[inline] + fn read(&'_ self) -> Self::Ref<'_> { self.0.read() } + + #[inline] + fn clone_reader(&self) -> Self::Reader { self.0.clone_reader() } + + #[inline] + fn origin_reader(&self) -> &Self::OriginReader { self } + + #[inline] + fn modifies(&self) -> BoxOp<'static, (), Infallible> { self.0.modifies() } + + #[inline] + fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { self.0.raw_modifies() } +} + +impl StateWriter for Writer { + type Writer = Self; + type OriginWriter = Self; + type RefWrite<'a> = WriteRef<'a, W> where W:'a; + + #[inline] + fn write(&'_ self) -> Self::RefWrite<'_> { self.0.write() } + + #[inline] + fn silent(&'_ self) -> Self::RefWrite<'_> { self.0.silent() } + + #[inline] + fn shallow(&'_ self) -> Self::RefWrite<'_> { self.0.shallow() } + + #[inline] + fn clone_writer(&self) -> Self { self.0.clone_writer() } + + #[inline] + fn origin_writer(&self) -> &Self::OriginWriter { self } +} + +impl Drop for Reader { + fn drop(&mut self) { + // The `Stateful` is a writer but used as a reader in `Reader` that not + // increment the writer count. So we increment the writer count before drop the + // `Stateful` to keep its count correct. + self.0.inc_writer(); } +} - fn forget_modifies(&self) -> ModifyScope { - let ref_state = self.ref_state.replace(BorrowState::None); - let b = &self.value.borrow_cnt; - match ref_state { - BorrowState::Reading => b.set(b.get() - 1), - BorrowState::Writing => b.set(b.get() + 1), - BorrowState::None => {} +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(); } - if BorrowState::Writing == ref_state { - self.modify_scope - } else { - ModifyScope::empty() + // 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 ()); } } } -/// A single linked list. -pub(crate) struct SlotNode { - _value: Box, - next: Option>, +impl Reader { + fn from_stateful(stateful: &Stateful) -> Self { + Reader(Stateful { + inner: stateful.inner.clone(), + notifier: stateful.notifier.clone(), + }) + } } -type BorrowFlag = isize; -const UNUSED: BorrowFlag = 0; +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(), + }) + } +} pub(crate) struct StateData { - data: UnsafeCell, - borrow_cnt: Cell, - #[cfg(debug_assertions)] - /// the place where the data be borrowed. - borrowed_at: Cell>>, - /// A link list to store anonymous data, so keep it live as long as the - /// `data`. For example, when this `State` subscribe to a upstream, append - /// the unsubscribe handle to this list let you can unsubscribe when this - /// `data` drop. - slot_link: UnsafeCell>, + data: RefCell, /// The batched modifies of the `State` which will be notified. - share_scope: Rc>>, -} - -macro_rules! debug_borrow_location { - ($this: ident) => { - #[cfg(debug_assertions)] - { - let caller = std::panic::Location::caller(); - $this.value.borrowed_at.set(Some(caller)); - } - }; + batch_modified: Sc>, + /// The counter of the writer may be modified the data. + writer_count: Cell, } #[repr(transparent)] pub(crate) struct RenderFul(pub(crate) Stateful); impl_proxy_query!(paths [0], RenderFul, , where R: Render + 'static); -impl_proxy_render!(proxy 0.query_ref(), RenderFul, , where R: Render + 'static); -impl_proxy_query!(paths[0.query_ref()], RenderFul>); -impl_proxy_render!(proxy 0.query_ref(), RenderFul>); - -impl Clone for Stateful { - #[inline] - fn clone(&self) -> Self { - Self { - inner: self.inner.clone(), - modifier: self.modifier.clone(), - } - } -} +impl_proxy_render!(proxy 0.read(), RenderFul, , where R: Render + 'static); +impl_proxy_query!(paths[0.read()], RenderFul>); +impl_proxy_render!(proxy 0.read(), RenderFul>); impl Stateful { pub fn new(data: W) -> Self { Stateful { - inner: Rc::new(StateData::new(data)), - modifier: <_>::default(), + inner: Sc::new(StateData::new(data)), + notifier: <_>::default(), } } - /// 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. + /// 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 silent_ref(&self) -> RefState { - RefState::new(&self.inner, ModifyScope::DATA, Some(&self.modifier)) + pub fn clone_stateful(&self) -> Stateful { self.clone_writer().0 } + + /// 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)) } - /// Clone the stateful widget of which the reference point to. Require mutable - /// reference because we try to early release inner borrow when clone occur. + // unsubscribe the `subscription` when the inner state data will drop. + #[inline] + 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 clone_stateful(&self) -> Stateful { self.clone() } + fn heap_ptr(&self) -> *const W { self.inner.data.as_ptr() } pub(crate) fn from_state_data(data: StateData) -> Self { Self { - inner: Rc::new(data), - modifier: <_>::default(), + inner: Sc::new(data), + notifier: <_>::default(), } } pub(crate) fn try_into_inner(self) -> Result { - if Rc::strong_count(&self.inner) == 1 && unsafe { &*self.inner.slot_link.get() }.is_none() { + 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 { Rc::try_unwrap(self.inner).unwrap_unchecked() }; + let inner = unsafe { Sc::try_unwrap(inner).unwrap_unchecked() }; Ok(inner.data.into_inner()) } else { Err(self) } } - /// This is a unsafe method to get the inner data reference. This method will - /// avoid to mark the borrow flag of the inner data, and is only designed for - /// the framework query. So this method will not bring any borrow restriction. - /// - /// # Safety - /// - /// The caller should guarantee that only use to query the inner data and do a - /// callback on it. And not use the return value to do any other thing. - /// Otherwise, the borrow check may be broken. - /// - /// Panic if there is any other mutable borrow to the inner data. - /// - /// # Notice - /// - /// Always keep this method be private. - fn query_ref(&self) -> &W { - assert!(!self.inner.is_writing()); - unsafe { &*self.inner.data.get() } + 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] + #[track_caller] + pub(crate) fn assert_is_not_used(&self) { self.data.borrow_mut(); } + + #[inline] pub(crate) fn new(data: W) -> Self { Self { - data: UnsafeCell::new(data), - borrow_cnt: Cell::new(0), - #[cfg(debug_assertions)] - borrowed_at: Cell::new(None), - slot_link: <_>::default(), - share_scope: <_>::default(), + // the `StateData` in `Stateful` or `State` is a writer + writer_count: Cell::new(1), + data: RefCell::new(data), + batch_modified: <_>::default(), } } - pub(crate) fn into_inner(self) -> (W, Option) { - let data = self.data.into_inner(); - let slot_link = self.slot_link.into_inner(); - (data, slot_link) - } + pub(crate) fn into_inner(self) -> W { self.data.into_inner() } - #[track_caller] - pub(crate) fn own_data(&self, data: impl Any) { - if self.is_used() { - self.already_borrow_panic(); - } + pub(crate) fn read(&self) -> ReadRef { ReadRef(self.data.borrow()) } +} - let link = unsafe { &mut *self.slot_link.get() }; - let mut node = SlotNode { _value: Box::new(data), next: None }; - if let Some(next) = link.take() { - node.next = Some(Box::new(next)); - } - *link = Some(node); - } +impl<'a, W> Deref for ReadRef<'a, W> { + type Target = W; - #[inline(always)] - pub(crate) fn is_reading(&self) -> bool { self.borrow_cnt.get() > UNUSED } - #[inline(always)] - pub(crate) fn is_writing(&self) -> bool { self.borrow_cnt.get() < UNUSED } - #[inline(always)] - pub(crate) fn is_used(&self) -> bool { self.borrow_cnt.get() != UNUSED } - - pub(crate) fn already_borrow_panic(&self) { - if self.borrow_cnt.get() != UNUSED { - #[cfg(debug_assertions)] - { - let location = self.borrowed_at.get().unwrap(); - panic!("already borrowed at {}", location); - } - #[cfg(not(debug_assertions))] - panic!("already borrowed"); - } - } + #[track_caller] + fn deref(&self) -> &Self::Target { &*self.0 } } -impl<'a, W> Deref for RefState<'a, W> { +impl<'a, W> Deref for WriteRef<'a, W> { type Target = W; - #[track_caller] - fn deref(&self) -> &Self::Target { - self.read(); - // SAFETY: `BorrowFlag` guarantees unique access. - unsafe { &*self.value.data.get() } - } + fn deref(&self) -> &Self::Target { &*self.value } } -impl<'a, W> DerefMut for RefState<'a, W> { +impl<'a, W> DerefMut for WriteRef<'a, W> { #[track_caller] fn deref_mut(&mut self) -> &mut Self::Target { - if log::log_enabled!(log::Level::Debug) { - let caller = std::panic::Location::caller(); - log::trace!("state modified at {caller}"); - } - self.write(); - - // SAFETY: `BorrowFlag` guarantees unique access. - let ptr = self.value.data.get(); - unsafe { &mut *ptr } + self.modified = true; + &mut *self.value } } -impl<'a, W> RefState<'a, W> { +impl<'a, W> RefWrite for WriteRef<'a, W> { #[inline] - pub fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { - match self.modifier.as_ref() { - Some(m) => m.raw_modifies(), - None => unreachable!( - "this method exist only for old version syntax compatibility,\ - modifier is always exist in old version." - ), - } - } - - #[inline] - pub fn modifies(&self) -> BoxOp<'static, (), Infallible> { - match self.modifier.as_ref() { - Some(m) => m.modifies(), - None => unreachable!( - "this method exist only for old version syntax compatibility,\ - modifier is always exist in old version." - ), - } - } + fn forget_modifies(&mut self) -> bool { std::mem::replace(&mut self.modified, false) } +} +impl<'a, W> WriteRef<'a, W> { pub(crate) fn new( - value: &'a StateData, - modify_scope: ModifyScope, - modifier: Option<&'a Modifier>, + value: RefMut<'a, W>, + scope: ModifyScope, + batch_scope: &'a Sc>, + modifier: Option<&'a Notifier>, ) -> Self { Self { + modified: false, + modify_scope: scope, value, - modifier, - ref_state: <_>::default(), - modify_scope, - } - } - - #[track_caller] - pub(crate) fn read(&self) { - // if this references already during reading or writing, we needn't to mark - // borrow flag. - if self.ref_state.get() == BorrowState::None { - self.ref_state.set(BorrowState::Reading); - let b = &self.value.borrow_cnt; - b.set(b.get() + 1); - if b.get() == 1 { - debug_borrow_location!(self); - } else if !self.value.is_reading() { - self.value.already_borrow_panic(); - } - } - } - - #[track_caller] - pub(crate) fn write(&self) { - if log::log_enabled!(log::Level::Debug) { - let caller = std::panic::Location::caller(); - log::trace!("state modified at {caller}"); - } - - let b = &self.value.borrow_cnt; - match self.ref_state.get() { - BorrowState::None => { - debug_borrow_location!(self); - b.set(b.get() - 1); - self.ref_state.set(BorrowState::Writing) - } - BorrowState::Reading => { - // current ref is borrowed value, we release the borrow and mutably - // borrow the value. - b.set(b.get() - 2); - self.ref_state.set(BorrowState::Writing) - } - BorrowState::Writing => { - // Already mutably the value, directly return. - } - } - - if b.get() != -1 { - self.value.already_borrow_panic(); + batched_modify: batch_scope, + notifier: modifier, } } } @@ -387,35 +351,39 @@ impl SingleChild for Stateful {} impl MultiChild for Stateful {} impl_proxy_query!( - paths [modifier, query_ref()], + paths [notifier, read()], Stateful, , where R: Query + 'static ); -impl_query_self_only!(Modifier); +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 RefState<'a, W> { +impl<'a, W> Drop for WriteRef<'a, W> { fn drop(&mut self) { - let scope = self.forget_modifies(); - - if !scope.is_empty() { - if let Some(s) = self.value.share_scope.get() { - self.value.share_scope.set(Some(s | scope)); - } else { - self.value.share_scope.set(Some(scope)); - if let Some(m) = self.modifier.as_mut() { - let mut subject = m.raw_modifies(); - let share_scope = self.value.share_scope.clone(); - AppCtx::spawn_local(async move { - let scope = share_scope.replace(None); - subject.next(unsafe { scope.unwrap_unchecked() }); - }) - .unwrap(); - } + if !self.modified { + return; + } + + 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 Modifier { +impl Notifier { pub fn modifies(&self) -> BoxOp<'static, (), Infallible> { self .raw_modifies() @@ -428,7 +396,7 @@ impl Modifier { impl std::fmt::Debug for Stateful { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("Stateful").field(&*self.state_ref()).finish() + f.debug_tuple("Stateful").field(&*self.read()).finish() } } @@ -467,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); @@ -480,7 +448,7 @@ 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(); @@ -504,7 +472,7 @@ mod tests { fn change_notify() { 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() @@ -519,7 +487,7 @@ mod tests { assert_eq!(notified.borrow().deref(), &[ModifyScope::BOTH]); { - let _ = &mut w.silent_ref().size; + let _ = &mut w.silent().size; } Timer::wake_timeout_futures(); @@ -530,12 +498,16 @@ mod tests { ); { - 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(); diff --git a/core/src/widget.rs b/core/src/widget.rs index 9821b43f9..3cb1b6fa1 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -298,7 +298,7 @@ macro_rules! impl_proxy_render { impl WidgetBuilder for C { #[inline] - fn build(self, ctx: &BuildCtx) -> WidgetId { State::from_value(self).build(ctx) } + fn build(self, ctx: &BuildCtx) -> WidgetId { State::value(self).build(ctx) } } impl WidgetBuilder for Stateful { diff --git a/core/src/widget_children.rs b/core/src/widget_children.rs index 8e64321da..9f6c3079e 100644 --- a/core/src/widget_children.rs +++ b/core/src/widget_children.rs @@ -152,12 +152,13 @@ impl BoxedMultiParent for W { #[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::*; #[test] fn compose_template_child() { + reset_test_env!(); #[derive(Declare)] struct Page; #[derive(Declare, SingleChild)] @@ -193,6 +194,8 @@ mod tests { #[test] fn compose_option_child() { + reset_test_env!(); + #[derive(Declare)] struct Parent; #[derive(Declare, SingleChild)] @@ -215,6 +218,8 @@ mod tests { #[test] fn compose_option_dyn_parent() { + reset_test_env!(); + fn_widget! { let p = Some(MockBox { size: Size::zero() }); @$p { @{ Void } } @@ -223,6 +228,8 @@ mod tests { #[test] fn tuple_as_vec() { + reset_test_env!(); + #[derive(Declare)] struct A; #[derive(Declare)] @@ -245,8 +252,10 @@ mod tests { #[test] fn expr_with_child() { + reset_test_env!(); + let size = Stateful::new(Size::zero()); - let c_size = size.clone_state(); + let c_size = size.clone_reader(); // with single child let _e = fn_widget! { let p = pipe!{ @@ -268,7 +277,7 @@ mod tests { } }; - let c_size = size.clone_state(); + let c_size = size.clone_reader(); // option with single child let _e = fn_widget! { let p = pipe!(($c_size.area() > 0.).then(|| @MockBox { size: Size::zero() })); @@ -284,6 +293,8 @@ mod tests { #[test] fn compose_expr_option_widget() { + reset_test_env!(); + let _ = fn_widget! { @MockBox { size: ZERO_SIZE, @@ -294,6 +305,8 @@ mod tests { #[test] fn pair_to_pair() { + reset_test_env!(); + #[derive(Declare)] struct P; @@ -309,6 +322,8 @@ mod tests { #[test] fn fix_multi_fill_for_pair() { + reset_test_env!(); + struct X; impl ComposeChild for X { type Child = WidgetOf; diff --git a/core/src/widget_children/compose_child_impl.rs b/core/src/widget_children/compose_child_impl.rs index bf4f02aaf..2c121fa77 100644 --- a/core/src/widget_children/compose_child_impl.rs +++ b/core/src/widget_children/compose_child_impl.rs @@ -52,7 +52,7 @@ where { type Target = as ComposeWithChild>::Target; fn with_child(self, child: C, ctx: &BuildCtx) -> Self::Target { - State::from_value(self).with_child(child, ctx) + State::value(self).with_child(child, ctx) } } diff --git a/core/src/widget_tree.rs b/core/src/widget_tree.rs index f39906d4b..ccda24c81 100644 --- a/core/src/widget_tree.rs +++ b/core/src/widget_tree.rs @@ -276,11 +276,11 @@ mod tests { fn bench_recursive_repair(width: usize, depth: usize, b: &mut Bencher) { let w = Stateful::new(Recursive { width, depth }); - let trigger = w.clone(); + let trigger = w.clone_writer(); let mut wnd = TestWindow::new(w); b.iter(|| { { - let _: &mut Recursive = &mut trigger.state_ref(); + let _ = trigger.write(); } wnd.draw_frame(); }); @@ -292,7 +292,7 @@ mod tests { let expect_size = Size::new(20., 20.); let no_boundary_size = Stateful::new(INFINITY_SIZE); - let c_size = no_boundary_size.clone(); + let c_size = no_boundary_size.clone_writer(); let w = fn_widget! { @MockBox { size: expect_size, @@ -307,7 +307,7 @@ mod tests { // when relayout the inner `MockBox`, its clamp should same with its previous // layout, and clamp its size. { - *c_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(); @@ -320,8 +320,8 @@ mod tests { let parent = Stateful::new(true); let child = Stateful::new(true); - let c_p = parent.clone_state(); - let c_c = child.clone_state(); + let c_p = parent.clone_writer(); + let c_c = child.clone_writer(); let w = fn_widget! { @ { pipe!(*$parent).map(move |p|{ @@ -337,8 +337,8 @@ mod tests { wnd.draw_frame(); { - *c_c.state_ref() = false; - *c_p.state_ref() = false; + *c_c.write() = false; + *c_p.write() = false; } // fix crash here. @@ -350,14 +350,14 @@ mod tests { reset_test_env!(); let trigger = Stateful::new(true); - let c_trigger = trigger.clone(); + let c_trigger = trigger.clone_writer(); let w = fn_widget! { @ { pipe!($trigger.then(|| Void)) }}; let mut wnd = TestWindow::new(w); wnd.draw_frame(); { - *c_trigger.state_ref() = false; + *c_trigger.write() = false; } // fix crash here @@ -434,13 +434,13 @@ mod tests { 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.)); }); @@ -479,7 +479,7 @@ mod tests { reset_test_env!(); let trigger = Stateful::new(1); - let c_trigger = trigger.clone(); + let c_trigger = trigger.clone_stateful(); let widget = fn_widget! { @ MockMulti { @{ @@ -499,7 +499,7 @@ mod tests { let mut tree = wnd.widget_tree.borrow_mut(); tree.layout(Size::new(100., 100.)); { - *c_trigger.silent_ref() = 2; + *c_trigger.write() = 2; } assert!(!tree.is_dirty()) } diff --git a/core/src/widget_tree/layout_info.rs b/core/src/widget_tree/layout_info.rs index 8f803dea3..b98b6ffeb 100644 --- a/core/src/widget_tree/layout_info.rs +++ b/core/src/widget_tree/layout_info.rs @@ -339,15 +339,15 @@ mod tests { // info, but its parent have. let child_box = Stateful::new(MockBox { size: Size::zero() }); let root_layout_cnt = Stateful::new(0); - let c_child_box = child_box.clone(); - let c_root_layout_cnt = root_layout_cnt.clone(); + 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 += 1, + 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_state()) + Widget::from(child_box.clone_writer().into_inner()) }) } } @@ -355,12 +355,12 @@ mod tests { let mut wnd = TestWindow::new(w); wnd.draw_frame(); - assert_eq!(*c_root_layout_cnt.state_ref(), 1); + assert_eq!(*c_root_layout_cnt.read(), 1); { - c_child_box.state_ref().size = Size::new(2., 2.); + c_child_box.write().size = Size::new(2., 2.); } wnd.draw_frame(); - assert_eq!(*c_root_layout_cnt.state_ref(), 2); + assert_eq!(*c_root_layout_cnt.read(), 2); } #[test] @@ -371,8 +371,8 @@ mod tests { 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, @@ -404,7 +404,7 @@ mod tests { 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.), @@ -451,7 +451,7 @@ mod tests { 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.), @@ -501,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 b4deb0ddb..06310b641 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::{Modifier, ModifyScope}, + state::{ModifyScope, Notifier}, widget::{QueryOrder, Render}, window::DelayEvent, }; @@ -116,7 +116,7 @@ impl WidgetId { pub(crate) fn on_mounted(self, tree: &WidgetTree) { self.assert_get(&tree.arena).query_all_type( - |notifier: &Modifier| { + |notifier: &Notifier| { let state_changed = tree.dirty_set.clone(); notifier .raw_modifies() diff --git a/core/src/window.rs b/core/src/window.rs index a961418f1..f7a6ec6a9 100644 --- a/core/src/window.rs +++ b/core/src/window.rs @@ -52,7 +52,7 @@ pub struct Window { frame_pool: RefCell, shell_wnd: RefCell>, /// A vector store the widget id pair of (parent, child). The child need to - /// drop after its `DelayDropWidget::delay_drop_until` be false or its parent + /// 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. @@ -303,9 +303,7 @@ impl Window { let tree = self.widget_tree.borrow(); let drop_conditional = wid .assert_get(&self.widget_tree.borrow().arena) - .query_on_first_type(QueryOrder::OutsideFirst, |d: &DelayDropWidget| { - d.delay_drop_until - }) + .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()) @@ -323,7 +321,6 @@ impl Window { let mut ctx = PaintingCtx::new(*wid, self.id(), &mut painter); wid.paint_subtree(&mut ctx); } - !need_drop }); } @@ -353,7 +350,7 @@ impl Window { let delay_drop = id .assert_get(&self.widget_tree.borrow().arena) - .contain_type::(); + .contain_type::(); if delay_drop { self.delay_drop_widgets.borrow_mut().push((parent, id)); 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/examples/counter/src/counter.rs b/examples/counter/src/counter.rs index 92e457b61..584920694 100644 --- a/examples/counter/src/counter.rs +++ b/examples/counter/src/counter.rs @@ -8,12 +8,12 @@ pub fn counter() -> impl Into { h_align: HAlign::Center, align_items: Align::Center, @FilledButton { - on_tap: move |_: &mut _| *$cnt += 1, + on_tap: move |_: &mut _| *$cnt.write() += 1, @{ Label::new("Add") } } @H1 { text: pipe!($cnt.to_string()) } @FilledButton { - on_tap: move |_: &mut _| *$cnt += -1, + on_tap: move |_: &mut _| *$cnt.write() += -1, @{ Label::new("Sub") } } } diff --git a/examples/todos/src/todos.rs b/examples/todos/src/todos.rs index 14bd93cdf..6970ab16a 100644 --- a/examples/todos/src/todos.rs +++ b/examples/todos/src/todos.rs @@ -25,8 +25,8 @@ impl Compose for Todos { @{ let mut input = @Input { }; let add_task = move |_: &mut _| if !$input.text().is_empty() { - $this.new_task($input.text().to_string()); - $input.set_text(""); + $this.write().new_task($input.text().to_string()); + $input.write().set_text(""); }; @Row { @Container { @@ -48,15 +48,15 @@ impl Compose for Todos { pos: Position::Top, @Tab { @TabLabel { @{ Label::new("ALL") } } - @TabPane { @{ Self::pane(this.clone_state(), |_| true) } } + @TabPane { @{ Self::pane(this.clone_writer(), |_| true) } } } @Tab { @TabLabel { @{ Label::new("ACTIVE") } } - @TabPane { @{ Self::pane(this.clone_state(), |task| !task.finished)} } + @TabPane { @{ Self::pane(this.clone_writer(), |task| !task.finished)} } } @Tab { @TabLabel { @{ Label::new("DONE") } } - @TabPane { @{ Self::pane(this.clone_state(), |task| task.finished) } } + @TabPane { @{ Self::pane(this.clone_writer(), |task| task.finished) } } } } } @@ -66,7 +66,7 @@ impl Compose for Todos { } impl Todos { - fn pane(this: State, cond: fn(&Task) -> bool) -> Widget { + fn pane(this: Writer, cond: fn(&Task) -> bool) -> Widget { fn_widget! { // todo: pipe only for list items, not lists @VScrollBar { @ { pipe! { @@ -74,7 +74,7 @@ impl Todos { @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.), @ { let task_widget_iter = $this @@ -83,7 +83,7 @@ impl Todos { .enumerate() .filter_map(move |(idx, task)| { cond(task).then_some(idx) }) .map(move |idx| { - let mut task = partial_state!($this.tasks[idx]); + let mut task = split_writer!($this.tasks[idx]); let mut key = @KeyWidget { key: $task.id, value: () }; let mut mount_idx = Stateful::new(0); @@ -93,14 +93,14 @@ impl Todos { duration: Duration::from_millis(150), easing: easing::EASE_IN, }.into_inner(), - state: route_state!($key.transform), + state: map_writer!($key.transform), from: Transform::translation(-400., 0. ), }; @ $key { @ListItem { on_mounted: move |_| if $key.is_enter() { - *$mount_idx = *$mount_task_cnt; - *$mount_task_cnt += 1; + *$mount_idx.write() = *$mount_task_cnt; + *$mount_task_cnt.write() += 1; mount_animate.run(); }, @{ HeadlineText(Label::new($task.label.clone())) } @@ -112,14 +112,14 @@ impl Todos { }; watch!($checkbox.checked) .distinct_until_changed() - .subscribe(move |v| $task.finished = v); + .subscribe(move |v| $task.write().finished = v); CustomEdgeWidget(checkbox.into()) } } @Trailing { cursor: CursorIcon::Hand, visible: $key.mouse_hover(), - on_tap: move |_| { $this.tasks.remove(idx); }, + on_tap: move |_| { $this.write().tasks.remove(idx); }, @{ svgs::CLOSE } } } 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/declare_derive2.rs b/macros/src/declare_derive2.rs index bc38b0a46..5f005f83d 100644 --- a/macros/src/declare_derive2.rs +++ b/macros/src/declare_derive2.rs @@ -137,6 +137,7 @@ pub(crate) fn declare_derive(input: &mut syn::DeriveInput) -> syn::Result Self::Target { self } } }; @@ -271,24 +272,36 @@ fn struct_with_fields_gen( 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::from_value(#name { + let mut _ribir_ಠ_ಠ = State::value(#name { #(#field_names2 : #field_names2.0),* }); + let mut _unsub_ಠ_ಠ = None; + #( if let Some(u) = #field_names3.1 { - let mut _ribir2 = _ribir.clone_state(); - let h = u.subscribe(move |v| _ribir2.state_ref().#field_names3 = v) - .unsubscribe_when_dropped(); - _ribir.own_data(h); + let mut _ribir2 = _ribir_ಠ_ಠ.clone_writer(); + let h = u.subscribe(move |v| _ribir2.write().#field_names3 = v); + _unsub_ಠ_ಠ = if let Some(u) = _unsub_ಠ_ಠ { + let unsub = ZipSubscription::new(u, h); + Some(BoxSubscription::new(unsub)) + } else { + Some(h) + }; } );* - _ribir + if let Some(unsub) = _unsub_ಠ_ಠ { + _ribir_ಠ_ಠ.as_stateful().unsubscribe_on_drop(unsub); + } + + _ribir_ಠ_ಠ } } }; diff --git a/macros/src/declare_obj.rs b/macros/src/declare_obj.rs index 78a4066eb..747565e32 100644 --- a/macros/src/declare_obj.rs +++ b/macros/src/declare_obj.rs @@ -1,6 +1,6 @@ use crate::{ rdl_macro::{DeclareField, RdlParent, StructLiteral}, - widget_macro::{ribir_variable, WIDGETS, WIDGET_OF_BUILTIN_FIELD}, + widget_macro::{WIDGETS, WIDGET_OF_BUILTIN_FIELD}, }; use inflector::Inflector; use proc_macro2::{Span, TokenStream}; @@ -85,80 +85,94 @@ impl<'a> DeclareObj<'a> { impl<'a> ToTokens for DeclareObj<'a> { fn to_tokens(&self, tokens: &mut TokenStream) { - let Self { this, span, children, builtin } = self; + Brace(self.span).surround(tokens, |tokens| { + match &self.this { + Some(this @ ObjNode::Obj { span, .. }) => { + // declare the host widget before builtin widget and children. + // so that we can use variable if it moved in builtin widget and children. + // this is consistent with the user's declaration. - if children.is_empty() && builtin.is_empty() { - quote_spanned! { *span => FatObj::from_host(#this) }.to_tokens(tokens) - } else { - Brace(*span).surround(tokens, |tokens| { - let this = this.as_ref().map(|this| match this { - ObjNode::Obj { span, .. } => { - let name = ribir_variable("ribir", *span); - // declare the host widget before builtin widget and children. - // so that we can use variable moved in builtin widget and children. - // this is consistent with the user's declaration. - quote_spanned! { *span => let #name = #this; }.to_tokens(tokens); - name - } - ObjNode::Var(var) => (*var).clone(), - }); + quote_spanned! { *span => let _ribir_ಠ_ಠ = #this; }.to_tokens(tokens); + let name = Ident::new(&format!("_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) + } + } + }) + } +} - let mut builtin_names = vec![]; - for (ty_str, fields) in builtin { - // 'b is live longer than 'a, safe convert, but we can't directly convert - // `SmallVec<[&'b DeclareField; 1]>` to `SmallVec<[&'a DeclareField; - // 1]>`, because `SmallVec` is invariant over `T`. - fn shorter_lifetime<'a, 'b: 'a>( - fields: SmallVec<[&'b DeclareField; 1]>, - ) -> SmallVec<[&'a DeclareField; 1]> { - unsafe { std::mem::transmute(fields.clone()) } - } - let fields = shorter_lifetime(fields.clone()); +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 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 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) + } - let mut children_names = vec![]; - for (i, c) in children.iter().enumerate() { - let child = ribir_variable(&format!("child_{i}"), c.span()); - quote_spanned! { c.span() => let #child = #c; }.to_tokens(tokens); - children_names.push(child) - } + (builtin_names, children_names) + } - // if obj has child and has no builtin widgets, we can directly declare it - // without `FatObj`, because it's not referenced by others, also no need to - // compose with builtin widgets. - let composed_tokens = if !children.is_empty() && builtin.is_empty() { - assert!( - this.is_some(), - "If no builtin widgets and no children, the host widget must be existed." - ); - quote_spanned! { *span => #this #(.with_child(#children_names, ctx!()))* } - } else { - let builtin_init = builtin_names - .iter() - .map(|name| Ident::new(&format!("set_builtin_{name}"), name.span())); - if let Some(this) = this { - quote_spanned! { *span => - FatObj::new(#this, BuiltinObj::default()#(.#builtin_init(#builtin_names))*) - #(.with_child(#children_names, ctx!()))* - } - } else { - quote_spanned! { *span => - BuiltinObj::default()#(.#builtin_init(#builtin_names))* - #(.with_child(#children_names, ctx!()))* - } - } - }; - composed_tokens.to_tokens(tokens) - }); + /// 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); + return; + } 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) } } } diff --git a/macros/src/fn_widget_macro.rs b/macros/src/fn_widget_macro.rs index ac1a1c80e..6a41786ab 100644 --- a/macros/src/fn_widget_macro.rs +++ b/macros/src/fn_widget_macro.rs @@ -4,17 +4,17 @@ use proc_macro2::TokenStream; use quote::{quote, ToTokens}; use syn::{fold::Fold, parse_macro_input, Stmt}; -use crate::symbol_process::DollarRefs; +use crate::symbol_process::DollarRefsCtx; pub struct FnWidgetMacro(Vec); impl FnWidgetMacro { - pub(crate) fn gen_code(input: TokenStream, outside: Option<&mut DollarRefs>) -> TokenStream1 { - let input = ok!(symbol_to_macro(input.into())); + 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); - let mut refs = DollarRefs::new(outside.map_or(0, |o| o.in_capture_level())); - let stmts = body.0.into_iter().map(|s| refs.fold_stmt(s)).collect(); - + 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() } } diff --git a/macros/src/lib.rs b/macros/src/lib.rs index ce07fad65..615b9160a 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -10,15 +10,15 @@ mod widget_macro; use fn_widget_macro::FnWidgetMacro; use proc_macro::TokenStream; use quote::quote; -use symbol_process::DollarRefs; +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 route_state_macro; mod watch_macro; +mod writer_map_macro; pub(crate) use rdl_macro::*; use crate::pipe_macro::PipeMacro; @@ -143,7 +143,7 @@ pub fn widget(input: TokenStream) -> TokenStream { gen_widget_macro(input, None) /// like: `let row = rdl!{ Widget::new(Void) };` #[proc_macro] pub fn rdl(input: TokenStream) -> TokenStream { - RdlMacro::gen_code(input.into(), &mut DollarRefs::default()) + RdlMacro::gen_code(input.into(), &mut DollarRefsCtx::top_level()) } /// The `fn_widget` is a macro that create a widget from a function widget from @@ -151,7 +151,15 @@ pub fn rdl(input: TokenStream) -> TokenStream { /// `$` in the expression, the `@` is a short hand of `rdl` macro, and `$name` /// use to expression a state reference of `name`. #[proc_macro] -pub fn fn_widget(input: TokenStream) -> TokenStream { FnWidgetMacro::gen_code(input.into(), None) } +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. @@ -181,35 +189,33 @@ pub fn ctx(input: TokenStream) -> TokenStream { /// expression modify. Use the `$` mark the state reference and auto subscribe /// to its modify. #[proc_macro] -pub fn pipe(input: TokenStream) -> TokenStream { PipeMacro::gen_code(input.into(), None) } +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(), None) } - -/// macro split state from another state as a new state. For example, -/// `partial_state!($label.visible);` will return a state of `bool` that is -/// partial of the `Visibility`. This macros use `Share::partial_state` to split -/// the state. -#[proc_macro] -pub fn partial_state(input: TokenStream) -> TokenStream { - route_state_macro::gen_partial_state_macro(input.into(), &mut DollarRefs::default()) +pub fn watch(input: TokenStream) -> TokenStream { + WatchMacro::gen_code(input.into(), &mut DollarRefsCtx::top_level()) } -/// macro use `Share::route_state` split a state from another state as a new -/// state. For example, `route_state!($label.visible)` will return a state of -/// `bool` that is partial of the `Visibility`. +/// 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 route_state(input: TokenStream) -> TokenStream { - route_state_macro::gen_route_state_macro(input.into(), &mut DollarRefs::default()) +pub fn split_writer(input: TokenStream) -> TokenStream { + writer_map_macro::gen_split_path_writer(input.into(), &mut DollarRefsCtx::top_level()) } -/// The macro to use a state as its state reference. Transplanted from the `$`. +/// macro map a write to another state write. For example, +/// `map_writer!($label.visible);` will return a writer of `bool` that is +/// partial of the `Visibility`. This macros is a convenient way for +/// `StateWriter::map_writer` #[proc_macro] -pub fn _dollar_ಠ_ಠ(input: TokenStream) -> TokenStream { - let input: proc_macro2::TokenStream = input.into(); - quote! { #input.state_ref() }.into() +pub fn map_writer(input: TokenStream) -> TokenStream { + writer_map_macro::gen_map_path_writer(input.into(), &mut DollarRefsCtx::top_level()) } #[proc_macro] diff --git a/macros/src/pipe_macro.rs b/macros/src/pipe_macro.rs index 21af135c4..807b1d79f 100644 --- a/macros/src/pipe_macro.rs +++ b/macros/src/pipe_macro.rs @@ -1,8 +1,9 @@ -use crate::symbol_process::{fold_body_in_capture_and_require_refs, DollarRefs}; +use crate::symbol_process::{not_subscribe_anything, DollarRefsCtx, DollarRefsScope}; use crate::{ok, symbol_process::symbol_to_macro}; use proc_macro::TokenStream as TokenStream1; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; +use syn::fold::Fold; use syn::{ parse::{Parse, ParseStream}, parse_macro_input, @@ -12,19 +13,24 @@ use syn::{ pub(crate) struct BodyExpr(pub(crate) Vec); pub(crate) struct PipeMacro { - refs: DollarRefs, + refs: DollarRefsScope, expr: Vec, } impl PipeMacro { - pub fn gen_code(input: TokenStream, outside: Option<&mut DollarRefs>) -> TokenStream1 { + pub fn gen_code(input: TokenStream, refs_ctx: &mut DollarRefsCtx) -> TokenStream1 { let span = input.span(); - let input = ok!(symbol_to_macro(input.into())); + let input = ok!(symbol_to_macro(TokenStream1::from(input))); let expr = parse_macro_input! { input as BodyExpr }; - let (expr, mut refs) = ok!(fold_body_in_capture_and_require_refs(span, expr.0, outside)); - refs.sort_and_dedup(); - PipeMacro { refs, expr }.to_token_stream().into() + 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() + } } } @@ -42,13 +48,12 @@ impl ToTokens for PipeMacro { quote! {{ #refs let upstream = #upstream; + let _ctx_handle_ಠ_ಠ = ctx!().handle(); let mut expr_value = move |ctx!(): &BuildCtx<'_>| { #(#expr)* }; - let _ctx_handle = ctx!().handle(); - Pipe::new( expr_value(ctx!()), upstream - .filter_map(move |_| _ctx_handle.with_ctx(&mut expr_value)) + .filter_map(move |_| _ctx_handle_ಠ_ಠ.with_ctx(&mut expr_value)) .box_it() ) }} diff --git a/macros/src/rdl_macro.rs b/macros/src/rdl_macro.rs index 60b3e8b9f..fade30cef 100644 --- a/macros/src/rdl_macro.rs +++ b/macros/src/rdl_macro.rs @@ -1,7 +1,7 @@ use crate::{ declare_obj::DeclareObj, ok, - symbol_process::{kw, symbol_to_macro, DollarRefs}, + symbol_process::{kw, symbol_to_macro, DollarRefsCtx}, }; use proc_macro::TokenStream as TokenStream1; use proc_macro2::{Span, TokenStream}; @@ -57,8 +57,8 @@ pub struct DeclareField { } impl RdlMacro { - pub fn gen_code(input: TokenStream, refs: &mut DollarRefs) -> TokenStream1 { - let input = ok!(symbol_to_macro(input.into())); + 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) => { diff --git a/macros/src/route_state_macro.rs b/macros/src/route_state_macro.rs deleted file mode 100644 index a5d1324dc..000000000 --- a/macros/src/route_state_macro.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::{ - ok, - symbol_process::{symbol_to_macro, KW_DOLLAR_STR}, -}; -use proc_macro::TokenStream as TokenStream1; -use proc_macro2::{Ident, TokenStream}; -use quote::{quote, ToTokens}; -use syn::{ - fold::Fold, - parse::{Parse, ParseStream}, - parse_macro_input, parse_quote, - spanned::Spanned, - Expr, ExprMacro, Result, -}; - -use crate::symbol_process::DollarRefs; - -pub fn gen_route_state_macro(input: TokenStream, outside: &mut DollarRefs) -> TokenStream1 { - let tokens = gen_route_macro(input.clone(), "route_state", outside); - tokens -} - -pub fn gen_partial_state_macro(input: TokenStream, outside: &mut DollarRefs) -> TokenStream1 { - gen_route_macro(input, "partial_state", outside) -} - -fn gen_route_macro( - input: TokenStream, - method: &'static str, - refs: &mut DollarRefs, -) -> TokenStream1 { - let input = ok!(symbol_to_macro(input.into())); - let expr = parse_macro_input! { input as Expr }; - let expr = refs.fold_expr(expr); - let RouterExpr { host, path } = parse_quote!(#expr); - RouterMacro { host, path, method }.to_token_stream().into() -} - -struct RouterExpr { - host: TokenStream, - path: TokenStream, -} - -impl Parse for RouterExpr { - fn parse(input: ParseStream) -> Result { - let mac = input.parse::()?; - if !mac.mac.path.is_ident(KW_DOLLAR_STR) { - return Err(syn::Error::new_spanned( - mac, - "expected `$` as the first token", - )); - } - - Ok(Self { - host: mac.mac.tokens, - path: input.parse()?, - }) - } -} - -struct RouterMacro { - host: TokenStream, - path: TokenStream, - method: &'static str, -} - -impl ToTokens for RouterMacro { - fn to_tokens(&self, tokens: &mut TokenStream) { - let Self { host, path, method } = self; - let route_method = Ident::new(method, host.span()); - - quote!( - #host.#route_method( - move |origin: &_| &origin #path, - move |origin: &mut _| &mut origin #path - ) - ) - .to_tokens(tokens) - } -} diff --git a/macros/src/symbol_process.rs b/macros/src/symbol_process.rs index 2c85c4868..79efbb25f 100644 --- a/macros/src/symbol_process.rs +++ b/macros/src/symbol_process.rs @@ -1,7 +1,7 @@ use crate::fn_widget_macro::FnWidgetMacro; use crate::pipe_macro::PipeMacro; use crate::rdl_macro::RdlMacro; -use crate::route_state_macro::{gen_partial_state_macro, gen_route_state_macro}; +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}, @@ -9,13 +9,13 @@ use crate::{ use inflector::Inflector; use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, quote_spanned, ToTokens}; -use smallvec::SmallVec; +use smallvec::{smallvec, SmallVec}; use syn::{ fold::Fold, parse::{Parse, ParseStream}, spanned::Spanned, token::Dollar, - Expr, ExprField, ExprMethodCall, Macro, Member, Stmt, + Expr, ExprField, ExprMethodCall, Macro, Member, }; use syn::{parse_quote, parse_quote_spanned}; @@ -25,9 +25,9 @@ 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_PARTIAL_STATE: &str = "partial_state"; -pub const KW_ROUTE_STATE: &str = "route_state"; -pub const KW_MAP_STATE_REF: &str = "map_state_ref"; +pub const KW_SPLIT_WRITER: &str = "split_writer"; +pub const KW_MAP_WRITER: &str = "map_writer"; + pub use tokens_pre_process::*; pub mod kw { @@ -37,24 +37,31 @@ pub mod kw { #[derive(Hash, PartialEq, Eq, Debug, Clone)] pub struct BuiltinInfo { - host: Ident, - member: Ident, + 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 DollarRefs { - refs: SmallVec<[DollarRef; 1]>, - ctx_used: bool, - in_capture: usize, +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>, } -pub struct StackGuard<'a>(&'a mut DollarRefs); +#[derive(Debug, Default)] +pub struct DollarRefsScope { + refs: SmallVec<[DollarRef; 1]>, + used_ctx: bool, +} + +pub struct StackGuard<'a>(&'a mut DollarRefsCtx); mod tokens_pre_process { @@ -83,7 +90,9 @@ mod tokens_pre_process { /// Convert `@` and `$` symbol to a `rdl!` or `_dollar_ಠ_ಠ!` macro, make it /// conform to Rust syntax - pub fn symbol_to_macro(input: TokenStream) -> Result { + pub fn symbol_to_macro( + input: impl IntoIterator, + ) -> Result { let mut iter = input.into_iter(); let mut tokens = vec![]; @@ -151,7 +160,6 @@ mod tokens_pre_process { None => return dollar_err(p.span()), }; } - Some(TokenTree::Group(mut g)) => { // not process symbol in macro, because it's maybe as part of the macro syntax. if !in_macro(&tokens) { @@ -177,12 +185,17 @@ mod tokens_pre_process { } } -impl Fold for DollarRefs { +impl Fold for DollarRefsCtx { fn fold_block(&mut self, i: syn::Block) -> syn::Block { - let mut this = self.push_stack(); + 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) @@ -200,37 +213,37 @@ impl Fold for DollarRefs { } fn fold_expr_block(&mut self, i: syn::ExprBlock) -> syn::ExprBlock { - let mut this = self.push_stack(); + 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_stack(); + 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_stack(); + 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_stack(); + 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_stack(); + 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_stack(); + 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_stack(); + let mut this = self.push_code_stack(); syn::fold::fold_expr_while(&mut *this, i) } @@ -241,9 +254,13 @@ impl Fold for DollarRefs { fn fold_expr_field(&mut self, mut i: ExprField) -> ExprField { let ExprField { base, member, .. } = &mut i; + if let Member::Named(member) = member { - if let Some(builtin_ty) = WIDGET_OF_BUILTIN_FIELD.get(member.to_string().as_str()) { - self.replace_builtin_ident(&mut *base, &builtin_ty.to_snake_case()); + let dollar = WIDGET_OF_BUILTIN_FIELD + .get(member.to_string().as_str()) + .and_then(|builtin_ty| self.replace_builtin_ident(&mut *base, &builtin_ty.to_snake_case())); + if dollar.is_some() { + return i; } } @@ -251,66 +268,75 @@ impl Fold for DollarRefs { } fn fold_expr_method_call(&mut self, mut i: ExprMethodCall) -> ExprMethodCall { - if let Some(builtin_ty) = WIDGET_OF_BUILTIN_METHOD.get(i.method.to_string().as_str()) { - self.replace_builtin_ident(&mut i.receiver, &builtin_ty.to_snake_case()); + // fold builtin method on state + let dollar = WIDGET_OF_BUILTIN_METHOD + .get(i.method.to_string().as_str()) + .and_then(|builtin_ty| { + self.replace_builtin_ident(&mut i.receiver, &builtin_ty.to_snake_case()) + }); + if dollar.is_some() { + return i; + } + + // fold if write on state. + let write_mac = is_state_write_method(&i).then(|| { + let Expr::Macro(m) = &mut *i.receiver else { return None }; + parse_dollar_macro(&m.mac).map(|d| (d.name, &mut m.mac)) + }); + if let Some(Some((name, mac))) = write_mac { + mac.tokens = expand_write_method(name.to_token_stream()); + mark_macro_expanded(mac); + let dollar_ref = DollarRef { name, builtin: None, write: true }; + self.add_dollar_ref(dollar_ref); + return i; } syn::fold::fold_expr_method_call(self, i) } fn fold_macro(&mut self, mut mac: Macro) -> Macro { - if let Some(DollarMacro { name, .. }) = parse_clean_dollar_macro(&mac) { - mac.tokens = name.to_token_stream(); - if !self.is_local_variable(&name) { - self.refs.push(DollarRef { name, builtin: None }); - } + 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, Some(self)).into(); - mac.path = parse_quote_spanned! { mac.path.span() => ribir_expanded_ಠ_ಠ }; + mac.tokens = WatchMacro::gen_code(mac.tokens, self).into(); + mark_macro_expanded(&mut mac); } else if mac.path.is_ident(KW_PIPE) { - self.ctx_used = true; - mac.tokens = PipeMacro::gen_code(mac.tokens, Some(self)).into(); - mac.path = parse_quote_spanned! { mac.path.span() => ribir_expanded_ಠ_ಠ }; + 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.ctx_used = true; + self.mark_used_ctx(); mac.tokens = RdlMacro::gen_code(mac.tokens, self).into(); - mac.path = parse_quote_spanned! { mac.path.span() => ribir_expanded_ಠ_ಠ }; + mark_macro_expanded(&mut mac); } else if mac.path.is_ident(KW_FN_WIDGET) { - mac.tokens = FnWidgetMacro::gen_code(mac.tokens, Some(self)).into(); - mac.path = parse_quote_spanned! { mac.path.span() => ribir_expanded_ಠ_ಠ }; - } else if mac.path.is_ident(KW_PARTIAL_STATE) { - mac.tokens = gen_partial_state_macro(mac.tokens, self).into(); - mac.path = parse_quote_spanned! { mac.path.span() => ribir_expanded_ಠ_ಠ }; - } else if mac.path.is_ident(KW_ROUTE_STATE) { - mac.tokens = gen_route_state_macro(mac.tokens, self).into(); - mac.path = parse_quote_spanned! { mac.path.span() => ribir_expanded_ಠ_ಠ }; - } else if mac.path.is_ident(KW_MAP_STATE_REF) { - mac.path = parse_quote_spanned! { mac.path.span() => ribir_expanded_ಠ_ಠ }; - todo!("expand map_state_ref macro not implemented!"); + 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.ctx_used = true; + 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() => { - let mut closure_refs = DollarRefs { - in_capture: self.in_capture, - ..Default::default() - }; - - closure_refs.in_capture += 1; - let mut c = closure_refs.fold_expr_closure(c); + self.new_dollar_scope(true); + let mut c = self.fold_expr_closure(c); + let dollar_scope = self.pop_dollar_scope(true); - if closure_refs.ctx_used || !closure_refs.is_empty() { - let handle = closure_refs - .ctx_used - .then(|| quote_spanned! { c.span() => let _ctx_handle = ctx!().handle(); }); - - if closure_refs.ctx_used { + 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") @@ -323,20 +349,19 @@ impl Fold for DollarRefs { } } - closure_refs.sort_and_dedup(); - self.merge(&mut closure_refs); + let handle = dollar_scope + .used_ctx() + .then(|| quote_spanned! { c.span() => let _ctx_handle = ctx!().handle(); }); let expr = Expr::Verbatim(quote_spanned!(c.span() => { - #closure_refs + #dollar_scope #handle #c })); - closure_refs.in_capture -= 1; - expr } else { - Expr::Closure(c) + Expr::Closure(self.fold_expr_closure(c)) } } _ => syn::fold::fold_expr(self, i), @@ -344,55 +369,58 @@ impl Fold for DollarRefs { } } -impl ToTokens for DollarRefs { +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 } in &self.refs { + 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!()).clone_state(); + name.span() => let mut #name = #host.#member(ctx!()).#reader_or_writer(); } .to_tokens(tokens); } _ => { - quote_spanned! { name.span() => let mut #name = #name.clone_state();}.to_tokens(tokens); + quote_spanned! { + name.span() => let mut #name = #name.#reader_or_writer(); + } + .to_tokens(tokens); } } } } } -impl DollarRefs { - pub fn new(in_capture: usize) -> Self { Self { in_capture, ..Default::default() } } - - pub fn in_capture_level(&self) -> usize { self.in_capture } - - pub fn push_stack(&mut self) -> StackGuard<'_> { - self.variable_stacks.push(vec![]); - StackGuard(self) - } - - pub fn merge(&mut self, other: &mut Self) { - for r in other.refs.iter_mut() { - if let Some(builtin) = r.builtin.as_mut() { - if !self.is_local_variable(&builtin.host) { - self.refs.push(r.clone()); - r.builtin.take(); - } - } else if !self.is_local_variable(&r.name) { - self.refs.push(r.clone()) - } +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.ctx_used |= other.ctx_used; + self.scopes.push(<_>::default()); } - pub fn used_ctx(&self) -> bool { self.ctx_used } - - /// Removes duplicate elements 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. + /// 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: /// @@ -408,94 +436,165 @@ impl DollarRefs { /// 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 sort_and_dedup(&mut self) { - self - .refs - // sort by builtin first, then sort by name - .sort_by_key(|r: &_| (r.builtin.is_none(), r.name.to_string())); - self.refs.dedup() + pub fn push_code_stack(&mut self) -> StackGuard<'_> { + self.variable_stacks.push(vec![]); + StackGuard(self) } - pub fn upstream_tokens(&self) -> TokenStream { - match self.len() { - 0 => quote! {}, - 1 => { - let DollarRef { name, .. } = &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) } - } + pub fn builtin_host_tokens(&self, dollar_ref: &DollarRef) -> TokenStream { + let DollarRef { name, builtin, .. } = dollar_ref; + let BuiltinInfo { host, member } = builtin.as_ref().unwrap(); + + // if used in embedded closure, we directly use the builtin variable, the + // variable that capture by the closure is already a separate builtin variable. + + if !self.is_local_var(host) && self.capture_level_heads.len() > 1 { + name.to_token_stream() + } else { + quote_spanned! { host.span() => #host.#member(ctx!()) } } } + fn mark_used_ctx(&mut self) { self.current_dollar_scope_mut().used_ctx = true; } + fn replace_builtin_ident( &mut self, caller: &mut Expr, builtin_member: &str, ) -> Option<&DollarRef> { - let e = match caller { - Expr::MethodCall(ExprMethodCall { receiver, method, args, .. }) - if args.is_empty() && (method == "shallow" || method == "silent") => - { - &mut **receiver - } - e => e, + let mut write = false; + let e = if let Expr::MethodCall(m) = caller { + write = is_state_write_method(&m); + if write { &mut *m.receiver } else { caller } + } else { + caller }; + let Expr::Macro(m) = e else { return None }; + let DollarMacro { name: host, .. } = parse_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 Expr::Macro(m) = e else { return None }; - let DollarMacro { name: host, .. } = parse_clean_dollar_macro(&m.mac)?; 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 }; - // if used in embedded closure, we directly use the builtin variable, the - // variable that capture by the closure is already a separate builtin variable. - let is_local_declare = self.is_local_variable(&host); - - m.mac.tokens = if !is_local_declare && self.in_capture > 0 { - name.to_token_stream() + let state = self.builtin_host_tokens(&dollar_ref); + m.mac.tokens = if write { + expand_write_method(state) } else { - quote_spanned! { host.span() => #host.#get_builtin_method(ctx!()) } + expand_read(state) }; + mark_macro_expanded(&mut m.mac); - if !is_local_declare { - let builtin = Some(BuiltinInfo { host, member: get_builtin_method }); - self.refs.push(DollarRef { name, builtin }); - } - - self.last() + 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 is_local_variable(&self, name: &Ident) -> bool { - self - .variable_stacks + 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)) } } -fn parse_clean_dollar_macro(mac: &Macro) -> Option { +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) { - // parse fail may occur because the macro is already fold by `DollarRefs`. - mac.parse_body::().ok() + Some(mac.parse_body::().unwrap()) } else { None } } -impl std::ops::Deref for DollarRefs { +impl std::ops::Deref for DollarRefsScope { type Target = [DollarRef]; fn deref(&self) -> &Self::Target { &self.refs } } @@ -515,7 +614,7 @@ impl Parse for DollarMacro { } impl<'a> std::ops::Deref for StackGuard<'a> { - type Target = DollarRefs; + type Target = DollarRefsCtx; fn deref(&self) -> &Self::Target { self.0 } } @@ -527,34 +626,26 @@ impl<'a> Drop for StackGuard<'a> { fn drop(&mut self) { self.0.variable_stacks.pop(); } } -impl Default for DollarRefs { +impl Default for DollarRefsCtx { fn default() -> Self { Self { - refs: SmallVec::new(), - ctx_used: false, - in_capture: 0, + scopes: smallvec![DollarRefsScope::default()], + capture_level_heads: smallvec![], variable_stacks: vec![vec![]], } } } -pub fn fold_body_in_capture_and_require_refs( - span: Span, - stmts: Vec, - outside: Option<&mut DollarRefs>, -) -> Result<(Vec, DollarRefs), TokenStream> { - let in_capture = outside.as_ref().map_or(0, |o| o.in_capture) + 1; - let mut refs = DollarRefs::new(in_capture); - - let stmts = stmts.into_iter().map(|s| refs.fold_stmt(s)).collect(); - if refs.is_empty() { - Err(quote_spanned!(span => - compile_error!("expression not subscribe anything, it must contain at least one $") - )) - } else { - if let Some(outside) = outside { - outside.merge(&mut refs); - } - Ok((stmts, refs)) - } +pub fn not_subscribe_anything(span: Span) -> TokenStream { + quote_spanned!(span => + compile_error!("expression not subscribe anything, it must contain at least one $") + ) } + +fn is_state_write_method(m: &ExprMethodCall) -> bool { + m.method == "write" || m.method == "silent" || m.method == "shallow" +} + +fn expand_write_method(host: TokenStream) -> TokenStream { host } + +fn expand_read(name: TokenStream) -> TokenStream { quote_spanned!(name.span() => #name.read()) } diff --git a/macros/src/watch_macro.rs b/macros/src/watch_macro.rs index 498bd89be..d3b48d359 100644 --- a/macros/src/watch_macro.rs +++ b/macros/src/watch_macro.rs @@ -1,27 +1,31 @@ use crate::{ ok, pipe_macro::BodyExpr, - symbol_process::{fold_body_in_capture_and_require_refs, symbol_to_macro, DollarRefs}, + 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::{parse_macro_input, spanned::Spanned, Stmt}; +use syn::{fold::Fold, parse_macro_input, spanned::Spanned, Stmt}; pub(crate) struct WatchMacro { - refs: DollarRefs, + refs: DollarRefsScope, expr: Vec, } impl WatchMacro { - pub fn gen_code(input: TokenStream, outside: Option<&mut DollarRefs>) -> TokenStream1 { + pub fn gen_code(input: TokenStream, refs_ctx: &mut DollarRefsCtx) -> TokenStream1 { let span = input.span(); - let input = ok!(symbol_to_macro(input.into())); + let input = ok!(symbol_to_macro(TokenStream1::from(input))); let expr = parse_macro_input! { input as BodyExpr }; - - let (expr, mut refs) = ok!(fold_body_in_capture_and_require_refs(span, expr.0, outside)); - refs.sort_and_dedup(); - WatchMacro { refs, expr }.to_token_stream().into() + 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() + } } } @@ -34,9 +38,9 @@ impl ToTokens for WatchMacro { if refs.used_ctx() { quote! {{ #refs - let _ctx_handle = ctx!().handle(); + let _ctx_handle_ಠ_ಠ = ctx!().handle(); #upstream - .map(move |_| _ctx_handle.with_ctx(|ctx!(): &BuildCtx<'_>| { #(#expr)* })) + .map(move |_| _ctx_handle_ಠ_ಠ.with_ctx(|ctx!(): &BuildCtx<'_>| { #(#expr)* })) }} .to_tokens(tokens) } else { diff --git a/macros/src/writer_map_macro.rs b/macros/src/writer_map_macro.rs new file mode 100644 index 000000000..fe15b6088 --- /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/tests/rdl_macro_test.rs b/tests/rdl_macro_test.rs index 5ad779b36..560bc29f3 100644 --- a/tests/rdl_macro_test.rs +++ b/tests/rdl_macro_test.rs @@ -134,18 +134,18 @@ widget_layout_test!(dollar_as_middle_parent, width == 500., height == 500.,); fn pipe_as_field_value() -> impl Into { let size = Stateful::new(Size::zero()); - let size2 = size.clone(); + let size2 = size.clone_reader(); let w = fn_widget! { rdl! { SizedBox { size: pipe!(*$size2) }} }; - *size.state_ref() = Size::new(100., 100.); + *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(); + let margin2 = margin.clone_reader(); let w = fn_widget! { rdl! { SizedBox { @@ -153,20 +153,20 @@ fn pipe_as_builtin_field_value() -> impl Into { margin: pipe!(*$margin2) }} }; - *margin.state_ref() = EdgeInsets::all(50.); + *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(); + let scale2 = scale.clone_writer(); let w = fn_widget! { rdl! { SizedBox { size: pipe!(IconSize::of(ctx!()).tiny * *$scale) }} }; - *scale2.state_ref() = 2.; + *scale2.write() = 2.; w } widget_layout_test!(pipe_with_ctx, width == 36., height == 36.,); @@ -198,7 +198,7 @@ fn pipe_single_parent() { reset_test_env!(); let outside_blank = Stateful::new(true); - let outside_blank2 = outside_blank.clone(); + let outside_blank2 = outside_blank.clone_writer(); let w = fn_widget! { let edges = EdgeInsets::all(5.); let blank = pipe! { @@ -219,7 +219,7 @@ fn pipe_single_parent() { wnd.draw_frame(); assert_layout_result_by_path!(wnd, { path = [0], width == 110., height == 110., }); - *outside_blank2.state_ref() = false; + *outside_blank2.write() = false; wnd.draw_frame(); assert_layout_result_by_path!(wnd, { path = [0], width == 100., height == 100., }); } @@ -229,7 +229,7 @@ fn pipe_multi_parent() { reset_test_env!(); let stack_or_flex = Stateful::new(true); - let stack_or_flex2 = stack_or_flex.clone(); + let stack_or_flex2 = stack_or_flex.clone_writer(); let w = fn_widget! { let container = pipe! { let c: Box = if *$stack_or_flex { @@ -252,7 +252,7 @@ fn pipe_multi_parent() { wnd.draw_frame(); assert_layout_result_by_path!(wnd, { path = [0], width == 100., height == 100., }); - *stack_or_flex2.state_ref() = false; + *stack_or_flex2.write() = false; wnd.draw_frame(); assert_layout_result_by_path!(wnd, { path = [0], width == 200., height == 100., }); } @@ -262,7 +262,7 @@ fn pipe_as_child() { reset_test_env!(); let box_or_not = Stateful::new(true); - let box_or_not2 = box_or_not.clone(); + let box_or_not2 = box_or_not.clone_reader(); let w = fn_widget! { let blank: Pipe = pipe!{ if *$box_or_not2 { @@ -290,7 +290,7 @@ fn pipe_as_multi_child() { let fix_box = SizedBox { size: Size::new(100., 100.) }; let cnt = Stateful::new(0); - let cnt2 = cnt.clone(); + let cnt2 = cnt.clone_writer(); let w = fn_widget! { let boxes = pipe! { Multi::new((0..*$cnt).map(|_| fix_box.clone()).collect::>()) @@ -302,7 +302,7 @@ fn pipe_as_multi_child() { wnd.draw_frame(); assert_layout_result_by_path!(wnd, { path = [0], width == 0., height == 0., }); - *cnt2.state_ref() = 3; + *cnt2.write() = 3; wnd.draw_frame(); assert_layout_result_by_path!(wnd, { path = [0], width == 300., height == 100., }); } @@ -361,17 +361,17 @@ fn closure_in_fn_widget_capture() { reset_test_env!(); let hi_res = Stateful::new(CowArc::borrowed("")); - let hi_res2 = hi_res.clone(); + let hi_res2 = hi_res.clone_reader(); let w = fn_widget! { let mut text = @ Text { text: "hi" }; - let on_mounted = move |_: &mut _| *$hi_res =$text.text.clone(); + 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.state_ref(), "hi"); + assert_eq!(&**hi_res2.read(), "hi"); } fn at_embed_in_expression() -> impl Into { @@ -495,7 +495,7 @@ fn expression_for_children() { .map(move |v| v.then(|| @SizedBox { size: pipe!($sized_box.size) })); @Flex { - on_tap: move |_| $sized_box.size = size_five, + on_tap: move |_| $sized_box.write().size = size_five, @ { sized_box } @ { multi_box } @ { pipe_box } @@ -529,7 +529,7 @@ fn embed_widget_ref_outside() { let first = @SizedBox { size: Size::new(1., 1.) }; let three_box = @{ Multi::new((0..3).map(move |_| @ SizedBox { size: $first.size } ))}; @Flex { - @$first { on_tap: move |_| $first.size = Size::new(2., 2.)} + @$first { on_tap: move |_| $first.write().size = Size::new(2., 2.)} @{ three_box } } }; @@ -553,9 +553,9 @@ fn bind_fields() { let b = @SizedBox { size: pipe!($a.size) }; let c = @SizedBox { size }; watch!($a.size + $b.size) - .subscribe(move |v| $c.size = v); + .subscribe(move |v| $c.write().size = v); @Flex { - on_tap: move |_| $a.size *= 2., + on_tap: move |_| $a.write().size *= 2., @ { Multi::new([a, b, c]) } } }; @@ -689,18 +689,18 @@ fn builtin_method_support() { reset_test_env!(); let layout_size = Stateful::new(Size::zero()); - let c_layout_size = layout_size.clone_state(); + 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 = v); + .subscribe(move |v| *$layout_size.write() = v); sized_box }; let mut wnd = TestWindow::new(w); wnd.draw_frame(); - assert_eq!(&*c_layout_size.state_ref(), &Size::new(100., 100.)); + assert_eq!(*c_layout_size.read(), Size::new(100., 100.)); } #[test] @@ -734,13 +734,12 @@ fn fix_use_builtin_field_of_builtin_widget_gen_duplicate() { #[test] fn fix_access_builtin_with_gap() { - widget! { - Void { - id: this, - cursor: CursorIcon::Hand, + 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; + let _ = $this.silent().cursor; } } }; @@ -751,15 +750,13 @@ fn fix_subscribe_cancel_after_widget_drop() { reset_test_env!(); let notify_cnt = Stateful::new(0); - let cnt = notify_cnt.clone_state(); + let cnt: Writer = notify_cnt.clone_writer(); let trigger = Stateful::new(true); - let c_trigger = trigger.clone_state(); + let c_trigger = trigger.clone_reader(); let w = fn_widget! { - let container = @SizedBox { size: Size::zero() }; - let h = watch!(*$c_trigger) - .subscribe(move |_| *$cnt +=1 ) - .unsubscribe_when_dropped(); - container.own_data(h); + 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 { @ { @@ -773,20 +770,20 @@ fn fix_subscribe_cancel_after_widget_drop() { let mut wnd = TestWindow::new(w); wnd.draw_frame(); { - *trigger.state_ref() = true + *trigger.write() = true } wnd.draw_frame(); - assert_eq!(*notify_cnt.state_ref(), 1); + assert_eq!(*notify_cnt.read(), 1); { - *trigger.state_ref() = true + *trigger.write() = true } wnd.draw_frame(); - assert_eq!(*notify_cnt.state_ref(), 2); + assert_eq!(*notify_cnt.read(), 2); { - *trigger.state_ref() = true + *trigger.write() = true } wnd.draw_frame(); - assert_eq!(*notify_cnt.state_ref(), 3); + assert_eq!(*notify_cnt.read(), 3); } fn fix_local_assign_tuple() -> Widget { @@ -816,7 +813,7 @@ fn fix_silent_not_relayout_dyn_widget() { reset_test_env!(); let trigger_size = Stateful::new(ZERO_SIZE); - let c_trigger_size = trigger_size.clone_state(); + let c_trigger_size = trigger_size.clone_writer(); let w = fn_widget! { pipe! { if $trigger_size.area() > 0. { @@ -831,7 +828,7 @@ fn fix_silent_not_relayout_dyn_widget() { wnd.draw_frame(); assert_layout_result_by_path!(wnd, { path = [0], size == ZERO_SIZE,}); { - **c_trigger_size.state_ref().silent() = Size::new(100., 100.); + *c_trigger_size.silent() = Size::new(100., 100.); } // after silent modified, dyn widget not rebuild. wnd.draw_frame(); @@ -844,7 +841,7 @@ fn no_watch() { let size = Stateful::new(ZERO_SIZE); let w = widget! { - states { size: size.clone() } + states { size: size.clone_stateful() } SizedBox { size: no_watch!(*size) } }; @@ -853,7 +850,7 @@ fn no_watch() { assert_layout_result_by_path!(wnd, { path = [0], size == ZERO_SIZE,}); { - *size.state_ref() = Size::new(100., 100.) + *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 a7600d448..d490d5ff8 100644 --- a/themes/material/src/lib.rs +++ b/themes/material/src/lib.rs @@ -268,7 +268,7 @@ fn override_compose_decorator(theme: &mut FullTheme) { let host = scrollbar_thumb(host, EdgeInsets::vertical(1.)); let mut thumb = @ $host { left_anchor: pipe!($this.offset) }; - let left_trans = route_state!($thumb.left_anchor); + 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()), @@ -284,7 +284,7 @@ fn override_compose_decorator(theme: &mut FullTheme) { let host = scrollbar_thumb(host, EdgeInsets::vertical(1.)); let mut thumb = @ $host { top_anchor: pipe!($this.offset) }; - let top_trans = route_state!($thumb.top_anchor); + 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()), @@ -321,7 +321,7 @@ fn override_compose_decorator(theme: &mut FullTheme) { let ease_in = transitions::EASE_IN.of(ctx!()); match $style.pos { Position::Top | Position::Bottom => { - let left = route_state!($indicator.left_anchor); + 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()), @@ -330,7 +330,7 @@ fn override_compose_decorator(theme: &mut FullTheme) { } Position::Left | Position::Right => { - let top = route_state!($indicator.top_anchor); + let top = map_writer!($indicator.top_anchor); top.transition_with( ease_in, move |from, to, rate| PositionUnit::lerp(from, to, rate, $style.rect.height()), diff --git a/themes/material/src/ripple.rs b/themes/material/src/ripple.rs index 1337487b7..36d881fcb 100644 --- a/themes/material/src/ripple.rs +++ b/themes/material/src/ripple.rs @@ -41,7 +41,7 @@ impl ComposeChild for Ripple { fn compose_child(this: State, child: Self::Child) -> Widget { fn_widget! { let mut container = @Stack { fit: StackFit::Passthrough }; - let mut ripple_at = $this.ripple_at.clone_state(); + let mut ripple_at = $this.ripple_at.clone_writer(); let ripple_widget = pipe!(*$ripple_at) .map(move |launch_at| { @@ -61,7 +61,7 @@ impl ComposeChild for Ripple { let mut ripper_enter = @Animate { transition: transitions::LINEAR.of(ctx!()), state: LerpFnState::new( - route_state!($ripple.path), + map_writer!($ripple.path), move |_, _, rate| { let radius = Lerp::lerp(&0., &radius, rate); Path::circle(launch_at, radius) @@ -75,11 +75,11 @@ impl ComposeChild for Ripple { // the ripple used only once, so we unsubscribe it after the animate finished. .take(1) .subscribe(move |_| { - $ripple_at.take(); + $ripple_at.write().take(); }); - let mut ripper_fade_out = route_state!($ripple.opacity) + let mut ripper_fade_out = map_writer!($ripple.opacity) .transition(transitions::EASE_OUT.of(ctx!()), ctx!()); let bounded = $this.bounded; @@ -95,7 +95,7 @@ impl ComposeChild for Ripple { Some(@IgnorePointer { delay_drop_until: pipe!(!$ripper_fade_out.is_running()), - on_disposed: move |_| $ripple.opacity = 0., + on_disposed: move |_| $ripple.write().opacity = 0., on_mounted: move |_| { ripper_enter.run(); }, @Container { size: $container.layout_size(), @@ -105,7 +105,7 @@ impl ComposeChild for Ripple { }); @ $container { - on_pointer_down: move |e| *$ripple_at = if $this.center { + 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 { @@ -121,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.ripple_at.state_ref() = Some(pos); } + pub fn launch_at(&mut self, pos: Point) { *self.ripple_at.write() = Some(pos); } } diff --git a/widgets/src/checkbox.rs b/widgets/src/checkbox.rs index b549cb5d7..ca3dbbf5d 100644 --- a/widgets/src/checkbox.rs +++ b/widgets/src/checkbox.rs @@ -101,9 +101,9 @@ impl ComposeChild for Checkbox { @ $checkbox { cursor: CursorIcon::Hand, - on_tap: move |_| $this.switch_check(), + on_tap: move |_| $this.write().switch_check(), on_key_up: move |k| if k.key == VirtualKeyCode::Space { - $this.switch_check() + $this.write().switch_check() } } } diff --git a/widgets/src/input.rs b/widgets/src/input.rs index c845f4b8f..264700a37 100644 --- a/widgets/src/input.rs +++ b/widgets/src/input.rs @@ -107,13 +107,13 @@ impl ComposeChild for Input { area.modifies() .delay(Duration::ZERO, ctx!().window().frame_scheduler()) .subscribe(move |_| { - let mut this = $this; + let mut this = $this.silent(); let area = $area; if area.caret != this.caret { - this.silent().caret = area.caret.clone(); + this.caret = area.caret.clone(); } if area.text != this.text { - this.silent().text = area.text.clone(); + this.text = area.text.clone(); } }); @FocusScope { @@ -150,13 +150,13 @@ impl ComposeChild for TextArea { area.modifies() .delay(Duration::ZERO, ctx!().window().frame_scheduler()) .subscribe(move |_| { - let mut this = $this; + let mut this = $this.silent(); let mut area = $area; if area.caret != this.caret { - this.silent().caret = area.caret.clone(); + this.caret = area.caret.clone(); } if area.text != this.text { - this.silent().text = area.text.clone(); + this.text = area.text.clone(); } }); @FocusScope { diff --git a/widgets/src/input/caret.rs b/widgets/src/input/caret.rs index 0599f29d1..424697e5d 100644 --- a/widgets/src/input/caret.rs +++ b/widgets/src/input/caret.rs @@ -23,13 +23,13 @@ impl Compose for Caret { .distinct_until_changed() .subscribe(move |focused| { if focused { - $caret.opacity = 1.; + $caret.write().opacity = 1.; let unsub = interval(blink_interval, AppCtx::scheduler()) - .subscribe(move |idx| $caret.opacity = (idx % 2) as f32) + .subscribe(move |idx| $caret.write().opacity = (idx % 2) as f32) .unsubscribe_when_dropped(); _guard = Some(unsub); } else { - $caret.opacity = 0.; + $caret.write().opacity = 0.; _guard = None; } }); diff --git a/widgets/src/input/editarea.rs b/widgets/src/input/editarea.rs index 35e6d5961..19b985ae7 100644 --- a/widgets/src/input/editarea.rs +++ b/widgets/src/input/editarea.rs @@ -51,17 +51,18 @@ impl ComposeChild for TextEditorArea { watch!(Point::new($caret.left_anchor.abs_value(1.), $caret.top_anchor.abs_value(1.))) .scan_initial((Point::zero(), Point::zero()), |pair, v| (pair.1, v)) .subscribe(move |(before, after)| { - let pos = auto_scroll_pos(&*$scrollable, before, after, $caret.layout_size()); - scrollable.state_ref().silent().jump_to(pos); + let mut scrollable = $scrollable.silent(); + let pos = auto_scroll_pos(&*scrollable, before, after, $caret.layout_size()); + scrollable.jump_to(pos); }); selectable.modifies() .delay(Duration::ZERO, ctx!().window().frame_scheduler()) .subscribe(move |_| { - let mut this = $this; + let mut this = $this.silent(); let selectable = $selectable; if selectable.caret != this.caret { - this.silent().caret = selectable.caret.clone(); + this.caret = selectable.caret.clone(); } }); @@ -74,16 +75,16 @@ impl ComposeChild for TextEditorArea { .sample(tick_of_layout_ready) .subscribe(move |_| { let (offset, height) = $selectable.cursor_layout(); - $caret.top_anchor = PositionUnit::Pixel(offset.y); - $caret.left_anchor = PositionUnit::Pixel(offset.x); - $caret.height = height; + $caret.write().top_anchor = PositionUnit::Pixel(offset.y); + $caret.write().left_anchor = PositionUnit::Pixel(offset.x); + $caret.write().height = height; }); @FocusScope { - on_key_down: move|key| Self::key_handle(&mut $this, key), - on_chars: move|ch| Self::edit_handle(&mut $this, ch), + on_key_down: move|key| Self::key_handle(&mut $this.write(), key), + on_chars: move|ch| Self::edit_handle(&mut $this.write(), ch), @$container { @{ placeholder.map(move |mut holder| @Text { diff --git a/widgets/src/input/handle.rs b/widgets/src/input/handle.rs index 21053cceb..a1a2bdec4 100644 --- a/widgets/src/input/handle.rs +++ b/widgets/src/input/handle.rs @@ -1,7 +1,7 @@ use std::ops::{Deref, DerefMut}; use ribir_core::prelude::{ - AppCtx, CharsEvent, GraphemeCursor, KeyboardEvent, RefState, TextWriter, VirtualKeyCode, + AppCtx, CharsEvent, GraphemeCursor, KeyboardEvent, TextWriter, VirtualKeyCode, }; #[macro_export] @@ -50,7 +50,7 @@ macro_rules! declare_writer { declare_writer!(InputWriter, TextEditorArea); use super::TextEditorArea; impl TextEditorArea { - pub(crate) fn edit_handle(this: &mut RefState, event: &mut CharsEvent) { + pub(crate) fn edit_handle(this: &mut TextEditorArea, event: &mut CharsEvent) { if event.common.with_command_key() { return; } @@ -67,7 +67,7 @@ impl TextEditorArea { } } - pub(crate) fn key_handle(this: &mut RefState, event: &mut KeyboardEvent) { + pub(crate) fn key_handle(this: &mut TextEditorArea, event: &mut KeyboardEvent) { let mut deal = false; if event.with_command_key() { deal = key_with_command(this, event) @@ -78,7 +78,7 @@ impl TextEditorArea { } } -fn key_with_command(this: &mut RefState, event: &mut KeyboardEvent) -> bool { +fn key_with_command(this: &mut TextEditorArea, event: &mut KeyboardEvent) -> bool { if !event.with_command_key() { return false; } @@ -111,7 +111,7 @@ fn key_with_command(this: &mut RefState, event: &mut KeyboardEve } } -fn single_key(this: &mut RefState, key: &mut KeyboardEvent) -> bool { +fn single_key(this: &mut TextEditorArea, key: &mut KeyboardEvent) -> bool { match key.key { VirtualKeyCode::NumpadEnter | VirtualKeyCode::Return => { if this.multi_line { diff --git a/widgets/src/input/selected_text.rs b/widgets/src/input/selected_text.rs index 85af5e215..349859d17 100644 --- a/widgets/src/input/selected_text.rs +++ b/widgets/src/input/selected_text.rs @@ -21,7 +21,7 @@ impl CustomStyle for SelectedTextStyle { impl Compose for SelectedText { fn compose(this: State) -> Widget { fn_widget! { - let color = SelectedTextStyle::of(ctx).brush.clone(); + let color = SelectedTextStyle::of(ctx!()).brush.clone(); @Stack { @ { pipe!{ let color = color.clone(); diff --git a/widgets/src/input/text_selectable.rs b/widgets/src/input/text_selectable.rs index 7d13ef2e0..05c4a787d 100644 --- a/widgets/src/input/text_selectable.rs +++ b/widgets/src/input/text_selectable.rs @@ -14,7 +14,7 @@ pub struct TextSelectable { } impl ComposeChild for TextSelectable { - type Child = State; + type Child = FatObj>; fn compose_child(this: State, text: Self::Child) -> Widget { fn_widget! { let mut host = @Stack { fit: StackFit::Passthrough }; @@ -25,7 +25,7 @@ impl ComposeChild for TextSelectable { @$host { on_pointer_move: move |e| { - let mut this = $this; + let mut this = $this.write(); if let CaretState::Selecting(begin, _) = this.caret { if e.point_type == PointerType::Mouse && e.mouse_buttons() == MouseButtons::PRIMARY { @@ -47,10 +47,10 @@ impl ComposeChild for TextSelectable { } else { end }; - $this.caret = CaretState::Selecting(begin, end); + $this.write().caret = CaretState::Selecting(begin, end); }, on_pointer_up: move |_| { - let mut this = $this; + let mut this = $this.write(); if let CaretState::Selecting(begin, end) = this.caret { this.caret = if begin == end { CaretState::Caret(begin) @@ -63,14 +63,14 @@ impl ComposeChild for TextSelectable { let position = e.position(); let caret = $this.helper.caret_position_from_pos(position.x, position.y); let rg = select_word(&$text.text, caret.cluster); - $this.caret = CaretState::Select( + $this.write().caret = CaretState::Select( CaretPosition { cluster: rg.start, position: None }, CaretPosition { cluster: rg.end, position: None } ); }, - on_key_down: move |event| key_handle(&mut $this, &$text.text, event), + on_key_down: move |event| key_handle(&mut $this.write(), &$text.text, event), on_performed_layout: move |e: &mut LifecycleEvent| { - let mut this = $this; + let mut this = $this.write(); let bound = e.layout_clamp().expect("layout info must exit in performed_layout"); this.helper.glyphs = Some($text.text_layout(bound.max)); this.forget_modifies(); @@ -89,7 +89,7 @@ impl TextSelectable { fn selected_rect(&self) -> Vec { self.helper.selection(&self.caret.select_range()) } } -fn key_handle(this: &mut RefState, text: &CowArc, event: &mut KeyboardEvent) { +fn key_handle(this: &mut TextSelectable, text: &CowArc, event: &mut KeyboardEvent) { let mut deal = false; if event.with_command_key() { deal = deal_with_command(this, text, event); @@ -101,7 +101,7 @@ fn key_handle(this: &mut RefState, text: &CowArc, event: &m } fn deal_with_command( - this: &mut RefState, + this: &mut TextSelectable, text: &CowArc, event: &mut KeyboardEvent, ) -> bool { @@ -132,7 +132,7 @@ fn is_move_by_word(event: &KeyboardEvent) -> bool { return event.with_ctrl_key(); } -fn deal_with_selection(this: &mut RefState, text: &str, event: &mut KeyboardEvent) { +fn deal_with_selection(this: &mut TextSelectable, text: &str, event: &mut KeyboardEvent) { let old_caret = this.caret; match event.key { VirtualKeyCode::Left => { diff --git a/widgets/src/scrollbar.rs b/widgets/src/scrollbar.rs index fe0852bf6..ac4fc0f3e 100644 --- a/widgets/src/scrollbar.rs +++ b/widgets/src/scrollbar.rs @@ -59,19 +59,19 @@ impl ComposeChild for HScrollBar { scroll_pos: Point::new($this.offset, 0.), }; let scrollbar = @HRawScrollbar { - scrolling: scrolling.get_builtin_scrollable_widget(ctx!()).clone_state(), + 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.offset = v); + .subscribe(move |v| $this.write().offset = v); watch!($this.offset) .distinct_until_changed() .subscribe(move |v| { let y = $scrolling.scroll_pos.y; - $scrolling.jump_to(Point::new(v, y)); + $scrolling.write().jump_to(Point::new(v, y)); }); @Stack { @@ -103,19 +103,19 @@ impl ComposeChild for VScrollBar { }; let scrollbar = @VRawScrollbar { - scrolling: scrolling.get_builtin_scrollable_widget(ctx!()).clone_state(), + 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.offset = v); + .subscribe(move |v| $this.write().offset = v); watch!($this.offset) .distinct_until_changed() .subscribe(move |v| { let x = $scrolling.scroll_pos.x; - $scrolling.jump_to(Point::new(x, v)); + $scrolling.write().jump_to(Point::new(x, v)); }); @Stack { @@ -145,11 +145,11 @@ impl ComposeChild for BothScrollbar { scroll_pos: $this.offset, }; let mut h_bar = @HRawScrollbar { - scrolling: scrolling.get_builtin_scrollable_widget(ctx!()).clone_state(), + 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_state(), + scrolling: scrolling.get_builtin_scrollable_widget(ctx!()).clone_reader(), h_align: HAlign::Right, margin: EdgeInsets::only_bottom($h_bar.layout_height()) }; @@ -157,10 +157,10 @@ impl ComposeChild for BothScrollbar { // `scrolling` and `this` have same lifetime, so we needn't unsubscribe. watch!($scrolling.scroll_pos) .distinct_until_changed() - .subscribe(move |v| $this.offset = v); + .subscribe(move |v| $this.write().offset = v); watch!($this.offset) .distinct_until_changed() - .subscribe(move |v| $scrolling.jump_to(v) ); + .subscribe(move |v| $scrolling.write().jump_to(v) ); @Stack{ fit: StackFit::Passthrough, @@ -177,13 +177,14 @@ impl ComposeChild for BothScrollbar { /// `scrolling` widget. #[derive(Declare, Declare2)] pub struct HRawScrollbar { - scrolling: State, + scrolling: Reader, } impl Compose for HRawScrollbar { fn compose(this: State) -> Widget { fn_widget! { @ { + let mut scrolling = $this.scrolling.clone_reader(); let ScrollBarStyle { thickness, thumb_min_size, @@ -196,8 +197,7 @@ impl Compose for HRawScrollbar { }; let thumb_outline = @HScrollBarThumbDecorator { - offset: { - let scrolling = &$this.scrolling; + 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() @@ -206,7 +206,6 @@ impl Compose for HRawScrollbar { let mut container = @Container { size: { - let scrolling = &$this.scrolling; let scrolling = $scrolling; let page_width = scrolling.scroll_view_size().width; let content_width = scrolling.scroll_content_size().width; @@ -217,13 +216,10 @@ impl Compose for HRawScrollbar { watch!($container.layout_height()) .distinct_until_changed() - .subscribe(move |v| { - $track_box.size.height = v; - }); + .subscribe(move |v| $track_box.write().size.height = v); @Stack { visible: pipe! { - let scrolling = &$this.scrolling; let scrolling = $scrolling; scrolling.can_scroll() }, @@ -243,14 +239,14 @@ impl Compose for HRawScrollbar { /// `scrolling` widget. #[derive(Declare, Declare2)] pub struct VRawScrollbar { - scrolling: State, + scrolling: Reader, } impl Compose for VRawScrollbar { fn compose(this: State) -> Widget { fn_widget! { @ { - let mut scrolling = $this.scrolling.clone_state(); + let mut scrolling = $this.scrolling.clone_reader(); let ScrollBarStyle { thickness, thumb_min_size, @@ -264,7 +260,6 @@ impl Compose for VRawScrollbar { 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() @@ -283,9 +278,7 @@ impl Compose for VRawScrollbar { watch!($container.layout_width()) .distinct_until_changed() - .subscribe(move |v| { - $track_box.size.width = v; - }); + .subscribe(move |v| $track_box.write().size.width = v); @Stack { visible: pipe! { $scrolling.can_scroll() }, @@ -357,18 +350,18 @@ mod test { let offset = Stateful::new(Point::zero()); let v_offset = Stateful::new(0.); let h_offset = Stateful::new(0.); - let c_offset = offset.clone(); - let c_v_offset = v_offset.clone(); - let c_h_offset = h_offset.clone(); + 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 = v); + .subscribe(move|v| *$v_offset.write() = v); watch!($h_bar.offset) - .subscribe(move|v| *$h_offset = v); + .subscribe(move|v| *$h_offset.write() = v); let container_size = Size::new(100., 100.); @Column { @@ -390,13 +383,13 @@ mod test { let mut wnd = TestWindow::new_with_size(w, Size::new(1024., 1024.)); wnd.draw_frame(); { - *c_offset.state_ref() = Point::new(-10., -10.); + *c_offset.write() = Point::new(-10., -10.); } { - *c_offset.state_ref() = Point::new(-20., -20.); + *c_offset.write() = Point::new(-20., -20.); } wnd.draw_frame(); - assert_eq!(*c_v_offset.state_ref(), c_offset.state_ref().y); - assert_eq!(*c_h_offset.state_ref(), c_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 162ebf4e3..f89f9ae6e 100644 --- a/widgets/src/tabs.rs +++ b/widgets/src/tabs.rs @@ -173,8 +173,8 @@ impl Tabs { fn tab_header( headers: Vec<(Option, Option>)>, tabs_style: TabsStyle, - tabs: State, - indicator: State, + tabs: impl StateWriter + 'static, + indicator: impl StateWriter + 'static, ) -> impl Iterator { let TabsStyle { icon_size: size, @@ -188,11 +188,11 @@ impl Tabs { .into_iter() .enumerate() .map(move |(idx, (icon, label))| { - let tabs = tabs.clone_state(); + let tabs = tabs.clone_writer(); let active_color = active_color.clone(); let foreground = foreground.clone(); let label_style = label_style.clone(); - let indicator = indicator.clone_state(); + let indicator = indicator.clone_writer(); fn_widget! { let icon_widget = icon.map(|icon| @Icon { size, @ { icon }}); let label_widget = label.map(|label| { @@ -208,13 +208,13 @@ impl Tabs { @ { let mut tab_header = @Expanded { on_tap: move |_| if $tabs.cur_idx != idx { - $tabs.cur_idx = idx; + $tabs.write().cur_idx = idx; }, }; watch!(($tabs.cur_idx == idx, $tab_header.layout_rect())) .filter_map(|(active, rect)| active.then_some(rect)) - .subscribe(move |v| $indicator.rect = v); + .subscribe(move |v| $indicator.write().rect = v); @TabDecorator { @$tab_header { @@ -314,8 +314,8 @@ impl ComposeChild for Tabs { Multi::new( Tabs::tab_header( headers, tabs_style.clone(), - this.clone_state(), - indicator_decorator.clone_state() + this.clone_writer(), + indicator_decorator.clone_writer() ) ) } diff --git a/widgets/src/text_field.rs b/widgets/src/text_field.rs index c300efee2..04df03a2c 100644 --- a/widgets/src/text_field.rs +++ b/widgets/src/text_field.rs @@ -106,7 +106,7 @@ impl ComposeChild for TextFieldThemeProxy { fn_widget! { @ $child { on_tap: move |_| { - let mut this = $this; + let mut this = $this.write(); match this.state { TextFieldState::Enabled => this.state = TextFieldState::Focused, TextFieldState::Hovered => this.state = TextFieldState::Focused, @@ -114,15 +114,15 @@ impl ComposeChild for TextFieldThemeProxy { }; }, on_pointer_move: move |_| { - let mut this = $this; + let mut this = $this.write(); if this.state == TextFieldState::Enabled { this.state = TextFieldState::Hovered } }, on_pointer_leave: move |_| { - let mut this = $this; + let mut this = $this.write(); if this.state == TextFieldState::Hovered { this.state = TextFieldState::Enabled } }, on_focus_out: move |_| { - let mut this = $this; + let mut this = $this.write(); if this.state == TextFieldState::Focused { this.state = TextFieldState::Enabled } }, } @@ -290,7 +290,9 @@ impl ComposeChild for TextField { let mut theme = @TextFieldThemeProxy { suit: theme_suit, state: TextFieldState::default(), - }; + }.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)), @@ -307,7 +309,7 @@ impl ComposeChild for TextField { } @Expanded { flex: 1., - @{ build_content_area(this.clone_state(), theme.clone_state(), config) } + @{ build_content_area(this, theme, config) } } @{ trailing_icon.map(|t| @Icon { @@ -318,8 +320,8 @@ impl ComposeChild for TextField { } @Container { v_align: VAlign::Bottom, - size: pipe!(Size::new(f32::MAX, $theme.indicator_height)), - background: pipe!($theme.indicator), + size: indicator_size, + background: indicator_bg, } } } @@ -339,11 +341,11 @@ fn build_input_area( visible: pipe!(!$this.text.is_empty() || $theme.state == TextFieldState::Focused), }; - let mut visible = route_state!($input_area.visible); + let mut visible = map_writer!($input_area.visible); visible.transition(transitions::LINEAR.of(ctx!()), ctx!()); let mut input = @Input{ style: pipe!($theme.text.clone()) }; - $input.set_text($this.text.clone()); + $input.write().set_text($this.text.clone()); watch!($input.text()) .distinct_until_changed() @@ -351,15 +353,13 @@ fn build_input_area( watch!($this.text.clone()) .distinct_until_changed() - .subscribe(move |val| $input.set_text(val)); + .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()) - .unsubscribe_when_dropped(); - input.own_data(h); - + .subscribe(move |_| $input.request_focus()); + input.as_stateful().unsubscribe_on_drop(h); @Row { @{ @@ -400,8 +400,8 @@ impl Compose for TextFieldLabel { text_style: pipe!($this.style.clone()), }; - let mut font_size = route_state!($this.style.font_size); - font_size.transition(transitions::LINEAR.of(ctx!()), ctx!()); + map_writer!($this.style.font_size) + .transition(transitions::LINEAR.of(ctx!()), ctx!()); label } @@ -420,8 +420,8 @@ fn build_content_area( padding: pipe!($theme.input_padding($this.text.is_empty())), }; - let mut padding = route_state!($content_area.padding); - padding.transition(transitions::LINEAR.of(ctx!()), ctx!()); + map_writer!($content_area.padding) + .transition(transitions::LINEAR.of(ctx!()), ctx!()); @ $content_area { @ { @@ -433,7 +433,7 @@ fn build_content_area( } }) } - @ { build_input_area(this.clone_state(), theme.clone_state(), prefix, suffix, placeholder)} + @ { build_input_area(this, theme, prefix, suffix, placeholder)} } } .into()