layout_engine 0.7.0

A small project to mimic css flexbox and css grid
Documentation
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
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
use crate::{
    flexbox::divide_integer,
    layout::{Rect, Vec2},
    Layout, Orientation,
};

/// Properties for all grid items
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug)]
pub struct Props {
    pub column: usize,
    pub row: usize,
    pub column_span: usize,
    pub row_span: usize,
}

impl Props {
    /// Determines if the item is in a specific row
    pub const fn is_in_row(&self, row: usize) -> bool {
        self.row <= row && row < self.row + self.row_span
    }

    /// Determines if the item is in a specific column
    pub const fn is_in_column(&self, column: usize) -> bool {
        self.column <= column && column < self.column + self.column_span
    }

    /// Determines if the item is in a specific row and column
    pub const fn is_in(&self, vec: Vec2) -> bool {
        self.is_in_column(vec.x) && self.is_in_row(vec.y)
    }
}

impl Props {
    /// Creates new `Props`
    pub const fn new(
        column: usize,
        row: usize,
        column_span: usize,
        row_span: usize,
    ) -> Self {
        Self {
            column,
            row,
            column_span,
            row_span,
        }
    }
}

/// A trait for all grid children to implement to allow them
/// to be layouted.
pub trait GridLayout: Layout {
    fn props(&self) -> &Props;
}

/// How much space should different columns and rows take up?
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Sizing {
    /// A fixed size
    Fixed(usize),
    /// Will take up a fraction of the remaining space
    Fractional(usize),
    /// Will take up the widget's prefered size, and
    /// if there is space left over it will take up that space.
    Auto,
    /// Will take up a fixed amount of space and expand
    /// if there is remaining space.
    AutoFixed(usize),
    /// A [`Fractional`] with a minimum amount of space
    FrFixed { fr: usize, fixed: usize },
}

/// The information which results from the grid layout.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Layouted {
    pub sizes: Vec<Rect>,
    pub columns: Vec<usize>,
    pub rows: Vec<usize>,
}

/// Settings for [`layout_grid`]
#[derive(Clone, Copy, Debug)]
pub struct GridConfig<'a> {
    /// The size of the rows
    pub template_rows: &'a [Sizing],
    /// The size of the columns
    pub template_columns: &'a [Sizing],
    /// The gap between rows
    pub row_gap: usize,
    /// The gap between columns
    pub column_gap: usize,
    /// If `true` the grid will try to take up the maximum amount of space
    pub fill_space: bool,
    /// If `true` the grid will use minimum as opposed to naturual sizing
    pub minimum: bool,
}

/// Layouts items in a grid layout, akin to css grid.
pub fn layout_grid(
    rect: Rect,
    cfg: GridConfig,
    items: &[impl GridLayout],
) -> Layouted {
    // Remove the column/row gap
    let mut ungapped_rect = rect;
    ungapped_rect.size.x = ungapped_rect.size.x.saturating_sub(
        (cfg.template_columns.len().max(1) - 1) * cfg.column_gap,
    );
    ungapped_rect.size.y = ungapped_rect
        .size
        .y
        .saturating_sub((cfg.template_rows.len().max(1) - 1) * cfg.row_gap);

    let mut sizes = Vec::with_capacity(items.len());
    let (rows, columns) = calculate(ungapped_rect, items, &cfg);

    // And calculate it all
    for item in items {
        let props = item.props();

        // Get an in-range column
        let column_idx = props.column.min(columns.len());
        let column_end_idx =
            (column_idx + props.column_span).min(columns.len());
        let row_idx = props.row.min(rows.len());
        let row_end_idx = (row_idx + props.row_span).min(rows.len());

        // Sum the length of the previous columns
        let mut column: usize = columns[..column_idx].iter().sum();
        let mut column_end: usize = columns[..column_end_idx].iter().sum();
        let mut row: usize = rows[..row_idx].iter().sum();
        let mut row_end: usize = rows[..row_end_idx].iter().sum();

        // Add in the gap
        column += column_idx * cfg.column_gap;
        row += row_idx * cfg.row_gap;
        column_end += (column_end_idx - 1) * cfg.column_gap;
        row_end += (row_end_idx - 1) * cfg.row_gap;

        let rect = Rect::new(
            rect.start.x + column,
            rect.start.y + row,
            column_end - column,
            row_end - row,
        );

        sizes.push(rect);
    }

    Layouted {
        sizes,
        rows,
        columns,
    }
}

