1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
//! The [`LayoutDsl`] type used to bring layout bundles to the [`cuicui_dsl::dsl`] macro.

use std::{fmt, mem};

use bevy::log::error;
use bevy::prelude::{Bundle, Deref, DerefMut};
use cuicui_dsl::{BaseDsl, DslBundle, EntityCommands};

use crate::bundles::{Layout, LayoutBundle, RootBundle};
use crate::{Alignment, Distribution, Flow, LeafRule, Node, Oriented, Rule};
#[cfg(doc)]
use crate::{Container, Root, ScreenRoot};

/// Something that can be converted into a bevy [`Bundle`].
///
/// Implement this trait on anything you want, then you can use [`LayoutDsl::ui`]
/// with anything you want!
///
/// `Marker` is completely ignored. It only exists to make it easier for
/// consumers of the API to extend the DSL with their own bundle.
///
/// # Example
///
/// ```
/// # use bevy::prelude::*;
/// use cuicui_layout::{LayoutDsl, dsl};
/// use cuicui_layout::dsl::IntoUiBundle;
/// use cuicui_layout::dsl_functions::px;
///
/// # #[derive(Component)] struct TextBundle;
/// enum MyDsl {}
///
/// impl IntoUiBundle<MyDsl> for &'_ str {
///     type Target = TextBundle;
///
///     fn into_ui_bundle(self) -> Self::Target {
///         TextBundle {
///             // ...
///             // text: Text::from_section(self, Default::default()),
///             // ...
///         }
///     }
/// }
///
/// fn setup(mut cmds: Commands) {
///     dsl! {
///         <LayoutDsl> &mut cmds.spawn_empty(),
///         Entity {
///             Entity(ui("Hello world") width(px(350)))
///             Entity(ui("Even hi!") width(px(350)))
///             Entity(ui("Howdy partner") width(px(350)))
///         }
///     };
/// }
/// ```
pub trait IntoUiBundle<Marker> {
    /// The [`Bundle`] this can be converted into.
    type Target: Bundle;

    /// Convert `self` into an [`Self::Target`], will be directly inserted by
    /// [`LayoutDsl::ui`] with an additional [`Node`] component.
    ///
    /// Since `Target` is inserted _after_ the [`Node`] component, you can
    /// overwrite it by including it in the bundle.
    fn into_ui_bundle(self) -> Self::Target;
}
impl IntoUiBundle<()> for () {
    type Target = ();
    fn into_ui_bundle(self) {}
}

#[derive(Default, Debug)]
enum RootKind {
    ScreenRoot,
    Root,
    #[default]
    None,
}

/// The [`DslBundle`] for layouting.
///
/// The defaults are:
///
/// - For containers: Child sized, meaning a container node has the minimum size
///   to fit all its children.
/// - [`Distribution::FillMain`]
/// - [`Alignment::Center`]
///
/// For terminal nodes (spawned through `spawn` or `ui`) the default
/// size is [`LeafRule::Fixed(0.)`], or content-sized.
///
/// [`EntityCommands`]: bevy::ecs::system::EntityCommands
#[derive(Default, Deref, DerefMut)]
pub struct LayoutDsl<T = BaseDsl> {
    #[deref]
    inner: T,
    root: RootKind,
    layout: Layout,
    // TODO(clean): Shouldn't layout.flow: Option<Flow> instead?
    set_flow: bool,
    ui_bundle: Option<Box<dyn FnOnce(&mut EntityCommands)>>,
    layout_bundle: Option<LayoutBundle>,
}
impl<D: fmt::Debug> fmt::Debug for LayoutDsl<D> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let has_bundle = self.ui_bundle.is_some();
        let ui_bundle = if has_bundle { "Some([FnOnce(EntityCommands)])" } else { "None" };
        f.debug_struct("LayoutDsl")
            .field("inner", &self.inner)
            .field("root", &self.root)
            .field("layout", &self.layout)
            .field("set_flow", &self.set_flow)
            .field("ui_bundle", &ui_bundle)
            .field("layout_bundle", &self.layout_bundle)
            .finish()
    }
}

