sktablelayout/
lib.rs

1#[macro_use]
2extern crate bitflags;
3
4use std::cmp::max;
5use std::collections::BTreeMap;
6use std::f32;
7
8/// Rectangle for padding and spacing constraints.
9#[derive(Default, Clone, Copy)]
10pub struct Rectangle {
11    pub top: f32,
12    pub left: f32,
13    pub bottom: f32,
14    pub right: f32,
15}
16
17/// Individual size constraint for a cell.
18#[derive(Clone)]
19pub struct Size {
20    pub width: f32,
21    pub height: f32,
22}
23
24impl Size {
25    pub fn join_max(a: &Size, b: &Size) -> Self {
26        Size {
27            width: f32::max(a.width, b.width),
28            height: f32::max(a.height, b.height),
29        }
30    }
31
32    pub fn join_min(a: &Size, b: &Size) -> Self {
33        Size {
34            width: f32::min(a.width, b.width),
35            height: f32::min(a.height, b.height),
36        }
37    }
38
39    /// Divides the width and height by a given division level. Used when
40    /// a size must be spread across multiple table cells.
41    pub fn spread(&self, divisions: f32) -> Self {
42        Size {
43            width: self.width / divisions,
44            height: self.height / divisions,
45        }
46    }
47
48    /// Adds padding from a supplied padding rectangle.
49    pub fn padded(&self, padding: Rectangle) -> Self {
50        Size {
51            width: self.width + padding.left + padding.right,
52            height: self.height + padding.top + padding.bottom,
53        }
54    }
55
56    /// Returns whether this size should fit within another size.
57    pub fn within(&self, other: &Size) -> bool {
58        other.width > self.width && other.height > self.height
59    }
60}
61
62/// Combines the maximum, minimum and preferred sizes for a cell.
63#[derive(Clone)]
64pub struct SizeGrouping {
65    pub minimum: Size,
66    pub maximum: Size,
67    pub preferred: Size,
68}
69
70impl Default for SizeGrouping {
71    fn default() -> Self {
72        SizeGrouping {
73            minimum: Size {
74                width: 0.0,
75                height: 0.0,
76            },
77            preferred: Size {
78                width: 0.0,
79                height: 0.0,
80            },
81            maximum: Size {
82                width: f32::MAX,
83                height: f32::MAX,
84            },
85        }
86    }
87}
88
89impl SizeGrouping {
90    pub fn join(a: &SizeGrouping, b: &SizeGrouping) -> SizeGrouping {
91        SizeGrouping {
92            minimum: Size::join_max(&a.minimum, &b.minimum),
93            preferred: Size::join_max(&a.preferred, &b.preferred),
94            maximum: Size::join_min(&a.maximum, &b.maximum),
95        }
96    }
97
98    pub fn spread(&self, divisions: f32) -> SizeGrouping {
99        SizeGrouping {
100            minimum: self.minimum.spread(divisions),
101            preferred: self.preferred.spread(divisions),
102            maximum: self.maximum.spread(divisions),
103        }
104    }
105
106    pub fn padded(&self, padding: Rectangle) -> SizeGrouping {
107        SizeGrouping {
108            minimum: self.minimum.padded(padding),
109            preferred: self.preferred.padded(padding),
110            maximum: self.maximum.padded(padding),
111        }
112    }
113
114    /// Attempts to fit an `item` of a given size within an `area`, subject
115    /// to layout rules specified by `flags`. Returns the X, Y coordinates
116    /// as well as width and height of the box fitted to the area.
117    pub fn box_fit(&self, area: &Size, prop: &CellProperties) -> (f32, f32, f32, f32) {
118        let pad_width = prop.padding.left + prop.padding.right;
119        let pad_height = prop.padding.top + prop.padding.bottom;
120
121        // combine maximum width and area width, depending on if fill has been activated
122        let w = if prop.flags.contains(CellFlags::FillHorizontal) {
123            f32::min(self.maximum.width, area.width - pad_width)
124        } else {
125            f32::min(self.preferred.width, area.width - pad_width)
126        };
127
128        // combine maximum height and area height, depending on if fill has been activated
129        let h = if prop.flags.contains(CellFlags::FillVertical) {
130            f32::min(self.maximum.height, area.height - pad_height)
131        } else {
132            f32::min(self.preferred.height, area.height - pad_height)
133        };
134
135        // find horizontal location of output box
136        let x = if prop.flags.contains(CellFlags::AnchorRight) {
137            // take size of the area and remove width, will anchor us to the right side
138            area.width - prop.padding.right - w
139        } else if prop.flags.contains(CellFlags::AnchorHorizontalCenter) {
140            // tricky because we have to find the midpoint, then adjust by half of width
141            // XXX this ought to still work, because the padding is on the "outside" of our center
142            (area.width / 2.0) - (w / 2.0)
143        } else {
144            // AnchorLeft is the same as doing nothing, so we just put this on the left side.
145            prop.padding.left
146        };
147
148        // find vertical location of output box
149        let y = if prop.flags.contains(CellFlags::AnchorBottom) {
150            // take size of the area and remove height, will anchor us to the top side
151            area.height - prop.padding.bottom - h
152        } else if prop.flags.contains(CellFlags::AnchorHorizontalCenter) {
153            // tricky because we have to find the midpoint, then adjust by half of height
154            // XXX this ought to still work, because the padding is on the "outside" of our center
155            (area.height / 2.0) - (h / 2.0)
156        } else {
157            // AnchorTop is the same as doing nothing, so we just put this on the top side.
158            prop.padding.top
159        };
160
161        (x, y, w, h)
162    }
163}
164
165bitflags! {
166    pub struct CellFlags: u16 {
167        const None                   = 0b0000_0000_0000_0000;
168        /// Expands cell to fill all remaining horizontal space.
169        const ExpandHorizontal       = 0b0000_0000_0000_0001;
170        /// Expands cell to fill all remaining vertical space.
171        const ExpandVertical         = 0b0000_0000_0000_0010;
172        /// Expands cell's contents to fill all remaining horizontal space.
173        const FillHorizontal         = 0b0000_0000_0000_0100;
174        /// Expands cell's contents to fill all remaining vertical space.
175        const FillVertical           = 0b0000_0000_0000_1000;
176        /// Anchors the cell to the top of its available space.
177        const AnchorTop              = 0b0000_0000_0001_0000;
178        /// Anchors the cell to the bottom of its available space.
179        const AnchorBottom           = 0b0000_0000_0010_0000;
180        /// Anchors the cell to the left of its available space.
181        const AnchorLeft             = 0b0000_0000_0100_0000;
182        /// Anchors the cell to the right of its available space.
183        const AnchorRight            = 0b0000_0000_1000_0000;
184        /// Anchors the cell to the center of its available space, horizontally.
185        const AnchorHorizontalCenter = 0b0000_0001_0000_0000;
186        /// Anchors the cell to the center of its available space, vertically.
187        const AnchorVerticalCenter   = 0b0000_0010_0000_0000;
188        /// Cell will be the same size as all cells which are uniform.
189        const Uniform                = 0b0000_0100_0000_0000;
190    }
191}
192
193/// Allows a closure to ensure a layout item has been placed where the
194/// layout engine decided it should go. The parameters are the `x`,
195/// `y` coordinates, and the `width`/`height` respectively.
196pub type PositioningFn = FnMut(f32, f32, f32, f32);
197
198/// Encapsulates all properties for a cell; contributes to eventual layout decisions.
199pub struct CellProperties {
200    /// Controls the desired sizes for this cell.
201    pub size: SizeGrouping,
202    /// Controls various binary flags for the cell.
203    pub flags: CellFlags,
204    /// Controls how many columns this cell will occupy.
205    pub colspan: u8,
206    /// Controls how many pixels are intentionally wasted around this cell.
207    pub padding: Rectangle,
208    /// Applies positioning updates for this cell. Note that this
209    /// value always becomes `None` when cloned, so you cannot set
210    /// default callbacks for cell policies.
211    pub callback: Option<Box<PositioningFn>>,
212}
213
214impl Default for CellProperties {
215    fn default() -> Self {
216        CellProperties {
217            size: Default::default(),
218            flags: CellFlags::None,
219            padding: Default::default(),
220            colspan: 1,
221            callback: None,
222        }
223    }
224}
225
226impl Clone for CellProperties {
227    fn clone(&self) -> Self {
228        CellProperties {
229            size: self.size.clone(),
230            flags: self.flags,
231            padding: self.padding,
232            colspan: self.colspan,
233            callback: None,
234        }
235    }
236}
237
238pub enum LayoutOp {
239    /// Inserts a cell in the resulting layout.
240    Cell(CellProperties),
241    /// Inserts a row break in the resulting layout.
242    Row,
243}
244
245#[derive(Default)]
246pub struct TableLayout {
247    pub cell_defaults: CellProperties,
248    pub row_defaults: BTreeMap<u8, CellProperties>,
249    pub column_defaults: BTreeMap<u8, CellProperties>,
250    pub opcodes: Vec<LayoutOp>,
251
252    pub row: u8,
253    pub column: u8,
254}
255
256impl CellProperties {
257    pub fn new() -> Self {
258        Default::default()
259    }
260
261    /// Inherits the default settings as determined by a
262    /// `TableLayout`. Will first try to match the defaults for the
263    /// column this would be added to, then the row, then the fallback
264    /// defaults. Note that these defaults apply only if the cell
265    /// was added next and if the defaults have not been changed
266    /// since. The correct use of `with_defaults` is to initialize
267    /// `CellProperties` for immediate insertion to a layout.
268    pub fn with_defaults(layout: &TableLayout) -> Self {
269        // try to get the column default
270        let column_value = layout.column_defaults.get(&layout.column);
271        if column_value.is_some() {
272            return (*column_value.unwrap()).clone();
273        }
274
275        // try to get the row default
276        let row_value = layout.row_defaults.get(&layout.row);
277        if row_value.is_some() {
278            return (*row_value.unwrap()).clone();
279        }
280
281        // just get the default i guess
282        CellProperties {
283            ..layout.cell_defaults.clone()
284        }
285    }
286
287    pub fn minimum_size(mut self, minimum: Size) -> Self {
288        self.size.minimum = minimum;
289        self
290    }
291
292    pub fn maximum_size(mut self, maximum: Size) -> Self {
293        self.size.maximum = maximum;
294        self
295    }
296
297    pub fn preferred_size(mut self, preferred: Size) -> Self {
298        self.size.preferred = preferred;
299        self
300    }
301
302    pub fn expand(mut self) -> Self {
303        self.flags |= CellFlags::ExpandHorizontal | CellFlags::ExpandVertical;
304        self
305    }
306
307    pub fn expand_horizontal(mut self) -> Self {
308        self.flags |= CellFlags::ExpandHorizontal;
309        self
310    }
311
312    pub fn expand_vertical(mut self) -> Self {
313        self.flags |= CellFlags::ExpandVertical;
314        self
315    }
316
317    pub fn fill(mut self) -> Self {
318        self.flags |= CellFlags::FillHorizontal | CellFlags::FillVertical;
319        self
320    }
321
322    pub fn fill_horizontal(mut self) -> Self {
323        self.flags |= CellFlags::FillHorizontal;
324        self
325    }
326
327    pub fn fill_vertical(mut self) -> Self {
328        self.flags |= CellFlags::FillVertical;
329        self
330    }
331
332    pub fn anchor_top(mut self) -> Self {
333        self.flags |= CellFlags::AnchorTop;
334        self
335    }
336
337    pub fn anchor_bottom(mut self) -> Self {
338        self.flags |= CellFlags::AnchorBottom;
339        self
340    }
341
342    pub fn anchor_left(mut self) -> Self {
343        self.flags |= CellFlags::AnchorLeft;
344        self
345    }
346
347    pub fn anchor_right(mut self) -> Self {
348        self.flags |= CellFlags::AnchorRight;
349        self
350    }
351
352    pub fn anchor_center(mut self) -> Self {
353        self.flags |= CellFlags::AnchorHorizontalCenter | CellFlags::AnchorVerticalCenter;
354        self
355    }
356
357    pub fn anchor_horizontal_center(mut self) -> Self {
358        self.flags |= CellFlags::AnchorHorizontalCenter;
359        self
360    }
361
362    pub fn anchor_vertical_center(mut self) -> Self {
363        self.flags |= CellFlags::AnchorVerticalCenter;
364        self
365    }
366
367    pub fn uniform(mut self) -> Self {
368        self.flags |= CellFlags::Uniform;
369        self
370    }
371
372    pub fn colspan(mut self, span: u8) -> Self {
373        self.colspan = span;
374        self
375    }
376
377    pub fn callback(mut self, fun: Box<PositioningFn>) -> Self {
378        self.callback = Option::Some(fun);
379        self
380    }
381
382    /// Sets the padding around this cell to the supplied top, left, right and bottom values as
383    /// specified by a rectangle struct.
384    pub fn padding(mut self, pad: &Rectangle) -> Self {
385        self.padding = *pad;
386        self
387    }
388
389    pub fn padding_all(mut self, pad: f32) -> Self {
390        self.padding.top = pad;
391        self.padding.left = pad;
392        self.padding.bottom = pad;
393        self.padding.right = pad;
394        self
395    }
396
397    /// Sets the padding on the top side of this cell.
398    pub fn padding_top(mut self, pad: f32) -> Self {
399        self.padding.top = pad;
400        self
401    }
402
403    /// Sets the padding on the left side of this cell.
404    pub fn padding_left(mut self, pad: f32) -> Self {
405        self.padding.left = pad;
406        self
407    }
408
409    /// Sets the padding on the bottom side of this cell.
410    pub fn padding_bottom(mut self, pad: f32) -> Self {
411        self.padding.bottom = pad;
412        self
413    }
414
415    /// Sets the padding on the right side of this cell.
416    pub fn padding_right(mut self, pad: f32) -> Self {
417        self.padding.right = pad;
418        self
419    }
420}
421
422impl TableLayout {
423    pub fn new() -> TableLayout {
424        Default::default()
425    }
426
427    /// Calculates the number of rows and columns which exist in this table layout.
428    pub fn get_rows_cols(&self) -> (u8, u8) {
429        let mut cols = 0;
430        let mut colcur = 0;
431        let mut rows = 0;
432
433        for op in &self.opcodes {
434            match op {
435                LayoutOp::Cell(cp) => colcur += cp.colspan,
436                LayoutOp::Row => {
437                    cols = max(cols, colcur);
438                    colcur = 0;
439                    rows += 1
440                }
441            }
442        }
443
444        if colcur > 0 {
445            cols = max(cols, colcur);
446            rows += 1;
447        }
448
449        (rows, cols)
450    }
451
452    /// Removes all layout declarations from the table. Does not remove row or column defaults.
453    pub fn clear(&mut self) {
454        self.row = 0;
455        self.column = 0;
456        self.opcodes.clear()
457    }
458
459    /// Removes all layout declarations and resets ALL settings to factory default.
460    pub fn full_clear(&mut self) {
461        self.clear();
462        self.row_defaults.clear();
463        self.column_defaults.clear();
464        self.cell_defaults = Default::default()
465    }
466
467    /// Adds a new row to the layout.
468    pub fn with_row(&mut self) -> &mut Self {
469        self.opcodes.push(LayoutOp::Row);
470        self.row += 1;
471        self.column = 0;
472        self
473    }
474
475    /// Hands the cell off to the layout.
476    pub fn with_cell(&mut self, properties: CellProperties) -> &mut Self {
477        self.column += properties.colspan;
478        self.opcodes.push(LayoutOp::Cell(properties));
479        self
480    }
481
482    pub fn impose(&mut self, width: f32, height: f32) {
483        let mut row: u8 = 0;
484        let mut col: u8 = 0;
485
486        let (total_rows, total_cols) = self.get_rows_cols();
487        if total_cols == 0 {
488            return;
489        } // short-circuiting opportunity
490
491        let mut col_sizes: Vec<SizeGrouping> = Vec::with_capacity(total_cols as usize);
492        // XXX resize_with is unstable, but would do what we want just fine
493        for _i in 0..total_cols {
494            col_sizes.push(Default::default());
495        }
496
497        // XXX resize_with is unstable, but would do what we want just fine
498        let mut row_sizes: Vec<SizeGrouping> = Vec::with_capacity(total_cols as usize);
499        for _i in 0..total_rows {
500            row_sizes.push(Default::default());
501        }
502
503        let mut has_xexpand: Vec<bool> = Vec::with_capacity(total_cols as usize);
504        for _i in 0..total_cols {
505            has_xexpand.push(false);
506        }
507
508        let mut has_yexpand: Vec<bool> = Vec::with_capacity(total_rows as usize);
509        for _i in 0..total_rows {
510            has_yexpand.push(false);
511        }
512
513        // We determine size preferences for each column in the layout.
514        for op in &self.opcodes {
515            match op {
516                LayoutOp::Cell(cp) => {
517                    match cp.colspan {
518                        // If a cell has a span of zero, that is kind of stupid and it basically doesn't exist.
519                        0 => {}
520                        _ => {
521                            let midget = cp.size.padded(cp.padding).spread(f32::from(cp.colspan));
522                            row_sizes[row as usize] =
523                                SizeGrouping::join(&row_sizes[row as usize], &cp.size);
524                            if cp.flags.contains(CellFlags::ExpandVertical) {
525                                has_yexpand[row as usize] = true
526                            }
527                            for _i in 0..cp.colspan {
528                                if cp.flags.contains(CellFlags::ExpandHorizontal) {
529                                    has_xexpand[col as usize] = true
530                                }
531                                col_sizes[col as usize] =
532                                    SizeGrouping::join(&col_sizes[col as usize], &midget);
533                                col += 1;
534                            }
535                        }
536                    }
537                }
538                // flop to a new row
539                LayoutOp::Row => {
540                    row += 1;
541                    col = 0;
542                }
543            }
544        }
545
546        let mut slack: Vec<f32> = Vec::new();
547
548        // Calculate error along width distribution
549        let mut error = width;
550        for c in &col_sizes {
551            // Error is what remains once we have given each column its preferred size.
552            error -= c.preferred.width;
553        }
554
555        if error > 0.0 {
556            // Extra space; relax the layout if we need to
557            // Figure out how many columns are expanding horizontally.
558            let expansions = has_xexpand.iter().filter(|x| **x).count();
559            if expansions > 0 {
560                let amount = error / expansions as f32;
561                for (i, e) in has_xexpand.iter().enumerate() {
562                    if *e {
563                        col_sizes[i].preferred.width += amount;
564                    }
565                }
566            }
567        } else if error < 0.0 {
568            // Not enough space; tense up some more!
569            let error = -error;
570            // We need to find slack space for each column
571            let mut total_slack: f32 = 0.0;
572            slack.clear();
573            slack.resize(total_cols as usize, 0.0);
574            for (i, x) in col_sizes
575                .iter()
576                .map(|x| x.preferred.width - x.minimum.width)
577                .enumerate()
578            {
579                slack[i] = x;
580                total_slack += x;
581            }
582
583            // XXX if error > total_slack, it is impossible to solve this constraint
584            // spread error across slack space, proportionate to this areas slack participation
585            for mut s in &mut slack {
586                let norm = *s / total_slack;
587                let error_over_slack = error * norm;
588                *s -= error_over_slack
589            }
590
591            // Spread error across slack space.
592            for (i, x) in slack.iter().enumerate() {
593                col_sizes[i].preferred.width = f32::max(col_sizes[i].minimum.width + *x, 0.0);
594            }
595        }
596
597        // Calculate error along height distribution
598        let mut error = height;
599        for c in &row_sizes {
600            // Error is what remains once we have given each row its preferred size.
601            error -= c.preferred.height;
602        }
603
604        if error > 0.0 {
605            // Extra space; relax the layout if we need to
606            // Figure out how many columns are expanding horizontally.
607            let expansions = has_yexpand.iter().filter(|y| **y).count();
608            if expansions > 0 {
609                let amount = error / expansions as f32;
610                for (i, e) in has_yexpand.iter().enumerate() {
611                    if *e {
612                        row_sizes[i].preferred.height += amount;
613                    }
614                }
615            }
616        } else if error < 0.0 {
617            // Not enough space; tense up some more!
618            let error = -error;
619            // We need to find slack space for each row
620            let mut total_slack: f32 = 0.0;
621            slack.clear();
622            slack.resize(total_rows as usize, 0.0);
623            for (i, y) in row_sizes
624                .iter()
625                .map(|y| y.preferred.height - y.minimum.height)
626                .enumerate()
627            {
628                slack[i] = y;
629                total_slack += y;
630            }
631
632            // XXX if error > total_slack, it is impossible to solve this constraint
633            // spread error across slack space, proportionate to this areas slack participation
634            for mut s in &mut slack {
635                let norm = *s / total_slack;
636                let error_over_slack = error * norm;
637                *s -= error_over_slack
638            }
639
640            // Spread error across slack space.
641            for (i, y) in slack.iter().enumerate() {
642                row_sizes[i].preferred.height = f32::max(row_sizes[i].minimum.height + *y, 0.0);
643            }
644        }
645
646        // Preparations complete. Now we pass the news along to our client.
647        let mut x = 0.0;
648        let mut y = 0.0;
649        row = 0;
650        col = 0;
651        for mut op in &mut self.opcodes {
652            // NB can probably make this mutable, and update it only when the row changes
653            let height = row_sizes[row as usize].preferred.height;
654            match op {
655                // Something that needs to be placed.
656                LayoutOp::Cell(cp) => match &cp.colspan {
657                    0 => {} // Ignore this cell.
658                    _ => {
659                        let mut width: f32 = 0.0;
660                        for _i in 0..cp.colspan {
661                            width += col_sizes[col as usize].preferred.width;
662                            col += 1;
663                        }
664                        let s = Size { width, height };
665                        let (bx, by, bw, bh) = cp.size.box_fit(&s, &cp);
666
667                        // Run callback to impose layout.
668                        match &mut cp.callback {
669                            Some(cb) => {
670                                (*cb)(x + bx, y + by, bw, bh);
671                            }
672                            None => {}
673                        }
674
675                        x += width;
676                    }
677                },
678                // Increment to next row; reset placement cursors.
679                LayoutOp::Row => {
680                    x = 0.0;
681                    y += height;
682                    row += 1;
683                    col = 0;
684                }
685            }
686        }
687    }
688}
689
690#[cfg(test)]
691mod test;