zng_wgt/
layout_props.rs

1use std::fmt;
2
3use zng_layout::context::DIRECTION_VAR;
4
5use crate::prelude::*;
6
7/// Margin space around the widget.
8///
9/// This property adds side offsets to the widget inner visual, it will be combined with the other
10/// layout properties of the widget to define the inner visual position and widget size.
11///
12/// This property disables inline layout for the widget.
13#[property(LAYOUT, default(0))]
14pub fn margin(child: impl IntoUiNode, margin: impl IntoVar<SideOffsets>) -> UiNode {
15    let margin = margin.into_var();
16    match_node(child, move |child, op| match op {
17        UiNodeOp::Init => {
18            WIDGET.sub_var_layout(&margin);
19        }
20        UiNodeOp::Measure { wm, desired_size } => {
21            let margin = margin.layout();
22            let size_increment = PxSize::new(margin.horizontal(), margin.vertical());
23            *desired_size = LAYOUT.with_constraints(LAYOUT.constraints().with_less_size(size_increment), || {
24                wm.measure_block(child.node())
25            });
26            child.delegated();
27            desired_size.width += size_increment.width;
28            desired_size.height += size_increment.height;
29        }
30        UiNodeOp::Layout { wl, final_size } => {
31            let margin = margin.layout();
32            let size_increment = PxSize::new(margin.horizontal(), margin.vertical());
33
34            *final_size = LAYOUT.with_constraints(LAYOUT.constraints().with_less_size(size_increment), || child.layout(wl));
35            let mut translate = PxVector::zero();
36            final_size.width += size_increment.width;
37            translate.x = margin.left;
38            final_size.height += size_increment.height;
39            translate.y = margin.top;
40            wl.translate(translate);
41        }
42        _ => {}
43    })
44}
45
46/// Aligns the widget within the available space.
47///
48/// This property disables inline layout for the widget.
49///
50/// See [`Align`] for more details.
51///
52///  [`Align`]: zng_layout::unit::Align
53#[property(LAYOUT, default(Align::FILL))]
54pub fn align(child: impl IntoUiNode, alignment: impl IntoVar<Align>) -> UiNode {
55    let alignment = alignment.into_var();
56    match_node(child, move |child, op| match op {
57        UiNodeOp::Init => {
58            WIDGET.sub_var_layout(&alignment);
59        }
60        UiNodeOp::Measure { wm, desired_size } => {
61            let align = alignment.get();
62            let child_size = LAYOUT.with_constraints(align.child_constraints(LAYOUT.constraints()), || wm.measure_block(child.node()));
63            child.delegated();
64            *desired_size = align.measure(child_size, LAYOUT.constraints());
65        }
66        UiNodeOp::Layout { wl, final_size } => {
67            let align = alignment.get();
68            let child_size = LAYOUT.with_constraints(align.child_constraints(LAYOUT.constraints()), || child.layout(wl));
69            let (size, offset, baseline) = align.layout(child_size, LAYOUT.constraints(), LAYOUT.direction());
70            wl.translate(offset);
71            if baseline {
72                wl.translate_baseline(true);
73            }
74            *final_size = size;
75        }
76        _ => {}
77    })
78}
79
80/// If the layout direction is right-to-left.
81///
82/// The `state` is bound to [`DIRECTION_VAR`].
83///
84/// [`DIRECTION_VAR`]: zng_layout::context::DIRECTION_VAR
85#[property(LAYOUT)]
86pub fn is_rtl(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
87    bind_state(child, DIRECTION_VAR.map(|s| s.is_rtl()), state)
88}
89
90/// If the layout direction is left-to-right.
91///
92/// The `state` is bound to [`DIRECTION_VAR`].
93///
94/// [`DIRECTION_VAR`]: zng_layout::context::DIRECTION_VAR
95#[property(LAYOUT)]
96pub fn is_ltr(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
97    bind_state(child, DIRECTION_VAR.map(|s| s.is_ltr()), state)
98}
99
100/// Inline mode explicitly selected for a widget.
101#[derive(Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
102pub enum InlineMode {
103    /// Widget does inline if requested by the parent widget layout and is composed only of properties that support inline.
104    ///
105    /// This is the default behavior.
106    #[default]
107    Allow,
108    /// Widget always does inline.
109    ///
110    /// If the parent layout does not setup an inline layout environment the widget itself will. This
111    /// can be used to force the inline visual, such as background clipping or any other special visual
112    /// that is only enabled when the widget is inlined.
113    ///
114    /// Note that the widget will only inline if composed only of properties that support inline.
115    Inline,
116    /// Widget disables inline.
117    ///
118    /// If the parent widget requests inline the request does not propagate for child nodes and
119    /// inline is disabled on the widget.
120    Block,
121}
122impl fmt::Debug for InlineMode {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        if f.alternate() {
125            write!(f, "InlineMode::")?;
126        }
127        match self {
128            Self::Allow => write!(f, "Allow"),
129            Self::Inline => write!(f, "Inline"),
130            Self::Block => write!(f, "Block"),
131        }
132    }
133}
134impl_from_and_into_var! {
135    fn from(inline: bool) -> InlineMode {
136        if inline { InlineMode::Inline } else { InlineMode::Block }
137    }
138}
139
140/// Enforce an inline mode on the widget.
141///
142/// See [`InlineMode`] for more details.
143#[property(WIDGET, default(InlineMode::Allow))]
144pub fn inline(child: impl IntoUiNode, mode: impl IntoVar<InlineMode>) -> UiNode {
145    let mode = mode.into_var();
146    match_node(child, move |child, op| match op {
147        UiNodeOp::Init => {
148            WIDGET.sub_var_layout(&mode);
149        }
150        UiNodeOp::Measure { wm, desired_size } => {
151            *desired_size = match mode.get() {
152                InlineMode::Allow => child.measure(wm),
153                InlineMode::Inline => {
154                    if LAYOUT.inline_constraints().is_none() {
155                        // enable inline for content.
156                        wm.with_inline_visual(|wm| child.measure(wm))
157                    } else {
158                        // already enabled by parent
159                        child.measure(wm)
160                    }
161                }
162                InlineMode::Block => {
163                    // disable inline, method also disables in `WidgetMeasure`
164                    child.delegated();
165                    wm.measure_block(child.node())
166                }
167            };
168        }
169        UiNodeOp::Layout { wl, final_size } => {
170            *final_size = match mode.get() {
171                InlineMode::Allow => child.layout(wl),
172                InlineMode::Inline => {
173                    if LAYOUT.inline_constraints().is_none() {
174                        wl.to_measure(None).with_inline_visual(|wm| child.measure(wm));
175                        wl.with_inline_visual(|wl| child.layout(wl))
176                    } else {
177                        // already enabled by parent
178                        child.layout(wl)
179                    }
180                }
181                InlineMode::Block => {
182                    if wl.inline().is_some() {
183                        tracing::error!("inline enabled in `layout` when it signaled disabled in the previous `measure`");
184                        child.delegated();
185                        wl.layout_block(child.node())
186                    } else {
187                        child.layout(wl)
188                    }
189                }
190            };
191        }
192        _ => {}
193    })
194}
195
196context_var! {
197    /// Variable that indicates the context should use mobile UI themes.
198    ///
199    /// This is `true` by default in Android and iOS builds. It can also be set using [`force_mobile`](fn@force_mobile).
200    pub static IS_MOBILE_VAR: bool = cfg!(any(target_os = "android", target_os = "ios"));
201}
202
203/// Requests mobile UI themes in desktop builds.
204///
205/// This property sets the [`IS_MOBILE_VAR`].
206#[property(CONTEXT, default(IS_MOBILE_VAR))]
207pub fn force_mobile(child: impl IntoUiNode, is_mobile: impl IntoVar<bool>) -> UiNode {
208    with_context_var(child, IS_MOBILE_VAR, is_mobile)
209}
210
211/// Gets the [`IS_MOBILE_VAR`] that indicates the window or widget should use mobile UI themes.
212#[property(EVENT)]
213pub fn is_mobile(child: impl IntoUiNode, is_mobile: impl IntoVar<bool>) -> UiNode {
214    zng_wgt::node::bind_state(child, IS_MOBILE_VAR, is_mobile)
215}