zng_wgt_style/
lib.rs

1#![doc(html_favicon_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo.png")]
3//!
4//! Style building blocks.
5//!
6//! # Crate
7//!
8#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9#![warn(unused_extern_crates)]
10#![warn(missing_docs)]
11// suppress nag about very simple boxed closure signatures.
12#![expect(clippy::type_complexity)]
13
14use zng_app::widget::builder::{Importance, PropertyId};
15use zng_wgt::prelude::*;
16
17#[doc(hidden)]
18pub use zng_wgt::prelude::clmv as __clmv;
19
20use std::sync::Arc;
21use std::{fmt, ops};
22
23/// Represents a set of properties that can be applied to any styleable widget.
24///
25/// Style can be instantiated using the same syntax as any widget, but it produces a [`StyleBuilder`]
26/// instance instead of a widget. Widgets that have [`StyleMix<P>`] can be modified using properties
27/// defined in a style, the properties are dynamically spliced into each widget instance.
28///
29/// # Extend/Replace
30///
31/// Style instances extend the contextual style by default, meaning all properties set on the style are inserted over
32/// the parent style, so properties set on the contextual style that are not reset in the new style are retained. You
33/// can set [`replace`](#replace) on a style to `true` to fully remove all contextual properties and only use the
34/// new style properties.
35///
36/// # Inherit Style
37///
38/// Note that you can declare a custom style *widget* using the same inheritance mechanism of normal widgets, as long
39/// as they build to [`StyleBuilder`]. This is different from the *extend/replace* mechanism as it operates on the style
40/// type, not the instances.
41#[widget($crate::Style)]
42pub struct Style(zng_app::widget::base::NonWidgetBase);
43impl Style {
44    /// Build the style.
45    pub fn widget_build(&mut self) -> StyleBuilder {
46        StyleBuilder::from_builder(self.widget_take())
47    }
48}
49
50/// Fully replace the contextual style.
51///
52/// This is not enabled by default, if set to `true` the contextual style properties are removed.
53#[property(WIDGET, capture, default(false), widget_impl(Style))]
54pub fn replace(replace: impl IntoValue<bool>) {}
55
56/// Styleable widget mixin.
57///
58/// Widgets that inherit this mix-in have a `style_fn` property that can be set to a [`style_fn!`]
59/// that generates properties that are dynamically injected into the widget to alter its appearance.
60///
61/// The style mixin drastically affects the widget build process, only the `style_base_fn`, `style_fn` and `when` condition
62/// properties that affects these are instantiated with the widget, all the other properties and intrinsic nodes are instantiated
63/// on init, after the style is generated.
64///
65/// Widgets that inherit this mix-in must call [`style_intrinsic`] in their own `widget_intrinsic`, the call is missing
66/// the widget will log an error on instantiation and only the `style_base_fn` will be used. You can use the [`impl_style_fn!`]
67/// macro to generate the style var and property.
68///
69/// [`style_intrinsic`]: StyleMix::style_intrinsic
70#[widget_mixin]
71pub struct StyleMix<P>(P);
72impl<P: WidgetImpl> StyleMix<P> {
73    fn widget_intrinsic(&mut self) {
74        self.base()
75            .widget_builder()
76            .set_custom_build(|b| StyleMix::<()>::custom_build(b, None));
77    }
78
79    /// Setup the style build.
80    pub fn style_intrinsic(&mut self, style_var: ContextVar<StyleFn>, style_fn: PropertyId) {
81        self.base()
82            .widget_builder()
83            .set_custom_build(move |b| StyleMix::<()>::custom_build(b, Some((style_var, style_fn))));
84    }
85}
86impl<P> StyleMix<P> {
87    /// The custom build that is set on intrinsic by the mixin.
88    pub fn custom_build(mut wgt: WidgetBuilder, cfg: Option<(ContextVar<StyleFn>, PropertyId)>) -> BoxedUiNode {
89        let (style_var, style_id) = cfg.unwrap_or_else(|| {
90            tracing::error!("missing `style_intrinsic` in `{}`", wgt.widget_type().path);
91            (MISSING_STYLE_VAR, property_id!(self::missing_style_fn))
92        });
93
94        // 1 - "split_off" the properties `style_base_fn` and `style_fn`
95        //     this moves the properties and any `when` that affects them to a new widget builder.
96        let style_base_id = property_id!(style_base_fn);
97        let mut style_builder = WidgetBuilder::new(wgt.widget_type());
98        wgt.split_off([style_base_id, style_id], &mut style_builder);
99
100        if style_builder.has_properties() {
101            // 2.a - There was a `style_fn` property, build a "mini widget" that is only the style property
102            //       and when condition properties that affect it.
103
104            #[cfg(feature = "trace_widget")]
105            wgt.push_build_action(|wgt| {
106                // avoid double trace as the style builder already inserts a widget tracer.
107                wgt.disable_trace_widget();
108            });
109
110            let mut wgt = Some(wgt);
111            style_builder.push_build_action(move |b| {
112                // 3 - The actual style_node and builder is a child of the "mini widget".
113
114                let style_base = b
115                    .capture_var::<StyleFn>(style_base_id)
116                    .unwrap_or_else(|| LocalVar(StyleFn::nil()).boxed());
117                let style = b
118                    .capture_var::<StyleFn>(style_id)
119                    .unwrap_or_else(|| LocalVar(StyleFn::nil()).boxed());
120
121                b.set_child(style_node(None, wgt.take().unwrap(), style_base, style_var, style));
122            });
123            // 4 - Build the "mini widget",
124            //     if the `style` property was not affected by any `when` this just returns the `StyleNode`.
125            style_builder.build()
126        } else {
127            // 2.b - There was no `style_fn` property, this widget is not styleable, just build the default.
128            wgt.build()
129        }
130    }
131}
132
133#[doc(hidden)]
134pub mod __impl_style_context_util {
135    pub use zng_wgt::prelude::{IntoVar, UiNode, context_var, property};
136}
137
138/// Implements the contextual `STYLE_FN_VAR` and `style_fn`.
139///
140/// This is a helper for [`StyleMix<P>`](struct@StyleMix) implementers, see the `zng::style` module level
141/// documentation for more details.
142#[macro_export]
143macro_rules! impl_style_fn {
144    ($Widget:ty) => {
145        $crate::__impl_style_context_util::context_var! {
146            /// Contextual style variable.
147            ///
148            /// Use [`style_fn`](fn@style_fn) to set.
149            pub static STYLE_FN_VAR: $crate::StyleFn = $crate::StyleFn::nil();
150        }
151
152        /// Extends or replaces the widget style.
153        ///
154        /// Properties and `when` conditions in the style are applied to the widget. Style instances extend the contextual style
155        /// by default, you can set `replace` on a style to `true` to fully replace.
156        #[$crate::__impl_style_context_util::property(WIDGET, default($crate::StyleFn::nil()), widget_impl($Widget))]
157        pub fn style_fn(
158            child: impl $crate::__impl_style_context_util::UiNode,
159            style_fn: impl $crate::__impl_style_context_util::IntoVar<$crate::StyleFn>,
160        ) -> impl $crate::__impl_style_context_util::UiNode {
161            $crate::with_style_fn(child, STYLE_FN_VAR, style_fn)
162        }
163    };
164}
165
166/// Widget's base style. All other styles set using `style_fn` are applied over this style.
167///
168/// Is `nil` by default.
169#[property(WIDGET, capture, default(StyleFn::nil()), widget_impl(StyleMix<P>))]
170pub fn style_base_fn(style: impl IntoVar<StyleFn>) {}
171
172/// Helper for declaring the `style_fn` property.
173///
174/// The [`impl_style_fn!`] macro uses this function as the implementation of `style_fn`.
175pub fn with_style_fn(child: impl UiNode, style_context: ContextVar<StyleFn>, style: impl IntoVar<StyleFn>) -> impl UiNode {
176    with_context_var(
177        child,
178        style_context,
179        merge_var!(style_context, style.into_var(), |base, over| {
180            base.clone().with_extend(over.clone())
181        }),
182    )
183}
184
185fn style_node(
186    child: Option<BoxedUiNode>,
187    builder: WidgetBuilder,
188    captured_style_base: BoxedVar<StyleFn>,
189    style_var: ContextVar<StyleFn>,
190    captured_style: BoxedVar<StyleFn>,
191) -> impl UiNode {
192    let style_vars = [captured_style_base, style_var.boxed(), captured_style];
193    match_node_typed(child, move |c, op| match op {
194        UiNodeOp::Init => {
195            let mut style_builder = StyleBuilder::default();
196            for var in &style_vars {
197                WIDGET.sub_var(var);
198
199                if let Some(style) = var.get().call(&StyleArgs {}) {
200                    style_builder.extend(style);
201                }
202            }
203
204            if !style_builder.is_empty() {
205                let mut builder = builder.clone();
206                builder.extend(style_builder.into_builder());
207                *c.child() = Some(builder.default_build());
208            } else {
209                *c.child() = Some(builder.clone().default_build());
210            }
211        }
212        UiNodeOp::Deinit => {
213            c.deinit();
214            *c.child() = None;
215        }
216        UiNodeOp::Update { .. } => {
217            if style_vars.iter().any(|v| v.is_new()) {
218                WIDGET.reinit();
219                WIDGET.update_info().layout().render();
220                c.delegated();
221            }
222        }
223        _ => {}
224    })
225}
226
227/// Represents a style instance.
228///
229/// Use the [`Style!`] *widget* to declare.
230///
231/// [`Style!`]: struct@Style
232#[derive(Debug, Clone)]
233pub struct StyleBuilder {
234    builder: WidgetBuilder,
235    replace: bool,
236}
237impl Default for StyleBuilder {
238    fn default() -> Self {
239        Self {
240            builder: WidgetBuilder::new(Style::widget_type()),
241            replace: false,
242        }
243    }
244}
245impl StyleBuilder {
246    /// Importance of style properties set by default in style widgets.
247    ///
248    /// Is `Importance::WIDGET - 10`.
249    pub const WIDGET_IMPORTANCE: Importance = Importance(Importance::WIDGET.0 - 10);
250
251    /// Importance of style properties set in style instances.
252    ///
253    /// Is `Importance::INSTANCE - 10`.
254    pub const INSTANCE_IMPORTANCE: Importance = Importance(Importance::INSTANCE.0 - 10);
255
256    /// Negative offset on the position index of style properties.
257    ///
258    /// Is `1`.
259    pub const POSITION_OFFSET: u16 = 1;
260
261    /// New style from a widget builder.
262    ///
263    /// The importance and position index of properties are adjusted,
264    /// any custom build or widget build action is ignored.
265    pub fn from_builder(mut wgt: WidgetBuilder) -> StyleBuilder {
266        wgt.clear_build_actions();
267        wgt.clear_custom_build();
268        let replace = wgt.capture_value_or_default(property_id!(self::replace));
269        for p in wgt.properties_mut() {
270            *p.importance = match *p.importance {
271                Importance::WIDGET => StyleBuilder::WIDGET_IMPORTANCE,
272                Importance::INSTANCE => StyleBuilder::INSTANCE_IMPORTANCE,
273                other => other,
274            };
275            p.position.index = p.position.index.saturating_sub(Self::POSITION_OFFSET);
276        }
277        StyleBuilder { builder: wgt, replace }
278    }
279
280    /// Unwrap the style dynamic widget.
281    pub fn into_builder(self) -> WidgetBuilder {
282        self.builder
283    }
284
285    /// Override or replace `self` with `other`.
286    pub fn extend(&mut self, other: StyleBuilder) {
287        if other.is_replace() {
288            *self = other;
289        } else {
290            self.builder.extend(other.builder);
291        }
292    }
293
294    /// if the style removes all contextual properties.
295    pub fn is_replace(&self) -> bool {
296        self.replace
297    }
298
299    /// If the style does nothing.
300    pub fn is_empty(&self) -> bool {
301        !self.builder.has_properties() && !self.builder.has_whens() && !self.builder.has_unsets()
302    }
303}
304impl From<StyleBuilder> for WidgetBuilder {
305    fn from(t: StyleBuilder) -> Self {
306        t.into_builder()
307    }
308}
309impl From<WidgetBuilder> for StyleBuilder {
310    fn from(p: WidgetBuilder) -> Self {
311        StyleBuilder::from_builder(p)
312    }
313}
314impl_from_and_into_var! {
315    /// Singleton.
316    fn from(style: StyleBuilder) -> StyleFn {
317        StyleFn::singleton(style)
318    }
319}
320
321/// Arguments for [`StyleFn`] closure.
322///
323/// Empty struct, there are no style args in the current release, this struct is declared so that if
324/// args may be introduced in the future with minimal breaking changes.
325#[derive(Debug, Default)]
326pub struct StyleArgs {}
327
328/// Boxed shared closure that generates a style instance for a given widget context.
329///
330/// You can also use the [`style_fn!`] macro to instantiate.
331#[derive(Clone)]
332pub struct StyleFn(Option<Arc<dyn Fn(&StyleArgs) -> Option<StyleBuilder> + Send + Sync>>);
333impl Default for StyleFn {
334    fn default() -> Self {
335        Self::nil()
336    }
337}
338impl PartialEq for StyleFn {
339    fn eq(&self, other: &Self) -> bool {
340        match (&self.0, &other.0) {
341            (None, None) => true,
342            (Some(a), Some(b)) => Arc::ptr_eq(a, b),
343            _ => false,
344        }
345    }
346}
347impl StyleFn {
348    /// Default function, produces an empty style.
349    pub fn nil() -> Self {
350        Self(None)
351    }
352
353    /// If this function represents no style.
354    pub fn is_nil(&self) -> bool {
355        self.0.is_none()
356    }
357
358    /// New style function, the `func` closure is called for each styleable widget, before the widget is inited.
359    pub fn new(func: impl Fn(&StyleArgs) -> StyleBuilder + Send + Sync + 'static) -> Self {
360        Self(Some(Arc::new(move |a| {
361            let style = func(a);
362            if style.is_empty() { None } else { Some(style) }
363        })))
364    }
365
366    /// New style function that returns clones of `style`.
367    ///
368    /// Note that if the `style` contains properties with node values the nodes will be moved to
369    /// the last usage of the style, as nodes can't be cloned.
370    ///
371    /// Also note that the `style` will stay in memory for the lifetime of the `StyleFn`.
372    pub fn singleton(style: StyleBuilder) -> Self {
373        Self::new(move |_| style.clone())
374    }
375
376    /// Call the function to create a style for the styleable widget in the context.
377    ///
378    /// Returns `None` if [`is_nil`] or empty, otherwise returns the style.
379    ///
380    /// Note that you can call the style function directly:
381    ///
382    /// ```
383    /// # use zng_wgt_style::{StyleFn, StyleArgs};
384    /// fn foo(func: &StyleFn) {
385    ///     let a = func.call(&StyleArgs {});
386    ///     let b = func(&StyleArgs {});
387    /// }
388    /// ```
389    ///
390    /// In the example above `a` and `b` are both calls to the style function.
391    ///
392    /// [`is_nil`]: Self::is_nil
393    pub fn call(&self, args: &StyleArgs) -> Option<StyleBuilder> {
394        self.0.as_ref()?(args)
395    }
396
397    /// New style function that instantiates `self` and `other` and then [`extend`] `self` with `other`.
398    ///
399    /// [`extend`]: StyleBuilder::extend
400    pub fn with_extend(self, other: StyleFn) -> StyleFn {
401        if self.is_nil() {
402            other
403        } else if other.is_nil() {
404            self
405        } else {
406            StyleFn::new(move |args| match (self(args), other(args)) {
407                (Some(mut a), Some(b)) => {
408                    a.extend(b);
409                    a
410                }
411                (Some(r), None) | (None, Some(r)) => r,
412                _ => StyleBuilder::default(),
413            })
414        }
415    }
416}
417impl fmt::Debug for StyleFn {
418    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
419        write!(f, "StyleFn(_)")
420    }
421}
422impl ops::Deref for StyleFn {
423    type Target = dyn Fn(&StyleArgs) -> Option<StyleBuilder>;
424
425    fn deref(&self) -> &Self::Target {
426        if let Some(func) = &self.0 { &**func } else { &nil_func }
427    }
428}
429fn nil_func(_: &StyleArgs) -> Option<StyleBuilder> {
430    None
431}
432
433/// <span data-del-macro-root></span> Declares a style function closure.
434///
435/// The output type is a [`StyleFn`], the input can be a function name path or a closure,
436/// with input type `&StyleArgs`. The closure syntax is clone-move ([`clmv!`]).
437///
438/// # Examples
439///
440/// The example below declares a closure that prints every time it is used, the closure captures `cloned` by clone-move
441/// and `moved` by move. The closure ignores the [`StyleArgs`] because it is empty.
442///
443/// ```
444/// # zng_wgt::enable_widget_macros!();
445/// # use zng_wgt::prelude::*;
446/// # use zng_wgt_style::*;
447/// # fn main() {
448/// let cloned = var(10u32);
449/// let moved = var(20u32);
450/// let style_fn = style_fn!(cloned, |_| {
451///     println!(
452///         "style instantiated in {:?}, with captured values, {} and {}",
453///         WIDGET.try_id(),
454///         cloned.get(),
455///         moved.get()
456///     );
457///
458///     Style! {
459///         // ..
460///     }
461/// });
462/// # }
463/// ```
464///
465/// [`clmv!`]: zng_wgt::prelude::clmv
466#[macro_export]
467macro_rules! style_fn {
468    ($fn:path) => {
469        $crate::StyleFn::new($fn)
470    };
471    ($($tt:tt)+) => {
472        $crate::StyleFn::new($crate::__clmv! {
473            $($tt)+
474        })
475    };
476    () => {
477        $crate::StyleFn::nil()
478    };
479}
480
481context_var! {
482    static MISSING_STYLE_VAR: StyleFn = StyleFn::nil();
483}
484#[property(WIDGET)]
485fn missing_style_fn(child: impl UiNode, _s: impl IntoVar<StyleFn>) -> impl UiNode {
486    child
487}