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