#[cfg_attr(
    feature = "chirp",
    cuicui_chirp::parse_dsl_impl(delegate = inner, type_parsers(Rule = args::from_str))
)]
impl<D: DslBundle> LayoutDsl<D> {
    /// Set the flow direction of a container node.
    #[cfg_attr(feature = "chirp", parse_dsl(ignore))]
    pub fn flow(&mut self, flow: Flow) {
        self.set_flow = true;
        self.layout.flow = flow;
    }
    /// Spawn this [`Node`] as a [`Node::Container`] with children flowing vertically.
    ///
    /// `f` will then build the children of this [`Container`].
    pub fn column(&mut self) {
        self.flow(Flow::Vertical);
    }
    /// Spawn this [`Node`] as a [`Node::Container`] with children flowing horizontally.
    ///
    /// `f` will then build the children of this [`Container`].
    pub fn row(&mut self) {
        self.flow(Flow::Horizontal);
    }
    /// Push children of this [`Node`] to the start of the main flow axis,
    /// the default is [`Distribution::FillMain`].
    pub fn distrib_start(&mut self) {
        self.layout.distrib = Distribution::Start;
    }
    /// Push children of this [`Node`] to the end of the main flow axis,
    /// the default is [`Distribution::FillMain`].
    pub fn distrib_end(&mut self) {
        self.layout.distrib = Distribution::End;
    }
    /// Distribute the children of this [`Node`] to fill this [`Container`]'s main flow axis.
    ///
    /// Note that this is the default.
    pub fn fill_main_axis(&mut self) {
        self.layout.distrib = Distribution::FillMain;
    }

    /// Set properties based on the given `spec`.
    ///
    /// `spec` specifies the [flow][Self::flow], (d)istribution
    /// and (a)lignment of the container in the form: `[v>]d[SEC]a[SEC]`.
    ///
    /// legal values are: `S`tart, `E`nd or `C`enter.
    ///
    /// An error is logged on illegal values.
    pub fn layout(&mut self, spec: &str) {
        let correct_len = spec.len() == 5;
        if !correct_len {
            error!("'layout' method accpets '[v>]d[SEC]a[SEC]', got '{spec}'");
            return;
        };
        let (Ok(flow), Ok(distrib), Ok(align)) =
            (spec[0..1].parse(), spec[1..3].parse(), spec[3..5].parse())
        else {
            error!("'layout' method accpets '[v>]d[SEC]a[SEC]', got '{spec}'");
            return;
        };
        self.set_flow = true;
        self.layout.flow = flow;
        self.layout.distrib = distrib;
        self.layout.align = align;
    }
    /// Set both the [cross][Self::cross_margin] and [main][Self::main_margin]
    /// margins.
    pub fn margins(&mut self, main: f32, cross: f32) {
        self.main_margin(main);
        self.cross_margin(cross);
    }
    /// Set both the [cross][Self::cross_margin] and [main][Self::main_margin]
    /// margins to `pixels`
    pub fn margin(&mut self, pixels: f32) {
        self.main_margin(pixels);
        self.cross_margin(pixels);
    }
    /// Set this [`Container`]'s margin on the main flow axis.
    pub fn main_margin(&mut self, pixels: f32) {
        self.layout.margin.main = pixels;
    }
    /// Set this [`Container`]'s margin on the cross flow axis.
    pub fn cross_margin(&mut self, pixels: f32) {
        self.layout.margin.cross = pixels;
    }
    /// Set both [width](Self::width) and [height](Self::height) rules.
    pub fn rules(&mut self, width: Rule, height: Rule) {
        self.width(width);
        self.height(height);
    }
    /// Set the width [`Rule`] of this [`Node`].
    pub fn width(&mut self, rule: Rule) {
        self.layout.size.width = Some(rule);
    }
    /// Set the height [`Rule`] of this [`Node`].
    pub fn height(&mut self, rule: Rule) {
        self.layout.size.height = Some(rule);
    }

    /// Use [`Alignment::Start`] for this [`Node`], the default is [`Alignment::Center`].
    pub fn align_start(&mut self) {
        self.layout.align = Alignment::Start;
    }
    /// Use [`Alignment::End`] for this [`Node`], the default is [`Alignment::Center`].
    pub fn align_end(&mut self) {
        self.layout.align = Alignment::End;
    }

