flex_grow/
lib.rs

1//!
2//! Tiny utility computing the allocation of a size among "children".
3//!
4//! Typical use case: decide what columns to show in an UI, and what size to give to each column.
5//!
6//! Each child can have a min and max size, be optional with a priority, have a `grow` factor.
7//!
8//! Example:
9//!
10//! ```
11//! use flex_grow::{Child, Container};
12//!
13//! let container = Container::builder_in(50)
14//!     .with_margin_between(1)
15//!     .with(Child::new("name").clamp(5, 10))
16//!     .with(Child::new("price").with_size(8).optional_with_priority(7))
17//!     .with(Child::new("quantity").with_size(8).optional())
18//!     .with(Child::new("total").with_size(8))
19//!     .with(Child::new("comments").with_min(10).with_grow(2.0))
20//!     .with(Child::new("vendor").with_size(60).optional_with_priority(9))
21//!     .build()
22//!     .unwrap();
23//! assert_eq!(container.sizes(), vec![7, 8, 8, 8, 15, 0]);
24//! ```
25//!
26//! You can give any argument to `Child::new`, it's stored in the child and returned by the `content()` method.
27//!
28
29use std::fmt;
30
31pub struct ContainerBuilder<C> {
32    available: usize,
33    margin_between: usize,
34    children: Vec<Child<C>>,
35}
36
37#[derive(Debug, Clone, Copy, Default)]
38pub enum Optionality {
39    #[default]
40    Required,
41    Optional {
42        priority: usize, // bigger is more important
43    },
44}
45
46pub struct Child<C> {
47    content: C,
48    constraints: ChildConstraints,
49    size: Option<usize>, // None if not (yet) included
50}
51
52#[derive(Debug, Clone, Copy)]
53pub struct ChildConstraints {
54    pub min: usize,
55    pub max: Option<usize>,
56    pub optionality: Optionality,
57    pub grow: f64,
58}
59
60impl Default for ChildConstraints {
61    fn default() -> Self {
62        ChildConstraints {
63            min: 0,
64            max: None,
65            optionality: Optionality::default(),
66            grow: 1.0,
67        }
68    }
69}
70
71pub struct Container<C> {
72    pub children: Vec<Child<C>>,
73}
74
75#[derive(Debug, Clone)]
76pub enum Error {
77    NotEnoughSpace,
78}
79impl std::error::Error for Error {}
80impl fmt::Display for Error {
81    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
82        match self {
83            Error::NotEnoughSpace => write!(f, "Not enough space"),
84        }
85    }
86}
87
88impl ChildConstraints {}
89
90impl<C> ContainerBuilder<C> {
91    pub fn with_available(available: usize) -> Self {
92        ContainerBuilder {
93            available,
94            children: Vec::new(),
95            margin_between: 0,
96        }
97    }
98    pub fn with_margin_between(mut self, margin: usize) -> Self {
99        self.margin_between = margin;
100        self
101    }
102    pub fn with(mut self, child: Child<C>) -> Self {
103        self.add(child);
104        self
105    }
106    pub fn add(&mut self, child: Child<C>) {
107        self.children.push(child);
108    }
109    pub fn build(self) -> Result<Container<C>, Error> {
110        let Self {
111            mut available,
112            mut children,
113            margin_between,
114        } = self;
115
116        // first pass: we only add the required children. If their min size
117        // is too big, we return an error.
118        let mut added_children = 0;
119        for child in &mut children {
120            child.size = if child.is_optional() {
121                None
122            } else {
123                let margin = if added_children > 0 {
124                    margin_between
125                } else {
126                    0
127                };
128                if child.constraints.min + margin > available {
129                    return Err(Error::NotEnoughSpace);
130                }
131                available -= child.constraints.min;
132                available -= margin;
133                added_children += 1;
134                Some(child.constraints.min)
135            };
136        }
137
138        // second pass: we add the optional children until we run out of space,
139        // by priority
140        let mut optional_children = children
141            .iter_mut()
142            .filter(|c| c.is_optional())
143            .collect::<Vec<_>>();
144        optional_children.sort_by_key(|c| {
145            std::cmp::Reverse(match c.constraints.optionality {
146                Optionality::Optional { priority } => priority,
147                _ => 0,
148            })
149        });
150        for child in optional_children {
151            let margin = if added_children > 0 {
152                margin_between
153            } else {
154                0
155            };
156            if child.constraints.min + margin > available {
157                continue;
158            }
159            available -= child.constraints.min;
160            available -= margin;
161            added_children += 1;
162            child.size = Some(child.constraints.min);
163        }
164
165        // then we distribute the remaining space to the growable children
166        let mut growths = vec![0.0; children.len()];
167        let mut sum_growths = 0.0;
168        for (i, child) in children.iter().enumerate() {
169            let Some(size) = child.size else {
170                continue;
171            };
172            growths[i] = child.constraints.grow
173                * (match child.constraints.max {
174                    None => available,
175                    Some(max) => max - size,
176                } as f64);
177            sum_growths += growths[i];
178        }
179        for i in 0..children.len() {
180            let Some(size) = children[i].size else {
181                continue;
182            };
183            let growth = growths[i] as f64 * (available as f64 / sum_growths);
184            available -= growth as usize;
185            children[i].size = Some(size + growth as usize);
186        }
187
188        // Due to down rounding, it's probable that there's some available space left.
189        while available > 0 {
190            let mut given = 0;
191            for child in &mut children {
192                let Some(size) = child.size else {
193                    continue;
194                };
195                if child.constraints.max.map_or(true, |max| size < max) {
196                    child.size = Some(size + 1);
197                    given += 1;
198                    available -= 1;
199                    if available == 0 {
200                        break;
201                    }
202                }
203            }
204            if given == 0 {
205                break;
206            }
207        }
208
209        let con = Container { children };
210        Ok(con)
211    }
212}
213
214impl<C> Child<C> {
215    pub fn new(content: C) -> Self {
216        let constraints = ChildConstraints::default();
217        Child {
218            content,
219            constraints,
220            size: None,
221        }
222    }
223    pub fn content(&self) -> &C {
224        &self.content
225    }
226    pub fn optional(self) -> Self {
227        self.optional_with_priority(0)
228    }
229    pub fn optional_with_priority(mut self, priority: usize) -> Self {
230        self.constraints.optionality = Optionality::Optional { priority };
231        self
232    }
233    pub fn with_min(mut self, min: usize) -> Self {
234        self.constraints.min = min;
235        self
236    }
237    pub fn with_max(mut self, max: usize) -> Self {
238        self.constraints.max = Some(max);
239        self
240    }
241    pub fn clamp(mut self, min: usize, max: usize) -> Self {
242        self.constraints.min = min;
243        self.constraints.max = Some(max);
244        self
245    }
246    pub fn with_size(mut self, size: usize) -> Self {
247        self.constraints.min = size;
248        self.constraints.max = Some(size);
249        self
250    }
251    pub fn with_grow(mut self, grow: f64) -> Self {
252        self.constraints.grow = grow;
253        self
254    }
255    pub fn constraints(&self) -> ChildConstraints {
256        self.constraints
257    }
258    fn is_optional(&self) -> bool {
259        matches!(self.constraints.optionality, Optionality::Optional { .. })
260    }
261    /// Return the size, if the child is included in the container, or none
262    /// if there wasn't enough space to include it
263    pub fn size(&self) -> Option<usize> {
264        self.size
265    }
266}
267
268impl<C> Container<C> {
269    pub fn builder_in(available: usize) -> ContainerBuilder<C> {
270        ContainerBuilder::with_available(available)
271    }
272    /// Return the sizes of the children, in the order they were added,
273    /// with 0 for non-included children
274    pub fn sizes(&self) -> Vec<usize> {
275        self.children
276            .iter()
277            .map(|sc| sc.size.unwrap_or(0))
278            .collect()
279    }
280    pub fn children(&self) -> &[Child<C>] {
281        &self.children
282    }
283    pub fn to_children(self) -> Vec<Child<C>> {
284        self.children
285    }
286}