From 04eb825ac07fac79d7e211ccdebe12dddcfdf805 Mon Sep 17 00:00:00 2001 From: Adoo Date: Sun, 3 Sep 2023 16:06:25 +0800 Subject: [PATCH 01/10] =?UTF-8?q?feat(algo):=20=F0=9F=8E=B8=20add=20a=20st?= =?UTF-8?q?rong=20reference=20counter=20only=20smart=20pointer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- algo/src/lib.rs | 2 + algo/src/sc.rs | 227 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 algo/src/sc.rs diff --git a/algo/src/lib.rs b/algo/src/lib.rs index 272de592c..1caaa9fe2 100644 --- a/algo/src/lib.rs +++ b/algo/src/lib.rs @@ -4,3 +4,5 @@ pub use cow_rc::{CowArc, Substr}; pub use frame_cache::*; mod resource; pub use resource::*; +mod sc; +pub use sc::*; diff --git a/algo/src/sc.rs b/algo/src/sc.rs new file mode 100644 index 000000000..326cc7eb2 --- /dev/null +++ b/algo/src/sc.rs @@ -0,0 +1,227 @@ +use std::{ + any::Any, + cell::Cell, + fmt::{Debug, Display, Formatter, Pointer}, + ptr::NonNull, +}; + +/// A single-thread smart pointer with strong reference count only. +/// This is a simplified version of `std::rc::Sc` with the weak reference count. +/// Use it when you are sure that there is no cycle in the reference graph or in +/// a inner resource manage that will break the cycle by itself. +pub struct Sc(NonNull>); + +struct ScBox { + ref_cnt: Cell, + value: T, +} + +impl Sc { + /// Constructs a new `Sc`. + /// + /// # Examples + /// + /// ``` + /// use ribir_algo::Sc; + /// + /// let five = Sc::new(5); + /// ``` + #[inline] + pub fn new(value: T) -> Self { + Self::from_inner(Box::leak(Box::new(ScBox { ref_cnt: Cell::new(1), value })).into()) + } + + /// Returns the inner value, if the `Sc` has exactly one strong reference. + /// + /// Otherwise, an [`Err`] is returned with the same `Sc` that was + /// passed in. + /// + /// This will succeed even if there are outstanding weak references. + /// + /// # Examples + /// + /// ``` + /// use ribir_algo::Sc; + /// + /// let x = Sc::new(3); + /// assert_eq!(Sc::try_unwrap(x), Ok(3)); + /// + /// let x = Sc::new(4); + /// let _y = Sc::clone(&x); + /// assert_eq!(*Sc::try_unwrap(x).unwrap_err(), 4); + /// ``` + #[inline] + pub fn try_unwrap(this: Self) -> Result { + if Sc::ref_count(&this) == 1 { + unsafe { + let val = std::ptr::read(&*this); // copy the contained object + + // avoid to call `drop` but release the memory. + let layout = std::alloc::Layout::for_value(this.0.as_ref()); + let ptr = this.0.as_ptr(); + std::mem::forget(this); + std::alloc::dealloc(ptr as *mut _, layout); + + Ok(val) + } + } else { + Err(this) + } + } +} + +impl Sc { + /// + /// todo: prefer implement `CoerceUnsized` if it stable. + #[inline] + pub fn new_any(value: T) -> Self { + let inner: Box> = Box::new(ScBox { ref_cnt: Cell::new(1), value }); + Self::from_inner(Box::leak(inner).into()) + } +} + +impl Sc { + // Gets the number of pointers to this allocation. + /// + /// # Examples + /// + /// ``` + /// use ribir_algo::Sc; + /// + /// let five = Sc::new(5); + /// let _also_five = Sc::clone(&five); + /// + /// assert_eq!(2, Sc::ref_count(&five)); + /// ``` + #[inline] + pub fn ref_count(&self) -> usize { self.inner().ref_cnt() } + + /// Returns `true` if the two `Sc`s point to the same allocation in a vein + /// similar to [`ptr::eq`]. See [that function][`ptr::eq`] for caveats when + /// comparing `dyn Trait` pointers. + /// + /// # Examples + /// + /// ``` + /// use ribir_algo::Sc; + /// + /// let five = Sc::new(5); + /// let same_five = Sc::clone(&five); + /// let other_five = Sc::new(5); + /// + /// assert!(Sc::ptr_eq(&five, &same_five)); + /// assert!(!Sc::ptr_eq(&five, &other_five)); + /// ``` + pub fn ptr_eq(this: &Self, other: &Self) -> bool { this.0.as_ptr() == other.0.as_ptr() } + + fn from_inner(ptr: NonNull>) -> Self { Self(ptr) } + + fn inner(&self) -> &ScBox { + // Safety: we're guaranteed that the inner pointer is valid when the `Sc` is + // alive + unsafe { self.0.as_ref() } + } +} + +impl Sc { + /// Attempt to downcast the `Sc` to a concrete type. + /// + /// # Examples + /// + /// ``` + /// use std::any::Any; + /// use ribir_algo::Sc; + /// + /// fn print_if_string(value: Sc) { + /// if let Ok(string) = value.downcast::() { + /// println!("String ({}): {}", string.len(), string); + /// } + /// } + /// + /// let my_string = "Hello World".to_string(); + /// print_if_string(Sc::new_any(my_string)); + /// print_if_string(Sc::new_any(0i8)); + /// ``` + pub fn downcast(self) -> Result, Sc> { + if (*self).is::() { + let ptr = self.0.cast::>(); + std::mem::forget(self); + Ok(Sc::from_inner(ptr)) + } else { + Err(self) + } + } +} + +impl ScBox { + fn inc(&self) { self.ref_cnt.set(self.ref_cnt.get() + 1); } + fn dec(&self) { self.ref_cnt.set(self.ref_cnt.get() - 1) } + fn ref_cnt(&self) -> usize { self.ref_cnt.get() } +} + +impl std::ops::Deref for Sc { + type Target = T; + #[inline] + fn deref(&self) -> &Self::Target { &self.inner().value } +} + +impl Drop for Sc { + fn drop(&mut self) { + self.inner().dec(); + if self.inner().ref_cnt() == 0 { + unsafe { + let layout = std::alloc::Layout::for_value(self.0.as_ref()); + let ptr = self.0.as_ptr(); + std::ptr::drop_in_place(ptr); + std::alloc::dealloc(ptr as *mut _, layout) + } + } + } +} + +impl Clone for Sc { + #[inline] + fn clone(&self) -> Self { + self.inner().inc(); + Self(self.0) + } +} + +impl Display for Sc { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { Display::fmt(&**self, f) } +} + +impl Debug for Sc { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { Debug::fmt(&**self, f) } +} + +impl Pointer for Sc { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Pointer::fmt(&(&**self as *const T), f) + } +} + +impl Default for Sc { + #[inline] + fn default() -> Sc { Sc::new(Default::default()) } +} + +impl PartialEq for Sc { + #[inline] + fn eq(&self, other: &Sc) -> bool { **self == **other } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_sc() { + let a = Sc::new(1); + assert_eq!(Sc::ref_count(&a), 1); + let b = Sc::clone(&a); + assert_eq!(Sc::ref_count(&b), 2); + drop(a); + assert_eq!(Sc::ref_count(&b), 1); + drop(b); + } +} From 26334cf221a0af1035dde6b3b22bbb8cd4ce93af Mon Sep 17 00:00:00 2001 From: Adoo Date: Tue, 4 Jul 2023 21:52:24 +0800 Subject: [PATCH 02/10] =?UTF-8?q?feat(macros):=20=F0=9F=8E=B8=20the=20new?= =?UTF-8?q?=20syntax=20split=20`widget`=20into=20several=20small=20macros,?= =?UTF-8?q?=20more=20flexible=20and=20consistent.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/src/animation/animate.rs | 2 +- core/src/assign_observable.rs | 45 -- core/src/builtin_widgets.rs | 148 +++++++ core/src/builtin_widgets/align.rs | 4 +- core/src/builtin_widgets/anchor.rs | 8 +- core/src/builtin_widgets/box_decoration.rs | 2 +- core/src/builtin_widgets/cursor.rs | 2 +- core/src/builtin_widgets/delay_drop_widget.rs | 2 +- core/src/builtin_widgets/fitted_box.rs | 2 +- core/src/builtin_widgets/focus_node.rs | 4 +- core/src/builtin_widgets/has_focus.rs | 2 +- core/src/builtin_widgets/layout_box.rs | 2 +- core/src/builtin_widgets/lifecycle.rs | 44 +- core/src/builtin_widgets/margin.rs | 2 +- core/src/builtin_widgets/mouse_hover.rs | 2 +- core/src/builtin_widgets/opacity.rs | 2 +- core/src/builtin_widgets/padding.rs | 2 +- core/src/builtin_widgets/pointer_pressed.rs | 2 +- core/src/builtin_widgets/scrollable.rs | 2 +- core/src/builtin_widgets/theme.rs | 2 + core/src/builtin_widgets/transform_widget.rs | 2 +- core/src/builtin_widgets/visibility.rs | 2 +- core/src/builtin_widgets/void.rs | 2 +- core/src/context/app_ctx.rs | 9 +- core/src/context/build_context.rs | 94 ++++- core/src/declare.rs | 74 +++- core/src/dynamic_widget.rs | 18 +- core/src/events/listener_impl_helper.rs | 49 ++- core/src/lib.rs | 15 +- core/src/pipe.rs | 255 ++++++++++++ core/src/state.rs | 111 ++++- core/src/state/readonly.rs | 4 +- core/src/state/stateful.rs | 40 +- core/src/test_helper.rs | 20 +- core/src/widget.rs | 55 +-- core/src/widget_children.rs | 36 +- core/src/widget_children/child_convert.rs | 20 +- .../src/widget_children/compose_child_impl.rs | 26 +- core/src/widget_children/multi_child_impl.rs | 54 +-- core/src/widget_children/single_child_impl.rs | 71 ++-- core/src/widget_tree.rs | 41 +- core/src/widget_tree/layout_info.rs | 2 +- core/src/widget_tree/widget_id.rs | 12 +- core/src/window.rs | 33 +- dev-helper/src/example_framework.rs | 2 +- docs/essentials/widget_language.md | 2 +- examples/counter/src/counter.rs | 22 +- examples/messages/src/messages.rs | 120 +++--- examples/todos/src/todos.rs | 9 +- macros/Cargo.toml | 8 +- macros/src/child_template.rs | 6 + macros/src/declare_derive.rs | 1 - macros/src/declare_derive2.rs | 353 ++++++++++++++++ macros/src/declare_obj.rs | 186 +++++++++ macros/src/fn_widget_macro.rs | 35 ++ macros/src/lib.rs | 130 +++++- macros/src/pipe_macro.rs | 73 ++++ macros/src/rdl_macro.rs | 181 +++++++++ macros/src/symbol_process.rs | 334 +++++++++++++++ macros/src/watch_macro.rs | 42 ++ macros/src/widget_macro/code_gen.rs | 2 +- macros/src/widget_macro/visit_mut.rs | 2 +- ribir/src/app.rs | 2 + tests/Cargo.toml | 7 +- ...only_allow_in_init_and_finally_fail.stderr | 4 +- .../declare/dollar_position_fail.rs | 11 + .../declare/dollar_position_fail.stderr | 13 + .../compile_msg/declare/duplicate_id_fail.rs | 66 --- .../declare/duplicate_id_fail.stderr | 64 --- tests/declare_builder_test.rs | 12 +- tests/{code_gen_test.rs => rdl_macro_test.rs} | 383 +++++++++++++++++- widgets/src/avatar.rs | 2 +- widgets/src/buttons/filled_button.rs | 2 +- widgets/src/checkbox.rs | 14 +- widgets/src/common_widget.rs | 8 +- widgets/src/divider.rs | 2 +- widgets/src/icon.rs | 28 +- widgets/src/input/handle.rs | 10 +- widgets/src/input/text_selectable.rs | 14 +- widgets/src/layout/column.rs | 2 +- widgets/src/layout/flex.rs | 2 +- widgets/src/layout/row.rs | 2 +- widgets/src/layout/sized_box.rs | 2 +- widgets/src/layout/stack.rs | 2 +- widgets/src/lists.rs | 6 +- widgets/src/scrollbar.rs | 2 +- widgets/src/tabs.rs | 6 +- widgets/src/text.rs | 6 +- widgets/src/text_field.rs | 10 +- 89 files changed, 2973 insertions(+), 543 deletions(-) delete mode 100644 core/src/assign_observable.rs create mode 100644 core/src/pipe.rs create mode 100644 macros/src/declare_derive2.rs create mode 100644 macros/src/declare_obj.rs create mode 100644 macros/src/fn_widget_macro.rs create mode 100644 macros/src/pipe_macro.rs create mode 100644 macros/src/rdl_macro.rs create mode 100644 macros/src/symbol_process.rs create mode 100644 macros/src/watch_macro.rs create mode 100644 tests/compile_msg/declare/dollar_position_fail.rs create mode 100644 tests/compile_msg/declare/dollar_position_fail.stderr delete mode 100644 tests/compile_msg/declare/duplicate_id_fail.rs delete mode 100644 tests/compile_msg/declare/duplicate_id_fail.stderr rename tests/{code_gen_test.rs => rdl_macro_test.rs} (59%) diff --git a/core/src/animation/animate.rs b/core/src/animation/animate.rs index b3c577289..db9d1d93e 100644 --- a/core/src/animation/animate.rs +++ b/core/src/animation/animate.rs @@ -36,7 +36,7 @@ pub struct AnimateInfo { _tick_msg_guard: Option>>, } -impl<'a, T: Roc, P: AnimateProperty> StateRef<'a, Animate> +impl<'a, T: Roc, P: AnimateProperty> StatefulRef<'a, Animate> where Animate: 'static, { diff --git a/core/src/assign_observable.rs b/core/src/assign_observable.rs deleted file mode 100644 index 3a0431b5a..000000000 --- a/core/src/assign_observable.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::convert::Infallible; - -use rxrust::{ops::map::MapOp, prelude::ObservableExt}; - -/// Is a struct contain a initialize value and an observable that should be -/// subscribed to update the value. -pub struct AssignObservable> { - value: V, - observable: S, -} - -impl AssignObservable -where - S: ObservableExt, -{ - #[inline] - pub fn new(init: V, observable: S) -> Self { Self { value: init, observable } } - - /// map the inner observable stream to another observable that emit same type - /// value. - pub fn stream_map(self, f: impl FnOnce(S) -> R) -> AssignObservable - where - R: ObservableExt, - { - let Self { value, observable } = self; - let observable = f(observable); - AssignObservable { value, observable } - } - - /// Creates a new `AssignObservable` which calls a closure on each element and - /// uses its return as the value. - pub fn map(self, mut f: F) -> AssignObservable> - where - F: FnMut(V) -> R, - { - let Self { value, observable } = self; - AssignObservable { - value: f(value), - observable: observable.map(f), - } - } - - #[inline] - pub fn unzip(self) -> (V, S) { (self.value, self.observable) } -} diff --git a/core/src/builtin_widgets.rs b/core/src/builtin_widgets.rs index 916a85e68..e096433af 100644 --- a/core/src/builtin_widgets.rs +++ b/core/src/builtin_widgets.rs @@ -1,3 +1,8 @@ +//! Built-in widgets is a set of minimal widgets that describes the most common +//! UI elements. The most of them can be used to extend other object in the +//! declare syntax, so other objects can use the builtin fields and methods like +//! self fields and methods. + pub mod key; pub use key::{Key, KeyWidget}; pub mod image_widget; @@ -53,3 +58,146 @@ pub mod focus_node; pub use focus_node::*; pub mod focus_scope; pub use focus_scope::*; + +use crate::{prelude::*, widget::WidgetBuilder}; + +macro_rules! impl_fat_obj { + ($($builtin_ty: ty),*) => { + paste::paste! { + #[doc="A fat object that extend the `T` object with all builtin widgets \ + ability.\ + During the compose phase, the `FatObj` will be created when a object \ + use the builtin fields and methods. And they are only a help obj to \ + build the finally widget, so they will be dropped after composed." + ] + pub struct FatObj { + pub host: T, + $([< $builtin_ty: snake:lower >]: Option>),* + } + + impl FatObj { + pub fn new(host: T) -> Self { + Self { + host, + $([< $builtin_ty: snake:lower >]: None),* + } + } + + $( + pub fn [< with_ $builtin_ty: snake:lower >]( + mut self, builtin: State<$builtin_ty> + ) -> Self { + self.[< $builtin_ty: snake:lower >] = Some(builtin); + self + } + )* + + $( + pub fn [< $builtin_ty: snake:lower >](&mut self, ctx: &BuildCtx) + -> &mut State<$builtin_ty> + { + self + .[< $builtin_ty: snake:lower >] + .get_or_insert_with(|| $builtin_ty::declare2_builder().build(ctx)) + } + )* + } + + impl WidgetBuilder for FatObj + where + T: Into + { + fn build(self, ctx: &BuildCtx) -> WidgetId { + let mut host: Widget = self.host.into(); + $( + if let Some(builtin) = self.[< $builtin_ty: snake:lower >] { + host = builtin.with_child(host, ctx).into(); + } + )* + host.build(ctx) + } + } + + impl, C> SingleWithChild for FatObj{ + type Target = FatObj; + fn with_child(self, child: C, ctx: &BuildCtx) -> Self::Target { + let Self { host, $([< $builtin_ty: snake:lower >]),* } = self; + let host = host.with_child(child, ctx); + FatObj { + host, + $([< $builtin_ty: snake:lower >]),* + } + } + } + + impl, C> MultiWithChild for FatObj{ + type Target = FatObj; + fn with_child(self, child: C, ctx: &BuildCtx) -> Self::Target { + let Self { host, $([< $builtin_ty: snake:lower >]),* } = self; + let host = host.with_child(child, ctx); + FatObj { + host, + $([< $builtin_ty: snake:lower >]),* + } + } + } + impl ComposeWithChild for FatObj + where + T: ComposeWithChild + { + type Target = FatObj; + fn with_child(self, child: C, ctx: &BuildCtx) -> Self::Target { + let Self { host, $([< $builtin_ty: snake:lower >]),* } = self; + let host = host.with_child(child, ctx); + FatObj { + host, + $([< $builtin_ty: snake:lower >]),* + } + } + } + } + }; +} + +impl_fat_obj!( + PointerListener, + FocusNode, + RequestFocus, + FocusListener, + FocusBubbleListener, + HasFocus, + KeyboardListener, + CharsListener, + WheelListener, + MouseHover, + PointerPressed, + FittedBox, + BoxDecoration, + Padding, + LayoutBox, + Cursor, + Margin, + ScrollableWidget, + TransformWidget, + HAlignWidget, + VAlignWidget, + LeftAnchor, + RightAnchor, + TopAnchor, + BottomAnchor, + Visibility, + Opacity, + LifecycleListener, + DelayDropWidget +); + +impl std::ops::Deref for FatObj { + type Target = T; + #[inline] + fn deref(&self) -> &Self::Target { &self.host } +} + +impl std::ops::DerefMut for FatObj { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.host } +} diff --git a/core/src/builtin_widgets/align.rs b/core/src/builtin_widgets/align.rs index 8f6650c29..979ea3992 100644 --- a/core/src/builtin_widgets/align.rs +++ b/core/src/builtin_widgets/align.rs @@ -54,14 +54,14 @@ pub enum VAlign { } /// A widget that align its child in x-axis, base on child's width. -#[derive(Declare, SingleChild)] +#[derive(Declare, Declare2, SingleChild)] pub struct HAlignWidget { #[declare(default, builtin)] pub h_align: HAlign, } /// A widget that align its child in y-axis, base on child's height. -#[derive(Declare, SingleChild)] +#[derive(Declare, Declare2, SingleChild)] pub struct VAlignWidget { #[declare(default, builtin)] pub v_align: VAlign, diff --git a/core/src/builtin_widgets/anchor.rs b/core/src/builtin_widgets/anchor.rs index be20e411e..9a4862101 100644 --- a/core/src/builtin_widgets/anchor.rs +++ b/core/src/builtin_widgets/anchor.rs @@ -11,21 +11,21 @@ pub enum PositionUnit { } /// Widget use to anchor child constraints with the left edge of parent widget. -#[derive(Declare, SingleChild)] +#[derive(Declare, Declare2, SingleChild)] pub struct LeftAnchor { #[declare(convert=into, builtin)] pub left_anchor: PositionUnit, } /// Widget use to anchor child constraints with the right edge of parent widget. -#[derive(Declare, SingleChild)] +#[derive(Declare, Declare2, SingleChild)] pub struct RightAnchor { #[declare(convert=into, builtin)] pub right_anchor: PositionUnit, } /// Widget use to anchor child constraints with the top edge of parent widget. -#[derive(Declare, SingleChild)] +#[derive(Declare, Declare2, SingleChild)] pub struct TopAnchor { #[declare(convert=into, builtin)] pub top_anchor: PositionUnit, @@ -33,7 +33,7 @@ pub struct TopAnchor { /// Widget use to anchor child constraints with the bottom edge of parent /// widget. -#[derive(Declare, SingleChild)] +#[derive(Declare, Declare2, SingleChild)] pub struct BottomAnchor { #[declare(convert=into, builtin)] pub bottom_anchor: PositionUnit, diff --git a/core/src/builtin_widgets/box_decoration.rs b/core/src/builtin_widgets/box_decoration.rs index 70ed2ee03..aa77739cd 100644 --- a/core/src/builtin_widgets/box_decoration.rs +++ b/core/src/builtin_widgets/box_decoration.rs @@ -1,7 +1,7 @@ use crate::{impl_query_self_only, prelude::*}; /// The BoxDecoration provides a variety of ways to draw a box. -#[derive(SingleChild, Default, Clone, Declare)] +#[derive(SingleChild, Default, Clone, Declare, Declare2)] pub struct BoxDecoration { /// The background of the box. #[declare(builtin, default, convert=custom)] diff --git a/core/src/builtin_widgets/cursor.rs b/core/src/builtin_widgets/cursor.rs index ab5d407b8..3dc4d37f4 100644 --- a/core/src/builtin_widgets/cursor.rs +++ b/core/src/builtin_widgets/cursor.rs @@ -4,7 +4,7 @@ use winit::window::CursorIcon; /// `Cursor` is an attribute to assign an `cursor` to a widget. -#[derive(Declare, Debug)] +#[derive(Declare, Debug, Declare2)] pub struct Cursor { #[declare(convert=custom, builtin, default)] pub cursor: Rc>, diff --git a/core/src/builtin_widgets/delay_drop_widget.rs b/core/src/builtin_widgets/delay_drop_widget.rs index 989c8d405..9c11ff019 100644 --- a/core/src/builtin_widgets/delay_drop_widget.rs +++ b/core/src/builtin_widgets/delay_drop_widget.rs @@ -1,6 +1,6 @@ use crate::{impl_query_self_only, prelude::*}; -#[derive(Declare)] +#[derive(Declare, Declare2)] pub struct DelayDropWidget { #[declare(builtin)] pub delay_drop_until: bool, diff --git a/core/src/builtin_widgets/fitted_box.rs b/core/src/builtin_widgets/fitted_box.rs index 552424e21..b9ac76232 100644 --- a/core/src/builtin_widgets/fitted_box.rs +++ b/core/src/builtin_widgets/fitted_box.rs @@ -19,7 +19,7 @@ pub enum BoxFit { } /// Widget set how its child should be scale to fit its box. -#[derive(Declare, SingleChild)] +#[derive(Declare, Declare2, SingleChild)] pub struct FittedBox { #[declare(builtin)] pub box_fit: BoxFit, diff --git a/core/src/builtin_widgets/focus_node.rs b/core/src/builtin_widgets/focus_node.rs index 56c6fdda9..b36265c62 100644 --- a/core/src/builtin_widgets/focus_node.rs +++ b/core/src/builtin_widgets/focus_node.rs @@ -5,7 +5,7 @@ use crate::{ prelude::*, }; -#[derive(Default, Declare)] +#[derive(Default, Declare, Declare2)] pub struct FocusNode { /// Indicates that `widget` can be focused, and where it participates in /// sequential keyboard navigation (usually with the Tab key, hence the name. @@ -97,7 +97,7 @@ pub(crate) fn dynamic_compose_focus_node(widget: Widget) -> Widget { }) .into() } -#[derive(Declare)] +#[derive(Declare, Declare2)] pub struct RequestFocus { #[declare(default)] handle: Option, diff --git a/core/src/builtin_widgets/has_focus.rs b/core/src/builtin_widgets/has_focus.rs index d5d5607b5..37133b19d 100644 --- a/core/src/builtin_widgets/has_focus.rs +++ b/core/src/builtin_widgets/has_focus.rs @@ -1,5 +1,5 @@ use crate::prelude::*; -#[derive(PartialEq, Clone, Declare)] +#[derive(PartialEq, Clone, Declare, Declare2)] pub struct HasFocus { #[declare(skip, default)] focused: bool, diff --git a/core/src/builtin_widgets/layout_box.rs b/core/src/builtin_widgets/layout_box.rs index 64c43ab64..d5e64644a 100644 --- a/core/src/builtin_widgets/layout_box.rs +++ b/core/src/builtin_widgets/layout_box.rs @@ -1,7 +1,7 @@ use crate::prelude::*; /// Widget let user to access the layout result of its child. -#[derive(Declare)] +#[derive(Declare, Declare2)] pub struct LayoutBox { #[declare(skip)] /// the rect box of its child and the coordinate is relative to its parent. diff --git a/core/src/builtin_widgets/lifecycle.rs b/core/src/builtin_widgets/lifecycle.rs index 31e55f789..465cb974c 100644 --- a/core/src/builtin_widgets/lifecycle.rs +++ b/core/src/builtin_widgets/lifecycle.rs @@ -12,7 +12,7 @@ define_widget_context!(LifecycleEvent); crate::events::impl_event_subject!(Lifecycle, event_name = AllLifecycle); -#[derive(Declare, Default)] +#[derive(Declare, Declare2, Default)] pub struct LifecycleListener { #[declare(skip)] lifecycle: LifecycleSubject, @@ -86,6 +86,48 @@ impl LifecycleListenerDeclarer { } } +impl LifecycleListenerDeclarer2 { + pub fn on_mounted( + mut self, + handler: impl for<'r> FnMut(&'r mut LifecycleEvent<'_>) + 'static, + ) -> Self { + let _ = self + .subject() + .filter_map(match_closure!(Mounted)) + .take(1) + .subscribe(handler); + + self + } + + pub fn on_performed_layout(mut self, handler: impl FnMut(&mut LifecycleEvent) + 'static) -> Self { + let _ = self + .subject() + .filter_map(match_closure!(PerformedLayout)) + .subscribe(handler); + + self + } + + pub fn on_disposed(mut self, handler: impl FnMut(&mut LifecycleEvent) + 'static) -> Self { + let _ = self + .subject() + .filter_map(match_closure!(Disposed)) + .take(1) + .subscribe(handler); + + self + } + + fn subject(&mut self) -> LifecycleSubject { + self + .lifecycle + .get_or_insert_with(DeclareInit::default) + .value() + .clone() + } +} + impl_query_self_only!(LifecycleListener); impl EventListener for LifecycleListener { diff --git a/core/src/builtin_widgets/margin.rs b/core/src/builtin_widgets/margin.rs index 2a2e0883f..8369f087b 100644 --- a/core/src/builtin_widgets/margin.rs +++ b/core/src/builtin_widgets/margin.rs @@ -9,7 +9,7 @@ pub struct EdgeInsets { } /// A widget that create space around its child. -#[derive(SingleChild, Default, Clone, PartialEq, Declare)] +#[derive(SingleChild, Default, Clone, PartialEq, Declare, Declare2)] pub struct Margin { #[declare(builtin, default)] pub margin: EdgeInsets, diff --git a/core/src/builtin_widgets/mouse_hover.rs b/core/src/builtin_widgets/mouse_hover.rs index 913b81a61..639c7dd2f 100644 --- a/core/src/builtin_widgets/mouse_hover.rs +++ b/core/src/builtin_widgets/mouse_hover.rs @@ -1,6 +1,6 @@ use crate::prelude::*; -#[derive(PartialEq, Clone, Declare)] +#[derive(PartialEq, Clone, Declare, Declare2)] pub struct MouseHover { #[declare(skip, default)] hover: bool, diff --git a/core/src/builtin_widgets/opacity.rs b/core/src/builtin_widgets/opacity.rs index 53663c9f9..ebc611435 100644 --- a/core/src/builtin_widgets/opacity.rs +++ b/core/src/builtin_widgets/opacity.rs @@ -1,7 +1,7 @@ use crate::impl_query_self_only; use crate::prelude::*; -#[derive(Declare, Default, Clone, SingleChild)] +#[derive(Declare, Declare2, Default, Clone, SingleChild)] pub struct Opacity { #[declare(builtin)] pub opacity: f32, diff --git a/core/src/builtin_widgets/padding.rs b/core/src/builtin_widgets/padding.rs index 4778a1956..ad33362e7 100644 --- a/core/src/builtin_widgets/padding.rs +++ b/core/src/builtin_widgets/padding.rs @@ -1,7 +1,7 @@ use crate::{impl_query_self_only, prelude::*}; /// A widget that insets its child by the given padding. -#[derive(SingleChild, Clone, Declare)] +#[derive(SingleChild, Clone, Declare, Declare2)] pub struct Padding { #[declare(builtin)] pub padding: EdgeInsets, diff --git a/core/src/builtin_widgets/pointer_pressed.rs b/core/src/builtin_widgets/pointer_pressed.rs index 18a3ee1c1..a8dcf20a7 100644 --- a/core/src/builtin_widgets/pointer_pressed.rs +++ b/core/src/builtin_widgets/pointer_pressed.rs @@ -2,7 +2,7 @@ use crate::prelude::*; /// Widget keep the pointer press state of its child. As a builtin widget, user /// can call `pointer_pressed` method to get the pressed state of a widget. -#[derive(Declare)] +#[derive(Declare, Declare2)] pub struct PointerPressed { #[declare(skip, builtin)] pointer_pressed: bool, diff --git a/core/src/builtin_widgets/scrollable.rs b/core/src/builtin_widgets/scrollable.rs index a8d6284f6..b26d315f5 100644 --- a/core/src/builtin_widgets/scrollable.rs +++ b/core/src/builtin_widgets/scrollable.rs @@ -15,7 +15,7 @@ pub enum Scrollable { } /// Helper struct for builtin scrollable field. -#[derive(Declare)] +#[derive(Declare, Declare2)] pub struct ScrollableWidget { #[declare(builtin, default)] pub scrollable: Scrollable, diff --git a/core/src/builtin_widgets/theme.rs b/core/src/builtin_widgets/theme.rs index 0650e86d5..1247c49b3 100644 --- a/core/src/builtin_widgets/theme.rs +++ b/core/src/builtin_widgets/theme.rs @@ -84,6 +84,8 @@ impl ComposeChild for ThemeWidget { fn compose_child(this: State, child: Self::Child) -> Widget { use crate::prelude::*; FnWidget::new(move |ctx| { + let ctx = ctx.force_as_mut(); + let theme = match this { State::Stateless(t) => t.theme, State::Stateful(s) => s.state_ref().theme.clone(), diff --git a/core/src/builtin_widgets/transform_widget.rs b/core/src/builtin_widgets/transform_widget.rs index 2e389ce89..89ebc0a04 100644 --- a/core/src/builtin_widgets/transform_widget.rs +++ b/core/src/builtin_widgets/transform_widget.rs @@ -1,6 +1,6 @@ use crate::{impl_query_self_only, prelude::*, widget::hit_test_impl}; -#[derive(SingleChild, Declare, Clone)] +#[derive(SingleChild, Declare, Declare2, Clone)] pub struct TransformWidget { #[declare(builtin, default)] pub transform: Transform, diff --git a/core/src/builtin_widgets/visibility.rs b/core/src/builtin_widgets/visibility.rs index cec501cf4..709925370 100644 --- a/core/src/builtin_widgets/visibility.rs +++ b/core/src/builtin_widgets/visibility.rs @@ -1,6 +1,6 @@ use crate::{impl_query_self_only, prelude::*}; -#[derive(Declare)] +#[derive(Declare, Declare2)] pub struct Visibility { #[declare(builtin)] pub visible: bool, diff --git a/core/src/builtin_widgets/void.rs b/core/src/builtin_widgets/void.rs index b83d44cd7..3cb635375 100644 --- a/core/src/builtin_widgets/void.rs +++ b/core/src/builtin_widgets/void.rs @@ -4,7 +4,7 @@ use crate::{impl_query_self_only, prelude::*}; /// node in `widget!` macro, or hold a place in tree. When it have a child /// itself will be dropped when build tree, otherwise as a render widget but do /// nothing. -#[derive(Declare)] +#[derive(Declare, Declare2)] pub struct Void; impl Render for Void { diff --git a/core/src/context/app_ctx.rs b/core/src/context/app_ctx.rs index 79362a8c7..6f7bc2b07 100644 --- a/core/src/context/app_ctx.rs +++ b/core/src/context/app_ctx.rs @@ -189,7 +189,14 @@ impl AppCtx { #[track_caller] pub unsafe fn new_lock_scope() -> MutexGuard<'static, ()> { static LOCK: Mutex<()> = Mutex::new(()); - let locker = LOCK.lock().unwrap(); + + let locker = LOCK.lock().unwrap_or_else(|e| { + // Only clear for test, so we have a clear error message. + #[cfg(test)] + LOCK.clear_poison(); + + e.into_inner() + }); APP_CTX_INIT = Once::new(); locker } diff --git a/core/src/context/build_context.rs b/core/src/context/build_context.rs index fe7e55856..ff2e57d2b 100644 --- a/core/src/context/build_context.rs +++ b/core/src/context/build_context.rs @@ -1,23 +1,43 @@ use crate::{ prelude::*, - widget::{widget_id::new_node, WidgetTree}, + widget::{widget_id::new_node, TreeArena, WidgetTree}, + window::{DelayEvent, WindowId}, }; use std::{ops::Deref, rc::Rc}; +/// A context provide during build the widget tree. pub struct BuildCtx<'a> { pub(crate) themes: Option>>, /// The widget which this `BuildCtx` is created from. It's not means this /// is the parent of the widget which is builded by this `BuildCtx`. ctx_from: Option, - tree: &'a mut WidgetTree, + pub(crate) tree: &'a mut WidgetTree, +} + +/// A handle of `BuildCtx` that you can store it and access the `BuildCtx` later +/// in anywhere. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct BuildCtxHandle { + ctx_from: Option, + wnd_id: WindowId, } impl<'a> BuildCtx<'a> { + /// Return the window of this context is created from. pub fn window(&self) -> Rc { self.tree.window() } /// Get the widget which this `BuildCtx` is created from. pub fn ctx_from(&self) -> WidgetId { self.ctx_from.unwrap_or_else(|| self.tree.root()) } + /// Create a handle of this `BuildCtx` which support `Clone`, `Copy` and + /// convert back to this `BuildCtx`. This let you can store the `BuildCtx`. + pub fn handle(&self) -> BuildCtxHandle { + BuildCtxHandle { + wnd_id: self.tree.window().id(), + ctx_from: self.ctx_from, + } + } + pub fn reset_ctx_from(&mut self, reset: Option) -> Option { std::mem::replace(&mut self.ctx_from, reset) } @@ -51,11 +71,58 @@ impl<'a> BuildCtx<'a> { new_node(arena, widget) } - pub(crate) fn append_child(&self, parent: WidgetId, child: WidgetId) { - let arena = &mut self.force_as_mut().tree.arena; - parent.append(child, arena); + pub(crate) fn append_child(&mut self, parent: WidgetId, child: WidgetId) { + parent.append(child, &mut self.tree.arena); + } + + /// Insert `next` after `prev` + pub(crate) fn insert_after(&mut self, prev: WidgetId, next: WidgetId) { + let arena = &mut self.tree.arena; + prev.insert_after(next, arena); + } + + /// After insert new subtree to the widget tree, call this to watch the + /// subtree and fire mount events. + pub(crate) fn on_subtree_mounted(&self, id: WidgetId) { + id.descendants(&self.tree.arena) + .for_each(|w| self.on_widget_mounted(w)); + self.tree.mark_dirty(id); } + /// After insert new widget to the widget tree, call this to watch the widget + /// and fire mount events. + pub(crate) fn on_widget_mounted(&self, id: WidgetId) { + self.assert_get(id).query_all_type( + |notifier: &StateChangeNotifier| { + let state_changed = self.tree.dirty_set.clone(); + notifier + .raw_modifies() + .filter(|b| b.contains(ModifyScope::FRAMEWORK)) + .subscribe(move |_| { + state_changed.borrow_mut().insert(id); + }); + true + }, + QueryOrder::OutsideFirst, + ); + + self.window().add_delay_event(DelayEvent::Mounted(id)); + } + + /// Dispose the whole subtree of `id`, include `id` itself. + pub(crate) fn dispose_subtree(&self, id: WidgetId) { + // todo: delay drop query + let tree = &mut self.force_as_mut().tree; + tree.detach(id); + self + .window() + .add_delay_event(DelayEvent::Disposed { id, delay_drop: false }); + let (arena1, arena2) = unsafe { split_arena(&mut tree.arena) }; + id.descendants(arena1).for_each(|id| id.mark_drop(arena2)) + } + + pub(crate) fn mark_dirty(&mut self, id: WidgetId) { self.tree.mark_dirty(id); } + #[inline] pub(crate) fn push_theme(&self, theme: Rc) { self.themes().push(theme); } @@ -96,6 +163,23 @@ impl<'a> BuildCtx<'a> { } } +impl BuildCtxHandle { + /// Acquires a reference to the `BuildCtx` in this handle, maybe not exist if + /// the window is closed or widget is removed. + pub fn with_ctx(self, f: impl FnOnce(&BuildCtx) -> R) -> Option { + AppCtx::get_window(self.wnd_id).map(|wnd| { + let mut tree = wnd.widget_tree.borrow_mut(); + let ctx = BuildCtx::new(self.ctx_from, &mut tree); + f(&ctx) + }) + } +} + +pub(crate) unsafe fn split_arena(tree: &mut TreeArena) -> (&mut TreeArena, &mut TreeArena) { + let ptr = tree as *mut TreeArena; + (&mut *ptr, &mut *ptr) +} + #[cfg(test)] mod tests { use super::*; diff --git a/core/src/declare.rs b/core/src/declare.rs index 56f97b516..d36d77d6f 100644 --- a/core/src/declare.rs +++ b/core/src/declare.rs @@ -1,10 +1,19 @@ -use crate::context::BuildCtx; +use crate::{context::BuildCtx, prelude::Pipe}; +use rxrust::ops::box_it::BoxOp; +use std::convert::Infallible; pub trait Declare { type Builder: DeclareBuilder; fn declare_builder() -> Self::Builder; } +/// The next version of `Declare` trait. It will replace the `Declare` trait +/// after it is stable. +pub trait Declare2 { + type Builder: DeclareBuilder; + fn declare2_builder() -> Self::Builder; +} + /// widget builder use to construct a widget in `widget!`. See the [mod level /// document](declare) to know how to use it. pub trait DeclareBuilder { @@ -12,6 +21,69 @@ pub trait DeclareBuilder { fn build(self, ctx: &BuildCtx) -> Self::Target; } +/// The type use to store the init value of the field when declare a object. +pub enum DeclareInit { + Value(V), + Pipe(Pipe), +} + +impl DeclareInit { + pub fn unzip(self) -> (V, Option>) { + match self { + Self::Value(v) => (v, None), + Self::Pipe(v) => { + let (v, pipe) = v.unzip(); + (v, Some(pipe)) + } + } + } + + pub fn value(&self) -> &V { + match self { + Self::Value(v) => v, + Self::Pipe(v) => v.value(), + } + } + + pub fn value_mut(&mut self) -> &mut V { + match self { + Self::Value(v) => v, + Self::Pipe(v) => v.value_mut(), + } + } +} + +impl Default for DeclareInit { + #[inline] + fn default() -> Self { Self::Value(T::default()) } +} + +pub trait DeclareFrom { + fn declare_from(value: V) -> Self; +} + +impl> DeclareFrom for DeclareInit { + #[inline] + fn declare_from(value: V) -> Self { Self::Value(value.into()) } +} + +impl> DeclareFrom> for DeclareInit> { + #[inline] + fn declare_from(value: V) -> Self { Self::Value(Some(value.into())) } +} + +impl + 'static> DeclareFrom, Pipe<()>> for DeclareInit { + #[inline] + fn declare_from(value: Pipe) -> Self { Self::Pipe(value.map(U::from)) } +} + +impl + 'static> DeclareFrom, Option>> + for DeclareInit> +{ + #[inline] + fn declare_from(value: Pipe) -> Self { Self::Pipe(value.map(|v| Some(v.into()))) } +} + #[derive(Debug, PartialEq, Hash)] pub struct DeclareStripOption(O); diff --git a/core/src/dynamic_widget.rs b/core/src/dynamic_widget.rs index 69da355c7..8400c24bd 100644 --- a/core/src/dynamic_widget.rs +++ b/core/src/dynamic_widget.rs @@ -256,8 +256,9 @@ impl DynRender { tree: &mut WidgetTree, ) { let mut handlers = SmallVec::<[_; 1]>::new(); + tree.detach(wid); + let arena = &mut tree.arena; - wid.detach(arena); wid.assert_get(arena).query_all_type( |notifier: &StateChangeNotifier| { let state_changed = tree.dirty_set.clone(); @@ -285,7 +286,7 @@ impl DynRender { .delay(std::time::Duration::ZERO, tree.window().frame_scheduler()) .subscribe(move |_| { if let Some(wnd) = AppCtx::get_window(wnd_id) { - wid.remove_subtree(&mut wnd.widget_tree.borrow_mut()); + wnd.widget_tree.borrow_mut().remove_subtree(wid); } host.wids.borrow_mut().remove(&wid); handlers.clear(); @@ -309,7 +310,8 @@ impl DynRender { self.drop_until_widgets.add(wid); tree.dirty_set.borrow_mut().insert(wid); } else { - wid.detach(&mut tree.arena); + tree.detach(wid); + let (arena1, arena2) = unsafe { split_arena(&mut tree.arena) }; wid .descendants(arena1) @@ -394,12 +396,10 @@ impl_query_self_only!(DynWidget, , where D: 'static); fn inspect_key(id: &WidgetId, tree: &TreeArena, mut cb: impl FnMut(&dyn AnyKey)) { #[allow(clippy::borrowed_box)] - id.assert_get(tree).query_on_first_type( - QueryOrder::OutsideFirst, - |key_widget: &Box| { - cb(&**key_widget); - }, - ); + id.assert_get(tree) + .query_on_first_type(QueryOrder::OutsideFirst, |key_widget: &Box| { + cb(&**key_widget) + }); } fn single_down(id: WidgetId, arena: &TreeArena, mut down_level: isize) -> Option { diff --git a/core/src/events/listener_impl_helper.rs b/core/src/events/listener_impl_helper.rs index e6e856dab..0a2742f75 100644 --- a/core/src/events/listener_impl_helper.rs +++ b/core/src/events/listener_impl_helper.rs @@ -43,7 +43,7 @@ macro_rules! impl_listener { ($doc: literal, $name: ident, $event_ty: ident) => { paste::paste! { #[doc= $doc] - #[derive(Declare)] + #[derive(Declare, Declare2)] pub struct [<$name Listener>]{ #[declare(skip)] [<$name:snake _subject>]: [<$name Subject>] @@ -58,6 +58,16 @@ macro_rules! impl_listener { } } + impl [<$name ListenerDeclarer2>] { + fn subject(&mut self) -> [<$name Subject>] { + self + .[<$name:snake _subject>] + .get_or_insert_with(DeclareInit::default) + .value() + .clone() + } + } + impl [<$name Listener>] { /// Convert a observable stream of this event. pub fn [<$name:snake _stream>](&self) -> [<$name Subject>] { @@ -88,6 +98,28 @@ macro_rules! impl_multi_event_listener { impl_all_event!($name, $($on_doc, $event_ty),+); impl_listener!($doc, $name, []); + impl [<$name ListenerDeclarer2>] { + $( + #[doc = "Sets up a function that will be called whenever the `" $event_ty "` is delivered"] + pub fn []( + mut self, + handler: impl for<'r> FnMut(&'r mut [<$name Event>]<'_>) + 'static + ) -> Self + { + self + .subject() + .filter_map( + (|e| match e { + []::$event_ty(e) => Some(e), + _ => None, + }) as for<'a, 'b> fn(&'a mut []::<'b>) -> Option<&'a mut [<$name Event>] <'b>> + ) + .subscribe(handler); + self + } + )+ + } + impl [<$name ListenerDeclarer>] { $( #[doc = "Sets up a function that will be called \ @@ -120,11 +152,24 @@ macro_rules! impl_single_event_listener { paste::paste! { impl_listener!($doc, $name); + impl [<$name ListenerDeclarer2>] { + #[doc = "Sets up a function that will be called whenever the `" [<$name Event>] "` is delivered"] + pub fn []( + self, + handler: impl FnMut(&'_ mut [<$name Event>]<'_>) + 'static + ) -> Self { + self + .subject() + .subscribe(handler); + self + } + } + impl [<$name ListenerDeclarer>] { #[doc = "Sets up a function that will be called whenever the `" [<$name Event>] "` is delivered"] pub fn []( self, - handler: impl for<'r> FnMut(&'r mut [<$name Event>]<'_>) + 'static + handler: impl FnMut(&'_ mut [<$name Event>]<'_>) + 'static ) -> Self { self .subject() diff --git a/core/src/lib.rs b/core/src/lib.rs index cb39b7f46..b900d9138 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,4 +1,5 @@ -#![feature(test, decl_macro)] +#![feature(decl_macro)] +#![cfg_attr(test, feature(mutex_unpoison, test))] #[macro_use] extern crate bitflags; @@ -11,11 +12,11 @@ pub mod data_widget; mod state; pub(crate) mod widget_tree; -pub mod assign_observable; pub mod clipboard; pub mod declare; pub mod dynamic_widget; pub mod events; +pub mod pipe; pub mod ticker; pub mod timer; pub mod widget; @@ -25,8 +26,6 @@ pub mod window; pub mod prelude { pub use crate::animation::*; #[doc(no_inline)] - pub use crate::assign_observable::AssignObservable; - #[doc(no_inline)] pub use crate::builtin_widgets::*; #[doc(no_inline)] pub use crate::context::*; @@ -38,6 +37,9 @@ pub mod prelude { pub use crate::dynamic_widget::*; #[doc(no_inline)] pub use crate::events::*; + pub use crate::path_state_splitter; + #[doc(no_inline)] + pub use crate::pipe::Pipe; #[doc(no_inline)] pub use crate::state::*; #[doc(no_inline)] @@ -57,7 +59,10 @@ pub mod prelude { pub use ribir_algo::CowArc; pub use ribir_geom::*; #[doc(no_inline)] - pub use ribir_macros::{include_svg, widget, Declare, Lerp, MultiChild, SingleChild, Template}; + pub use ribir_macros::{ + ctx, fn_widget, include_svg, pipe, rdl, set_build_ctx, watch, widget, Declare, Declare2, Lerp, + MultiChild, SingleChild, Template, _dollar_ಠ_ಠ, + }; #[doc(no_inline)] pub use ribir_painter::*; #[doc(no_inline)] diff --git a/core/src/pipe.rs b/core/src/pipe.rs new file mode 100644 index 000000000..87bf74063 --- /dev/null +++ b/core/src/pipe.rs @@ -0,0 +1,255 @@ +use rxrust::{ + ops::box_it::BoxOp, + prelude::{BoxIt, ObservableExt, ObservableItem}, + subscription::Subscription, +}; +use std::{ + cell::{Cell, RefCell}, + convert::Infallible, + rc::Rc, +}; + +use crate::{ + builtin_widgets::{key::AnyKey, Void}, + context::BuildCtx, + data_widget::attach_to_id, + prelude::{AnonymousData, DataWidget, Multi, MultiChild, SingleChild}, + widget::{QueryOrder, Render, Widget, WidgetBuilder, WidgetId}, +}; + +/// A value that can be subscribed its continuous change from the observable +/// stream. +pub struct Pipe { + value: V, + observable: BoxOp<'static, V, Infallible>, +} + +impl Pipe { + #[inline] + pub fn new(init: V, observable: BoxOp<'static, V, Infallible>) -> Self { + Self { value: init, observable } + } + + /// map the inner observable stream to another observable that emit same type + /// value. + pub fn stream_map(self, f: impl FnOnce(BoxOp<'static, V, Infallible>) -> R) -> Pipe + where + R: BoxIt>, + { + let Self { value, observable } = self; + let observable = f(observable).box_it(); + Pipe { value, observable } + } + + /// Creates a new `Pipe` which calls a closure on each element and + /// uses its return as the value. + pub fn map(self, mut f: F) -> Pipe + where + F: FnMut(V) -> R + 'static, + V: 'static, + { + let Self { value, observable } = self; + Pipe { + value: f(value), + observable: observable.map(f).box_it(), + } + } + + /// Unzip the `Pipe` into its inner value and the changes stream of the + /// value. + #[inline] + pub fn unzip(self) -> (V, BoxOp<'static, V, Infallible>) { (self.value, self.observable) } + + #[inline] + pub fn value(&self) -> &V { &self.value } + + #[inline] + pub fn value_mut(&mut self) -> &mut V { &mut self.value } +} + +impl> WidgetBuilder for Pipe { + #[inline] + fn build(self, ctx: &crate::context::BuildCtx) -> WidgetId { + let (v, modifies) = self.unzip(); + let id = v.into().build(ctx); + let id_share = Rc::new(Cell::new(id)); + let handle = ctx.handle(); + let h = modifies + .subscribe(move |v| { + handle.with_ctx(|ctx| { + let id = id_share.get(); + let ctx = ctx.force_as_mut(); + let new_id = v.into().build(ctx); + + update_key_status_single(id, new_id, ctx); + + ctx.insert_after(id, new_id); + ctx.dispose_subtree(id); + ctx.on_subtree_mounted(new_id); + id_share.set(new_id); + ctx.mark_dirty(new_id) + }); + }) + .unsubscribe_when_dropped(); + + let h = AnonymousData::new(Box::new(h)); + attach_to_id(id, ctx.force_as_mut(), |d| Box::new(DataWidget::new(d, h))); + + id + } +} + +impl>> Pipe { + pub(crate) fn build_as_render_parent(self, ctx: &mut BuildCtx) -> WidgetId { + let (v, modifies) = self.unzip(); + let id = ctx.alloc_widget(v.into()); + let id_share = Rc::new(Cell::new(id)); + let handle = ctx.handle(); + let h = modifies + .subscribe(move |v| { + handle.with_ctx(|ctx| { + let id = id_share.get(); + let ctx = ctx.force_as_mut(); + let new_id = ctx.alloc_widget(v.into()); + + update_key_status_single(id, new_id, ctx); + let mut cursor = id.first_child(&ctx.tree.arena); + while let Some(c) = cursor { + cursor = c.next_sibling(&ctx.tree.arena); + ctx.append_child(new_id, c); + } + + ctx.insert_after(id, new_id); + ctx.dispose_subtree(id); + + ctx.on_widget_mounted(new_id); + id_share.set(new_id); + ctx.mark_dirty(new_id); + }); + }) + .unsubscribe_when_dropped(); + + let h = AnonymousData::new(Box::new(h)); + attach_to_id(id, ctx.force_as_mut(), |d| Box::new(DataWidget::new(d, h))); + + id + } +} + +impl Pipe> { + pub(crate) fn build_multi(self, vec: &mut Vec, ctx: &mut BuildCtx) + where + W: IntoIterator, + W::Item: Into, + { + fn build_multi( + v: Multi>>, + ctx: &mut BuildCtx, + s_guard: impl Clone + 'static, + ) -> Box<[WidgetId]> { + let mut ids = v + .into_inner() + .into_iter() + .map(|w| w.into().build(ctx)) + .collect::>(); + + if ids.is_empty() { + ids.push(Void.build(ctx)); + } + for id in &ids { + attach_to_id(*id, ctx, |d| { + let h = AnonymousData::new(Box::new(s_guard.clone())); + Box::new(DataWidget::new(d, h)) + }); + } + + ids.into_boxed_slice() + } + + let s_guard = Rc::new(RefCell::new(None)); + let (m, modifies) = self.unzip(); + + let ids = build_multi(m, ctx, s_guard.clone()); + vec.extend(&*ids); + + let ids_share = Rc::new(RefCell::new(ids)); + + let handle = ctx.handle(); + + let s_guard2 = s_guard.clone(); + let guard = modifies + .subscribe(move |m| { + handle.with_ctx(|ctx| { + let ctx = ctx.force_as_mut(); + let old = ids_share.borrow(); + let new = build_multi(m, ctx, s_guard.clone()); + + update_key_state_multi(&new, &old, ctx); + + new.iter().for_each(|w| ctx.insert_after(old[0], *w)); + old.iter().for_each(|id| ctx.dispose_subtree(*id)); + new.iter().for_each(|w| { + ctx.on_subtree_mounted(*w); + ctx.mark_dirty(*w) + }); + }); + }) + .unsubscribe_when_dropped(); + + s_guard2.borrow_mut().replace(guard); + } +} + +fn update_key_status_single(new: WidgetId, old: WidgetId, ctx: &BuildCtx) { + inspect_key(old, ctx, |old_key| { + inspect_key(new, ctx, |new_key| { + if old_key.key() == new_key.key() { + new_key.record_before_value(old_key); + } else { + old_key.disposed(); + new_key.mounted(); + } + }) + }) +} + +fn update_key_state_multi(old: &[WidgetId], new: &[WidgetId], ctx: &BuildCtx) { + let mut old_key_list = ahash::HashMap::default(); + + for o in old { + inspect_key(*o, ctx, |old_key: &dyn AnyKey| { + let key = old_key.key(); + old_key_list.insert(key, *o); + }); + } + + for n in new { + inspect_key(*n, ctx, |new_key: &dyn AnyKey| { + let key = &new_key.key(); + if let Some(o) = old_key_list.get(key) { + inspect_key(*o, ctx, |old_key_widget: &dyn AnyKey| { + new_key.record_before_value(old_key_widget) + }); + old_key_list.remove(key); + } else { + new_key.mounted(); + } + }); + } + + old_key_list + .values() + .for_each(|o| inspect_key(*o, ctx, |old_key| old_key.disposed())); +} + +fn inspect_key(id: WidgetId, ctx: &BuildCtx, mut cb: impl FnMut(&dyn AnyKey)) { + #[allow(clippy::borrowed_box)] + ctx + .assert_get(id) + .query_on_first_type::, _>(QueryOrder::OutsideFirst, |key_widget| { + cb(&**key_widget) + }); +} + +impl SingleChild for Pipe {} +impl MultiChild for Pipe {} diff --git a/core/src/state.rs b/core/src/state.rs index c95772618..c7649a412 100644 --- a/core/src/state.rs +++ b/core/src/state.rs @@ -1,24 +1,60 @@ mod readonly; mod stateful; -use std::rc::Rc; +use std::{convert::Infallible, mem::MaybeUninit, rc::Rc}; pub use readonly::*; -use rxrust::prelude::ObservableItem; +use rxrust::{ops::box_it::BoxOp, prelude::ObservableItem}; pub use stateful::*; use crate::{ context::BuildCtx, dynamic_widget::DynWidget, - widget::{Compose, WidgetBuilder, WidgetId}, + prelude::{BoxMultiParent, BoxedSingleParent, MultiChild, SingleChild}, + widget::{Compose, Render, RenderFul, WidgetBuilder, WidgetId}, }; /// Enum to store both stateless and stateful object. -#[derive(Clone)] pub enum State { Stateless(W), Stateful(Stateful), } +pub enum StateRef<'a, W> { + Stateful(StatefulRef<'a, W>), + Stateless(&'a mut W), +} + +impl SingleChild for State {} +impl MultiChild for State {} + +impl BoxedSingleParent for State { + fn into_render(self: Box) -> Box { + match *self { + State::Stateless(w) => Box::new(w), + State::Stateful(w) => Box::new(RenderFul(w)), + } + } +} + +impl BoxMultiParent for State { + fn into_render(self: Box) -> Box { + match *self { + State::Stateless(w) => Box::new(w), + State::Stateful(w) => Box::new(RenderFul(w)), + } + } +} + +impl From> for Box { + #[inline] + fn from(s: State) -> Self { + match s { + State::Stateless(w) => w.into(), + State::Stateful(w) => w.into(), + } + } +} + impl WidgetBuilder for State { #[inline] fn build(self, ctx: &BuildCtx) -> WidgetId { Compose::compose(self).build(ctx) } @@ -41,6 +77,44 @@ impl State { }, } } + + pub fn clone_stateful(&mut self) -> Stateful { self.to_stateful().clone() } + + pub fn modifies(&mut self) -> BoxOp<'static, (), Infallible> { self.to_stateful().modifies() } + + pub fn clone(&mut self) -> State { + let stateful = self.to_stateful().clone(); + State::Stateful(stateful) + } + + pub fn stateful_ref(&mut self) -> StatefulRef { self.to_stateful().state_ref() } + + pub fn to_stateful(&mut self) -> &mut Stateful { + match self { + State::Stateless(w) => { + // convert the stateless value to stateful first. + let uninit: MaybeUninit<_> = MaybeUninit::zeroed(); + let v = std::mem::replace(w, unsafe { uninit.assume_init() }); + let stateful = State::Stateful(Stateful::new(v)); + let uninit = std::mem::replace(self, stateful); + // the tmp value not init, so we need forget it. + std::mem::forget(uninit); + + match self { + State::Stateful(w) => w, + _ => unreachable!(), + } + } + State::Stateful(w) => w, + } + } + + pub fn state_ref(&mut self) -> StateRef { + match self { + State::Stateless(w) => StateRef::Stateless(w), + State::Stateful(w) => StateRef::Stateful(w.state_ref()), + } + } } pub(crate) trait StateFrom { @@ -83,6 +157,15 @@ impl StateFrom>> for State { } } +impl<'a, T> StateRef<'a, T> { + pub fn forget_modifies(&self) { + match self { + StateRef::Stateless(_) => {} + StateRef::Stateful(w) => w.forget_modifies(), + } + } +} + impl From for State where Self: StateFrom, @@ -90,6 +173,26 @@ where fn from(value: T) -> Self { StateFrom::state_from(value) } } +impl<'a, W> std::ops::Deref for StateRef<'a, W> { + type Target = W; + + fn deref(&self) -> &Self::Target { + match self { + StateRef::Stateful(s) => s.deref(), + StateRef::Stateless(r) => r, + } + } +} + +impl<'a, W> std::ops::DerefMut for StateRef<'a, W> { + fn deref_mut(&mut self) -> &mut Self::Target { + match self { + StateRef::Stateful(s) => s.deref_mut(), + StateRef::Stateless(r) => r, + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/core/src/state/readonly.rs b/core/src/state/readonly.rs index ebcb77048..218f334bb 100644 --- a/core/src/state/readonly.rs +++ b/core/src/state/readonly.rs @@ -1,4 +1,4 @@ -use super::{ModifyScope, StateRef, Stateful}; +use super::{ModifyScope, Stateful, StatefulRef}; use rxrust::{observable, ops::box_it::BoxOp, prelude::BoxIt, subject::Subject}; use std::{convert::Infallible, rc::Rc}; @@ -8,7 +8,7 @@ pub enum Readonly { } pub enum ReadRef<'a, W> { - Stateful(StateRef<'a, W>), + Stateful(StatefulRef<'a, W>), Stateless(&'a Rc), } diff --git a/core/src/state/stateful.rs b/core/src/state/stateful.rs index 3198d462e..7f13523a0 100644 --- a/core/src/state/stateful.rs +++ b/core/src/state/stateful.rs @@ -3,6 +3,7 @@ pub use guards::ModifyGuard; use rxrust::{ops::box_it::BoxOp, prelude::*}; use std::{ cell::{Cell, RefCell, UnsafeCell}, + collections::LinkedList, convert::Infallible, ops::{Deref, DerefMut}, rc::Rc, @@ -17,6 +18,7 @@ pub struct Stateful { /// notify downstream when widget state changed, the value mean if the change it /// as silent or not. #[derive(Default, Clone)] +// todo: remove rc pub(crate) struct StateChangeNotifier(Rc>>); /// A reference of `Stateful which tracked the state change across if user @@ -35,7 +37,7 @@ pub(crate) struct StateChangeNotifier(Rc { +pub struct StatefulRef<'a, W> { /// - None, Not used the value /// - Some(false), borrow used the value /// - Some(true), mutable borrow used the value @@ -107,6 +109,11 @@ struct InnerStateful { borrowed_at: Cell>>, guard_cnt: Cell, data: UnsafeCell, + /// A link list to store anonymous data, so keep it live as long as the + /// `Stateful` data. When this `Stateful` subscribe to a stream, append the + /// unsubscribe handle to this list let you can unsubscribe when this + /// `Stateful` drop. + slot_link: UnsafeCell>>, } impl Clone for Stateful { @@ -129,6 +136,7 @@ impl Stateful { #[cfg(debug_assertions)] borrowed_at: Cell::new(None), guard_cnt: Cell::new(0), + slot_link: <_>::default(), }), modify_notifier: <_>::default(), } @@ -141,7 +149,7 @@ impl Stateful { /// Return a reference of `Stateful`, modify across this reference will notify /// data and framework. #[inline] - pub fn state_ref(&self) -> StateRef { StateRef::new(self, ModifyScope::BOTH) } + pub fn state_ref(&self) -> StatefulRef { StatefulRef::new(self, ModifyScope::BOTH) } /// Return a reference of `Stateful`, modify across this reference will notify /// data only, the relayout or paint depends on this object will not be skip. @@ -149,7 +157,7 @@ impl Stateful { /// If you not very clear how `silent_ref` work, use [`Stateful::state_ref`]! /// instead of. #[inline] - pub fn silent_ref(&self) -> StateRef { StateRef::new(self, ModifyScope::DATA) } + pub fn silent_ref(&self) -> StatefulRef { StatefulRef::new(self, ModifyScope::DATA) } /// Return a reference of `Stateful`, modify across this reference will notify /// framework only. That means this modify only effect framework but not @@ -158,7 +166,9 @@ impl Stateful { /// If you not very clear how `shallow_ref` work, use [`Stateful::state_ref`]! /// instead of. #[inline] - pub(crate) fn shallow_ref(&self) -> StateRef { StateRef::new(self, ModifyScope::FRAMEWORK) } + pub(crate) fn shallow_ref(&self) -> StatefulRef { + StatefulRef::new(self, ModifyScope::FRAMEWORK) + } pub fn raw_modifies(&self) -> Subject<'static, ModifyScope, Infallible> { self.modify_notifier.raw_modifies() @@ -179,6 +189,11 @@ impl Stateful { #[inline] pub fn clone_stateful(&self) -> Stateful { self.clone() } + pub fn own_data(&self, data: impl Any) { + let ptr = self.state_ref().value.inner.slot_link.get(); + unsafe { &mut *ptr }.push_back(Box::new(data)); + } + pub(crate) fn try_into_inner(self) -> Result { if Rc::strong_count(&self.inner) == 1 { let inner = Rc::try_unwrap(self.inner).unwrap_or_else(|_| unreachable!()); @@ -211,7 +226,7 @@ macro_rules! already_borrow_panic { }; } -impl<'a, W> Deref for StateRef<'a, W> { +impl<'a, W> Deref for StatefulRef<'a, W> { type Target = W; #[track_caller] @@ -239,7 +254,7 @@ impl<'a, W> Deref for StateRef<'a, W> { } } -impl<'a, W> DerefMut for StateRef<'a, W> { +impl<'a, W> DerefMut for StatefulRef<'a, W> { #[track_caller] fn deref_mut(&mut self) -> &mut Self::Target { let b = &self.value.inner.borrow_flag; @@ -277,11 +292,11 @@ impl<'a, W> DerefMut for StateRef<'a, W> { } } -impl<'a, W> StateRef<'a, W> { +impl<'a, W> StatefulRef<'a, W> { /// Fork a silent reference - pub fn silent(&self) -> StateRef<'a, W> { + pub fn silent(&self) -> StatefulRef<'a, W> { self.release_borrow(); - StateRef::new(self.value.inner_ref(), ModifyScope::DATA) + StatefulRef::new(self.value.inner_ref(), ModifyScope::DATA) } /// Forget all modifies record in this reference. So the downstream will no @@ -341,10 +356,13 @@ impl<'a, W> StateRef<'a, W> { impl SingleChild for Stateful {} impl MultiChild for Stateful {} -impl_proxy_query!(paths [modify_notifier, state_ref()], Stateful, , where R: Query + 'static ); +impl_proxy_query!( + paths [modify_notifier, state_ref()], + Stateful, , where R: Query + 'static +); impl_query_self_only!(StateChangeNotifier); -impl<'a, W> Drop for StateRef<'a, W> { +impl<'a, W> Drop for StatefulRef<'a, W> { fn drop(&mut self) { self.release_borrow(); } } diff --git a/core/src/test_helper.rs b/core/src/test_helper.rs index 238ff4997..715f7a45f 100644 --- a/core/src/test_helper.rs +++ b/core/src/test_helper.rs @@ -19,19 +19,19 @@ pub struct TestWindow(pub Rc); impl TestWindow { /// Create a 1024x1024 window for test - pub fn new(root: impl Into) -> Self { - let _ = NEW_TIMER_FN.set(Timer::new_timer_future); - let wnd = Window::new(root.into(), Box::new(TestShellWindow::new(None))); - AppCtx::windows().borrow_mut().insert(wnd.id(), wnd.clone()); - Self(wnd) - } + pub fn new(root: impl Into) -> Self { Self::new_wnd(root, None) } pub fn new_with_size(root: impl Into, size: Size) -> Self { + Self::new_wnd(root, Some(size)) + } + + fn new_wnd(root: impl Into, size: Option) -> Self { let _ = NEW_TIMER_FN.set(Timer::new_timer_future); - Self(Window::new( - root.into(), - Box::new(TestShellWindow::new(Some(size))), - )) + let wnd = Window::new(root.into(), Box::new(TestShellWindow::new(size))); + let id = wnd.id(); + AppCtx::windows().borrow_mut().insert(wnd.id(), wnd.clone()); + AppCtx::get_window(id).unwrap().emit_events(); + Self(wnd) } /// Ues a index path to access widget tree and return the layout info, diff --git a/core/src/widget.rs b/core/src/widget.rs index 676eb32fa..23d2521a7 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -51,10 +51,6 @@ pub trait Render: Query { fn get_transform(&self) -> Option { None } } -pub(crate) fn hit_test_impl(ctx: &HitTestCtx, pos: Point) -> bool { - ctx.box_rect().map_or(false, |rect| rect.contains(pos)) -} - /// The common type of all widget can convert to. pub struct Widget(Box WidgetId>); @@ -105,7 +101,7 @@ impl<'a> dyn Render + 'a { pub fn query_all_type(&self, mut callback: impl FnMut(&T) -> bool, order: QueryOrder) { self.query_all( TypeId::of::(), - &mut |a: &dyn Any| a.downcast_ref().map_or(true, &mut callback), + &mut |a| a.downcast_ref().map_or(true, &mut callback), order, ) } @@ -148,29 +144,17 @@ pub struct FnWidget(F); pub(crate) trait WidgetBuilder { fn build(self, ctx: &BuildCtx) -> WidgetId; } -pub(crate) trait WidgetOrId { - fn build_id(self, ctx: &BuildCtx) -> WidgetId; -} - -impl WidgetOrId for T { - #[inline] - fn build_id(self, ctx: &BuildCtx) -> WidgetId { self.build(ctx) } -} - -impl WidgetOrId for WidgetId { - #[inline] - fn build_id(self, _: &BuildCtx) -> WidgetId { self } -} -impl WidgetOrId for Widget { +impl From for Widget { #[inline] - fn build_id(self, ctx: &BuildCtx) -> WidgetId { self.build(ctx) } + fn from(id: WidgetId) -> Self { Widget(Box::new(move |_| id)) } } impl FnWidget { + #[inline] pub fn new(f: F) -> Self where - F: FnOnce(&BuildCtx) -> R, + F: FnOnce(&BuildCtx) -> R + Into>, { FnWidget(f) } @@ -182,9 +166,9 @@ impl FnWidget { impl WidgetBuilder for FnWidget where F: FnOnce(&BuildCtx) -> R + 'static, - R: WidgetOrId, + R: Into, { - fn build(self, ctx: &BuildCtx) -> WidgetId { (self.0)(ctx).build_id(ctx) } + fn build(self, ctx: &BuildCtx) -> WidgetId { (self.0)(ctx).into().build(ctx) } } #[macro_export] @@ -327,6 +311,8 @@ pub(crate) struct RenderFul(pub(crate) Stateful); impl_proxy_query!(paths [0], RenderFul, , where R: Render + 'static); impl_proxy_render!(proxy 0.state_ref(), RenderFul, , where R: Render + 'static); +impl_proxy_query!(paths[0.state_ref()], RenderFul>); +impl_proxy_render!(proxy 0.state_ref(), RenderFul>); impl Compose for R { fn compose(this: State) -> Widget { @@ -346,11 +332,30 @@ impl From for Widget { fn from(value: W) -> Self { Self(Box::new(|ctx| value.build(ctx))) } } +impl From for FnWidget +where + F: FnOnce(&BuildCtx) -> R, + R: Into, +{ + #[inline] + fn from(value: F) -> Self { Self(value) } +} + impl Widget { #[inline] pub fn build(self, ctx: &BuildCtx) -> WidgetId { (self.0)(ctx) } } +impl From for Box { + #[inline] + fn from(value: R) -> Self { Box::new(value) } +} + +impl From> for Box { + #[inline] + fn from(value: Stateful) -> Self { Box::new(RenderFul(value)) } +} + impl_proxy_query!(paths [deref()], ShareResource, , where T: Render + 'static); impl_proxy_render!(proxy deref(), ShareResource, , where T: Render + 'static); impl_proxy_query!(paths [deref()], Rc, , where W: Query + 'static); @@ -371,3 +376,7 @@ pub fn then(b: bool, f: impl FnOnce() -> W) -> Option { b.then(f) } /// calls the closure on `value` and returns #[inline] pub fn map(value: T, f: impl FnOnce(T) -> W) -> W { f(value) } + +pub(crate) fn hit_test_impl(ctx: &HitTestCtx, pos: Point) -> bool { + ctx.box_rect().map_or(false, |rect| rect.contains(pos)) +} diff --git a/core/src/widget_children.rs b/core/src/widget_children.rs index dad605087..3d4436084 100644 --- a/core/src/widget_children.rs +++ b/core/src/widget_children.rs @@ -38,19 +38,19 @@ pub use multi_child_impl::*; pub use single_child_impl::*; pub mod child_convert; pub use child_convert::{ChildFrom, FromAnother}; -/// Trait to tell Ribir a widget can have one child. +/// Trait to tell Ribir a object can have one child. pub trait SingleChild {} -/// A render widget that has one child. -pub trait RenderSingleChild: Render + SingleChild { +/// A boxed render widget that has one child. +pub trait BoxedSingleParent: SingleChild { fn into_render(self: Box) -> Box; } -/// Trait to tell Ribir a widget that has multi children. +/// Trait to tell Ribir a object that has multi children. pub trait MultiChild {} -/// A render widget that has multi children. -pub trait RenderMultiChild: Render + MultiChild { +/// A boxed render widget that has multi children. +pub trait BoxMultiParent: MultiChild { fn into_render(self: Box) -> Box; } @@ -63,16 +63,20 @@ pub trait ComposeChild: Sized { /// A alias of `WidgetPair`, means `Widget` is the /// child of the generic type. -pub type WidgetOf = WidgetPair; +pub type WidgetOf = SinglePair; -impl RenderSingleChild for T { - fn into_render(self: Box) -> Box { Box::new(*self) } -} +impl SingleChild for Box {} +impl MultiChild for Box {} -impl RenderMultiChild for T { - fn into_render(self: Box) -> Box { Box::new(*self) } +impl From> for Box { + #[inline] + fn from(v: Box) -> Self { v.into_render() } } +impl From> for Box { + #[inline] + fn from(value: Box) -> Self { value.into_render() } +} #[cfg(test)] mod tests { use super::*; @@ -124,7 +128,7 @@ mod tests { struct Child; impl ComposeChild for Parent { - type Child = Option>; + type Child = Option>; fn compose_child(_: State, _: Self::Child) -> Widget { unreachable!("Only for syntax support check"); @@ -272,9 +276,9 @@ mod tests { const COMPOSE_DYNS_CHILD_SIZE: Size = Size::new(100., 200.); fn compose_dyns_child() -> Widget { #[derive(Declare)] - struct X; + struct AcceptStateChild; - impl ComposeChild for X { + impl ComposeChild for AcceptStateChild { type Child = State; fn compose_child(_: State, child: Self::Child) -> Widget { child.into() } } @@ -283,7 +287,7 @@ mod tests { widget! { states { trigger: trigger } - X { + AcceptStateChild { DynWidget { dyns: if *trigger { MockBox { size: COMPOSE_DYNS_CHILD_SIZE } diff --git a/core/src/widget_children/child_convert.rs b/core/src/widget_children/child_convert.rs index c5a2cc174..d2101b1a4 100644 --- a/core/src/widget_children/child_convert.rs +++ b/core/src/widget_children/child_convert.rs @@ -1,6 +1,6 @@ use super::{ - decorate_tml_impl::IntoDecorateTml, ComposeChild, ComposePair, DecorateTml, Multi, TmlFlag, - WidgetPair, + decorate_tml_impl::IntoDecorateTml, ComposeChild, ComposePair, DecorateTml, Multi, SinglePair, + TmlFlag, }; use crate::{ dynamic_widget::{DynRender, DynWidget}, @@ -65,30 +65,30 @@ where } // WidgetPair --> WidgetPair -impl FromAnother, [(M1, M2); 0]> for WidgetPair +impl FromAnother, [(M1, M2); 0]> for SinglePair where W: FromAnother, C: ChildFrom, { #[inline] - fn from_another(value: WidgetPair) -> Self { - let WidgetPair { widget, child } = value; - WidgetPair { + fn from_another(value: SinglePair) -> Self { + let SinglePair { widget, child } = value; + SinglePair { widget: W::from_another(widget), child: C::child_from(child), } } } -impl FromAnother, [(M1, M2); 1]> for WidgetPair +impl FromAnother, [(M1, M2); 1]> for SinglePair where W: ChildFrom, C: FromAnother, { #[inline] - fn from_another(value: WidgetPair) -> Self { - let WidgetPair { widget, child } = value; - WidgetPair { + fn from_another(value: SinglePair) -> Self { + let SinglePair { widget, child } = value; + SinglePair { widget: W::child_from(widget), child: C::from_another(child), } diff --git a/core/src/widget_children/compose_child_impl.rs b/core/src/widget_children/compose_child_impl.rs index edfbdb80d..ae11b8b77 100644 --- a/core/src/widget_children/compose_child_impl.rs +++ b/core/src/widget_children/compose_child_impl.rs @@ -6,7 +6,7 @@ use crate::{ widget::{Widget, WidgetBuilder, WidgetId}, }; -use super::{child_convert::FillVec, ComposeChild, WidgetPair}; +use super::{child_convert::FillVec, ComposeChild, SinglePair}; /// Trait specify what child a compose child widget can have, and the target /// type after widget compose its child. @@ -78,15 +78,15 @@ where } } -impl ComposeWithChild for WidgetPair +impl ComposeWithChild for SinglePair where C1: ComposeWithChild, { - type Target = WidgetPair; + type Target = SinglePair; fn with_child(self, c: C2, ctx: &BuildCtx) -> Self::Target { - let WidgetPair { widget, child } = self; - WidgetPair { + let SinglePair { widget, child } = self; + SinglePair { widget, child: child.with_child(c, ctx), } @@ -182,9 +182,9 @@ pub(crate) mod decorate_tml_impl { impl DecorateTmlMarker<()> for DecorateTml {} - impl DecorateTmlMarker<[(); 0]> for WidgetPair {} + impl DecorateTmlMarker<[(); 0]> for SinglePair {} - impl> DecorateTmlMarker<[M; 1]> for WidgetPair {} + impl> DecorateTmlMarker<[M; 1]> for SinglePair {} impl> DecorateTmlMarker for ComposePair {} @@ -193,7 +193,7 @@ pub(crate) mod decorate_tml_impl { fn into_decorate_tml(self) -> DecorateTml; } - impl IntoDecorateTml for WidgetPair + impl IntoDecorateTml for SinglePair where W: TmlFlag, C: ChildFrom, @@ -201,26 +201,26 @@ pub(crate) mod decorate_tml_impl { type Flag = W; fn into_decorate_tml(self) -> DecorateTml { - let WidgetPair { widget: tml_flag, child } = self; + let SinglePair { widget: tml_flag, child } = self; let decorator = Box::new(|w| w); let child = C::child_from(child); DecorateTml { decorator, tml_flag, child } } } - impl IntoDecorateTml for WidgetPair + impl IntoDecorateTml for SinglePair where W: 'static, C2: IntoDecorateTml, - WidgetPair: WidgetBuilder, + SinglePair: WidgetBuilder, { type Flag = C2::Flag; fn into_decorate_tml(self) -> DecorateTml { - let WidgetPair { widget, child } = self; + let SinglePair { widget, child } = self; let DecorateTml { decorator, tml_flag, child } = ChildFrom::child_from(child); DecorateTml { - decorator: Box::new(move |w| WidgetPair { widget, child: decorator(w) }.into()), + decorator: Box::new(move |w| SinglePair { widget, child: decorator(w) }.into()), tml_flag, child, } diff --git a/core/src/widget_children/multi_child_impl.rs b/core/src/widget_children/multi_child_impl.rs index 7d1549b6d..d839f0f6a 100644 --- a/core/src/widget_children/multi_child_impl.rs +++ b/core/src/widget_children/multi_child_impl.rs @@ -1,5 +1,5 @@ use super::*; -use crate::widget::{RenderFul, WidgetBuilder}; +use crate::widget::WidgetBuilder; /// Trait specify what child a multi child widget can have, and the target type /// after widget compose its child. @@ -8,8 +8,8 @@ pub trait MultiWithChild { fn with_child(self, child: C, ctx: &BuildCtx) -> Self::Target; } -pub struct MultiChildWidget { - pub widget: Box, +pub struct MultiPair { + pub parent: WidgetId, pub children: Vec, } @@ -25,10 +25,6 @@ impl Multi { pub fn into_inner(self) -> W { self.0 } } -trait IntoMultiParent { - fn into_multi_parent(self) -> Box; -} - // Same with ChildConvert::FillVec, but only for MultiChild, // There are some duplicate implementations, but better compile error for // users @@ -71,34 +67,46 @@ where } } -impl IntoMultiParent for R { +impl FillVec for Pipe> +where + D: IntoIterator + 'static, + D::Item: Into, +{ + fn fill_vec(self, vec: &mut Vec, ctx: &BuildCtx) { + self.build_multi(vec, ctx.force_as_mut()); + } +} + +trait MultiParent { + fn build_as_parent(self, ctx: &mut BuildCtx) -> WidgetId; +} + +impl> + MultiChild> MultiParent for R { #[inline] - fn into_multi_parent(self) -> Box { Box::new(self) } + fn build_as_parent(self, ctx: &mut BuildCtx) -> WidgetId { ctx.alloc_widget(self.into()) } } -impl IntoMultiParent for Stateful { +impl> + MultiChild> MultiParent for Pipe { #[inline] - fn into_multi_parent(self) -> Box { Box::new(RenderFul(self)) } + fn build_as_parent(self, ctx: &mut BuildCtx) -> WidgetId { self.build_as_render_parent(ctx) } } impl MultiWithChild for R where - R: IntoMultiParent, + R: MultiParent, C: FillVec, { - type Target = MultiChildWidget; + type Target = MultiPair; fn with_child(self, child: C, ctx: &BuildCtx) -> Self::Target { + let parent = self.build_as_parent(ctx.force_as_mut()); let mut children = vec![]; child.fill_vec(&mut children, ctx); - MultiChildWidget { - widget: self.into_multi_parent(), - children, - } + MultiPair { parent, children } } } -impl MultiWithChild for MultiChildWidget +impl MultiWithChild for MultiPair where C: FillVec, { @@ -110,13 +118,13 @@ where } } -impl WidgetBuilder for MultiChildWidget { +impl WidgetBuilder for MultiPair { fn build(self, ctx: &BuildCtx) -> WidgetId { - let MultiChildWidget { widget, children } = self; - let p = ctx.alloc_widget(widget); + let MultiPair { parent, children } = self; children .into_iter() - .for_each(|child| ctx.append_child(p, child)); - p + .for_each(|child| ctx.force_as_mut().append_child(parent, child)); + + parent } } diff --git a/core/src/widget_children/single_child_impl.rs b/core/src/widget_children/single_child_impl.rs index 6102cba56..2de1879b8 100644 --- a/core/src/widget_children/single_child_impl.rs +++ b/core/src/widget_children/single_child_impl.rs @@ -1,4 +1,4 @@ -use crate::widget::{RenderFul, WidgetBuilder}; +use crate::widget::WidgetBuilder; use super::*; @@ -10,7 +10,7 @@ pub trait SingleWithChild { } /// A node of widget with not compose its child. -pub struct WidgetPair { +pub struct SinglePair { pub widget: W, pub child: C, } @@ -18,59 +18,59 @@ pub struct WidgetPair { impl SingleChild for Option {} impl SingleWithChild for W { - type Target = WidgetPair; + type Target = SinglePair; #[inline] - fn with_child(self, child: C, _: &BuildCtx) -> Self::Target { WidgetPair { widget: self, child } } + fn with_child(self, child: C, _: &BuildCtx) -> Self::Target { SinglePair { widget: self, child } } } -impl SingleWithChild for WidgetPair { - type Target = WidgetPair>; +impl SingleWithChild for SinglePair { + type Target = SinglePair>; fn with_child(self, c: C2, ctx: &BuildCtx) -> Self::Target { - let WidgetPair { widget, child } = self; - WidgetPair { + let SinglePair { widget, child } = self; + SinglePair { widget, child: child.with_child(c, ctx), } } } -trait IntoSingleParent { - fn into_single_parent(self) -> Box; +trait SingleParent { + fn build_as_parent(self, ctx: &mut BuildCtx) -> WidgetId; } trait WidgetChild { fn child_build(self, ctx: &BuildCtx) -> WidgetId; } -impl IntoSingleParent for Box { - fn into_single_parent(self) -> Box { self.into_render() } +impl> + SingleChild + 'static> SingleParent for W { + fn build_as_parent(self, ctx: &mut BuildCtx) -> WidgetId { ctx.alloc_widget(self.into()) } } -impl IntoSingleParent for W { - fn into_single_parent(self) -> Box { Box::new(self) } -} - -impl IntoSingleParent for Stateful { +impl> + SingleChild + 'static> SingleParent for Pipe { #[inline] - fn into_single_parent(self) -> Box { Box::new(RenderFul(self)) } + fn build_as_parent(self, ctx: &mut BuildCtx) -> WidgetId { self.build_as_render_parent(ctx) } } -impl IntoSingleParent for Stateful> +impl SingleParent for Stateful> where - D: RenderSingleChild + WidgetBuilder + 'static, + D: Render + SingleChild + WidgetBuilder + 'static, { #[inline] - fn into_single_parent(self) -> Box { Box::new(DynRender::single(self)) } + fn build_as_parent(self, ctx: &mut BuildCtx) -> WidgetId { + Box::new(DynRender::single(self)).build(ctx) + } } -impl IntoSingleParent for Stateful>> +impl SingleParent for Stateful>> where - D: RenderSingleChild + WidgetBuilder + 'static, + D: Render + SingleChild + WidgetBuilder + 'static, { #[inline] - fn into_single_parent(self) -> Box { Box::new(DynRender::option(self)) } + fn build_as_parent(self, ctx: &mut BuildCtx) -> WidgetId { + Box::new(DynRender::option(self)).build(ctx) + } } impl WidgetChild for Widget { @@ -83,47 +83,46 @@ impl WidgetChild for W { fn child_build(self, ctx: &BuildCtx) -> WidgetId { self.build(ctx) } } -impl WidgetBuilder for WidgetPair +impl WidgetBuilder for SinglePair where - W: IntoSingleParent, + W: SingleParent, C: WidgetChild, { fn build(self, ctx: &BuildCtx) -> WidgetId { let Self { widget, child } = self; - let p = ctx.alloc_widget(widget.into_single_parent()); + let p = widget.build_as_parent(ctx.force_as_mut()); let child = child.child_build(ctx); - ctx.append_child(p, child); + ctx.force_as_mut().append_child(p, child); p } } -impl WidgetBuilder for WidgetPair, C> +impl WidgetBuilder for SinglePair, C> where - W: IntoSingleParent, + W: SingleParent, C: WidgetChild, { fn build(self, ctx: &BuildCtx) -> WidgetId { let Self { widget, child } = self; if let Some(widget) = widget { - WidgetPair { widget, child }.build(ctx) + SinglePair { widget, child }.build(ctx) } else { child.child_build(ctx) } } } -impl WidgetBuilder for WidgetPair> +impl WidgetBuilder for SinglePair> where - W: IntoSingleParent, + W: SingleParent, C: WidgetChild, { fn build(self, ctx: &BuildCtx) -> WidgetId { let Self { widget, child } = self; if let Some(child) = child { - WidgetPair { widget, child }.build(ctx) + SinglePair { widget, child }.build(ctx) } else { - let node = widget.into_single_parent(); - ctx.alloc_widget(node) + widget.build_as_parent(ctx.force_as_mut()) } } } diff --git a/core/src/widget_tree.rs b/core/src/widget_tree.rs index 2c39f5ae8..428d08f5d 100644 --- a/core/src/widget_tree.rs +++ b/core/src/widget_tree.rs @@ -161,12 +161,41 @@ impl WidgetTree { needs_layout }) } + + pub fn detach(&mut self, id: WidgetId) { + if self.root() == id { + let root = self.root(); + let new_root = root + .next_sibling(&self.arena) + .or_else(|| root.prev_sibling(&self.arena)) + .expect("Try to remove the root and there is no other widget can be the new root."); + self.root = Some(new_root); + } + + id.0.detach(&mut self.arena); + } + + pub(crate) fn remove_subtree(&mut self, id: WidgetId) { + assert_ne!( + id, + self.root(), + "You should detach the root widget before remove it." + ); + + id.descendants(&self.arena).for_each(|id| { + self.store.remove(id); + }); + id.0.remove_subtree(&mut self.arena); + } } #[cfg(test)] mod tests { extern crate test; - use crate::test_helper::{MockBox, MockMulti, TestWindow}; + use crate::{ + test_helper::{MockBox, MockMulti, TestWindow}, + widget::widget_id::empty_node, + }; use super::*; use test::Bencher; @@ -349,11 +378,13 @@ mod tests { let root = tree.root(); tree.mark_dirty(root); + let new_root = empty_node(&mut tree.arena); + root.insert_after(new_root, &mut tree.arena); + tree.mark_dirty(new_root); + tree.detach(root); + tree.remove_subtree(root); - root.remove_subtree(&mut tree); - - assert_eq!(tree.layout_list(), None); - assert!(!tree.is_dirty()); + assert_eq!(tree.layout_list(), Some(vec![new_root])); } #[bench] diff --git a/core/src/widget_tree/layout_info.rs b/core/src/widget_tree/layout_info.rs index 3c614bf6e..b3f8f0be4 100644 --- a/core/src/widget_tree/layout_info.rs +++ b/core/src/widget_tree/layout_info.rs @@ -212,7 +212,7 @@ impl<'a> Layouter<'a> { self.id = ctx.id; } - let mut info = tree2.store.layout_info_or_default(id); + let info = tree2.store.layout_info_or_default(id); let size = clamp.clamp(size); info.clamp = clamp; info.size = Some(size); diff --git a/core/src/widget_tree/widget_id.rs b/core/src/widget_tree/widget_id.rs index 5250c5edf..44f74807c 100644 --- a/core/src/widget_tree/widget_id.rs +++ b/core/src/widget_tree/widget_id.rs @@ -108,6 +108,9 @@ impl WidgetId { pub(crate) fn next_sibling(self, tree: &TreeArena) -> Option { self.node_feature(tree, |node| node.next_sibling()) } + pub(crate) fn prev_sibling(self, tree: &TreeArena) -> Option { + self.node_feature(tree, Node::previous_sibling) + } pub(crate) fn previous_sibling(self, tree: &TreeArena) -> Option { self.node_feature(tree, |node| node.previous_sibling()) @@ -126,15 +129,6 @@ impl WidgetId { self.0.descendants(tree).map(WidgetId) } - pub(crate) fn detach(self, tree: &mut TreeArena) { self.0.detach(tree) } - - pub(crate) fn remove_subtree(self, tree: &mut WidgetTree) { - self.descendants(&tree.arena).for_each(|id| { - tree.store.remove(id); - }); - self.0.remove_subtree(&mut tree.arena); - } - pub(crate) fn on_mounted_subtree(self, tree: &WidgetTree) { self .descendants(&tree.arena) diff --git a/core/src/window.rs b/core/src/window.rs index e3b531017..1888040d3 100644 --- a/core/src/window.rs +++ b/core/src/window.rs @@ -120,6 +120,8 @@ impl Window { /// Draw an image what current render tree represent. #[track_caller] pub fn draw_frame(&self) { + self.emit_events(); + if !self.need_draw() || self.size().is_empty() { return; } @@ -136,11 +138,16 @@ impl Window { self.frame_pool.borrow_mut().run(); if !self.widget_tree.borrow().is_dirty() { - break; + self.focus_mgr.borrow_mut().refresh_focus(); + self.emit_events(); + + // focus refresh and event emit may cause widget tree dirty again. + if !self.widget_tree.borrow().is_dirty() { + break; + } } } - self.focus_mgr.borrow_mut().refresh_focus(); self.widget_tree.borrow().draw(); let surface = match AppCtx::app_theme() { @@ -199,7 +206,6 @@ impl Window { .borrow_mut() .init(root, Rc::downgrade(&window)); - window.emit_events(); window } @@ -288,7 +294,7 @@ impl Window { }); if !delay_drop { - id.remove_subtree(&mut self.widget_tree.borrow_mut()) + self.widget_tree.borrow_mut().remove_subtree(id); } } DelayEvent::Focus(id) => { @@ -394,14 +400,17 @@ impl Window { where L: EventListener + 'static, { - id.assert_get(&self.widget_tree.borrow().arena) - .query_all_type( - |m: &L| { - m.dispatch(&mut e); - true - }, - QueryOrder::InnerFirst, - ); + // Safety: we only use tree to query the inner data of a node and dispatch a + // event by it, and never read or write the node. And in the callback, there is + // no way to mut access the inner data of node or destroy the node. + let tree = unsafe { &*(&*self.widget_tree.borrow() as *const WidgetTree) }; + id.assert_get(&tree.arena).query_all_type( + |m: &L| { + m.dispatch(&mut e); + true + }, + QueryOrder::InnerFirst, + ); } fn bottom_down_emit<'a, L>(&self, e: &mut L::Event<'a>, bottom: WidgetId, up: Option) diff --git a/dev-helper/src/example_framework.rs b/dev-helper/src/example_framework.rs index 9370b579d..8777294bd 100644 --- a/dev-helper/src/example_framework.rs +++ b/dev-helper/src/example_framework.rs @@ -45,7 +45,7 @@ macro_rules! example_framework { AppCtx::set_app_theme(material::purple::light()); } let name = env!("CARGO_PKG_NAME"); - let id = App::new_window($widget_fn(), Some($size)); + let id = App::new_window($widget_fn().into(), Some($size)); AppCtx::get_window(id).unwrap().set_title(name); App::exec(); } diff --git a/docs/essentials/widget_language.md b/docs/essentials/widget_language.md index 3d16ba9fa..e3f35bd18 100644 --- a/docs/essentials/widget_language.md +++ b/docs/essentials/widget_language.md @@ -213,7 +213,7 @@ DynWidget { We added three lines of code. -The first line is `dyns := assign_watch!(*counter > 0)`, we use operator `:=` instead of `:` to initialize the `dyns`. Unlike `:`, `:=` accepts an `AssignObservable` as its initialization value and explicitly subscribes to it to update the field. `AssignObservable` is a type that contain the initialization value and an observable stream of that value. The `assign_watch!` macro is used to convert a expression to an `AssignObservable`. +The first line is `dyns := assign_watch!(*counter > 0)`, we use operator `:=` instead of `:` to initialize the `dyns`. Unlike `:`, `:=` accepts an `Pipe` as its initialization value and explicitly subscribes to it to update the field. `Pipe` is a type that contain the initialization value and an observable stream of that value. The `assign_watch!` macro is used to convert a expression to an `Pipe`. In the second line we use `stream_map` to chain `distinct_until_changed` on the stream observable. So we accept the changes only when the result of `*counter > 0` changed. diff --git a/examples/counter/src/counter.rs b/examples/counter/src/counter.rs index 40ff2a0ad..685f3422d 100644 --- a/examples/counter/src/counter.rs +++ b/examples/counter/src/counter.rs @@ -1,15 +1,21 @@ use ribir::prelude::*; -pub fn counter() -> Widget { - widget! { - states { cnt: Stateful::new(0) } - Column { +pub fn counter() -> impl Into { + fn_widget! { + let cnt = Stateful::new(0); + + @Column { h_align: HAlign::Center, align_items: Align::Center, - FilledButton { on_tap: move |_| *cnt += 1, Label::new("Add") } - H1 { text: cnt.to_string() } - FilledButton { on_tap: move |_| *cnt += -1, Label::new("Sub") } + @FilledButton { + on_tap: move |_: &'_ mut PointerEvent<'_>| *$cnt += 1, + @ { Label::new("Add") } + } + @H1 { text: pipe!($cnt.to_string()) } + @FilledButton { + on_tap: move |_: &'_ mut PointerEvent<'_>| *$cnt += -1, + @ { Label::new("Sub") } + } } } - .into() } diff --git a/examples/messages/src/messages.rs b/examples/messages/src/messages.rs index d1498f092..8df9c88b5 100644 --- a/examples/messages/src/messages.rs +++ b/examples/messages/src/messages.rs @@ -13,7 +13,7 @@ struct MessageList { messages: Vec, } -pub fn messages() -> Widget { +pub fn messages() -> impl Into { MessageList { messages: vec![ Message { @@ -38,95 +38,83 @@ pub fn messages() -> Widget { }, ], } - .into() } impl Compose for MessageList { - fn compose(this: State) -> Widget { - widget! { - states { this: this.into_readonly() } - init ctx => { - let title_style = TypographyTheme::of(ctx).title_large.text.clone(); - let title_icon_size = IconSize::of(ctx).tiny; - let background = Palette::of(ctx).surface(); - let foreground = Palette::of(ctx).on_surface(); - } - Column { - background, - Row { + fn compose(mut this: State) -> Widget { + fn_widget! { + let palette = Palette::of(ctx!()); + + @Column { + background: palette.surface(), + @Row { justify_content: JustifyContent::SpaceBetween, padding: EdgeInsets::new(8., 16., 8., 16.), align_items: Align::Center, - Row { + @Row { item_gap: 10., - Icon { - size: title_icon_size, - svgs::MENU - } - Text { + @TinyIcon { @{ svgs::MENU } } + @Text { text: "Message", - foreground, - text_style: title_style.clone(), + foreground: palette.on_surface(), + text_style: TypographyTheme::of(ctx!()).title_large.text.clone(), } } - Row { + @Row { item_gap: 10., - Icon { - size: title_icon_size, - svgs::SEARCH - } - Icon { - size: title_icon_size, - svgs::MORE_VERT - } + @TinyIcon { @{ svgs::SEARCH } } + @TinyIcon { @{ svgs::MORE_VERT } } } } - Tabs { + @Tabs { pos: Position::Bottom, - Tab { - TabItem { - material_svgs::SMS - Label::new("Messages") + @Tab { + @TabItem { + @{ material_svgs::SMS } + @{ Label::new("Messages") } } - TabPane { - VScrollBar { - Lists { - Multi::new(this.messages.clone().into_iter().map(move |message| { - let name = message.avatar.to_string(); - let mut avatar = format!("{}/examples/attachments/3DDD-{name}.png", env!("CARGO_WORKSPACE_DIR")); - let img = PixelImage::from_png(&std::fs::read(avatar).unwrap()); - let img = ShareResource::new(img); - - widget! { - Column { - ListItem { - line_number: 1, - HeadlineText(Label::new(message.nick_name.clone())) - SupportingText(Label::new(message.content.clone())) - Leading { Avatar { widget::from(img) } } - Trailing { svgs::MORE_HORIZ } + @TabPane { + @VScrollBar { + @Lists { + @{ + let message_gen = |message: Message| { + @Column { + @ListItem { + line_number: 1usize, + @HeadlineText(Label::new(message.nick_name.clone())) + @SupportingText(Label::new(message.content.clone())) + @Leading { + @Avatar { + @{ + let name = message.avatar.to_string(); + let mut avatar = format!("{}/examples/attachments/3DDD-{name}.png", env!("CARGO_WORKSPACE_DIR")); + let img = PixelImage::from_png(&std::fs::read(avatar).unwrap()); + ShareResource::new(img) + } + } + } + @Trailing { @{ svgs::MORE_HORIZ } } } - Divider {} + @Divider {} } - } - })) + }; + + Multi::new($this.messages.clone().into_iter().map(message_gen)) + } } } } } - Tab { - TabItem { - material_svgs::ACCOUNT_CIRCLE - Label::new("Person") - } - TabPane { - Text { - text: "Person" - } + @Tab { + @TabItem { + @{ material_svgs::ACCOUNT_CIRCLE } + @{ Label::new("Person") } } + @TabPane { @Text { text: "Person" } } } } } - }.into() + } + .into() } } diff --git a/examples/todos/src/todos.rs b/examples/todos/src/todos.rs index 4d0e7bfec..3fe21d001 100644 --- a/examples/todos/src/todos.rs +++ b/examples/todos/src/todos.rs @@ -76,7 +76,7 @@ impl Compose for TodoMVP { } impl TodoMVP { - fn pane(this: StateRef, cond: fn(&Task) -> bool) -> Widget { + fn pane(this: StatefulRef, cond: fn(&Task) -> bool) -> Widget { let this = this.clone_stateful(); widget! { states { this, mount_task_cnt: Stateful::new(0) } @@ -105,7 +105,12 @@ impl TodoMVP { .into() } - fn task(this: StateRef, task: Task, idx: usize, mount_task_cnt: StateRef) -> Widget { + fn task( + this: StatefulRef, + task: Task, + idx: usize, + mount_task_cnt: StatefulRef, + ) -> Widget { let this = this.clone_stateful(); let mount_task_cnt = mount_task_cnt.clone_stateful(); widget! { diff --git a/macros/Cargo.toml b/macros/Cargo.toml index b4f2e212e..f52f71cd3 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -22,10 +22,10 @@ bitflags.workspace = true lazy_static.workspace = true proc-macro2.workspace = true quote.workspace = true -ribir_builtin = {path = "./builtin", version = "0.0.1-alpha.4" } -ribir_painter = {path = "../painter", version = "0.0.1-alpha.4" } +ribir_builtin = {path = "./builtin", version = "0.0.1-alpha.4"} +ribir_painter = {path = "../painter", version = "0.0.1-alpha.4"} smallvec.workspace = true -syn = {workspace = true, features = ["visit-mut", "full", "extra-traits"]} +syn = {workspace = true, features = ["visit-mut", "fold", "full", "extra-traits"]} [build-dependencies] -ribir_builtin = {path = "./builtin", version = "0.0.1-alpha.4" } +ribir_builtin = {path = "./builtin", version = "0.0.1-alpha.4"} diff --git a/macros/src/child_template.rs b/macros/src/child_template.rs index 5958d10fc..999033e80 100644 --- a/macros/src/child_template.rs +++ b/macros/src/child_template.rs @@ -62,6 +62,12 @@ pub(crate) fn derive_child_template(input: &mut syn::DeriveInput) -> syn::Result fn declare_builder() -> Self::Builder { #name::builder() } } + impl #g_impl Declare2 for #name #g_ty #g_where { + type Builder = #builder #g_ty; + #[inline] + fn declare2_builder() -> Self::Builder { #name::builder() } + } + impl #g_impl DeclareBuilder for #builder #g_ty { type Target = Self; #[inline] diff --git a/macros/src/declare_derive.rs b/macros/src/declare_derive.rs index a1581a57e..966147fa2 100644 --- a/macros/src/declare_derive.rs +++ b/macros/src/declare_derive.rs @@ -253,7 +253,6 @@ pub(crate) fn declare_derive(input: &mut syn::DeriveInput) -> syn::Result, + value: Option, +} + +#[derive(Default)] +struct DeclareAttr { + rename: Option, + builtin: Option, + default: Option, + custom: Option, + // field with `skip` attr, will not generate setter method and use default to init value. + skip: Option, +} + +struct DeclareField<'a> { + attr: Option, + field: &'a syn::Field, +} +mod kw { + use syn::custom_keyword; + custom_keyword!(rename); + custom_keyword!(builtin); + custom_keyword!(default); + custom_keyword!(custom); + custom_keyword!(skip); + // todo: tmp code, only for compatibility. + custom_keyword!(convert); +} + +impl Parse for DefaultMeta { + fn parse(input: syn::parse::ParseStream) -> Result { + Ok(Self { + _default_kw: input.parse()?, + _eq_token: input.parse()?, + value: { + let ahead = input.fork(); + let expr = ahead.parse::(); + if expr.is_ok() { + input.advance_to(&ahead); + } + expr.ok() + }, + }) + } +} + +impl Parse for DeclareAttr { + fn parse(input: syn::parse::ParseStream) -> Result { + let mut attr = DeclareAttr::default(); + while !input.is_empty() { + let lookahead = input.lookahead1(); + + // use input instead of lookahead to peek builtin, because need't complicate in + // compile error. + if input.peek(kw::builtin) { + attr.builtin = Some(input.parse()?); + } else if lookahead.peek(kw::rename) { + input.parse::()?; + input.parse::()?; + attr.rename = Some(input.parse()?); + } else if lookahead.peek(kw::custom) { + attr.custom = Some(input.parse()?); + } else if lookahead.peek(kw::default) { + attr.default = Some(input.parse()?); + } else if lookahead.peek(kw::skip) { + attr.skip = Some(input.parse()?); + } else if lookahead.peek(kw::convert) { + input.parse::()?; + input.parse::()?; + input.parse::()?; + } else { + return Err(lookahead.error()); + } + if let (Some(rename), Some(builtin)) = (attr.rename.as_ref(), attr.builtin.as_ref()) { + let mut d = Diagnostic::new( + Level::Error, + "`rename` and `builtin` can not be used in same time.", + ); + d.set_spans(vec![rename.span().unwrap(), builtin.span().unwrap()]); + d.emit(); + } + if !input.is_empty() { + input.parse::()?; + } + } + Ok(attr) + } +} + +pub(crate) fn declare_derive(input: &mut syn::DeriveInput) -> syn::Result { + let syn::DeriveInput { vis, ident: name, generics, data, .. } = input; + + let stt = data_struct_unwrap(data, DECLARE)?; + + if stt.fields.is_empty() { + let tokens = quote! { + impl Declare2 for #name { + type Builder = #name; + fn declare2_builder() -> Self::Builder { #name } + } + + impl DeclareBuilder for #name { + type Target = #name; + fn build(self, _: &BuildCtx) -> Self::Target { self } + } + }; + Ok(tokens) + } else { + struct_with_fields_gen(stt, vis, generics, name) + } +} + +fn struct_with_fields_gen( + stt: &mut DataStruct, + vis: &syn::Visibility, + generics: &syn::Generics, + name: &syn::Ident, +) -> syn::Result { + let (g_impl, g_ty, g_where) = generics.split_for_impl(); + + let mut builder_fields = collect_filed_and_attrs(stt)?; + + // reverse name check. + builder_fields + .iter_mut() + .for_each(DeclareField::check_reserve); + + let declarer = Ident::new(&format!("{name}{DECLARER}"), name.span()); + + let mut builder_methods = quote! {}; + builder_fields + .iter() + .filter(|f| f.need_set_method()) + .for_each(|f| { + let field_name = f.field.ident.as_ref().unwrap(); + let ty = &f.field.ty; + let set_method = f.set_method_name(); + + builder_methods.extend(quote! { + #[inline] + #vis fn #set_method<_M, _V>(mut self, v: _V) -> Self + where DeclareInit<#ty>: DeclareFrom<_V, _M> + { + self.#field_name = Some(DeclareInit::declare_from(v)); + self + } + }); + }); + + // builder define + let def_fields = builder_fields.pairs().map(|p| { + let (f, c) = p.into_tuple(); + let mut f = f.field.clone(); + let ty = &f.ty; + f.ty = parse_quote!(Option>); + syn::punctuated::Pair::new(f, c) + }); + + // implement declare trait + + let fill_default = builder_fields.iter().filter_map(|f| { + let attr = f.attr.as_ref()?; + let field_name = f.member(); + + let set_default_value = match (&attr.default, &attr.skip) { + (Some(df), None) if df.value.is_some() => { + let v = df.value.as_ref(); + let method = f.set_method_name(); + Some(quote! { self = self.#method(#v); }) + } + (Some(df), Some(_)) if df.value.is_some() => { + let v = df.value.as_ref(); + Some(quote! { self.#field_name = Some(DeclareInit::declare_from(#v)); }) + } + (Some(_), _) | (_, Some(_)) => { + Some(quote! { self.#field_name = Some(DeclareInit::default()); }) + } + (None, None) => None, + }; + set_default_value.map(|set_default_value| { + quote! { + if self.#field_name.is_none() { + #set_default_value + } + } + }) + }); + + let unzip_fields = builder_fields.iter().map(|df| { + let field_name = df.field.ident.as_ref().unwrap(); + let method = df.set_method_name(); + quote_spanned! { field_name.span() => + let #field_name = self.#field_name.expect(&format!( + "Required field `{}::{}` not set, use method `{}` init it", + stringify!(#name), stringify!(#field_name), stringify!(#method) + )).unzip(); + } + }); + + let field_names = builder_fields.iter().map(|f| f.field.ident.as_ref()); + let field_names2 = field_names.clone(); + let field_names3 = field_names.clone(); + + let tokens = quote! { + #vis struct #declarer #g_impl #g_where { + #(#def_fields)* + } + + impl #g_impl Declare2 for #name #g_ty #g_where { + type Builder = #declarer #g_ty; + + fn declare2_builder() -> Self::Builder { + #declarer { #(#field_names : None ),*} + } + } + + impl #g_impl #declarer #g_ty #g_where { + #builder_methods + } + + impl #g_impl DeclareBuilder for #declarer #g_ty #g_where { + type Target = State<#name #g_ty>; + fn build(mut self, ctx: &BuildCtx) -> Self::Target { + set_build_ctx!(ctx); + + #(#fill_default)* + #(#unzip_fields)* + let mut _ribir = State::Stateless(#name { + #(#field_names2 : #field_names2.0),* + }); + #( + if let Some(u) = #field_names3.1 { + let mut _ribir2 = _ribir.clone_stateful(); + let h = u.subscribe(move |v| _ribir2.state_ref().#field_names3 = v) + .unsubscribe_when_dropped(); + _ribir.to_stateful().own_data(h); + } + );* + + _ribir + } + } + }; + + Ok(tokens) +} + +fn collect_filed_and_attrs(stt: &mut DataStruct) -> Result> { + let mut builder_fields = Punctuated::default(); + match &mut stt.fields { + Fields::Named(named) => { + named + .named + .pairs_mut() + .try_for_each::<_, syn::Result<()>>(|pair| { + let (field, comma) = pair.into_tuple(); + let idx = field + .attrs + .iter() + .position(|attr| attr.path.is_ident(DECLARE_ATTR)); + let builder_attr = if let Some(idx) = idx { + let attr = field.attrs.remove(idx); + let args: DeclareAttr = attr.parse_args()?; + Some(args) + } else { + None + }; + + builder_fields.push(DeclareField { attr: builder_attr, field }); + if let Some(c) = comma { + builder_fields.push_punct(*c); + } + + Ok(()) + })?; + } + Fields::Unit => <_>::default(), + Fields::Unnamed(unnamed) => { + let err = syn::Error::new( + unnamed.span(), + format!("`{DECLARE}` not be supported to derive for tuple struct"), + ); + return Err(err); + } + }; + Ok(builder_fields) +} + +impl<'a> DeclareField<'a> { + fn member(&self) -> &Ident { self.field.ident.as_ref().unwrap() } + + fn set_method_name(&self) -> &Ident { + self + .attr + .as_ref() + .and_then(|attr| attr.rename.as_ref()) + .or(self.field.ident.as_ref()) + .unwrap() + } + + fn need_set_method(&self) -> bool { + self + .attr + .as_ref() + .map_or(true, |attr| attr.custom.is_none() && attr.skip.is_none()) + } + + fn check_reserve(&mut self) { + // reverse name check. + let reserve_ident = &crate::widget_macro::RESERVE_IDENT; + + let not_builtin = self + .attr + .as_ref() + .map_or(true, |attr| attr.builtin.is_none()); + + if not_builtin { + let method_name = self.set_method_name(); + if let Some(r) = reserve_ident.get(method_name.to_string().as_str()) { + let msg = format!("the identify `{}` is reserved to {}", method_name, &r); + let mut field = self.field.clone(); + // not display the attrs in the help code. + + field.attrs.clear(); + Diagnostic::spanned(vec![method_name.span().unwrap()], Level::Error, msg) + .help(format! { + "use `rename` meta to avoid the name conflict in `widget!` macro.\n\n\ + #[declare(rename = xxx)] \n\ + {}", field.into_token_stream() + }) + .emit(); + } + } + } +} diff --git a/macros/src/declare_obj.rs b/macros/src/declare_obj.rs new file mode 100644 index 000000000..47cf5de54 --- /dev/null +++ b/macros/src/declare_obj.rs @@ -0,0 +1,186 @@ +use crate::{ + rdl_macro::{DeclareField, RdlParent, StructLiteral}, + widget_macro::{ribir_variable, WIDGETS, WIDGET_OF_BUILTIN_FIELD}, +}; +use inflector::Inflector; +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use quote::{quote_spanned, ToTokens}; +use smallvec::SmallVec; +use syn::{ + parse_str, + spanned::Spanned, + token::{Brace, Comma, Paren}, + Ident, Macro, Path, +}; + +pub struct DeclareObj<'a> { + this: ObjNode<'a>, + span: Span, + builtin: ahash::HashMap<&'static str, SmallVec<[&'a DeclareField; 1]>>, + children: &'a Vec, +} +enum ObjNode<'a> { + Obj { + span: Span, + ty: &'a Path, + fields: SmallVec<[&'a DeclareField; 1]>, + }, + Var(&'a Ident), +} + +impl<'a> DeclareObj<'a> { + pub fn from_literal(mac: &'a StructLiteral) -> Result { + let StructLiteral { parent, brace, fields, children } = mac; + let mut builtin: ahash::HashMap<_, SmallVec<[&DeclareField; 1]>> = <_>::default(); + let span = match parent { + RdlParent::Type(ty) => ty.span(), + RdlParent::Var(name) => name.span(), + }; + let span = span.join(brace.span).unwrap(); + + let this = match parent { + RdlParent::Type(ty) => { + let mut self_fields = SmallVec::default(); + for f in fields { + if let Some(ty) = WIDGET_OF_BUILTIN_FIELD + .get(f.member.to_string().as_str()) + .filter(|builtin_ty| !ty.is_ident(builtin_ty)) + { + builtin.entry(*ty).or_default().push(f); + } else { + self_fields.push(f) + } + } + ObjNode::Obj { ty, fields: self_fields, span } + } + RdlParent::Var(name) => { + for f in fields { + if let Some(ty) = WIDGET_OF_BUILTIN_FIELD.get(f.member.to_string().as_str()) { + builtin.entry(*ty).or_default().push(f); + } else { + return Err(quote_spanned! { f.span() => + compile_error!("Not allow to declare a field of a variable parent.") + }); + } + } + ObjNode::Var(name) + } + }; + + Ok(Self { this, span, builtin, children }) + } +} + +impl<'a> ToTokens for DeclareObj<'a> { + fn to_tokens(&self, tokens: &mut TokenStream) { + let Self { this, span, builtin, children } = self; + + // if children is empty, we declare a `FatObj`, so it's can be used and + // referenced by others, otherwise directly composed. + + if children.is_empty() && builtin.is_empty() { + quote_spanned! { *span => FatObj::new(#this) }.to_tokens(tokens) + } else { + Brace::default().surround(tokens, |tokens| { + let mut builtin_names = vec![]; + for (ty_str, fields) in builtin { + // 'b is live longer than 'a, safe convert, but we can't directly convert + // `SmallVec<[&'b DeclareField; 1]>` to `SmallVec<[&'a DeclareField; + // 1]>`, because `SmallVec` is invariant over `T`. + fn shorter_lifetime<'a, 'b: 'a>( + fields: SmallVec<[&'b DeclareField; 1]>, + ) -> SmallVec<[&'a DeclareField; 1]> { + unsafe { std::mem::transmute(fields.clone()) } + } + let fields = shorter_lifetime(fields.clone()); + + let builtin_span = fields[0].span(); + let ty = parse_str::(ty_str).unwrap(); + let obj = ObjNode::Obj { ty: &ty, span: builtin_span, fields }; + + let snaked_ty_str = ty_str.to_snake_case(); + let name = Ident::new(&snaked_ty_str, builtin_span); + quote_spanned! { builtin_span => let #name = #obj; }.to_tokens(tokens); + builtin_names.push(name); + } + + 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) + } + + if children.is_empty() { + let builtin_init = builtin_names + .iter() + .map(|name| Ident::new(&format!("with_{name}"), name.span())); + + quote_spanned! { + *span =>FatObj::new(#this)#(.#builtin_init(#builtin_names))* + } + .to_tokens(tokens); + } else { + // todo: tmp code, we should use FatObj to compose builtin widgets in every + // where, so we can keep the builtin widget compose order consistent. + builtin_names.sort_by_key(|name| { + WIDGETS + .iter() + .position(|b| *name == b.ty.to_snake_case()) + .unwrap() + }); + if !builtin.is_empty() { + let first = &builtin_names[0]; + let rest_builtin = &builtin_names[1..]; + + recursive_compose_with(first, rest_builtin.iter(), tokens, |tokens| { + quote_spanned! { *span => + #this #(.with_child(#children_names, ctx!()))* + } + .to_tokens(tokens) + }); + } else { + quote_spanned! { *span => + #this #(.with_child(#children_names, ctx!()))* + } + .to_tokens(tokens) + } + } + }); + } + } +} + +fn recursive_compose_with( + p: impl ToTokens, + mut child_chain: impl Iterator, + tokens: &mut TokenStream, + leaf: impl FnOnce(&mut TokenStream), +) { + p.to_tokens(tokens); + quote_spanned! { p.span() => .with_child}.to_tokens(tokens); + Paren(p.span()).surround(tokens, |tokens| { + let child = child_chain.next(); + if let Some(c) = child { + recursive_compose_with(c, child_chain, tokens, leaf) + } else { + leaf(tokens) + } + Comma::default().to_tokens(tokens); + quote! { ctx!() }.to_tokens(tokens); + }); +} + +impl<'a> ToTokens for ObjNode<'a> { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Self::Obj { ty, span, fields } => { + quote_spanned! { *span => #ty::declare2_builder() }.to_tokens(tokens); + fields.iter().for_each(|f| f.to_tokens(tokens)); + tokens.extend(quote_spanned! { *span => .build(ctx!()) }); + } + Self::Var(var) => var.to_tokens(tokens), + } + } +} diff --git a/macros/src/fn_widget_macro.rs b/macros/src/fn_widget_macro.rs new file mode 100644 index 000000000..20159c9d5 --- /dev/null +++ b/macros/src/fn_widget_macro.rs @@ -0,0 +1,35 @@ +use quote::{quote, ToTokens}; +use syn::{ + fold::Fold, + parse::{Parse, ParseStream}, + Stmt, +}; + +use crate::symbol_process::DollarRefs; + +pub struct FnWidgetMacro { + stmts: Vec, +} + +impl Parse for FnWidgetMacro { + fn parse(input: ParseStream) -> syn::Result { + let stmts = syn::Block::parse_within(input)?; + let mut refs = DollarRefs::default(); + let stmts = stmts.into_iter().map(|s| refs.fold_stmt(s)).collect(); + Ok(Self { stmts }) + } +} + +impl ToTokens for FnWidgetMacro { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let Self { stmts } = self; + quote! { + FnWidget::new(move |ctx: &BuildCtx| { + set_build_ctx!(ctx); + #[allow(unused_mut)] + { #(#stmts)* } + }) + } + .to_tokens(tokens) + } +} diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 11da7f222..f822fc1d9 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -1,18 +1,29 @@ #![feature(proc_macro_diagnostic, proc_macro_span)] extern crate proc_macro; -extern crate proc_macro2; mod declare_derive; +mod declare_derive2; mod error; -mod widget_macro; - mod lerp_derive; mod util; +mod widget_macro; +use fn_widget_macro::FnWidgetMacro; use proc_macro::TokenStream; -use quote::quote; +use quote::{quote, ToTokens}; +use symbol_process::symbol_to_macro; use syn::{parse_macro_input, DeriveInput}; use widget_macro::gen_widget_macro; mod child_template; +mod fn_widget_macro; +mod pipe_macro; +mod rdl_macro; +mod watch_macro; +pub(crate) use rdl_macro::*; + +use crate::pipe_macro::PipeExpr; +use crate::watch_macro::WatchMacro; +pub(crate) mod declare_obj; +pub(crate) mod symbol_process; pub(crate) const WIDGET_MACRO_NAME: &str = "widget"; pub(crate) const MOVE_TO_WIDGET_MACRO_NAME: &str = "move_to_widget"; @@ -76,6 +87,14 @@ pub fn declare_trait_macro_derive(input: TokenStream) -> TokenStream { .into() } +#[proc_macro_derive(Declare2, attributes(declare))] +pub fn declare_trait_macro_derive2(input: TokenStream) -> TokenStream { + let mut input = parse_macro_input!(input as DeriveInput); + declare_derive2::declare_derive(&mut input) + .unwrap_or_else(|e| e.into_compile_error()) + .into() +} + #[proc_macro_derive(Template, attributes(template))] pub fn child_template_trait_derive(input: TokenStream) -> TokenStream { let mut input = parse_macro_input!(input as DeriveInput); @@ -87,6 +106,109 @@ pub fn child_template_trait_derive(input: TokenStream) -> TokenStream { #[proc_macro] pub fn widget(input: TokenStream) -> TokenStream { gen_widget_macro(input, None) } +/// The macro use to declare a object, this macro will use `ctx!()` to access +/// the `BuildCtx`, so it can only use in the `fn_widget!` macro, or any scope +/// that called `set_build_ctx!` macro. +/// +/// # The Syntax +/// +/// `rdl` accept 3 kind of syntax: +/// +/// - 1. use struct literal syntax to declare a object tree, like `rdl!{ Row { +/// wrap: true } }`, if the `Row` contain any child, its child can be embed in +/// the struct literal, but must be use `rdl!` or `@` to declare, like: +/// +/// ```ignore +/// rdl!{ Row { wrap: true, child: rdl!{ Text { text: "hello" } } } } +/// ``` +/// - 2. similar to the first, but use a variable as parent and not accept any +/// fields of the parent(the builtin fields allowed), like: +/// +/// ```ignore +/// let row = rdl!{ Row { wrap: true } }; +/// rdl!{ $row { rdl!{ Text { text: "hello" } } } } +/// ``` +/// - 3. use expression to declare a object and not allow declare children, +/// like: `let row = rdl!{ Widget::new(Void) };` +#[proc_macro] +pub fn rdl(input: TokenStream) -> TokenStream { + symbol_to_macro(input).map_or_else( + |err| err, + |input| { + let declare = parse_macro_input! { input as RdlBody }; + declare.to_token_stream().into() + }, + ) +} + +/// The `fn_widget` is a macro that create a widget from a function widget from +/// a expression. Its syntax is extended from rust syntax, you can use `@` and +/// `$` in the expression, the `@` is a short hand of `rdl` macro, and `$name` +/// use to expression a state reference of `name`. +#[proc_macro] +pub fn fn_widget(input: TokenStream) -> TokenStream { + symbol_to_macro(input).map_or_else( + |err| err, + |input| { + let widget_macro = parse_macro_input!(input as FnWidgetMacro); + widget_macro.to_token_stream().into() + }, + ) +} + +/// set the `BuildCtx` to a special variable `_ctx_ಠ_ಠ`, so the user can use +/// `ctx!` to access it. +#[proc_macro] +pub fn set_build_ctx(input: TokenStream) -> TokenStream { + let input: proc_macro2::TokenStream = input.into(); + quote! { let _ctx_ಠ_ಠ = #input; }.into() +} + +/// get the `BuildCtx` set by `set_build_ctx!` macro, if no `BuildCtx` set. +#[proc_macro] +pub fn ctx(input: TokenStream) -> TokenStream { + let tokens = if !input.is_empty() { + quote!(compile_error!("ctx! macro does not accept any argument")) + } else { + quote! { _ctx_ಠ_ಠ } + }; + tokens.into() +} + +/// `pipe` macro use to create `Pipe` object that continuous trace the +/// expression modify. Use the `$` mark the state reference and auto subscribe +/// to its modify. +#[proc_macro] +pub fn pipe(input: TokenStream) -> TokenStream { + symbol_to_macro(input).map_or_else( + |err| err, + |input| { + let expr = parse_macro_input! { input as PipeExpr }; + expr.to_token_stream().into() + }, + ) +} + +/// `watch!` macro use to convert a expression to a `Observable` stream. Use the +/// `$` mark the state reference and auto subscribe to its modify. +#[proc_macro] +pub fn watch(input: TokenStream) -> TokenStream { + symbol_to_macro(input).map_or_else( + |err| err, + |input| { + let expr = parse_macro_input! { input as WatchMacro }; + expr.to_token_stream().into() + }, + ) +} + +/// The macro to use a state as its StateRef. Transplanted from the `$`. +#[proc_macro] +pub fn _dollar_ಠ_ಠ(input: TokenStream) -> TokenStream { + let name = parse_macro_input! { input as syn::Ident }; + quote! { #name.state_ref() }.into() +} + #[proc_macro] pub fn include_svg(input: TokenStream) -> TokenStream { let w = parse_macro_input! { input as syn::LitStr }; diff --git a/macros/src/pipe_macro.rs b/macros/src/pipe_macro.rs new file mode 100644 index 000000000..b4fba7708 --- /dev/null +++ b/macros/src/pipe_macro.rs @@ -0,0 +1,73 @@ +use crate::symbol_process::DollarRefs; +use quote::{quote, ToTokens}; +use syn::{ + fold::Fold, + parse::{Parse, ParseStream}, + Stmt, +}; + +pub(crate) struct PipeExpr { + refs: DollarRefs, + expr: Vec, +} + +impl Parse for PipeExpr { + fn parse(input: ParseStream) -> syn::Result { + let (refs, stmts) = fold_expr_as_in_closure(input)?; + Ok(Self { refs, expr: stmts }) + } +} + +pub fn fold_expr_as_in_closure(input: ParseStream) -> syn::Result<(DollarRefs, Vec)> { + let mut refs = DollarRefs::default(); + refs.in_capture += 1; + let stmts = syn::Block::parse_within(input)?; + let stmts = stmts.into_iter().map(|s| refs.fold_stmt(s)).collect(); + refs.in_capture -= 1; + if refs.is_empty() { + let err = syn::Error::new( + input.span(), + "expression not subscribe anything, it must contain at least one $", + ); + Err(err) + } else { + refs.dedup(); + Ok((refs, stmts)) + } +} + +impl ToTokens for PipeExpr { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let Self { refs, expr } = self; + + let upstream = refs.upstream_tokens(); + + if refs.used_ctx() { + quote! {{ + #refs + let upstream = #upstream; + let 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)) + .box_it() + ) + }} + .to_tokens(tokens) + } else { + quote! {{ + #refs + let upstream = #upstream; + let mut expr_value = move || { #(#expr)* }; + Pipe::new( + expr_value(), + upstream.map(move |_| expr_value()).box_it() + ) + }} + .to_tokens(tokens) + } + } +} diff --git a/macros/src/rdl_macro.rs b/macros/src/rdl_macro.rs new file mode 100644 index 000000000..9a7140796 --- /dev/null +++ b/macros/src/rdl_macro.rs @@ -0,0 +1,181 @@ +use proc_macro2::{Span, TokenStream}; +use quote::{quote_spanned, ToTokens}; +use std::collections::HashSet; +use syn::{ + braced, + fold::Fold, + parse::{Parse, ParseBuffer, ParseStream}, + parse_quote, + punctuated::Punctuated, + spanned::Spanned, + token::{At, Bang, Brace, Colon, Comma, Dollar}, + Expr, Ident, Macro, Path, Result as SynResult, Stmt, +}; + +use crate::{ + declare_obj::DeclareObj, + symbol_process::{kw, DollarRefs}, +}; + +pub enum RdlBody { + Literal(StructLiteral), + /// Declare an expression as a object, like `rdl! { Widget::new(...) }` + ExprObj { + span: Span, + stmts: Vec, + }, +} + +/// Declare a object use struct literal, like `rdl! { Row { ... } }` or +/// `@parent { ... }` +pub struct StructLiteral { + pub parent: RdlParent, + pub brace: Brace, + pub fields: Punctuated, + /// Declare a child in `rdl!` can use `rdl!` macro or `@` symbol. + /// `rdl! { Row { rdl! { SizedBox {...} } } }` + /// or + /// `rdl! { Row { @ SizedBox{ ... } } }` + /// but will be all processed as `rdl! { ... }` + pub children: Vec, +} + +pub enum RdlParent { + /// Declare parent use a type `Row { ... }` + Type(Path), + /// Declare parent use a variable prefixed with ` @parent { ... }` + Var(Ident), +} + +/// Declare a field of a widget. +pub struct DeclareField { + /// field member name. + pub member: Ident, + pub colon_tk: Option, + pub value: Expr, +} + +impl Parse for RdlBody { + fn parse(input: ParseStream) -> SynResult { + let fork = input.fork(); + if fork.parse::().is_ok() && fork.peek(Brace) { + Ok(RdlBody::Literal(input.parse()?)) + } else { + let span = input.span(); + let stmts = syn::Block::parse_within(input)?; + let mut refs = DollarRefs::default(); + let stmts = stmts.into_iter().map(|s| refs.fold_stmt(s)).collect(); + Ok(RdlBody::ExprObj { span, stmts }) + } + } +} + +impl Parse for StructLiteral { + fn parse(input: ParseStream) -> SynResult { + let parent = input.parse()?; + let content; + let brace = braced!(content in input); + let mut children = vec![]; + let mut fields = Punctuated::default(); + loop { + if content.is_empty() { + break; + } + + if content.peek(At) || content.peek(kw::rdl) && content.peek2(Bang) { + children.push(content.parse()?); + } else if content.peek(Ident) { + let f: DeclareField = content.parse()?; + if !children.is_empty() { + let err_msg = "Field should always declare before children."; + return Err(syn::Error::new(f.span(), err_msg)); + } + fields.push(f); + if !content.is_empty() { + fields.push_punct(content.parse()?); + } + } else { + return Err(syn::Error::new( + content.span(), + "expected a field or a child.", + )); + } + } + + check_duplicate_field(&fields)?; + Ok(StructLiteral { parent, brace, fields, children }) + } +} + +impl Parse for RdlParent { + fn parse(input: ParseStream) -> SynResult { + if input.peek(kw::_dollar_ಠ_ಠ) && input.peek2(Bang) { + let mac: Macro = input.parse()?; + + Ok(RdlParent::Var(mac.parse_body_with( + |input: &ParseBuffer| { + input.parse::()?; + input.parse() + }, + )?)) + } else { + Ok(RdlParent::Type(input.parse()?)) + } + } +} + +impl Parse for DeclareField { + fn parse(input: ParseStream) -> SynResult { + let member: Ident = input.parse()?; + let colon_tk: Option<_> = input.parse()?; + let value = if colon_tk.is_none() { + parse_quote!(#member) + } else { + let mut refs = DollarRefs::default(); + refs.fold_expr(input.parse()?) + }; + + Ok(DeclareField { member, colon_tk, value }) + } +} + +impl ToTokens for RdlBody { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + RdlBody::Literal(l) => match DeclareObj::from_literal(l) { + Ok(declare) => declare.to_tokens(tokens), + Err(err) => err.to_tokens(tokens), + }, + RdlBody::ExprObj { span, stmts } => { + if stmts.len() > 1 { + Brace(*span).surround(tokens, |tokens| { + stmts.iter().for_each(|s| s.to_tokens(tokens)); + }) + } else { + stmts.iter().for_each(|s| s.to_tokens(tokens)); + } + } + } + } +} + +impl ToTokens for DeclareField { + fn to_tokens(&self, tokens: &mut TokenStream) { + let DeclareField { member, value, .. } = self; + quote_spanned! {value.span()=> .#member(#value)}.to_tokens(tokens); + } +} + +/// Check if a field is declared more than once. +fn check_duplicate_field(fields: &Punctuated) -> syn::Result<()> { + let mut sets = HashSet::<&Ident, ahash::RandomState>::default(); + for f in fields { + if !sets.insert(&f.member) { + return Err(syn::Error::new( + f.member.span(), + format!("`{}` declare more than once", f.member).as_str(), + )); + } + } + Ok(()) +} diff --git a/macros/src/symbol_process.rs b/macros/src/symbol_process.rs new file mode 100644 index 000000000..51519bb9e --- /dev/null +++ b/macros/src/symbol_process.rs @@ -0,0 +1,334 @@ +use crate::widget_macro::{ + ribir_suffix_variable, WIDGET_OF_BUILTIN_FIELD, WIDGET_OF_BUILTIN_METHOD, +}; +use inflector::Inflector; +use proc_macro2::{Ident, TokenStream}; +use quote::{quote, quote_spanned, ToTokens}; +use smallvec::SmallVec; +use syn::{ + fold::Fold, + parse::{Parse, ParseStream}, + parse_quote, + spanned::Spanned, + token::Dollar, + Expr, ExprField, ExprMethodCall, Macro, Member, +}; + +pub const KW_DOLLAR_STR: &str = "_dollar_ಠ_ಠ"; +pub const KW_CTX: &str = "ctx"; +pub const KW_RDL: &str = "rdl"; +pub use tokens_pre_process::*; + +pub mod kw { + syn::custom_keyword!(_dollar_ಠ_ಠ); + syn::custom_keyword!(rdl); +} + +#[derive(Hash, PartialEq, Eq, Debug, Clone)] +pub struct DollarRef { + pub name: Ident, + pub builtin_shadow: Option, +} +#[derive(Default)] +pub struct DollarRefs { + refs: SmallVec<[DollarRef; 1]>, + ctx_used: bool, + pub in_capture: usize, +} + +mod tokens_pre_process { + + use proc_macro::{TokenTree, *}; + use quote::quote_spanned; + + use super::KW_DOLLAR_STR; + use crate::symbol_process::KW_RDL; + + fn rdl_syntax_err(span: Span) -> Result { + let err_token = quote_spanned! { span.into() => + compile_error!("Syntax Error: use `@` to declare object, must be: \n \ + 1. `@ XXX { ... }`, declare a new `XXX` type object;\n \ + 2. `@ $parent { ... }`, declare a variable as parent;\n \ + 3. `@ { ... } `, declare an object by an expression.") + }; + Err(err_token.into()) + } + + fn dollar_err(span: Span) -> Result { + let err_token = quote_spanned! { span.into() => + compile_error!("Syntax error: expected an identifier after `$`") + }; + Err(err_token.into()) + } + + /// Convert `@` and `$` symbol to a `rdl!` or `_dollar_ಠ_ಠ!` macro, make it + /// conform to Rust syntax + pub fn symbol_to_macro(input: TokenStream) -> Result { + let mut iter = input.into_iter(); + let mut tokens = vec![]; + + loop { + match iter.next() { + Some(TokenTree::Punct(at)) + // maybe rust identify bind syntax, `identify @` + if at.as_char() == '@' && !matches!(tokens.last(), Some(TokenTree::Ident(_))) => + { + tokens.push(TokenTree::Ident(Ident::new(KW_RDL, at.span()))); + tokens.push(TokenTree::Punct(Punct::new('!', Spacing::Alone))); + + let body = match iter.next() { + // declare a new widget: `@ SizedBox { ... }` + Some(TokenTree::Ident(name)) => { + let Some(TokenTree::Group(body)) = iter.next() else { + return rdl_syntax_err(at.span().join(name.span()).unwrap()) + }; + let tokens = TokenStream::from_iter([TokenTree::Ident(name), TokenTree::Group(body)]); + Group::new(Delimiter::Brace, tokens) + } + // declare a variable widget as parent, `@ $var { ... }` + Some(TokenTree::Punct(dollar)) if dollar.as_char() == '$' => { + if let Some(TokenTree::Ident(var)) = iter.next() { + let Some(TokenTree::Group(body)) = iter.next() else { + let span = at.span().join(dollar.span()).unwrap().join(var.span()).unwrap(); + return rdl_syntax_err(span) + }; + let tokens = TokenStream::from_iter([ + TokenTree::Punct(dollar), + TokenTree::Ident(var), + TokenTree::Group(body), + ]); + Group::new(Delimiter::Brace, tokens) + } else { + return dollar_err(dollar.span()); + } + } + // declare a expression widget `@ { ... }` + Some(TokenTree::Group(g)) => g, + n => { + let mut span = at.span(); + if let Some(n) = n { + span = span.join(n.span()).unwrap() + } + return rdl_syntax_err(span); + } + }; + tokens.push(TokenTree::Group(body)); + } + Some(TokenTree::Punct(p)) if p.as_char() == '$' => { + match iter.next() { + Some(TokenTree::Ident(name)) => { + tokens.push(TokenTree::Ident(Ident::new(KW_DOLLAR_STR, p.span()))); + tokens.push(TokenTree::Punct(Punct::new('!', Spacing::Alone))); + let span = name.span(); + let mut g = Group::new( + Delimiter::Parenthesis, + [TokenTree::Punct(p), TokenTree::Ident(name)].into_iter().collect() + ); + g.set_span(span); + tokens.push(TokenTree::Group(g)); + } + Some(t) => return dollar_err(t.span()), + None => return dollar_err(p.span()), + }; + } + + Some(TokenTree::Group(mut g)) => { + // not process symbol in macro, because it's maybe as part of the macro syntax. + if !in_macro(&tokens) { + let mut n = Group::new(g.delimiter(), symbol_to_macro(g.stream())?); + n.set_span(g.span()); + g = n; + } + + tokens.push(TokenTree::Group(g)); + } + Some(t) => tokens.push(t), + None => break, + }; + } + Ok(tokens.into_iter().collect()) + } + + fn in_macro(tokens: &[TokenTree]) -> bool { + let [.., TokenTree::Ident(_), TokenTree::Punct(p)] = tokens else { + return false; + }; + p.as_char() == '!' + } +} + +impl Fold for 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()); + } + } + + syn::fold::fold_expr_field(self, i) + } + + 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()); + } + + syn::fold::fold_expr_method_call(self, i) + } + + fn fold_macro(&mut self, mut mac: Macro) -> Macro { + if let Some(DollarMacro { name, .. }) = parse_clean_dollar_macro(&mac) { + mac.tokens = name.to_token_stream(); + self.refs.push(DollarRef { name, builtin_shadow: None }); + mac + } else { + self.ctx_used = mac.path.is_ident(KW_RDL) || mac.path.is_ident(KW_CTX); + syn::fold::fold_macro(self, mac) + } + } + + fn fold_expr(&mut self, i: Expr) -> Expr { + match i { + Expr::Closure(c) if c.capture.is_some() => { + let mut closure_refs = DollarRefs::default(); + closure_refs.in_capture += 1; + let mut c = closure_refs.fold_expr_closure(c); + closure_refs.in_capture -= 1; + + if !closure_refs.is_empty() || closure_refs.ctx_used { + closure_refs.dedup(); + + let body = &mut *c.body; + if closure_refs.ctx_used { + *body = Expr::Verbatim(quote_spanned!(body.span() => + _ctx_handle + .with_ctx(|ctx!(): &'_ BuildCtx<'_>| #body ) + .expect("The `BuildCtx` is not available.") + )); + } + + let handle = closure_refs + .ctx_used + .then(|| quote_spanned! { c.span() => let _ctx_handle = ctx!().handle(); }); + + // todo: seems need merge `closure_refs` into `self`. + + Expr::Verbatim(quote_spanned!(c.span() => { + #closure_refs + #handle + #c + })) + } else { + Expr::Closure(c) + } + } + _ => syn::fold::fold_expr(self, i), + } + } +} + +impl ToTokens for DollarRef { + fn to_tokens(&self, tokens: &mut TokenStream) { + let DollarRef { name, builtin_shadow: shadow } = self; + if let Some(shadow) = shadow { + quote_spanned! { shadow.span() => let mut #name = #shadow.clone();}.to_tokens(tokens); + } else { + quote_spanned! { shadow.span() => let mut #name = #name.clone();}.to_tokens(tokens); + } + } +} +impl ToTokens for DollarRefs { + fn to_tokens(&self, tokens: &mut TokenStream) { + for dollar_ref in &self.refs { + dollar_ref.to_tokens(tokens); + } + } +} + +impl DollarRefs { + pub fn used_ctx(&self) -> bool { self.ctx_used } + + pub fn dedup(&mut self) { + self.refs.sort_by_key(|r: &_| r.name.to_string()); + self.refs.dedup() + } + + pub fn upstream_tokens(&self) -> TokenStream { + match self.len() { + 0 => quote! {}, + 1 => { + let DollarRef { name, builtin_shadow: value } = &self.refs[0]; + quote_spanned! { value.span() => #name.modifies() } + } + _ => { + let upstream = self.iter().map(|DollarRef { name, .. }| { + quote! { #name.modifies() } + }); + quote! { observable::from_iter([#(#upstream),*]).merge_all(usize::MAX) } + } + } + } + + 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 Expr::Macro(m) = e else { return None }; + let DollarMacro { name: host, .. } = parse_clean_dollar_macro(&m.mac)?; + let builtin_name = ribir_suffix_variable(&host, builtin_member); + let builtin_member = Ident::new(builtin_member, host.span()); + + // When a builtin widget captured by a `move |_| {...}` closure, we need split + // the builtin widget from the `FatObj` so we only capture the used builtin + // part. + m.mac.tokens = if self.in_capture > 0 { + builtin_name.to_token_stream() + } else { + quote_spanned! { host.span() => #host.#builtin_member(ctx!()) } + }; + self.refs.push(DollarRef { + name: builtin_name, + builtin_shadow: Some(parse_quote! { #host.#builtin_member(ctx!()) }), + }); + self.last() + } +} + +fn parse_clean_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() + } else { + None + } +} + +impl std::ops::Deref for DollarRefs { + type Target = [DollarRef]; + fn deref(&self) -> &Self::Target { &self.refs } +} + +struct DollarMacro { + _dollar: Dollar, + name: Ident, +} + +impl Parse for DollarMacro { + fn parse(input: ParseStream) -> syn::Result { + Ok(Self { + _dollar: input.parse()?, + name: input.parse()?, + }) + } +} diff --git a/macros/src/watch_macro.rs b/macros/src/watch_macro.rs new file mode 100644 index 000000000..2c4e27b9c --- /dev/null +++ b/macros/src/watch_macro.rs @@ -0,0 +1,42 @@ +use crate::{pipe_macro::fold_expr_as_in_closure, symbol_process::DollarRefs}; +use quote::{quote, ToTokens}; +use syn::{ + parse::{Parse, ParseStream}, + Stmt, +}; + +pub(crate) struct WatchMacro { + refs: DollarRefs, + expr: Vec, +} + +impl Parse for WatchMacro { + fn parse(input: ParseStream) -> syn::Result { + let (refs, stmts) = fold_expr_as_in_closure(input)?; + Ok(Self { refs, expr: stmts }) + } +} + +impl ToTokens for WatchMacro { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let Self { refs, expr } = self; + + let upstream = refs.upstream_tokens(); + + if refs.used_ctx() { + quote! {{ + #refs + let _ctx_handle = ctx!().handle(); + #upstream + .map(move |_| _ctx_handle.with_ctx(|ctx!(): &BuildCtx<'_>| { #(#expr)* })) + }} + .to_tokens(tokens) + } else { + quote! {{ + #refs + #upstream.map(move |_| { #(#expr)* }) + }} + .to_tokens(tokens) + } + } +} diff --git a/macros/src/widget_macro/code_gen.rs b/macros/src/widget_macro/code_gen.rs index 913a1fff4..78c563765 100644 --- a/macros/src/widget_macro/code_gen.rs +++ b/macros/src/widget_macro/code_gen.rs @@ -86,7 +86,7 @@ pub(crate) fn gen_assign_watch(input: TokenStream, ctx: &mut VisitCtx) -> proc_m watch_expr.used_name_info.state_refs_tokens(tokens); watch_expr.to_tokens(tokens); }); - quote_spanned!(watch_expr.span() => AssignObservable::new(#value, #tokens)).into() + quote_spanned!(watch_expr.span() => Pipe::new(#value, #tokens.box_it())).into() } pub(crate) fn gen_prop_macro( diff --git a/macros/src/widget_macro/visit_mut.rs b/macros/src/widget_macro/visit_mut.rs index 10e8257ea..062031e85 100644 --- a/macros/src/widget_macro/visit_mut.rs +++ b/macros/src/widget_macro/visit_mut.rs @@ -440,7 +440,7 @@ impl VisitCtx { let field_subject = ribir_suffix_variable(&f.member, "subject"); let field_value = ribir_suffix_variable(&f.member, "init"); let pre_def = quote_spanned! { expr.span() => - let (#field_value, #field_subject) = AssignObservable::unzip(#expr); + let (#field_value, #field_subject) = Pipe::unzip(#expr); }; expr.expr = parse_quote_spanned! { expr.span() => #field_value }; let guards = guard_vec_ident(); diff --git a/ribir/src/app.rs b/ribir/src/app.rs index 78e5e75fe..8cd99193a 100644 --- a/ribir/src/app.rs +++ b/ribir/src/app.rs @@ -53,6 +53,8 @@ impl App { let wnd = Window::new(root, Box::new(shell_wnd)); let id = wnd.id(); AppCtx::windows().borrow_mut().insert(id, wnd); + AppCtx::get_window(id).unwrap().emit_events(); + if app.active_wnd.is_none() { app.active_wnd = Some(id); } diff --git a/tests/Cargo.toml b/tests/Cargo.toml index b3079431c..9a3d45208 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -16,9 +16,8 @@ version.workspace = true [dev-dependencies] paste.workspace = true ribir = {path = "../ribir", features = ["material", "widgets"]} -ribir_geom = {path = "../geom"} - ribir_dev_helper = {path = "../dev-helper"} +ribir_geom = {path = "../geom"} trybuild.workspace = true winit.workspace = true @@ -27,8 +26,8 @@ name = "compile_message" path = "compile_message.rs" [[test]] -name = "code_gen_logic" -path = "code_gen_test.rs" +name = "rdl_macro_test" +path = "rdl_macro_test.rs" [[test]] name = "animations_syntax" diff --git a/tests/compile_msg/declare/ctx_only_allow_in_init_and_finally_fail.stderr b/tests/compile_msg/declare/ctx_only_allow_in_init_and_finally_fail.stderr index 9285dcbf2..3b17b95cc 100644 --- a/tests/compile_msg/declare/ctx_only_allow_in_init_and_finally_fail.stderr +++ b/tests/compile_msg/declare/ctx_only_allow_in_init_and_finally_fail.stderr @@ -1,5 +1,5 @@ -error[E0425]: cannot find value `ctx` in this scope +error[E0423]: expected value, found macro `ctx` --> compile_msg/declare/ctx_only_allow_in_init_and_finally_fail.rs:31:48 | 31 | size: ZERO_SIZE, background: Palette::of(ctx).primary() - | ^^^ not found in this scope + | ^^^ not a value diff --git a/tests/compile_msg/declare/dollar_position_fail.rs b/tests/compile_msg/declare/dollar_position_fail.rs new file mode 100644 index 000000000..de662418b --- /dev/null +++ b/tests/compile_msg/declare/dollar_position_fail.rs @@ -0,0 +1,11 @@ +use ribir::prelude::*; + +fn main() { + let not_identify_after_dollar = fn_widget! { + rdl! { Row { x: $1 } } + }; + + let field_name_not_support_dollar = fn_widget! { + rdl! { Row { $x: 1} } + }; +} diff --git a/tests/compile_msg/declare/dollar_position_fail.stderr b/tests/compile_msg/declare/dollar_position_fail.stderr new file mode 100644 index 000000000..25ff4b875 --- /dev/null +++ b/tests/compile_msg/declare/dollar_position_fail.stderr @@ -0,0 +1,13 @@ +error: Syntax error: expected an identifier after `$` + --> compile_msg/declare/dollar_position_fail.rs:5:22 + | +5 | rdl! { Row { x: $1 } } + | ^ + +error: expected `,` + --> compile_msg/declare/dollar_position_fail.rs:9:5 + | +9 | rdl! { Row { $x: 1} } + | ^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `rdl` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/compile_msg/declare/duplicate_id_fail.rs b/tests/compile_msg/declare/duplicate_id_fail.rs deleted file mode 100644 index 89402c0a4..000000000 --- a/tests/compile_msg/declare/duplicate_id_fail.rs +++ /dev/null @@ -1,66 +0,0 @@ -use ribir::prelude::*; - -fn main() { - let _id_must_be_unique_err = widget! { - BoxDecoration { - id: same_id, - background: Some(Color::RED.into()), - SizedBox { - id: same_id, - size: Size::zero(), - } - } - }; - - let _id_conflict_with_states_err = widget! { - states { same_id: Stateful::new(0) } - BoxDecoration { - id: same_id, - background: Some(Color::RED.into()), - } - }; - - let _inner_id_conflict_outside_err = widget! { - BoxDecoration { - id: same_id, - background: Some(Color::RED.into()), - DynWidget { - dyns: widget!{ - SizedBox { id: same_id, size: Size::zero() } - } - } - } - }; - - let _inner_id_conflict_outside_states_err = widget! { - states { same_id: Stateful::new(0),} - DynWidget { - dyns: widget!{ - SizedBox { id: same_id, size: Size::zero(),} - } - } - }; - - let _inner_states_id_conflict_outside_states_err = widget! { - states { same_id: Stateful::new(0),} - DynWidget { - dyns: widget!{ - states { same_id: Stateful::new(0),} - SizedBox { size: Size::zero(),} - } - } - }; - - let _inner_states_id_conflict_outside_id_err = widget! { - BoxDecoration { - id: same_id, - background: Some(Color::RED.into()), - DynWidget { - dyns: widget!{ - states { same_id: Stateful::new(0),} - SizedBox { size: Size::zero(),} - } - } - } - }; -} diff --git a/tests/compile_msg/declare/duplicate_id_fail.stderr b/tests/compile_msg/declare/duplicate_id_fail.stderr deleted file mode 100644 index b3f865d0c..000000000 --- a/tests/compile_msg/declare/duplicate_id_fail.stderr +++ /dev/null @@ -1,64 +0,0 @@ -error: Same `id: same_id` assign to multiple objects, id must be unique. - --> compile_msg/declare/duplicate_id_fail.rs:6:11 - | -6 | id: same_id, - | ^^^^^^^ -... -9 | id: same_id, - | ^^^^^^^ - -error: Same `id: same_id` assign to multiple objects, id must be unique. - --> compile_msg/declare/duplicate_id_fail.rs:16:14 - | -16 | states { same_id: Stateful::new(0) } - | ^^^^^^^ -17 | BoxDecoration { -18 | id: same_id, - | ^^^^^^^ - -error: Same `id: same_id` assign to multiple objects, id must be unique. - --> compile_msg/declare/duplicate_id_fail.rs:25:11 - | -25 | id: same_id, - | ^^^^^^^ -... -29 | SizedBox { id: same_id, size: Size::zero() } - | ^^^^^^^ - -error: Same `id: same_id` assign to multiple objects, id must be unique. - --> compile_msg/declare/duplicate_id_fail.rs:36:14 - | -36 | states { same_id: Stateful::new(0),} - | ^^^^^^^ -... -39 | SizedBox { id: same_id, size: Size::zero(),} - | ^^^^^^^ - -error: Same `id: same_id` assign to multiple objects, id must be unique. - --> compile_msg/declare/duplicate_id_fail.rs:45:14 - | -45 | states { same_id: Stateful::new(0),} - | ^^^^^^^ -... -48 | states { same_id: Stateful::new(0),} - | ^^^^^^^ - -error: Same `id: same_id` assign to multiple objects, id must be unique. - --> compile_msg/declare/duplicate_id_fail.rs:56:11 - | -56 | id: same_id, - | ^^^^^^^ -... -60 | states { same_id: Stateful::new(0),} - | ^^^^^^^ - -error[E0282]: type annotations needed - --> compile_msg/declare/duplicate_id_fail.rs:4:7 - | -4 | let _id_must_be_unique_err = widget! { - | ^^^^^^^^^^^^^^^^^^^^^^ - | -help: consider giving `_id_must_be_unique_err` an explicit type - | -4 | let _id_must_be_unique_err: /* Type */ = widget! { - | ++++++++++++ diff --git a/tests/declare_builder_test.rs b/tests/declare_builder_test.rs index 76c42357f..2621f8cd9 100644 --- a/tests/declare_builder_test.rs +++ b/tests/declare_builder_test.rs @@ -36,26 +36,26 @@ fn panic_if_miss_require_field() { #[test] -fn empty_default_field() { +fn default_field() { #[derive(Declare)] - struct T { + struct DefaultDeclare { #[declare(default)] a: f32, } - let t = ::declare_builder().build(dummy_ctx()); + let t = ::declare_builder().build(dummy_ctx()); assert_eq!(t.a, 0.); } #[test] -fn string_default_field() { +fn default_field_with_value() { #[derive(Declare)] - struct T { + struct DefaultWithValue { #[declare(default = "hi!")] text: &'static str, } - let t = ::declare_builder().build(dummy_ctx()); + let t = ::declare_builder().build(dummy_ctx()); assert_eq!(t.text, "hi!"); } diff --git a/tests/code_gen_test.rs b/tests/rdl_macro_test.rs similarity index 59% rename from tests/code_gen_test.rs rename to tests/rdl_macro_test.rs index a3da5c6dc..f783c5b69 100644 --- a/tests/code_gen_test.rs +++ b/tests/rdl_macro_test.rs @@ -3,6 +3,387 @@ use ribir_dev_helper::*; use std::{cell::Cell, rc::Rc, time::Duration}; use winit::event::{DeviceId, ElementState, MouseButton, WindowEvent}; +fn simplest_leaf_rdl() -> impl Into { + fn_widget! { + rdl! { SizedBox { size: Size::new(500.,500.) } } + } +} +widget_layout_test!(simplest_leaf_rdl, width == 500., height == 500.,); + +fn with_child_rdl() -> impl Into { + fn_widget! { + rdl!{ + Row { + rdl!{ SizedBox { size: Size::new(500.,500.) } } + } + } + } +} +widget_layout_test!(with_child_rdl, width == 500., height == 500.,); + +fn with_builtin_child_rdl() -> impl Into { + fn_widget! { + rdl! { SizedBox { + size: Size::new(500.,500.), + margin: EdgeInsets::all(10.) + }} + } +} +widget_layout_test!(with_builtin_child_rdl, width == 520., height == 520.,); + +fn rdl_with_child() -> impl Into { + fn_widget! { + let single_p = rdl!{ SizedBox { size: Size::new(500.,500.) }}; + rdl! { $single_p { rdl! { Void } } } + } +} +widget_layout_test!(rdl_with_child, width == 500., height == 500.,); + +fn single_rdl_has_builtin_with_child() -> impl Into { + fn_widget! { + let single_p = rdl!{ SizedBox { + size: Size::new(500.,500.), + margin: EdgeInsets::all(10.) + }}; + rdl! { $single_p { rdl! { Void } } } + } +} +widget_layout_test!( + single_rdl_has_builtin_with_child, + width == 520., + height == 520., +); + +fn multi_child_rdl_has_builtin_with_child() -> impl Into { + fn_widget! { + let multi_p = rdl! { Flex { + margin: EdgeInsets::all(10.) + } }; + rdl! { $multi_p { rdl!{ Void } } } + } +} +widget_layout_test!( + multi_child_rdl_has_builtin_with_child, + width == 20., + height == 20., +); + +fn compose_child_rdl_has_builtin_with_child() -> impl Into { + fn_widget! { + let multi_p = rdl!{ Row { margin: EdgeInsets::all(10.) }}; + rdl! { $multi_p { rdl!{ Void {} }} } + } +} +widget_layout_test!( + compose_child_rdl_has_builtin_with_child, + width == 20., + height == 20., +); + +fn access_rdl_widget() -> impl Into { + fn_widget! { + let mut b = rdl! { SizedBox {size: Size::new(500.,500.)}}; + rdl! { Row { + rdl! { SizedBox { size: $b.size } } + rdl! { b } + }} + } +} +widget_layout_test!(access_rdl_widget, width == 1000., height == 500.,); + +fn access_builtin_rdl_widget() -> impl Into { + fn_widget! { + let mut b = rdl! { SizedBox { + size: Size::new(100.,100.), + margin: EdgeInsets::all(10.) + }}; + + rdl!{ + Row { + rdl! { + SizedBox { + size: $b.size, + margin: $b.margin, + } + } + rdl! { b } + } + } + } +} +widget_layout_test!(access_builtin_rdl_widget, width == 240., height == 120.,); + +fn dollar_as_rdl_parent() -> impl Into { + fn_widget! { + let b = rdl! {SizedBox { size: Size::new(500.,500.) }}; + rdl! { $b { rdl! { Void {}} } } + } +} +widget_layout_test!(dollar_as_rdl_parent, width == 500., height == 500.,); + +fn dollar_as_middle_parent() -> impl Into { + fn_widget! { + let b = rdl! { SizedBox { size: Size::new(500.,500.) }}; + rdl! { Row { rdl! { $b { rdl! { Void {} } } } } } + } +} +widget_layout_test!(dollar_as_middle_parent, width == 500., height == 500.,); + +fn pipe_as_field_value() -> impl Into { + let size = Stateful::new(Size::zero()); + let size2 = size.clone(); + let w = fn_widget! { + rdl! { SizedBox { size: pipe!(*$size2) }} + }; + *size.state_ref() = 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 w = fn_widget! { + rdl! { SizedBox { + size: Size::zero(), + margin: pipe!(*$margin2) + }} + }; + *margin.state_ref() = 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 w = fn_widget! { + rdl! { SizedBox { + size: pipe!(IconSize::of(ctx!()).tiny * *$scale) + }} + }; + *scale2.state_ref() = 2.; + w +} +widget_layout_test!(pipe_with_ctx, width == 36., height == 36.,); + +fn pipe_with_builtin_field() -> impl Into { + fn_widget! { + let mut box1 = @SizedBox { size: Size::zero(), margin: EdgeInsets::all(1.) }; + let mut box2 = @SizedBox { size: $box1.size, margin: pipe!($box1.margin) }; + @Row { + @{ box1 } + @{ box2 } + } + } +} +widget_layout_test!(pipe_with_builtin_field, width == 4., height == 2.,); + +fn capture_closure_used_ctx() -> impl Into { + fn_widget! { + let mut size_box = @SizedBox { size: ZERO_SIZE }; + @ $size_box { + on_mounted: move |_| $size_box.size = IconSize::of(ctx!()).tiny + } + } +} +widget_layout_test!(capture_closure_used_ctx, width == 18., height == 18.,); + +#[test] +fn pipe_single_parent() { + let _guard = unsafe { AppCtx::new_lock_scope() }; + + let outside_blank = Stateful::new(true); + let outside_blank2 = outside_blank.clone(); + let w = fn_widget! { + let edges = EdgeInsets::all(5.); + let blank = pipe! { + if *$outside_blank { + Box::new(rdl! { Margin { margin: edges } }) as Box + } else { + Box::new(rdl! { Padding { padding: edges } }) as Box + } + }; + rdl! { + $blank { + rdl!{ SizedBox { size: Size::new(100., 100.) } } + } + } + }; + + let mut wnd = TestWindow::new(w); + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], width == 110., height == 110., }); + + *outside_blank2.state_ref() = false; + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], width == 100., height == 100., }); +} + +#[test] +fn pipe_multi_parent() { + let _guard = unsafe { AppCtx::new_lock_scope() }; + + let stack_or_flex = Stateful::new(true); + let stack_or_flex2 = stack_or_flex.clone(); + let w = fn_widget! { + let container = pipe! { + let c: Box = if *$stack_or_flex { + Box::new(rdl! { Stack { } }) + } else { + Box::new(rdl! { Flex { } }) + }; + c + }; + + rdl! { + $container { + rdl!{ SizedBox { size: Size::new(100., 100.) } } + rdl!{ SizedBox { size: Size::new(100., 100.) } } + } + } + }; + + let mut wnd = TestWindow::new(w); + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], width == 100., height == 100., }); + + *stack_or_flex2.state_ref() = false; + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], width == 200., height == 100., }); +} + +#[test] +fn pipe_as_child() { + let _guard = unsafe { AppCtx::new_lock_scope() }; + + let box_or_not = Stateful::new(true); + let box_or_not2 = box_or_not.clone(); + let w = fn_widget! { + let blank: Pipe = pipe!{ + if *$box_or_not2 { + rdl! { SizedBox { size: Size::new(100., 100.) } }.into() + } else { + Void.into() + } + }; + rdl! { Stack { rdl! { blank } } } + }; + + let mut wnd = TestWindow::new(w); + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], width == 100., height == 100., }); + + *box_or_not.state_ref() = false; + + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], width == 0., height == 0., }); +} + +#[test] +fn pipe_as_multi_child() { + let _guard = unsafe { AppCtx::new_lock_scope() }; + + let fix_box = SizedBox { size: Size::new(100., 100.) }; + let cnt = Stateful::new(0); + let cnt2 = cnt.clone(); + let w = fn_widget! { + let boxes = pipe! { + Multi::new((0..*$cnt).map(|_| fix_box.clone()).collect::>()) + }; + rdl! { Flex { rdl!{ boxes } } } + }; + + let mut wnd = TestWindow::new(w); + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], width == 0., height == 0., }); + + *cnt2.state_ref() = 3; + wnd.draw_frame(); + assert_layout_result_by_path!(wnd, { path = [0], width == 300., height == 100., }); +} + +fn at_in_widget_macro() -> impl Into { + fn_widget! { + @SizedBox { size: Size::new(100., 100.) } + } +} +widget_layout_test!(at_in_widget_macro, width == 100., height == 100.,); + +fn at_as_variable_in_widget() -> impl Into { + fn_widget! { + let size = Size::new(100., 100.); + let row = @Row {}; + @ $row { + // @ in @ + @SizedBox { size } + // `rdl!` in @ + rdl! { SizedBox { size } } + } + } +} +widget_layout_test!(at_as_variable_in_widget, width == 200., height == 100.,); + +fn at_as_variable_in_rdl() -> impl Into { + fn_widget! { + let size = Size::new(100., 100.); + let row = @Row {}; + rdl! { + $row { + @SizedBox { size } + @SizedBox { size } + } + } + } +} +widget_layout_test!(at_as_variable_in_rdl, width == 200., height == 100.,); + +fn access_builtin_field_by_dollar() -> impl Into { + fn_widget! { + let size = Size::new(100., 100.); + let mut box1 = @SizedBox { size, margin: EdgeInsets::all(10.) }; + let box2 = @SizedBox { size, margin: $box1.margin }; + @Row { @ { box1 } @{ box2 } } + } +} +widget_layout_test!( + access_builtin_field_by_dollar, + width == 240., + height == 120., +); + +#[test] +fn closure_in_fn_widget_capture() { + let _guard = unsafe { AppCtx::new_lock_scope() }; + + let hi_res = Stateful::new(CowArc::borrowed("")); + let hi_res2 = hi_res.clone(); + let w = fn_widget! { + let mut text = @ Text { text: "hi" }; + let on_mounted = move |_: &'_ mut LifecycleEvent<'_>| { + *$hi_res =$text.text.clone() + }; + @ $text { on_mounted } + }; + + let mut wnd = TestWindow::new(w); + wnd.draw_frame(); + + assert_eq!(&**hi_res2.state_ref(), "hi"); +} + +fn at_embed_in_expression() -> impl Into { + fn_widget! { + @Row { + @{ Multi::new((0..3).map(|_| { + @SizedBox { size: Size::new(100., 100.) } + }))} + } + } +} +widget_layout_test!(at_embed_in_expression, width == 300., height == 100.,); + #[test] fn declare_smoke() { let _guard = unsafe { AppCtx::new_lock_scope() }; @@ -11,7 +392,7 @@ fn declare_smoke() { SizedBox { size: Size::new(500.,500.), background: Color::RED, - } + } }; } diff --git a/widgets/src/avatar.rs b/widgets/src/avatar.rs index 0e0c053b5..b9f7233ff 100644 --- a/widgets/src/avatar.rs +++ b/widgets/src/avatar.rs @@ -22,7 +22,7 @@ use ribir_core::prelude::*; /// } /// }; /// ``` -#[derive(Declare, Default, Clone)] +#[derive(Declare, Declare2, Default, Clone)] pub struct Avatar { #[declare(default=Palette::of(ctx).primary())] pub color: Color, diff --git a/widgets/src/buttons/filled_button.rs b/widgets/src/buttons/filled_button.rs index 2d41698cc..fc1247d28 100644 --- a/widgets/src/buttons/filled_button.rs +++ b/widgets/src/buttons/filled_button.rs @@ -73,7 +73,7 @@ impl ComposeDecorator for FilledButtonDecorator { /// } /// }; /// ``` -#[derive(Declare, Default)] +#[derive(Declare, Declare2, Default)] pub struct FilledButton { #[declare(default=Palette::of(ctx).primary())] color: Color, diff --git a/widgets/src/checkbox.rs b/widgets/src/checkbox.rs index 3fd5bf85f..67d3d5981 100644 --- a/widgets/src/checkbox.rs +++ b/widgets/src/checkbox.rs @@ -44,8 +44,8 @@ impl Checkbox { #[derive(Template)] pub enum CheckboxTemplate { - Before(WidgetPair>), - After(WidgetPair>), + Before(SinglePair>), + After(SinglePair>), } impl ComposeDecorator for CheckBoxDecorator { @@ -77,7 +77,7 @@ impl Checkbox { .into() } - fn label(label: &State