    /// Set this node as the [`ScreenRoot`], its size will follow that of the
    /// [`LayoutRootCamera`] camera.
    ///
    /// [`LayoutRootCamera`]: crate::LayoutRootCamera
    pub fn screen_root(&mut self) {
        self.root = RootKind::ScreenRoot;
    }
    /// Set this node as a [`Root`].
    pub fn root(&mut self) {
        self.root = RootKind::Root;
    }

    /// Spawn an empty [`Node::Axis`] with the `main` axis set to `percent`%
    /// of parent's size,
    /// and the `cross` axis to 0.
    ///
    /// # Panics
    /// If `percent` is greater than 100. It would mean this node overflows its parent.
    pub fn empty_pct(&mut self, percent: u8) {
        assert!(percent <= 100);
        let node = Node::Axis(Oriented {
            main: LeafRule::Parent(f32::from(percent) / 100.0),
            cross: LeafRule::Fixed(0.0),
        });
        self.layout_bundle = Some(LayoutBundle { node, ..Default::default() });
    }

    /// Spawn an empty [`Node::Axis`] with the `main` axis set to `pixels` pixels
    /// and the `cross` axis to 0.
    pub fn empty_px(&mut self, pixels: u16) {
        let node = Node::Axis(Oriented {
            main: LeafRule::Fixed(f32::from(pixels)),
            cross: LeafRule::Fixed(0.0),
        });
        self.layout_bundle = Some(LayoutBundle { node, ..Default::default() });
    }
    /// Spawn `ui_bundle`.
    ///
    /// Note that axis without set rules or [`Rule::Children`]
    /// are considered [content-sized](crate::content_sized::ComputeContentSize).
    #[cfg_attr(feature = "chirp", parse_dsl(ignore))]
    pub fn ui<M>(&mut self, ui_bundle: impl IntoUiBundle<M>) {
        let ui_bundle = ui_bundle.into_ui_bundle();
        self.ui_bundle = Some(Box::new(move |cmds| {
            cmds.insert(ui_bundle);
        }));
    }
}
impl<D: DslBundle> DslBundle for LayoutDsl<D> {
    fn insert(&mut self, cmds: &mut EntityCommands) {
        if self.set_flow {
            let container = self.layout.container();
            let root_bundle = || RootBundle::new(self.layout);
            let non_screen_root_bundle = || {
                let r = RootBundle::new(self.layout);
                (r.pos_rect, r.root)
            };
            match self.root {
                RootKind::ScreenRoot => cmds.insert(root_bundle()),
                RootKind::Root => cmds.insert(non_screen_root_bundle()),
                RootKind::None => cmds.insert(LayoutBundle::node(container)),
            };
        } else {
            let size = self.layout.size.map(LeafRule::from_rule);
            cmds.insert(LayoutBundle::boxy(size));
        }
        if let Some(layout) = mem::take(&mut self.layout_bundle) {
            cmds.insert(layout);
        }
        if let Some(ui_bundle_fn) = mem::take(&mut self.ui_bundle) {
            let size = self.layout.size.map(LeafRule::from_rule);
            cmds.insert(LayoutBundle::boxy(size));
            ui_bundle_fn(cmds);
        }
        self.inner.insert(cmds);
    }
}

/// Returns [`Rule::Fixed`] with given `pixels`.
#[must_use]
pub const fn px(pixels: u16) -> Rule {
    Rule::Fixed(pixels as f32)
}
/// Returns [`Rule::Parent`] as `percent` percent of parent size.
///
/// # Panics
/// If `percent` is greater than 100. It would mean this node overflows its parent.
#[must_use]
pub fn pct(percent: u8) -> Rule {
    assert!(percent <= 100);
    Rule::Parent(f32::from(percent) / 100.0)
}
/// Returns [`Rule::Children`] as `ratio` of its children size.
///
/// # Panics
/// If `ratio` is smaller than 1. It would mean the container couldn't fit its
/// children.
#[must_use]
pub fn child(ratio: f32) -> Rule {
    assert!(ratio >= 1.0);
    Rule::Children(ratio)
}