/// Calculates the whole grid layout into fixed values
///
/// Returns `(rows, columns)`
#[must_use]
pub fn calculate(
    rect: Rect,
    items: &[impl GridLayout],
    cfg: &GridConfig,
) -> (Vec<usize>, Vec<usize>) {
    // Tally up how much fractional must be allocated,
    // and how much fixed has been allocated
    let (auto_count_row, frac_row, frac_row_total, fixed_row) =
        count(cfg.template_rows);

    let (auto_count_column, frac_column, frac_column_total, fixed_column) =
        count(cfg.template_columns);

    // From the tally we can work out how much space is left.
    let mut remaining_row = rect.size.y.saturating_sub(fixed_row);
    let mut remaining_column = rect.size.x.saturating_sub(fixed_column);

    // For each auto column and row try and work out the absolute
    // minimum we can give them.
    let mut rows_autoed = cfg.template_rows.to_owned();
    calc_autos(
        rect,
        &mut rows_autoed,
        items,
        Orientation::Vertical,
        &mut remaining_row,
        cfg.minimum,
    );

    let mut columns_autoed = cfg.template_columns.to_owned();
    calc_autos(
        rect,
        &mut columns_autoed,
        items,
        Orientation::Horizontal,
        &mut remaining_column,
        cfg.minimum,
    );

    // Now we allocate the fractional amounts
    let rows_fred = calc_frfixed(
        rows_autoed,
        &mut remaining_row,
        frac_row,
        frac_row_total,
        cfg.fill_space,
    );

    let columns_fred = calc_frfixed(
        columns_autoed,
        &mut remaining_column,
        frac_column,
        frac_column_total,
        cfg.fill_space,
    );

    let rows = calc_auto_fixed(rows_fred, &mut remaining_row, cfg.fill_space);
    let columns =
        calc_auto_fixed(columns_fred, &mut remaining_column, cfg.fill_space);

    (rows, columns)
}

/// Counts to return the `auto_count_row`, `frac_row`, `frac_row_total`,
/// `fixed_row` and `auto_fixed_count_row`
#[must_use]
fn count(template: &[Sizing]) -> (usize, usize, usize, usize) {
    let mut auto_count = 0_usize;
    let mut fixed = 0_usize;
    let mut frac = 0_usize;
    let mut frac_total = 0_usize;

    for size in template {
        match size {
            Sizing::Fractional(n) => {
                frac += n;
                frac_total += 1;
            }
            Sizing::Fixed(n) => {
                fixed += n;
            }
            Sizing::AutoFixed(n) => {
                fixed += n;
            }
            Sizing::Auto => auto_count += 1,
            Sizing::FrFixed { fr, fixed: _ } => {
                frac += fr;
                frac_total += 1;
            }
        }
    }

    (auto_count, frac, frac_total, fixed)
}

fn calc_autos(
    rect: Rect,
    template: &mut [Sizing],
    items: &[impl GridLayout],
    orientation: Orientation,
    remaining: &mut usize,
    minimum: bool,
) {
    // Replace each Sizing::Auto with a Sizing::AutoFixed(0)
    for item in template.iter_mut() {
        if let Sizing::Auto = item {
            *item = Sizing::AutoFixed(0);
        }
        if let Sizing::Fractional(fr) = item {
            *item = Sizing::FrFixed { fr: *fr, fixed: 0 };
        }
    }

    // Get the maximum colrow span
    let max_colrows = items
        .iter()
        .map(|x| match orientation {
            Orientation::Vertical => x.props().row_span,
            Orientation::Horizontal => x.props().column_span,
        })
        .max()
        .unwrap_or(0);

    let minimum_iter = if minimum {
        [true].iter()
    } else {
        [true, false].iter()
    };

    // Loop through objects based of how many colrows they span
    for minimum in minimum_iter {
        for spans in 0..=max_colrows {
            // Filter the objects based of their colrows
            let items = items.iter().filter(|x| match orientation {
                Orientation::Vertical => x.props().row_span == spans,
                Orientation::Horizontal => x.props().column_span == spans,
            });

            for item in items {
                // Gets how much space this object needs
                // of the column / row
                let required = item.prefered_size();
                let required = if *minimum {
                    required.minimum
                } else {
                    required.natural
                };
                let required = required.in_orientation(orientation);

                // Get the first colrow this is on
                let start = match orientation {
                    Orientation::Vertical => item.props().row,
                    Orientation::Horizontal => item.props().column,
                };

                ensure_alloc(
                    &mut template[start..(start + spans)],
                    required,
                    remaining,
                );
            }
        }
    }
}

