diff --git a/CHANGELOG.md b/CHANGELOG.md index 42df7a2d9..f18472c34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,38 @@ Please only add new entries below the [Unreleased](#unreleased---releasedate) he ## [@Unreleased] - @ReleaseDate +## Features + +- **core**: All built-in widget abilities are now exported on `FatObj`. (#535 @M-Adoo) + You can directly use `FatObj` to configure built-in widget abilities such as `on_click`, `on_key_down`, etc. + ```rust + let _ = FatObj::new(Void) + .margin(EdgeInsets::all(1.0)) + .on_click(|_, _| { println!("click"); }); + ``` +- **macros**: `#[derive(Decalre)]` now generates a `FatObj>` instead of `State`, and supports initialization of all built-in widgets on its DeclareBuilder. (#535 @M-Adoo) + All pipes used to initialize the field will be unsubscribed when the FatObj is disposed. + ```rust + let row = Row::declare_builder() + .margin(...) + .on_click(...) + .build_declare(ctx); + ``` +- **macros**: Introduced `simple_declare` macro for types that don't use `Pipe` for initialization. (#535 @M-Adoo) + +## Changed + +- **macros**: removed crate `ribir_builtin` that is no longer needed. (#535 @M-Adoo) + +## Breaking + +- **core**: removed `FatObj::unzip` and `FatObj::from_host` (#535 @M-Adoo) +- **core**: removed `BuiltinObj`. (#535 @M-Adoo) +- **core**: `FatObj::new(host: T, builtin: BuiltinObj)` -> `FatObj::new(host: T)` + +While these are public APIs, they are typically not required for direct use in user code. + + ## [0.2.0-alpha.5] - 2024-03-05 ### Features @@ -32,12 +64,9 @@ Please only add new entries below the [Unreleased](#unreleased---releasedate) he ## [0.2.0-alpha.4] - 2024-02-27 -### Fixed - -- Optimization, StateReader auto unsubscribe if not writer(#532 @wjian23) - ### Changed +- **core**: StateReader now automatically unsubscribes when no writer is present (#532 @wjian23) - **core**: Consolidated all listener and `FocusNode` into a `MixBuiltin` widget (#534 @M-Adoo) - The `MixBuiltin` widget reduces memory usage and allows users to utilize all `on_xxx` event handlers, not only during the build declaration but also after the widget has been built. diff --git a/Cargo.toml b/Cargo.toml index 397bad155..b5cf2fdc1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,6 @@ members = [ "gpu", "painter", "macros", - "macros/builtin", "algo", "text", "widgets", @@ -37,7 +36,6 @@ readme = "README.md" version = "0.2.0-alpha.5" [workspace.dependencies] -Inflector = "0.11.4" ahash = "0.8.3" arboard = "3.2.0" bitflags = "2.0.0" @@ -90,6 +88,7 @@ macos-accessibility-client = { version = "0.0.1" } tokio = { version = "1.0" } tokio-stream = { version = "0.1" } priority-queue = "1.3.2" +phf = "0.11.2" [workspace.metadata.release] shared-version = true diff --git a/core/src/animation/animate.rs b/core/src/animation/animate.rs index 825630826..33a840513 100644 --- a/core/src/animation/animate.rs +++ b/core/src/animation/animate.rs @@ -1,7 +1,7 @@ use crate::{prelude::*, ticker::FrameMsg, window::WindowId}; use std::time::Instant; -#[derive(Declare)] +#[simple_declare] pub struct Animate where S: AnimateState + 'static, diff --git a/core/src/animation/stagger.rs b/core/src/animation/stagger.rs index 4b8b88555..606deadbe 100644 --- a/core/src/animation/stagger.rs +++ b/core/src/animation/stagger.rs @@ -25,7 +25,7 @@ //! let first_fade_in = @Animate { //! transition: transitions::EASE_IN.of(ctx!()), //! state: map_writer!($first.opacity), -//! }.into_inner(); +//! }; //! //! stagger.write().push_animation(first_fade_in); //! stagger.write().push_state(map_writer!($second.opacity), 0., ctx!()); @@ -103,7 +103,7 @@ impl Stagger { A: AnimateState + 'static, { let transition = Box::new(self.transition.clone()); - let animate = rdl! { Animate { transition, state, from } }.into_inner(); + let animate = rdl! { Animate { transition, state, from } }; self.push_animation_with(stagger, animate.clone_writer().into_inner()); animate } @@ -116,7 +116,7 @@ impl Stagger { /// Add an animation to the end of the stagger animation with a different /// stagger duration. /// - /// **stagger**: the duration between the previous animation start and this + /// **stagger**: the duration between the previous animation start and this /// animation start. pub fn push_animation_with( &mut self, @@ -237,7 +237,7 @@ mod tests { from: 0., }; - stagger.write().push_animation(animate.into_inner()); + stagger.write().push_animation(animate); stagger.write().push_state( map_writer!($mock_box.size), Size::new(200., 200.), diff --git a/core/src/builtin_widgets.rs b/core/src/builtin_widgets.rs index 1cc718424..42c9059ac 100644 --- a/core/src/builtin_widgets.rs +++ b/core/src/builtin_widgets.rs @@ -10,12 +10,12 @@ pub use key::{Key, KeyWidget}; pub mod image_widget; pub use image_widget::*; pub mod delay_drop; -pub use delay_drop::DelayDrop; +pub use delay_drop::*; mod theme; use ribir_algo::Sc; pub use theme::*; mod cursor; -pub use cursor::Cursor; +pub use cursor::*; pub use winit::window::CursorIcon; mod margin; pub use margin::*; @@ -71,107 +71,6 @@ use crate::{ widget::{Widget, WidgetBuilder}, }; -macro_rules! impl_builtin_obj { - ($($builtin_ty: ty),*) => { - paste::paste! { - #[doc="A builtin object contains all builtin widgets, and can be used to \ - extend other object in the declare syntax, so other objects can use the \ - builtin fields and methods like self fields and methods."] - #[derive(Default)] - pub struct BuiltinObj { - host_id: LazyWidgetId, - id: LazyWidgetId, - $([< $builtin_ty: snake:lower >]: Option>),* - } - - impl BuiltinObj { - pub fn is_empty(&self) -> bool { - self.host_id.ref_count() == 1 - && self.id.ref_count() == 1 - && $(self.[< $builtin_ty: snake:lower >].is_none())&& * - } - - $( - pub fn [< $builtin_ty: snake:lower >](&mut self, ctx: &BuildCtx) - -> &mut State<$builtin_ty> - { - self - .[< $builtin_ty: snake:lower >] - .get_or_insert_with(|| $builtin_ty::declare_builder().build_declare(ctx)) - } - )* - - $( - pub fn [< set_builtin_ $builtin_ty: snake:lower >]( - mut self, builtin: State<$builtin_ty> - ) -> Self { - self.[< $builtin_ty: snake:lower >] = Some(builtin); - self - } - )* - - $( - pub fn [< get_builtin_ $builtin_ty: snake:lower >](&mut self, ctx: &BuildCtx) - -> &mut State<$builtin_ty> - { - self - .[< $builtin_ty: snake:lower >] - .get_or_insert_with(|| $builtin_ty::declare_builder().build_declare(ctx)) - } - )* - - pub fn compose_with_host(self, mut host: Widget, ctx: &BuildCtx) -> Widget { - self.host_id.set(host.id()); - $( - if let Some(builtin) = self.[< $builtin_ty: snake:lower >] { - host = builtin.with_child(host, ctx).widget_build(ctx); - } - )* - self.id.set(host.id()); - host - } - - pub fn lazy_host_id(&self) -> LazyWidgetId { self.host_id.clone() } - - pub fn lazy_id(&self) -> LazyWidgetId { self.id.clone() } - } - - impl FatObj { - $( - pub fn [< get_builtin_ $builtin_ty: snake:lower >](&mut self, ctx: &BuildCtx) - -> &mut State<$builtin_ty> - { - self.builtin.[](ctx) - } - )* - } - } - }; -} - -impl_builtin_obj!( - MixBuiltin, - RequestFocus, - HasFocus, - MouseHover, - PointerPressed, - FittedBox, - BoxDecoration, - Padding, - LayoutBox, - Cursor, - Margin, - ScrollableWidget, - TransformWidget, - HAlignWidget, - VAlignWidget, - RelativeAnchor, - GlobalAnchor, - Visibility, - Opacity, - DelayDrop -); - #[derive(Clone)] /// LazyWidgetId is a widget id that will be valid after widget build. pub struct LazyWidgetId(Sc>>); @@ -179,9 +78,56 @@ pub struct LazyWidgetId(Sc>>); /// A fat object that extend the `T` object with all builtin widgets ability. A /// `FatObj` will create during the compose phase, and compose with the builtin /// widgets it actually use, and drop after composed. +/// +/// It's important to understand that `FatObj` is a temporary mixin object. It +/// doesn't persist in the final widget tree. Therefore, you can only clone a +/// portion of its real widget. However, if you're using the DSL macros, you +/// don't need to worry about this. +/// +/// # Example +/// +/// If you want to modify the margin of a `FatObj`, you need to clone the writer +/// of `Margin` widget within it. +/// +/// ```rust +/// use ribir_core::prelude::*; +/// use ribir_core::test_helper::*; +/// +/// let w = |ctx: &BuildCtx| { +/// let mut multi = FatObj::new(MockMulti::default()) +/// .margin(EdgeInsets::all(10.)); +/// +/// let w = multi.get_margin_widget().clone_writer(); +/// multi +/// .on_tap(move |_| w.write().margin = EdgeInsets::all(20.)) +/// .widget_build(ctx) +/// }; +/// ``` pub struct FatObj { host: T, - builtin: BuiltinObj, + host_id: LazyWidgetId, + id: LazyWidgetId, + mix_builtin: Option>, + request_focus: Option>, + has_focus: Option>, + mouse_hover: Option>, + pointer_pressed: Option>, + fitted_box: Option>, + box_decoration: Option>, + padding: Option>, + layout_box: Option>, + cursor: Option>, + margin: Option>, + scrollable: Option>, + transform: Option>, + h_align: Option>, + v_align: Option>, + relative_anchor: Option>, + global_anchor: Option>, + visibility: Option>, + opacity: Option>, + delay_drop: Option>, + delay_drop_unsubscribe_handle: Option>, } impl LazyWidgetId { @@ -199,20 +145,102 @@ impl Default for LazyWidgetId { } impl FatObj { - pub fn from_host(host: T) -> Self { Self { host, builtin: BuiltinObj::default() } } - - pub fn new(host: T, builtin: BuiltinObj) -> Self { Self { host, builtin } } + /// Create a new `FatObj` with the given host object. + pub fn new(host: T) -> Self { + Self { + host, + host_id: LazyWidgetId::default(), + id: LazyWidgetId::default(), + mix_builtin: None, + request_focus: None, + has_focus: None, + mouse_hover: None, + pointer_pressed: None, + fitted_box: None, + box_decoration: None, + padding: None, + layout_box: None, + cursor: None, + margin: None, + scrollable: None, + transform: None, + h_align: None, + v_align: None, + relative_anchor: None, + global_anchor: None, + visibility: None, + opacity: None, + delay_drop: None, + delay_drop_unsubscribe_handle: None, + } + } - pub fn unzip(self) -> (T, BuiltinObj) { (self.host, self.builtin) } + /// Maps an `FatObj` to `FatObj` by applying a function to the host + /// object. #[inline] - pub(crate) fn map(self, f: impl FnOnce(T) -> V) -> FatObj { - let Self { host, builtin } = self; - FatObj { host: f(host), builtin } + pub fn map(self, f: impl FnOnce(T) -> V) -> FatObj { + FatObj { + host: f(self.host), + host_id: self.host_id, + id: self.id, + mix_builtin: self.mix_builtin, + request_focus: self.request_focus, + has_focus: self.has_focus, + mouse_hover: self.mouse_hover, + pointer_pressed: self.pointer_pressed, + fitted_box: self.fitted_box, + box_decoration: self.box_decoration, + padding: self.padding, + layout_box: self.layout_box, + cursor: self.cursor, + margin: self.margin, + scrollable: self.scrollable, + transform: self.transform, + h_align: self.h_align, + v_align: self.v_align, + relative_anchor: self.relative_anchor, + global_anchor: self.global_anchor, + visibility: self.visibility, + opacity: self.opacity, + delay_drop: self.delay_drop, + delay_drop_unsubscribe_handle: self.delay_drop_unsubscribe_handle, + } } + /// Return true if the FatObj not contains any builtin widgets. + pub fn is_empty(&self) -> bool { + self.host_id.ref_count() == 1 + && self.id.ref_count() == 1 + && self.mix_builtin.is_none() + && self.request_focus.is_none() + && self.has_focus.is_none() + && self.mouse_hover.is_none() + && self.pointer_pressed.is_none() + && self.fitted_box.is_none() + && self.box_decoration.is_none() + && self.padding.is_none() + && self.layout_box.is_none() + && self.cursor.is_none() + && self.margin.is_none() + && self.scrollable.is_none() + && self.transform.is_none() + && self.h_align.is_none() + && self.v_align.is_none() + && self.relative_anchor.is_none() + && self.global_anchor.is_none() + && self.visibility.is_none() + && self.opacity.is_none() + && self.delay_drop.is_none() + } + + /// Return the host object of the FatObj. + /// + /// # Panics + /// + /// Panics if the FatObj contains builtin widgets. pub fn into_inner(self) -> T { assert!( - self.builtin.is_empty(), + self.is_empty(), "Unwrap a FatObj with contains builtin widgets is not allowed." ); self.host @@ -220,11 +248,740 @@ impl FatObj { /// Return the LazyWidgetId of the host widget, through which you can access /// the WidgetId after building. - pub fn lazy_host_id(&self) -> LazyWidgetId { self.builtin.lazy_host_id() } + pub fn lazy_host_id(&self) -> LazyWidgetId { self.host_id.clone() } /// Return the LazyWidgetId point to WidgetId of the root of the sub widget /// tree after the FatObj has built. - pub fn lazy_id(&self) -> LazyWidgetId { self.builtin.lazy_id() } + pub fn lazy_id(&self) -> LazyWidgetId { self.id.clone() } +} + +// builtin widgets accessors +impl FatObj { + pub fn get_mix_builtin_widget(&mut self) -> &mut State { + self + .mix_builtin + .get_or_insert_with(|| State::value(<_>::default())) + } + + /// Returns the `State` widget from the FatObj. If it doesn't + /// exist, a new one is created. + pub fn get_request_focus_widget(&mut self) -> &mut State { + self + .request_focus + .get_or_insert_with(|| State::value(<_>::default())) + } + + /// Returns the `State` widget from the FatObj. If it doesn't exist, + /// a new one is created. + pub fn get_has_focus_widget(&mut self) -> &mut State { + self + .has_focus + .get_or_insert_with(|| State::value(<_>::default())) + } + + /// Returns the `State` widget from the FatObj. If it doesn't + /// exist, a new one is created. + pub fn get_mouse_hover_widget(&mut self) -> &mut State { + self + .mouse_hover + .get_or_insert_with(|| State::value(<_>::default())) + } + + /// Returns the `State` widget from the FatObj. If it doesn't + /// exist, a new one is created. + pub fn get_pointer_pressed_widget(&mut self) -> &mut State { + self + .pointer_pressed + .get_or_insert_with(|| State::value(<_>::default())) + } + + /// Returns the `State` widget from the FatObj. If it doesn't + /// exist, a new one is created. + pub fn get_fitted_box_widget(&mut self) -> &mut State { + self + .fitted_box + .get_or_insert_with(|| State::value(<_>::default())) + } + + /// Returns the `State` widget from the FatObj. If it doesn't + /// exist, a new one is created. + pub fn get_box_decoration_widget(&mut self) -> &mut State { + self + .box_decoration + .get_or_insert_with(|| State::value(<_>::default())) + } + + /// Returns the `State` widget from the FatObj. If it doesn't exist, + /// a new one is created. + pub fn get_padding_widget(&mut self) -> &mut State { + self + .padding + .get_or_insert_with(|| State::value(<_>::default())) + } + + /// Returns the `State` widget from the FatObj. If it doesn't + /// exist, a new one is created. + pub fn get_layout_box_widget(&mut self) -> &mut State { + self + .layout_box + .get_or_insert_with(|| State::value(<_>::default())) + } + + /// Returns the `State` widget from the FatObj. If it doesn't exist, a + /// new one is created. + pub fn get_cursor_widget(&mut self) -> &mut State { + self + .cursor + .get_or_insert_with(|| State::value(<_>::default())) + } + + /// Returns the `State` widget from the FatObj. If it doesn't exist, a + /// new one is created. + pub fn get_margin_widget(&mut self) -> &mut State { + self + .margin + .get_or_insert_with(|| State::value(<_>::default())) + } + + /// Returns the `State` widget from the FatObj. If it + /// doesn't exist, a new one is created. + pub fn get_scrollable_widget(&mut self) -> &mut State { + self + .scrollable + .get_or_insert_with(|| State::value(<_>::default())) + } + + /// Returns the `State` widget from the FatObj. If it doesn't + /// exist, a new one is created. + pub fn get_transform_widget(&mut self) -> &mut State { + self + .transform + .get_or_insert_with(|| State::value(<_>::default())) + } + + /// Returns the `State` widget from the FatObj. If it doesn't + /// exist, a new one is created. + pub fn get_h_align_widget(&mut self) -> &mut State { + self + .h_align + .get_or_insert_with(|| State::value(<_>::default())) + } + + /// Returns the `State` widget from the FatObj. If it doesn't + /// exist, a new one is created. + pub fn get_v_align_widget(&mut self) -> &mut State { + self + .v_align + .get_or_insert_with(|| State::value(<_>::default())) + } + + /// Returns the `State` widget from the FatObj. If it doesn't + /// exist, a new one is created. + pub fn get_relative_anchor_widget(&mut self) -> &mut State { + self + .relative_anchor + .get_or_insert_with(|| State::value(<_>::default())) + } + + /// Returns the `State` widget from the FatObj. If it doesn't + /// exist, a new one is created. + pub fn get_global_anchor_widget(&mut self) -> &mut State { + self + .global_anchor + .get_or_insert_with(|| State::value(<_>::default())) + } + + /// Returns the `State` widget from the FatObj. If it doesn't + /// exist, a new one is created. + pub fn get_visibility_widget(&mut self) -> &mut State { + self + .visibility + .get_or_insert_with(|| State::value(<_>::default())) + } + + /// Returns the `State` widget from the FatObj. If it doesn't exist, + /// a new one is created. + pub fn get_opacity_widget(&mut self) -> &mut State { + self + .opacity + .get_or_insert_with(|| State::value(<_>::default())) + } + + /// Returns the `State` widget from the FatObj. If it doesn't + /// exist, a new one is created. + pub fn get_delay_drop_widget(&mut self) -> &mut State { + self + .delay_drop + .get_or_insert_with(|| State::value(<_>::default())) + } +} + +macro_rules! on_mixin { + ($this: ident, $on_method: ident, $f: ident) => {{ + $this.get_mix_builtin_widget().read().$on_method($f); + $this + }}; +} + +// report all builtin widgets apis +impl FatObj { + /// Attaches an event handler to the widget. It's triggered when any event or + /// lifecycle change happens. + pub fn on_event(mut self, f: impl FnMut(&mut Event) + 'static) -> Self { + on_mixin!(self, on_event, f) + } + + /// Adds an event handler that runs when the widget is first mounted to the + /// tree. + pub fn on_mounted(mut self, f: impl FnOnce(&mut LifecycleEvent) + 'static) -> Self { + on_mixin!(self, on_mounted, f) + } + + /// Adds an event handler that runs after the widget is performed layout. + pub fn on_performed_layout(mut self, f: impl FnMut(&mut LifecycleEvent) + 'static) -> Self { + on_mixin!(self, on_performed_layout, f) + } + + /// Adds an event handler that runs when the widget is disposed. + pub fn on_disposed(mut self, f: impl FnOnce(&mut LifecycleEvent) + 'static) -> Self { + on_mixin!(self, on_disposed, f) + } + + /// Attaches a handler to the widget that is triggered when a pointer down + /// occurs. + pub fn on_pointer_down(mut self, f: impl FnMut(&mut PointerEvent) + 'static) -> Self { + on_mixin!(self, on_pointer_down, f) + } + + /// Attaches a handler to the widget that is triggered during the capture + /// phase of a pointer down event. This is similar to `on_pointer_down`, but + /// it's triggered earlier in the event flow. For more information on event + /// capturing, see [Event capture](https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-flow-capture). + pub fn on_pointer_down_capture(mut self, f: impl FnMut(&mut PointerEvent) + 'static) -> Self { + on_mixin!(self, on_pointer_down_capture, f) + } + + /// Attaches a handler to the widget that is triggered when a pointer up + /// occurs. + pub fn on_pointer_up(mut self, f: impl FnMut(&mut PointerEvent) + 'static) -> Self { + on_mixin!(self, on_pointer_up, f) + } + + /// Attaches a handler to the widget that is triggered during the capture + /// phase of a pointer up event. This is similar to `on_pointer_up`, but it's + /// triggered earlier in the event flow. For more information on event + /// capturing, see [Event capture](https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-flow-capture). + pub fn on_pointer_up_capture(mut self, f: impl FnMut(&mut PointerEvent) + 'static) -> Self { + on_mixin!(self, on_pointer_up_capture, f) + } + + /// Attaches a handler to the widget that is triggered when a pointer move + /// occurs. + pub fn on_pointer_move(mut self, f: impl FnMut(&mut PointerEvent) + 'static) -> Self { + on_mixin!(self, on_pointer_move, f) + } + + /// Attaches a handler to the widget that is triggered during the capture + /// phase of a pointer move event. This is similar to `on_pointer_move`, but + /// it's triggered earlier in the event flow. For more information on event + /// capturing, see [Event capture](https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-flow-capture). + pub fn on_pointer_move_capture(mut self, f: impl FnMut(&mut PointerEvent) + 'static) -> Self { + on_mixin!(self, on_pointer_move_capture, f) + } + + /// Attaches a handler to the widget that is triggered when a pointer event + /// cancels. + pub fn on_pointer_cancel(mut self, f: impl FnMut(&mut PointerEvent) + 'static) -> Self { + on_mixin!(self, on_pointer_cancel, f) + } + + /// Attaches a handler to the widget that is triggered when a pointer device + /// is moved into the hit test boundaries of an widget or one of its + /// descendants. + pub fn on_pointer_enter(mut self, f: impl FnMut(&mut PointerEvent) + 'static) -> Self { + on_mixin!(self, on_pointer_enter, f) + } + + /// Attaches a handler to the widget that is triggered when a pointer device + /// is moved out of the hit test boundaries of an widget or one of its + /// descendants. + pub fn on_pointer_leave(mut self, f: impl FnMut(&mut PointerEvent) + 'static) -> Self { + on_mixin!(self, on_pointer_leave, f) + } + + /// Attaches a handler to the widget that is triggered when a tap(click) + /// occurs. + pub fn on_tap(mut self, f: impl FnMut(&mut PointerEvent) + 'static) -> Self { + on_mixin!(self, on_tap, f) + } + + /// Attaches a handler to the widget that is triggered during the capture + /// phase of a tap event. This is similar to `on_tap`, but it's triggered + /// earlier in the event flow. For more information on event capturing, see + /// [Event capture](https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-flow-capture). + pub fn on_tap_capture(mut self, f: impl FnMut(&mut PointerEvent) + 'static) -> Self { + on_mixin!(self, on_tap_capture, f) + } + + /// Attaches a handler to the widget that is triggered when a double tap + /// occurs. + pub fn on_double_tap(mut self, f: impl FnMut(&mut PointerEvent) + 'static) -> Self { + on_mixin!(self, on_double_tap, f) + } + + /// Attaches a handler to the widget that is triggered during the capture + /// phase of a double tap event. This is similar to `on_double_tap`, but it's + /// triggered earlier in the event flow. For more information on event + /// capturing, see [Event capture](https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-flow-capture). + pub fn on_double_tap_capture(mut self, f: impl FnMut(&mut PointerEvent) + 'static) -> Self { + on_mixin!(self, on_double_tap_capture, f) + } + + /// Attaches a handler to the widget that is triggered when a triple tap + /// occurs. + pub fn on_triple_tap(mut self, f: impl FnMut(&mut PointerEvent) + 'static) -> Self { + on_mixin!(self, on_triple_tap, f) + } + + /// Attaches a handler to the widget that is triggered when a triple tap + /// occurs. This is similar to `on_double_tap`, but it's triggered earlier + /// in the event flow. For more information on event capturing, see + /// [Event capture](https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-flow-capture). + pub fn on_triple_tap_capture(mut self, f: impl FnMut(&mut PointerEvent) + 'static) -> Self { + on_mixin!(self, on_triple_tap_capture, f) + } + + /// Attaches a handler to the widget that is triggered when a x-times tap + /// occurs. + pub fn on_x_times_tap( + mut self, + (times, f): (usize, impl FnMut(&mut PointerEvent) + 'static), + ) -> Self { + self + .get_mix_builtin_widget() + .read() + .on_x_times_tap((times, f)); + self + } + + /// Attaches a handler to the widget that is triggered during the capture + /// phase of a x-times tap event. This is similar to `on_x_times_tap`, but + /// it's triggered earlier in the event flow. For more information on event + /// capturing, see [Event capture](https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-flow-capture). + pub fn on_x_times_tap_capture( + mut self, + (times, f): (usize, impl FnMut(&mut PointerEvent) + 'static), + ) -> Self { + self + .get_mix_builtin_widget() + .read() + .on_x_times_tap_capture((times, f)); + self + } + + /// Attaches a handler to the widget that is triggered when the user rotates a + /// wheel button on a pointing device (typically a mouse). + pub fn on_wheel(mut self, f: impl FnMut(&mut WheelEvent) + 'static) -> Self { + on_mixin!(self, on_wheel, f) + } + + /// Attaches a handler to the widget that is triggered during the capture + /// phase of a wheel event. This is similar to `on_wheel`, but it's triggered + /// earlier in the event flow. For more information on event capturing, see + /// [Event capture](https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-flow-capture). + pub fn on_wheel_capture(mut self, f: impl FnMut(&mut WheelEvent) + 'static) -> Self { + on_mixin!(self, on_wheel_capture, f) + } + + /// Attaches a handler to the widget that is triggered when the input method + /// pre-edit area is changed. + pub fn on_ime_pre_edit(mut self, f: impl FnMut(&mut ImePreEditEvent) + 'static) -> Self { + on_mixin!(self, on_ime_pre_edit, f) + } + + /// Attaches a handler to the widget that is triggered during the capture + /// phase of a ime pre-edit event. This is similar to `on_ime_pre_edit`, + /// but it's triggered earlier in the event flow. For more information on + /// event capturing, see [Event capture](https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-flow-capture). + pub fn on_ime_pre_edit_capture(mut self, f: impl FnMut(&mut ImePreEditEvent) + 'static) -> Self { + on_mixin!(self, on_ime_pre_edit_capture, f) + } + + /// Attaches a handler to the widget that is triggered when the input method + /// commits text or keyboard pressed the text key. + pub fn on_chars(mut self, f: impl FnMut(&mut CharsEvent) + 'static) -> Self { + on_mixin!(self, on_chars, f) + } + + /// Attaches a handler to the widget that is triggered during the capture + /// phase of a chars event. This is similar to `on_chars`, but it's triggered + /// earlier in the event flow. For more information on event capturing, + /// see [Event capture](https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-flow-capture). + pub fn on_chars_capture(mut self, f: impl FnMut(&mut CharsEvent) + 'static) -> Self { + on_mixin!(self, on_chars_capture, f) + } + + /// Attaches a handler to the widget that is triggered when the keyboard key + /// is pressed. + pub fn on_key_down(mut self, f: impl FnMut(&mut KeyboardEvent) + 'static) -> Self { + on_mixin!(self, on_key_down, f) + } + + /// Attaches a handler to the widget that is triggered during the capture + /// phase of a key down event. This is similar to `on_key_down`, but it's + /// triggered earlier in the event flow. For more information on event + /// capturing, see [Event capture](https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-flow-capture). + pub fn on_key_down_capture(mut self, f: impl FnMut(&mut KeyboardEvent) + 'static) -> Self { + on_mixin!(self, on_key_down_capture, f) + } + + /// Attaches a handler to the widget that is triggered when the keyboard key + /// is released. + pub fn on_key_up(mut self, f: impl FnMut(&mut KeyboardEvent) + 'static) -> Self { + on_mixin!(self, on_key_up, f) + } + + /// Attaches a handler to the widget that is triggered during the capture + /// phase of a key up event. This is similar to `on_key_up`, but it's + /// triggered earlier in the event flow. For more information on event + /// capturing, see [Event capture](https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-flow-capture). + pub fn on_key_up_capture(mut self, f: impl FnMut(&mut KeyboardEvent) + 'static) -> Self { + on_mixin!(self, on_key_up_capture, f) + } + + /// Attaches a handler to the widget that is triggered when the widget is + /// focused. + pub fn on_focus(mut self, f: impl FnMut(&mut FocusEvent) + 'static) -> Self { + on_mixin!(self, on_focus, f) + } + + /// Attaches a handler to the widget that is triggered when the widget is lost + /// focus. + pub fn on_blur(mut self, f: impl FnMut(&mut FocusEvent) + 'static) -> Self { + on_mixin!(self, on_blur, f) + } + + /// Attaches a handler to the widget that is triggered when the widget or its + /// descendants are focused. The main difference between this event and focus + /// is that focusin bubbles while focus does not. + pub fn on_focus_in(mut self, f: impl FnMut(&mut FocusEvent) + 'static) -> Self { + on_mixin!(self, on_focus_in, f) + } + + /// Attaches a handler to the widget that is triggered during the capture + /// phase of a focus in event. This is similar to `on_focus_in`, but it's + /// triggered earlier in the event flow. For more information on event + /// capturing, see [Event capture](https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-flow-capture). + pub fn on_focus_in_capture(mut self, f: impl FnMut(&mut FocusEvent) + 'static) -> Self { + on_mixin!(self, on_focus_in_capture, f) + } + + /// Attaches a handler to the widget that is triggered when the widget or its + /// descendants are lost focus. The main difference between this event and + /// focusout is that focusout bubbles while blur does not. + pub fn on_focus_out(mut self, f: impl FnMut(&mut FocusEvent) + 'static) -> Self { + on_mixin!(self, on_focus_out, f) + } + + /// Attaches a handler to the widget that is triggered during the capture + /// phase of a focus out event. This is similar to `on_focus_out`, but it's + /// triggered earlier in the event flow. For more information on event + /// capturing, see [Event capture](https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-flow-capture). + pub fn on_focus_out_capture(mut self, f: impl FnMut(&mut FocusEvent) + 'static) -> Self { + on_mixin!(self, on_focus_out_capture, f) + } + + /// Initializes the widget with a tab index. The tab index is used to + /// allow or prevent widgets from being sequentially focusable(usually with + /// the Tab key, hence the name) and determine their relative ordering for + /// sequential focus navigation. It accepts an integer as a value, with + /// different results depending on the integer's value: + /// - A negative value (usually -1) means that the widget is not reachable via + /// sequential keyboard navigation, but could be focused with API or + /// visually by clicking with the mouse. + /// - Zero means that the element should be focusable in sequential keyboard + /// navigation, after any positive tab_index values and its order is defined + /// by the tree's source order. + /// - A positive value means the element should be focusable in sequential + /// keyboard navigation, with its order defined by the value of the number. + /// That is, tab_index=4 is focused before tab_index=5 and tab_index=0, but + /// after tab_index=3. If multiple elements share the same positive + /// tab_index value, their order relative to each other follows their + /// position in the tree source. The maximum value for tab_index is 32767. + /// If not specified, it takes the default value 0. + pub fn tab_index(self, tab_idx: V) -> Self + where + DeclareInit: DeclareFrom, + { + self.declare_builtin_init( + DeclareFrom::declare_from(tab_idx), + Self::get_mix_builtin_widget, + |mixin, v| { + mixin.set_tab_index(v); + }, + ) + } + + /// Initializes whether the `widget` should automatically get focus when the + /// window loads. + /// + /// Only one widget should have this attribute specified. If there are + /// several, the widget nearest the root, get the initial focus. + pub fn auto_focus(self, v: V) -> Self + where + DeclareInit: DeclareFrom, + { + self.declare_builtin_init( + DeclareFrom::declare_from(v), + Self::get_mix_builtin_widget, + |m, v| { + m.set_auto_focus(v); + }, + ) + } + + /// Initializes how its child should be scale to fit its box. + pub fn box_fit(self, v: V) -> Self + where + DeclareInit: DeclareFrom, + { + self.declare_builtin_init( + DeclareFrom::declare_from(v), + Self::get_fitted_box_widget, + |m, v| m.box_fit = v, + ) + } + + /// Initializes the background of the widget. + pub fn background(self, v: V) -> Self + where + DeclareInit>: DeclareFrom, + { + self.declare_builtin_init( + DeclareFrom::declare_from(v), + Self::get_box_decoration_widget, + |m, v| m.background = v, + ) + } + + /// Initializes the border of the widget. + pub fn border(self, v: V) -> Self + where + DeclareInit>: DeclareFrom, + { + self.declare_builtin_init( + DeclareFrom::declare_from(v), + Self::get_box_decoration_widget, + |m, v| m.border = v, + ) + } + + /// Initializes the border radius of the widget. + pub fn border_radius(self, v: V) -> Self + where + DeclareInit>: DeclareFrom, + { + self.declare_builtin_init( + DeclareFrom::declare_from(v), + Self::get_box_decoration_widget, + |m, v| m.border_radius = v, + ) + } + + /// Initializes the extra space within the widget. + pub fn padding(self, v: V) -> Self + where + DeclareInit: DeclareFrom, + { + self.declare_builtin_init( + DeclareFrom::declare_from(v), + Self::get_padding_widget, + |m, v| m.padding = v, + ) + } + + /// Initializes the cursor of the widget. + pub fn cursor(self, v: V) -> Self + where + DeclareInit: DeclareFrom, + { + self.declare_builtin_init( + DeclareFrom::declare_from(v), + Self::get_cursor_widget, + |m, v| m.cursor = v, + ) + } + + /// Initializes the space around the widget. + pub fn margin(self, v: V) -> Self + where + DeclareInit: DeclareFrom, + { + self.declare_builtin_init( + DeclareFrom::declare_from(v), + Self::get_margin_widget, + |m, v| m.margin = v, + ) + } + + /// Initializes how user can scroll the widget. + pub fn scrollable(self, v: V) -> Self + where + DeclareInit: DeclareFrom, + { + self.declare_builtin_init( + DeclareFrom::declare_from(v), + Self::get_scrollable_widget, + |m, v| m.scrollable = v, + ) + } + + /// Initializes the position of the widget's scroll. + pub fn scroll_pos(self, v: V) -> Self + where + DeclareInit: DeclareFrom, + { + self.declare_builtin_init( + DeclareFrom::declare_from(v), + Self::get_scrollable_widget, + |m, v| m.scroll_pos = v, + ) + } + + /// Initializes the transformation of the widget. + pub fn transform(self, v: V) -> Self + where + DeclareInit: DeclareFrom, + { + self.declare_builtin_init( + DeclareFrom::declare_from(v), + Self::get_transform_widget, + |m, v| m.transform = v, + ) + } + + /// Initializes how the widget should be aligned horizontally. + pub fn h_align(self, v: V) -> Self + where + DeclareInit: DeclareFrom, + { + self.declare_builtin_init( + DeclareFrom::declare_from(v), + Self::get_h_align_widget, + |m, v| m.h_align = v, + ) + } + + /// Initializes how the widget should be aligned vertically. + pub fn v_align(self, v: V) -> Self + where + DeclareInit: DeclareFrom, + { + self.declare_builtin_init( + DeclareFrom::declare_from(v), + Self::get_v_align_widget, + |m, v| m.v_align = v, + ) + } + + /// Initializes the relative anchor to the parent of the widget. + pub fn anchor(self, v: V) -> Self + where + DeclareInit: DeclareFrom, + { + self.declare_builtin_init( + DeclareFrom::declare_from(v), + Self::get_relative_anchor_widget, + |m, v| m.anchor = v, + ) + } + + /// Initializes the global anchor of the widget. + pub fn global_anchor(self, v: V) -> Self + where + DeclareInit: DeclareFrom, + { + self.declare_builtin_init( + DeclareFrom::declare_from(v), + Self::get_global_anchor_widget, + |m, v| m.global_anchor = v, + ) + } + + /// Initializes the visibility of the widget. + pub fn visible(self, v: V) -> Self + where + DeclareInit: DeclareFrom, + { + self.declare_builtin_init( + DeclareFrom::declare_from(v), + Self::get_visibility_widget, + |m, v| m.visible = v, + ) + } + + /// Initializes the opacity of the widget. + pub fn opacity(self, v: V) -> Self + where + DeclareInit: DeclareFrom, + { + self.declare_builtin_init( + DeclareFrom::declare_from(v), + Self::get_opacity_widget, + |m, v| m.opacity = v, + ) + } + + /// Initializes the `delay_drop_until` value of the `DelayDrop` widget. + pub fn delay_drop_until(mut self, v: V) -> Self + where + DeclareInit: DeclareFrom, + { + let (v, o) = DeclareInit::declare_from(v).unzip(); + let d = self.get_delay_drop_widget(); + d.write().delay_drop_until = v; + if let Some(o) = o { + let c_delay = d.clone_writer(); + + // DelayDropWidget may continue to exist after `on_disposed` is fired. It needs + // to accept value changes to determine when to drop. So instead of + // unsubscribing in `on_disposed`, we unsubscribe when the widget node is + // dropped. + let u = o + .subscribe(move |(_, v)| { + c_delay.write().delay_drop_until = v; + }) + .unsubscribe_when_dropped(); + self.delay_drop_unsubscribe_handle = Some(Box::new(u)); + } + self + } + + fn declare_builtin_init( + mut self, + init: DeclareInit, + get_builtin: impl FnOnce(&mut Self) -> &mut State, + set_value: fn(&mut B, V), + ) -> Self { + let builtin = get_builtin(&mut self); + let (v, o) = init.unzip(); + set_value(&mut *builtin.write(), v); + if let Some(o) = o { + let c_builtin = builtin.clone_writer(); + let u = o.subscribe(move |(_, v)| { + set_value(&mut *c_builtin.write(), v); + }); + self.on_disposed(move |_| u.unsubscribe()) + } else { + self + } + } +} + +impl DeclareBuilder for FatObj { + type Target = Self; + + fn build_declare(self, _: &BuildCtx) -> Self::Target { self } } impl SingleChild for FatObj {} @@ -241,8 +998,74 @@ crate::widget::multi_build_replace_impl! { impl WidgetBuilder for FatObj { #[inline] fn widget_build(self, ctx: &BuildCtx) -> Widget { - let Self { host, builtin } = self; - builtin.compose_with_host(host, ctx) + let mut host = self.host; + self.host_id.set(host.id()); + if let Some(mix_builtin) = self.mix_builtin { + host = mix_builtin.with_child(host, ctx).widget_build(ctx) + } + if let Some(request_focus) = self.request_focus { + host = request_focus.with_child(host, ctx).widget_build(ctx); + } + if let Some(has_focus) = self.has_focus { + host = has_focus.with_child(host, ctx).widget_build(ctx); + } + if let Some(mouse_hover) = self.mouse_hover { + host = mouse_hover.with_child(host, ctx).widget_build(ctx); + } + if let Some(pointer_pressed) = self.pointer_pressed { + host = pointer_pressed.with_child(host, ctx).widget_build(ctx); + } + if let Some(fitted_box) = self.fitted_box { + host = fitted_box.with_child(host, ctx).widget_build(ctx); + } + if let Some(box_decoration) = self.box_decoration { + host = box_decoration.with_child(host, ctx).widget_build(ctx); + } + if let Some(padding) = self.padding { + host = padding.with_child(host, ctx).widget_build(ctx); + } + if let Some(layout_box) = self.layout_box { + host = layout_box.with_child(host, ctx).widget_build(ctx); + } + if let Some(cursor) = self.cursor { + host = cursor.with_child(host, ctx).widget_build(ctx); + } + if let Some(margin) = self.margin { + host = margin.with_child(host, ctx).widget_build(ctx); + } + if let Some(scrollable) = self.scrollable { + host = scrollable.with_child(host, ctx).widget_build(ctx); + } + if let Some(transform) = self.transform { + host = transform.with_child(host, ctx).widget_build(ctx); + } + if let Some(h_align) = self.h_align { + host = h_align.with_child(host, ctx).widget_build(ctx); + } + if let Some(v_align) = self.v_align { + host = v_align.with_child(host, ctx).widget_build(ctx); + } + if let Some(relative_anchor) = self.relative_anchor { + host = relative_anchor.with_child(host, ctx).widget_build(ctx); + } + if let Some(global_anchor) = self.global_anchor { + host = global_anchor.with_child(host, ctx).widget_build(ctx); + } + if let Some(visibility) = self.visibility { + host = visibility.with_child(host, ctx).widget_build(ctx); + } + if let Some(opacity) = self.opacity { + host = opacity.with_child(host, ctx).widget_build(ctx); + } + if let Some(delay_drop) = self.delay_drop { + host = delay_drop.with_child(host, ctx).widget_build(ctx); + } + if let Some(h) = self.delay_drop_unsubscribe_handle { + let arena = &mut ctx.tree.borrow_mut().arena; + host.id().attach_anonymous_data(h, arena); + } + self.id.set(host.id()); + host } } @@ -255,6 +1078,13 @@ impl, C, M> ComposeWithChild for FatObj { } } +impl SingleWithChild for FatObj<()> { + type Target = FatObj; + + #[inline] + fn with_child(self, child: C, _: &BuildCtx) -> Self::Target { self.map(move |_| child) } +} + impl, C> PairWithChild for FatObj { type Target = Pair, C>; @@ -278,17 +1108,6 @@ impl MultiParent for FatObj { } } -impl ComposeChild for BuiltinObj { - type Child = Widget; - - fn compose_child(this: impl StateWriter, child: Self::Child) -> impl WidgetBuilder { - let Ok(this) = this.try_into_value() else { - unreachable!("BuiltinObj should never be a state.") - }; - fn_widget! { this.compose_with_host(child, ctx!()) } - } -} - impl std::ops::Deref for FatObj { type Target = T; #[inline] diff --git a/core/src/builtin_widgets/align.rs b/core/src/builtin_widgets/align.rs index 37d30f99d..29ac2f04b 100644 --- a/core/src/builtin_widgets/align.rs +++ b/core/src/builtin_widgets/align.rs @@ -54,19 +54,29 @@ pub enum VAlign { } /// A widget that align its child in x-axis, base on child's width. -#[derive(Declare, Query, SingleChild)] +#[derive(Query, SingleChild, Default)] 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, Query, SingleChild)] +#[derive(Query, SingleChild, Default)] pub struct VAlignWidget { - #[declare(default, builtin)] pub v_align: VAlign, } +impl Declare for HAlignWidget { + type Builder = FatObj<()>; + #[inline] + fn declare_builder() -> Self::Builder { FatObj::new(()) } +} + +impl Declare for VAlignWidget { + type Builder = FatObj<()>; + #[inline] + fn declare_builder() -> Self::Builder { FatObj::new(()) } +} + impl Render for HAlignWidget { fn perform_layout(&self, mut clamp: BoxClamp, ctx: &mut LayoutCtx) -> Size { let align: Align = self.h_align.into(); diff --git a/core/src/builtin_widgets/anchor.rs b/core/src/builtin_widgets/anchor.rs index cf57fd8c6..1959df706 100644 --- a/core/src/builtin_widgets/anchor.rs +++ b/core/src/builtin_widgets/anchor.rs @@ -155,12 +155,17 @@ impl Anchor { } /// Widget use to anchor child constraints relative to parent widget. -#[derive(Declare, Query, SingleChild)] +#[derive(Query, SingleChild, Default)] pub struct RelativeAnchor { - #[declare(builtin, default)] pub anchor: Anchor, } +impl Declare for RelativeAnchor { + type Builder = FatObj<()>; + #[inline] + fn declare_builder() -> Self::Builder { FatObj::new(()) } +} + impl Render for RelativeAnchor { fn perform_layout(&self, clamp: BoxClamp, ctx: &mut LayoutCtx) -> Size { let mut layouter = ctx.assert_single_child_layouter(); diff --git a/core/src/builtin_widgets/box_decoration.rs b/core/src/builtin_widgets/box_decoration.rs index da0133099..7e22c63d3 100644 --- a/core/src/builtin_widgets/box_decoration.rs +++ b/core/src/builtin_widgets/box_decoration.rs @@ -1,20 +1,23 @@ use crate::prelude::*; /// The BoxDecoration provides a variety of ways to draw a box. -#[derive(SingleChild, Default, Clone, Declare, Query)] +#[derive(SingleChild, Default, Clone, Query)] pub struct BoxDecoration { /// The background of the box. - #[declare(builtin, default)] pub background: Option, /// A border to draw above the background - #[declare(builtin, default)] pub border: Option, /// The corners of this box are rounded by this `BorderRadius`. The round /// corner only work if the two borders beside it are same style. - #[declare(builtin, default)] pub border_radius: Option, } +impl Declare for BoxDecoration { + type Builder = FatObj<()>; + #[inline] + fn declare_builder() -> Self::Builder { FatObj::new(()) } +} + #[derive(Debug, Default, Clone, PartialEq)] pub struct Border { pub left: BorderSide, @@ -197,7 +200,8 @@ mod tests { let dummy = std::mem::MaybeUninit::uninit(); // just for test, we know BoxDecoration not use `ctx` to build. let ctx: BuildCtx<'static> = unsafe { dummy.assume_init() }; - let w = BoxDecoration::declare_builder().build_declare(&ctx); + let mut w = BoxDecoration::declare_builder().build_declare(&ctx); + let w = w.get_box_decoration_widget(); assert_eq!(w.read().border, None); assert_eq!(w.read().border_radius, None); diff --git a/core/src/builtin_widgets/cursor.rs b/core/src/builtin_widgets/cursor.rs index acbcf4d7f..2cdd48b79 100644 --- a/core/src/builtin_widgets/cursor.rs +++ b/core/src/builtin_widgets/cursor.rs @@ -3,12 +3,17 @@ use winit::window::CursorIcon; /// `Cursor` is an attribute to assign an `cursor` to a widget. -#[derive(Default, Debug, Declare)] +#[derive(Default, Debug)] pub struct Cursor { - #[declare(builtin, default)] pub cursor: CursorIcon, } +impl Declare for Cursor { + type Builder = FatObj<()>; + #[inline] + fn declare_builder() -> Self::Builder { FatObj::new(()) } +} + impl ComposeChild for Cursor { type Child = Widget; fn compose_child(this: impl StateWriter, child: Self::Child) -> impl WidgetBuilder { diff --git a/core/src/builtin_widgets/delay_drop.rs b/core/src/builtin_widgets/delay_drop.rs index 3c7e81bca..3e4e39751 100644 --- a/core/src/builtin_widgets/delay_drop.rs +++ b/core/src/builtin_widgets/delay_drop.rs @@ -12,12 +12,17 @@ use crate::prelude::*; /// dropped. /// /// It's useful when you need run a leave animation for a widget. -#[derive(Declare, Query)] +#[derive(Query, Default)] pub struct DelayDrop { - #[declare(builtin)] pub delay_drop_until: bool, } +impl Declare for DelayDrop { + type Builder = FatObj<()>; + #[inline] + fn declare_builder() -> Self::Builder { FatObj::new(()) } +} + impl ComposeChild for DelayDrop { type Child = Widget; #[inline] @@ -28,3 +33,48 @@ impl ComposeChild for DelayDrop { } } } + +#[cfg(test)] +mod tests { + use std::cell::Ref; + + use super::*; + use crate::{reset_test_env, test_helper::*}; + + #[test] + fn smoke() { + reset_test_env!(); + + let delay_drop = Stateful::new(false); + let c_delay_drop = delay_drop.clone_writer(); + let remove_widget = Stateful::new(false); + let c_remove_widget = remove_widget.clone_writer(); + let mut wnd = TestWindow::new(fn_widget! { + pipe! { + if *$remove_widget { + Void.widget_build(ctx!()) + } else { + FatObj::new(Void) + .delay_drop_until(pipe!(*$delay_drop)) + .widget_build(ctx!()) + } + } + }); + + fn tree_arena(wnd: &TestWindow) -> Ref { + let tree = wnd.widget_tree.borrow(); + Ref::map(tree, |t| &t.arena) + } + + let root = wnd.widget_tree.borrow().content_root(); + wnd.draw_frame(); + + *c_remove_widget.write() = true; + wnd.draw_frame(); + assert!(!root.is_dropped(&tree_arena(&wnd))); + + *c_delay_drop.write() = true; + wnd.draw_frame(); + assert!(root.is_dropped(&tree_arena(&wnd))); + } +} diff --git a/core/src/builtin_widgets/fitted_box.rs b/core/src/builtin_widgets/fitted_box.rs index 381fd78eb..a5fe83d00 100644 --- a/core/src/builtin_widgets/fitted_box.rs +++ b/core/src/builtin_widgets/fitted_box.rs @@ -1,9 +1,10 @@ use crate::prelude::*; use std::cell::Cell; -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Copy, Clone, Debug, PartialEq, Default)] pub enum BoxFit { /// Widget will not be scale. + #[default] None, /// The entire widget will completely fill its container. If the widget's /// aspect ratio does not match the aspect ratio of its box, then the widget @@ -28,14 +29,22 @@ pub enum BoxFit { } /// Widget set how its child should be scale to fit its box. -#[derive(Declare, Query, SingleChild)] +#[derive(Query, SingleChild, Default)] pub struct FittedBox { - #[declare(builtin)] pub box_fit: BoxFit, - #[declare(default)] scale_cache: Cell, } +impl Declare for FittedBox { + type Builder = FatObj<()>; + #[inline] + fn declare_builder() -> Self::Builder { FatObj::new(()) } +} + +impl FittedBox { + pub fn new(box_fit: BoxFit) -> Self { Self { box_fit, scale_cache: <_>::default() } } +} + impl Render for FittedBox { fn perform_layout(&self, mut clamp: BoxClamp, ctx: &mut LayoutCtx) -> Size { let container_size = clamp.max; diff --git a/core/src/builtin_widgets/focus_node.rs b/core/src/builtin_widgets/focus_node.rs index d3ff173b1..0ae243aaa 100644 --- a/core/src/builtin_widgets/focus_node.rs +++ b/core/src/builtin_widgets/focus_node.rs @@ -1,11 +1,16 @@ use crate::{events::focus_mgr::FocusHandle, prelude::*}; -#[derive(Declare, Query)] +#[derive(Query, Default)] pub struct RequestFocus { - #[declare(default)] handle: Option, } +impl Declare for RequestFocus { + type Builder = FatObj<()>; + #[inline] + fn declare_builder() -> Self::Builder { FatObj::new(()) } +} + impl ComposeChild for RequestFocus { type Child = Widget; fn compose_child(this: impl StateWriter, child: Self::Child) -> impl WidgetBuilder { diff --git a/core/src/builtin_widgets/global_anchor.rs b/core/src/builtin_widgets/global_anchor.rs index f1163fe00..8e09a1ead 100644 --- a/core/src/builtin_widgets/global_anchor.rs +++ b/core/src/builtin_widgets/global_anchor.rs @@ -1,12 +1,17 @@ use crate::{prelude::*, ticker::FrameMsg}; use std::rc::Rc; -#[derive(Declare, Query, Default)] +#[derive(Query, Default)] pub struct GlobalAnchor { - #[declare(builtin, default)] pub global_anchor: Anchor, } +impl Declare for GlobalAnchor { + type Builder = FatObj<()>; + #[inline] + fn declare_builder() -> Self::Builder { FatObj::new(()) } +} + impl ComposeChild for GlobalAnchor { type Child = Widget; #[inline] @@ -91,7 +96,7 @@ impl FatObj { offset: f32, ctx: &BuildCtx, ) -> impl Subscription { - let this = self.get_builtin_global_anchor(ctx).clone_writer(); + let this = self.get_global_anchor_widget().clone_writer(); let wnd = ctx.window(); let wid = wid.clone(); let tick_of_layout_ready = wnd @@ -112,7 +117,7 @@ impl FatObj { relative: f32, ctx: &BuildCtx, ) -> impl Subscription { - let this = self.get_builtin_global_anchor(ctx).clone_writer(); + let this = self.get_global_anchor_widget().clone_writer(); let wnd = ctx.window(); let wid = wid.clone(); let tick_of_layout_ready = wnd @@ -134,7 +139,7 @@ impl FatObj { relative: f32, ctx: &BuildCtx, ) -> impl Subscription { - let this = self.get_builtin_global_anchor(ctx).clone_writer(); + let this = self.get_global_anchor_widget().clone_writer(); let wnd = ctx.window(); let wid = wid.clone(); let tick_of_layout_ready = wnd @@ -155,7 +160,7 @@ impl FatObj { relative: f32, ctx: &BuildCtx, ) -> impl Subscription { - let this = self.get_builtin_global_anchor(ctx).clone_writer(); + let this = self.get_global_anchor_widget().clone_writer(); let wnd = ctx.window(); let wid = wid.clone(); let tick_of_layout_ready = wnd diff --git a/core/src/builtin_widgets/has_focus.rs b/core/src/builtin_widgets/has_focus.rs index 7d9543249..bdf6b2245 100644 --- a/core/src/builtin_widgets/has_focus.rs +++ b/core/src/builtin_widgets/has_focus.rs @@ -1,7 +1,6 @@ use crate::prelude::*; -#[derive(PartialEq, Clone, Declare)] +#[derive(PartialEq, Clone, Default)] pub struct HasFocus { - #[declare(skip, default)] focused: bool, } @@ -9,6 +8,12 @@ impl HasFocus { pub fn has_focus(&self) -> bool { self.focused } } +impl Declare for HasFocus { + type Builder = FatObj<()>; + #[inline] + fn declare_builder() -> Self::Builder { FatObj::new(()) } +} + impl ComposeChild for HasFocus { type Child = Widget; fn compose_child(this: impl StateWriter, child: Self::Child) -> impl WidgetBuilder { diff --git a/core/src/builtin_widgets/layout_box.rs b/core/src/builtin_widgets/layout_box.rs index daa97857e..a8d3a5a5b 100644 --- a/core/src/builtin_widgets/layout_box.rs +++ b/core/src/builtin_widgets/layout_box.rs @@ -1,13 +1,18 @@ use crate::prelude::*; /// Widget let user to access the layout result of its child. -#[derive(Declare)] +#[derive(Default)] pub struct LayoutBox { - #[declare(skip)] /// the rect box of its child and the coordinate is relative to its parent. rect: Rect, } +impl Declare for LayoutBox { + type Builder = FatObj<()>; + #[inline] + fn declare_builder() -> Self::Builder { FatObj::new(()) } +} + impl ComposeChild for LayoutBox { type Child = Widget; fn compose_child(this: impl StateWriter, child: Self::Child) -> impl WidgetBuilder { diff --git a/core/src/builtin_widgets/margin.rs b/core/src/builtin_widgets/margin.rs index 1e031e9b7..267b2ebba 100644 --- a/core/src/builtin_widgets/margin.rs +++ b/core/src/builtin_widgets/margin.rs @@ -9,12 +9,17 @@ pub struct EdgeInsets { } /// A widget that create space around its child. -#[derive(SingleChild, Default, Query, Clone, PartialEq, Declare)] +#[derive(SingleChild, Default, Query, Clone, PartialEq)] pub struct Margin { - #[declare(builtin, default)] pub margin: EdgeInsets, } +impl Declare for Margin { + type Builder = FatObj<()>; + #[inline] + fn declare_builder() -> Self::Builder { FatObj::new(()) } +} + impl Render for Margin { fn perform_layout(&self, clamp: BoxClamp, ctx: &mut LayoutCtx) -> Size { let thickness = self.margin.thickness(); diff --git a/core/src/builtin_widgets/mix_builtin.rs b/core/src/builtin_widgets/mix_builtin.rs index 2e4857027..4c09e5457 100644 --- a/core/src/builtin_widgets/mix_builtin.rs +++ b/core/src/builtin_widgets/mix_builtin.rs @@ -94,13 +94,13 @@ impl MixBuiltin { self } - pub fn on_mounted(&self, handler: impl FnMut(&mut LifecycleEvent) + 'static) -> &Self { + pub fn on_mounted(&self, handler: impl FnOnce(&mut LifecycleEvent) + 'static) -> &Self { self.flag_mark(BuiltinFlags::Lifecycle); let _ = self .subject() .filter_map(event_map_filter!(Mounted, LifecycleEvent)) .take(1) - .subscribe(handler); + .subscribe(life_fn_once_to_fn_mut(handler)); self } @@ -109,13 +109,13 @@ impl MixBuiltin { impl_event_callback!(self, Lifecycle, PerformedLayout, LifecycleEvent, handler) } - pub fn on_disposed(&self, handler: impl FnMut(&mut LifecycleEvent) + 'static) -> &Self { + pub fn on_disposed(&self, handler: impl FnOnce(&mut LifecycleEvent) + 'static) -> &Self { self.flag_mark(BuiltinFlags::Lifecycle); let _ = self .subject() .filter_map(event_map_filter!(Disposed, LifecycleEvent)) .take(1) - .subscribe(handler); + .subscribe(life_fn_once_to_fn_mut(handler)); self } @@ -277,50 +277,31 @@ impl MixBuiltin { /// sequential keyboard navigation (usually with the Tab key). pub fn is_focus_node(&self) -> bool { self.flags.get().contains(BuiltinFlags::Focus) } - /// It accepts an integer as a value, with different results depending on the - /// integer's value: - /// - A negative value (usually -1) means that the widget is not reachable via - /// sequential keyboard navigation, but could be focused with API or - /// visually by clicking with the mouse. - /// - Zero means that the element should be focusable in sequential keyboard - /// navigation, after any positive tab_index values and its order is defined - /// by the tree's source order. - /// - A positive value means the element should be focusable in sequential - /// keyboard navigation, with its order defined by the value of the number. - /// That is, tab_index=4 is focused before tab_index=5 and tab_index=0, but - /// after tab_index=3. If multiple elements share the same positive - /// tab_index value, their order relative to each other follows their - /// position in the tree source. The maximum value for tab_index is 32767. - /// If not specified, it takes the default value 0. - pub fn tab_index(&self) -> i16 { (self.flags.get().bits() >> 48) as i16 } - - /// Set the tab index of the focus node - pub fn set_tab_index(&self, tab_idx: i16) { + pub fn get_tab_index(&self) -> i16 { (self.flags.get().bits() >> 48) as i16 } + + pub fn set_tab_index(&self, tab_idx: i16) -> &Self { + self.flag_mark(BuiltinFlags::Focus); let flags = self.flags.get().bits() | ((tab_idx as u64) << 48); self.flags.set(BuiltinFlags::from_bits_retain(flags)); + self } - /// Indicates whether the `widget` should automatically get focus when the - /// window loads. - /// - /// Only one widget should have this attribute specified. If there are - /// several, the widget nearest the root, get the initial - /// focus. - pub fn auto_focus(&self) -> bool { self.flags.get().contains(BuiltinFlags::AutoFocus) } + pub fn is_auto_focus(&self) -> bool { self.flags.get().contains(BuiltinFlags::AutoFocus) } - pub fn set_auto_focus(&self, v: bool) { + pub fn set_auto_focus(&self, v: bool) -> &Self { if v { - self.flag_mark(BuiltinFlags::AutoFocus); + self.flag_mark(BuiltinFlags::AutoFocus | BuiltinFlags::Focus); } else { let mut flag = self.flags.get(); flag.remove(BuiltinFlags::AutoFocus); - self.flags.set(flag) + self.flags.set(flag); } + self } fn merge(&self, other: Self) { - let tab_index = self.tab_index(); - let other_tab_index = other.tab_index(); + let tab_index = self.get_tab_index(); + let other_tab_index = other.get_tab_index(); self.flags.set(self.flags.get() | other.flags.get()); if other_tab_index != 0 { self.set_tab_index(other_tab_index); @@ -341,7 +322,7 @@ impl MixBuiltin { self .on_mounted(move |e| { e.query_type(|mix: &MixBuiltin| { - let auto_focus = mix.auto_focus(); + let auto_focus = mix.is_auto_focus(); e.window().add_focus_node(e.id, auto_focus, FocusType::Node) }); }) @@ -349,42 +330,51 @@ impl MixBuiltin { } } +fn life_fn_once_to_fn_mut( + handler: impl FnOnce(&mut LifecycleEvent), +) -> impl FnMut(&mut LifecycleEvent) { + let mut handler = Some(handler); + move |e| { + if let Some(h) = handler.take() { + h(e); + } + } +} + impl MixBuiltinDeclarer { - pub fn tab_index<_M, _V>(self, v: _V) -> Self + pub fn tab_index(self, v: V) -> Self where - DeclareInit: DeclareFrom<_V, _M>, + DeclareInit: DeclareFrom, { let inner = self.0.read(); self.0.read().flag_mark(BuiltinFlags::Focus); - let v = DeclareInit::::declare_from(v); - match v { - DeclareInit::Value(v) => inner.set_tab_index(v), - DeclareInit::Pipe(p) => { - let (v, p) = p.into_pipe().unzip(); - inner.set_tab_index(v); - let this = self.0.clone_reader(); - p.subscribe(move |(_, v)| this.read().set_tab_index(v)); - } + let (v, observable) = DeclareInit::declare_from(v).unzip(); + inner.set_tab_index(v); + if let Some(observable) = observable { + let this = self.0.clone_reader(); + let u = observable.subscribe(move |(_, v)| { + this.read().set_tab_index(v); + }); + inner.on_disposed(move |_| u.unsubscribe()); } drop(inner); self } - pub fn auto_focus<_M, _V>(self, v: _V) -> Self + pub fn auto_focus(self, v: V) -> Self where - DeclareInit: DeclareFrom<_V, _M>, + DeclareInit: DeclareFrom, { let inner = self.0.read(); inner.flag_mark(BuiltinFlags::Focus); - let v = DeclareInit::::declare_from(v); - match v { - DeclareInit::Value(v) => inner.set_auto_focus(v), - DeclareInit::Pipe(p) => { - let (v, p) = p.into_pipe().unzip(); - inner.set_auto_focus(v); - let this = self.0.clone_reader(); - p.subscribe(move |(_, v)| this.read().set_auto_focus(v)); - } + let (v, observable) = DeclareInit::declare_from(v).unzip(); + inner.set_auto_focus(v); + if let Some(observable) = observable { + let this = self.0.clone_reader(); + let u = observable.subscribe(move |(_, v)| { + this.read().set_auto_focus(v); + }); + inner.on_disposed(move |_| u.unsubscribe()); } drop(inner); self @@ -395,7 +385,7 @@ impl MixBuiltinDeclarer { self } - pub fn on_mounted(self, handler: impl FnMut(&mut LifecycleEvent) + 'static) -> Self { + pub fn on_mounted(self, handler: impl FnOnce(&mut LifecycleEvent) + 'static) -> Self { self.0.read().on_mounted(handler); self } @@ -405,7 +395,7 @@ impl MixBuiltinDeclarer { self } - pub fn on_disposed(self, handler: impl FnMut(&mut LifecycleEvent) + 'static) -> Self { + pub fn on_disposed(self, handler: impl FnOnce(&mut LifecycleEvent) + 'static) -> Self { self.0.read().on_disposed(handler); self } diff --git a/core/src/builtin_widgets/mouse_hover.rs b/core/src/builtin_widgets/mouse_hover.rs index f834eb929..4460b6e73 100644 --- a/core/src/builtin_widgets/mouse_hover.rs +++ b/core/src/builtin_widgets/mouse_hover.rs @@ -1,8 +1,7 @@ use crate::prelude::*; -#[derive(PartialEq, Clone, Declare)] +#[derive(PartialEq, Clone, Default)] pub struct MouseHover { - #[declare(skip, default)] hover: bool, } @@ -10,6 +9,12 @@ impl MouseHover { pub fn mouse_hover(&self) -> bool { self.hover } } +impl Declare for MouseHover { + type Builder = FatObj<()>; + #[inline] + fn declare_builder() -> Self::Builder { FatObj::new(()) } +} + impl ComposeChild for MouseHover { type Child = Widget; fn compose_child(this: impl StateWriter, child: Self::Child) -> impl WidgetBuilder { diff --git a/core/src/builtin_widgets/opacity.rs b/core/src/builtin_widgets/opacity.rs index 8b385eac1..4cb0d11e8 100644 --- a/core/src/builtin_widgets/opacity.rs +++ b/core/src/builtin_widgets/opacity.rs @@ -1,11 +1,21 @@ use crate::prelude::*; -#[derive(Declare, Default, Query, Clone, SingleChild)] +#[derive(Query, Clone, SingleChild)] pub struct Opacity { - #[declare(builtin, default = 1.)] pub opacity: f32, } +impl Declare for Opacity { + type Builder = FatObj<()>; + #[inline] + fn declare_builder() -> Self::Builder { FatObj::new(()) } +} + +impl Default for Opacity { + #[inline] + fn default() -> Self { Self { opacity: 1.0 } } +} + impl Render for Opacity { #[inline] fn perform_layout(&self, clamp: BoxClamp, ctx: &mut LayoutCtx) -> Size { diff --git a/core/src/builtin_widgets/padding.rs b/core/src/builtin_widgets/padding.rs index fe3392dd3..78497c98f 100644 --- a/core/src/builtin_widgets/padding.rs +++ b/core/src/builtin_widgets/padding.rs @@ -1,12 +1,17 @@ use crate::prelude::*; /// A widget that insets its child by the given padding. -#[derive(SingleChild, Query, Clone, Declare)] +#[derive(SingleChild, Query, Clone, Default)] pub struct Padding { - #[declare(builtin)] pub padding: EdgeInsets, } +impl Declare for Padding { + type Builder = FatObj<()>; + #[inline] + fn declare_builder() -> Self::Builder { FatObj::new(()) } +} + impl Render for Padding { fn perform_layout(&self, clamp: BoxClamp, ctx: &mut LayoutCtx) -> Size { let child = match ctx.single_child() { diff --git a/core/src/builtin_widgets/pointer_pressed.rs b/core/src/builtin_widgets/pointer_pressed.rs index 4c05269dc..e270f09f7 100644 --- a/core/src/builtin_widgets/pointer_pressed.rs +++ b/core/src/builtin_widgets/pointer_pressed.rs @@ -2,12 +2,17 @@ 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(Default)] pub struct PointerPressed { - #[declare(skip, builtin)] pointer_pressed: bool, } +impl Declare for PointerPressed { + type Builder = FatObj<()>; + #[inline] + fn declare_builder() -> Self::Builder { FatObj::new(()) } +} + impl PointerPressed { // return if its child widget is pressed. #[inline] diff --git a/core/src/builtin_widgets/scrollable.rs b/core/src/builtin_widgets/scrollable.rs index 9698ab32a..7b2177afc 100644 --- a/core/src/builtin_widgets/scrollable.rs +++ b/core/src/builtin_widgets/scrollable.rs @@ -15,18 +15,20 @@ pub enum Scrollable { } /// Helper struct for builtin scrollable field. -#[derive(Declare)] +#[derive(Default)] pub struct ScrollableWidget { - #[declare(builtin, default)] pub scrollable: Scrollable, - #[declare(builtin, default)] pub scroll_pos: Point, - #[declare(skip)] page: Size, - #[declare(skip)] content_size: Size, } +impl Declare for ScrollableWidget { + type Builder = FatObj<()>; + #[inline] + fn declare_builder() -> Self::Builder { FatObj::new(()) } +} + impl ComposeChild for ScrollableWidget { type Child = Widget; fn compose_child(this: impl StateWriter, child: Self::Child) -> impl WidgetBuilder { diff --git a/core/src/builtin_widgets/transform_widget.rs b/core/src/builtin_widgets/transform_widget.rs index ddcdd6786..0b9b4e5df 100644 --- a/core/src/builtin_widgets/transform_widget.rs +++ b/core/src/builtin_widgets/transform_widget.rs @@ -1,11 +1,16 @@ use crate::{prelude::*, widget::hit_test_impl}; -#[derive(SingleChild, Query, Declare, Clone)] +#[derive(SingleChild, Query, Clone, Default)] pub struct TransformWidget { - #[declare(builtin, default)] pub transform: Transform, } +impl Declare for TransformWidget { + type Builder = FatObj<()>; + #[inline] + fn declare_builder() -> Self::Builder { FatObj::new(()) } +} + impl Render for TransformWidget { #[inline] fn perform_layout(&self, clamp: BoxClamp, ctx: &mut LayoutCtx) -> Size { diff --git a/core/src/builtin_widgets/visibility.rs b/core/src/builtin_widgets/visibility.rs index 6def61daf..2dc698394 100644 --- a/core/src/builtin_widgets/visibility.rs +++ b/core/src/builtin_widgets/visibility.rs @@ -1,11 +1,16 @@ use crate::prelude::*; -#[derive(Declare)] +#[derive(Default)] pub struct Visibility { - #[declare(builtin)] pub visible: bool, } +impl Declare for Visibility { + type Builder = FatObj<()>; + #[inline] + fn declare_builder() -> Self::Builder { FatObj::new(()) } +} + impl ComposeChild for Visibility { type Child = Widget; fn compose_child(this: impl StateWriter, child: Self::Child) -> impl WidgetBuilder { diff --git a/core/src/declare.rs b/core/src/declare.rs index af30f42bd..5da44f10d 100644 --- a/core/src/declare.rs +++ b/core/src/declare.rs @@ -24,7 +24,7 @@ pub enum DeclareInit { Pipe(BoxPipe), } -type ValueStream = BoxOp<'static, (ModifyScope, V), Infallible>; +pub type ValueStream = BoxOp<'static, (ModifyScope, V), Infallible>; impl DeclareInit { pub fn unzip(self) -> (V, Option>) { @@ -52,8 +52,9 @@ impl> DeclareFrom for DeclareInit { fn declare_from(value: V) -> Self { Self::Value(value.into()) } } -impl DeclareFrom> for DeclareInit +impl DeclareFrom> for DeclareInit where + P: Pipe + 'static, V: From + 'static, { #[inline] diff --git a/core/src/events/focus_mgr.rs b/core/src/events/focus_mgr.rs index d33616636..d52bceced 100644 --- a/core/src/events/focus_mgr.rs +++ b/core/src/events/focus_mgr.rs @@ -362,7 +362,7 @@ impl FocusManager { let wid = self.get(node_id)?.wid?; let tree = wnd.widget_tree.borrow(); let r = wid.get(&tree.arena)?; - r.query_most_outside(|s: &MixBuiltin| s.tab_index()) + r.query_most_outside(|s: &MixBuiltin| s.get_tab_index()) }; get_index().unwrap_or(0) diff --git a/core/src/lib.rs b/core/src/lib.rs index 8e205d745..81188cb6f 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -59,8 +59,8 @@ pub mod prelude { pub use ribir_geom::*; #[doc(no_inline)] pub use ribir_macros::{ - ctx, fn_widget, include_svg, map_writer, pipe, rdl, ribir_expanded_ಠ_ಠ, split_writer, watch, - Declare, Lerp, MultiChild, PairChild, Query, SingleChild, Template, + ctx, fn_widget, include_svg, map_writer, pipe, rdl, ribir_expanded_ಠ_ಠ, simple_declare, + split_writer, watch, Declare, Lerp, MultiChild, PairChild, Query, SingleChild, Template, }; #[doc(no_inline)] pub use ribir_painter::*; diff --git a/core/src/test_helper.rs b/core/src/test_helper.rs index 60cbf8621..4a02e4e8d 100644 --- a/core/src/test_helper.rs +++ b/core/src/test_helper.rs @@ -200,7 +200,7 @@ impl Render for MockStack { fn paint(&self, _: &mut PaintingCtx) {} } -#[derive(Declare, Query, MultiChild)] +#[derive(Declare, Query, MultiChild, Default)] pub struct MockMulti; #[derive(Declare, Query, Clone, SingleChild)] diff --git a/core/src/widget_children.rs b/core/src/widget_children.rs index 1cf719cf4..3c8370544 100644 --- a/core/src/widget_children.rs +++ b/core/src/widget_children.rs @@ -152,6 +152,15 @@ impl Pair { pub fn parent(self) -> W { self.parent } } +impl Pair, C> { + /// Replace the host of the FatObj in parent with the child, this is useful + /// when the host of the `FatObj` is useless. + pub fn child_replace_host(self) -> FatObj { + let Self { parent, child } = self; + parent.map(|_| child) + } +} + pub trait PairWithChild { type Target; fn with_child(self, child: C, ctx: &BuildCtx) -> Self::Target; @@ -196,9 +205,9 @@ mod tests { #[derive(Template)] struct PageTml { - _header: WidgetOf
, - _content: WidgetOf, - _footer: WidgetOf