/// Ensures that a certain amount of capacity is allocated
/// over a section of template rows
fn ensure_alloc(
    template: &mut [Sizing],
    required: usize,
    remaining: &mut usize,
) {
    // Gets the sum of space in the column/rows this object is in
    let mut auto_fixed_count = 0usize;
    let mut sum = 0;

    for colrow in template.iter() {
        match colrow {
            Sizing::Fixed(x) => sum += *x,
            // TODO: Improve fractional handling
            Sizing::Fractional(_) => unreachable!(
                "all `Fractional`s have been converted to `FrFixed`s"
            ),
            Sizing::Auto => {
                unreachable!("all `Auto`s have been converted to `AutoFixed`s")
            }
            Sizing::AutoFixed(x) => {
                auto_fixed_count += 1;
                sum += *x;
            }
            Sizing::FrFixed { fr: _, fixed } => {
                auto_fixed_count += 1;
                sum += *fixed;
            }
        }
    }

    // Work out how much extra space is needed
    let mut needed = required.saturating_sub(sum);
    if needed > *remaining {
        needed = *remaining;
    }

    // Work out how to allocate them
    let mut divided = divide_integer(needed, auto_fixed_count);

    // And add the space to the template
    for colrow in template {
        if let Sizing::AutoFixed(x) = colrow {
            let next = divided.next().unwrap();
            *x += next;
            *remaining = remaining.saturating_sub(next);
        } else if let Sizing::FrFixed { fr: _, fixed } = colrow {
            let next = divided.next().unwrap();
            *fixed += next;
            *remaining = remaining.saturating_sub(next);
        }
    }
    assert!(divided.next().is_none());
}

#[must_use]
fn calc_frfixed(
    autoed: Vec<Sizing>,
    remaining: &mut usize,
    frac: usize,
    frac_total: usize,
    fill_space: bool,
) -> Vec<Sizing> {
    let mut added = 0_usize;
    let mut total = Vec::with_capacity(autoed.len());

    // Re add all the FrFixed to remaining
    *remaining += autoed
        .iter()
        .filter_map(|x| match x {
            Sizing::FrFixed { fr: _, fixed } => Some(fixed),
            _ => None,
        })
        .sum::<usize>();

    // Calculate the minimum part based of of the [`FrFixed`]s
    let min_part: usize = autoed
        .iter()
        .filter_map(|x| match x {
            Sizing::FrFixed { fr, fixed } => Some((fixed / fr) + fixed % fr),
            Sizing::Fractional(_) => {
                unreachable!("all Fractionals should be FrFixeds")
            }
            _ => None,
        })
        .sum();

    // Work out the size of 1 part
    let part = if fill_space {
        min_part.max(*remaining / frac.max(1))
    } else {
        min_part
    };

    for (i, size) in autoed.into_iter().enumerate() {
        match size {
            Sizing::FrFixed { fixed: _, fr } => {
                // Work out how much space to give this
                let mut space = part * fr;

                // Ensure remaining space is given
                if fill_space && i == frac_total - 1 {
                    space = remaining.saturating_sub(added);
                }
                added += space;

                total.push(Sizing::Fixed(space));
            }
            other => total.push(other),
        }
    }

    *remaining = remaining.saturating_sub(added);

    total
}

#[must_use]
fn calc_auto_fixed(
    fred: Vec<Sizing>,
    remaining: &mut usize,
    fill_space: bool,
) -> Vec<usize> {
    let auto_fixed_count = fred
        .iter()
        .filter(|x| matches!(x, Sizing::AutoFixed(_)))
        .map(|_| 1)
        .sum();
    let mut added = 0_usize;
    let mut total = Vec::with_capacity(fred.len());
    let mut space = divide_integer(*remaining, auto_fixed_count);

    // Add the space to each item
    for size in fred.into_iter() {
        match size {
            Sizing::AutoFixed(n) => {
                let space = space.next().unwrap().min(*remaining - added);

                if fill_space {
                    // Work out how much space to give this
                    added += space;

                    total.push(space + n);
                } else {
                    total.push(n);
                }
            }
            Sizing::Fixed(n) => total.push(n),
            _ => unreachable!("Only Fixed and AutoFixed!"),
        }
    }

    *remaining -= added;

    total
}