simple_layout/
linear.rs

1use std::num::Saturating;
2use std::{cmp::Ordering, marker::PhantomData, ops::Deref};
3
4use embedded_graphics::{
5    pixelcolor::PixelColor,
6    prelude::{DrawTarget, Point, Size},
7    primitives::Rectangle,
8};
9
10use crate::{layoutable::Layoutable, ComponentSize, ValueRange};
11
12pub trait Orientation {
13    fn split_component_size(
14        size: ComponentSize,
15    ) -> (ValueRange<Saturating<u32>>, ValueRange<Saturating<u32>>);
16    fn split_size(size: Size) -> (Saturating<u32>, Saturating<u32>);
17    fn split_point(p: Point) -> (Saturating<i32>, Saturating<i32>);
18    fn create_component_size(
19        along: ValueRange<Saturating<u32>>,
20        cross: ValueRange<Saturating<u32>>,
21    ) -> ComponentSize;
22    fn create_size(along: Saturating<u32>, across: Saturating<u32>) -> Size;
23    fn create_point(along: Saturating<i32>, cross: Saturating<i32>) -> Point;
24}
25
26pub struct Horizontal {}
27
28impl Orientation for Horizontal {
29    #[inline]
30    fn split_component_size(
31        size: ComponentSize,
32    ) -> (ValueRange<Saturating<u32>>, ValueRange<Saturating<u32>>) {
33        (size.width, size.height)
34    }
35
36    #[inline]
37    fn split_size(size: Size) -> (Saturating<u32>, Saturating<u32>) {
38        (Saturating(size.width), Saturating(size.height))
39    }
40
41    #[inline]
42    fn split_point(p: Point) -> (Saturating<i32>, Saturating<i32>) {
43        let Point { x, y } = p;
44        (Saturating(x), Saturating(y))
45    }
46
47    #[inline]
48    fn create_component_size(
49        along: ValueRange<Saturating<u32>>,
50        cross: ValueRange<Saturating<u32>>,
51    ) -> ComponentSize {
52        ComponentSize {
53            width: along,
54            height: cross,
55        }
56    }
57
58    #[inline]
59    fn create_size(along: Saturating<u32>, across: Saturating<u32>) -> Size {
60        Size {
61            width: along.0,
62            height: across.0,
63        }
64    }
65
66    #[inline]
67    fn create_point(along: Saturating<i32>, cross: Saturating<i32>) -> Point {
68        Point {
69            x: along.0,
70            y: cross.0,
71        }
72    }
73}
74
75pub struct Vertical {}
76
77impl Orientation for Vertical {
78    fn split_component_size(
79        size: ComponentSize,
80    ) -> (ValueRange<Saturating<u32>>, ValueRange<Saturating<u32>>) {
81        (size.height, size.width)
82    }
83
84    fn split_size(size: Size) -> (Saturating<u32>, Saturating<u32>) {
85        (Saturating(size.height), Saturating(size.width))
86    }
87
88    fn split_point(p: Point) -> (Saturating<i32>, Saturating<i32>) {
89        (Saturating(p.y), Saturating(p.x))
90    }
91
92    fn create_component_size(
93        along: ValueRange<Saturating<u32>>,
94        cross: ValueRange<Saturating<u32>>,
95    ) -> ComponentSize {
96        ComponentSize {
97            width: cross,
98            height: along,
99        }
100    }
101
102    fn create_size(along: Saturating<u32>, across: Saturating<u32>) -> Size {
103        Size {
104            width: across.0,
105            height: along.0,
106        }
107    }
108
109    fn create_point(along: Saturating<i32>, cross: Saturating<i32>) -> Point {
110        Point {
111            x: cross.0,
112            y: along.0,
113        }
114    }
115}
116
117pub trait LinearLayout<C: PixelColor, O: Orientation>: Sized {
118    fn len() -> usize;
119    fn fill_sizes(&self, sizes: &mut [ComponentSize]);
120    fn fill_weights(&self, weights: &mut [u32]);
121    fn draw_placed_components<DrawError>(
122        &self,
123        target: &mut impl DrawTarget<Color = C, Error = DrawError>,
124        places: &[Rectangle],
125    ) -> Result<(), DrawError>;
126}
127
128#[derive(Default, Debug)]
129pub struct SingleLinearLayout<L: Layoutable<C>, C: PixelColor, O: Orientation> {
130    layout: L,
131    weight: u32,
132    p1: PhantomData<C>,
133    p2: PhantomData<O>,
134}
135
136impl<L: Layoutable<C>, C: PixelColor, O: Orientation> Layoutable<C>
137    for SingleLinearLayout<L, C, O>
138{
139    fn size(&self) -> ComponentSize {
140        self.layout.size()
141    }
142
143    fn draw_placed<DrawError>(
144        &self,
145        target: &mut impl DrawTarget<Color = C, Error = DrawError>,
146        position: Rectangle,
147    ) -> Result<(), DrawError> {
148        self.layout.draw_placed(target, position)
149    }
150}
151
152impl<L: Layoutable<C>, C: PixelColor, O: Orientation> LinearLayout<C, O>
153    for SingleLinearLayout<L, C, O>
154{
155    #[inline]
156    fn len() -> usize {
157        1
158    }
159
160    #[inline]
161    fn fill_sizes(&self, sizes: &mut [ComponentSize]) {
162        sizes[0] = self.size();
163    }
164
165    #[inline]
166    fn fill_weights(&self, weights: &mut [u32]) {
167        weights[0] = self.weight;
168    }
169
170    #[inline]
171    fn draw_placed_components<DrawError>(
172        &self,
173        target: &mut impl DrawTarget<Color = C, Error = DrawError>,
174        places: &[Rectangle],
175    ) -> Result<(), DrawError> {
176        self.layout.draw_placed(target, places[0])
177    }
178}
179
180pub struct LayoutableLinearLayout<C: PixelColor, O: Orientation, LL: LinearLayout<C, O>>(
181    LL,
182    PhantomData<C>,
183    PhantomData<O>,
184);
185
186impl<C: PixelColor, O: Orientation, LL: LinearLayout<C, O>> LayoutableLinearLayout<C, O, LL> {
187    ///
188    /// append an additional element to the current linear stack
189    ///
190    /// # Arguments
191    ///
192    /// * `element`: new element
193    /// * `weight`: weight of the element
194    ///
195    /// returns: LayoutableLinearLayout<C, O, ChainingLinearLayout<LL, L, C, O>>
196    ///
197    pub fn append<L>(
198        self,
199        element: L,
200        weight: u32,
201    ) -> LayoutableLinearLayout<C, O, ChainingLinearLayout<LL, L, C, O>>
202    where
203        L: Layoutable<C>,
204    {
205        LayoutableLinearLayout(
206            ChainingLinearLayout {
207                base_layout: self.0,
208                layoutable: element,
209                weight,
210                p: Default::default(),
211                o: Default::default(),
212            },
213            PhantomData,
214            PhantomData,
215        )
216    }
217}
218
219impl<C: PixelColor, O: Orientation, LL: LinearLayout<C, O>> From<LL>
220    for LayoutableLinearLayout<C, O, LL>
221{
222    fn from(value: LL) -> Self {
223        LayoutableLinearLayout(value, PhantomData, PhantomData)
224    }
225}
226
227impl<C: PixelColor, O: Orientation, LL: LinearLayout<C, O>> Layoutable<C>
228    for LayoutableLinearLayout<C, O, LL>
229{
230    fn size(&self) -> ComponentSize {
231        let mut sizes = vec![ComponentSize::default(); LL::len()].into_boxed_slice();
232        self.0.fill_sizes(&mut sizes);
233        let mut total_along = ValueRange::default();
234        let mut total_cross = ValueRange::default();
235        for size in sizes.iter() {
236            let (along, cross) = O::split_component_size(*size);
237            total_along += along;
238            total_cross.expand(&cross);
239        }
240        O::create_component_size(total_along, total_cross)
241    }
242    fn draw_placed<DrawError>(
243        &self,
244        target: &mut impl DrawTarget<Color = C, Error = DrawError>,
245        position: Rectangle,
246    ) -> Result<(), DrawError> {
247        let (along_target, cross_target) = O::split_size(position.size);
248        let (mut along_offset, cross_offset) = O::split_point(position.top_left);
249
250        let mut sizes = vec![ComponentSize::default(); LL::len()].into_boxed_slice();
251        self.0.fill_sizes(&mut sizes);
252        let sizes = sizes
253            .iter()
254            .map(|s| O::split_component_size(*s).0)
255            .collect::<Box<_>>();
256        let preferred_sizes = sizes.iter().map(|s| s.preferred_value).collect::<Box<_>>();
257        let total_preferred: Saturating<u32> =
258            preferred_sizes.iter().fold(Saturating(0), |s, v| s + v);
259        let places = match along_target.cmp(&total_preferred) {
260            Ordering::Less => {
261                let min_sizes = sizes.iter().map(|s| s.min_value).collect::<Box<_>>();
262                let total_min = min_sizes.iter().fold(Saturating(0), |s, v| s + v);
263                if total_min >= along_target {
264                    min_sizes
265                } else {
266                    let mut remaining_budget = total_preferred - along_target;
267                    let mut result_sizes = preferred_sizes;
268                    let mut weights = vec![0; LL::len()].into_boxed_slice();
269                    self.0.fill_weights(&mut weights);
270                    while remaining_budget > Saturating(0) {
271                        let remaining_budget_before = remaining_budget;
272                        let mut entries_with_headroom = weights
273                            .iter()
274                            .zip(result_sizes.iter_mut())
275                            .zip(sizes.iter())
276                            .filter(|((weight, result_size), size)| {
277                                **weight > 0 && **result_size > size.min_value
278                            })
279                            .collect::<Box<_>>();
280                        let mut remaining_weights: u32 = entries_with_headroom
281                            .iter()
282                            .map(|((weight, _), _)| **weight)
283                            .sum();
284                        if remaining_weights == 0 {
285                            break;
286                        }
287                        for ((weight, result_size), size) in entries_with_headroom.iter_mut() {
288                            let theoretical_decrease = remaining_budget * Saturating(**weight)
289                                / Saturating(remaining_weights);
290                            let selected_decrease =
291                                (theoretical_decrease).min(**result_size - size.min_value);
292                            **result_size -= selected_decrease;
293                            remaining_budget -= theoretical_decrease;
294                            remaining_weights -= *weight;
295                        }
296                        if remaining_budget_before == remaining_budget {
297                            // nothing more to distribute -> break
298                            break;
299                        }
300                    }
301                    result_sizes
302                }
303            }
304            Ordering::Equal => preferred_sizes,
305            Ordering::Greater => {
306                let max_sizes = sizes.iter().map(|s| s.max_value).collect::<Box<_>>();
307                let total_max = max_sizes.iter().fold(Saturating(0), |s, v| s + v);
308                if total_max <= along_target {
309                    max_sizes
310                } else {
311                    let mut remaining_budget = along_target - total_preferred;
312                    let mut result_sizes = preferred_sizes;
313                    let mut weights = vec![0; LL::len()].into_boxed_slice();
314                    self.0.fill_weights(&mut weights);
315                    while remaining_budget > Saturating(0) {
316                        let remaining_budget_before = remaining_budget;
317                        let mut entries_with_headroom = weights
318                            .iter()
319                            .zip(result_sizes.iter_mut())
320                            .zip(sizes.iter())
321                            .filter(|((weight, result_size), size)| {
322                                **weight > 0 && **result_size < size.max_value
323                            })
324                            .collect::<Box<_>>();
325                        let mut remaining_weights: u32 = entries_with_headroom
326                            .iter()
327                            .map(|((weight, _), _)| **weight)
328                            .sum();
329                        if remaining_weights == 0 {
330                            break;
331                        }
332
333                        for ((weight, result_size), size) in entries_with_headroom.iter_mut() {
334                            let theoretical_increase = remaining_budget * Saturating(**weight)
335                                / Saturating(remaining_weights);
336                            let selected_increase =
337                                (theoretical_increase).min(size.max_value - **result_size);
338                            **result_size += selected_increase;
339                            remaining_budget -= theoretical_increase;
340                            remaining_weights -= *weight;
341                        }
342                        if remaining_budget_before == remaining_budget {
343                            // nothing more to distribute -> break
344                            break;
345                        }
346                    }
347                    result_sizes
348                }
349            }
350        }
351        .iter()
352        .map(|l| {
353            let place = Rectangle {
354                top_left: O::create_point(along_offset, cross_offset),
355                size: O::create_size(*l, cross_target),
356            };
357            along_offset += Saturating(l.0 as i32);
358            place
359        })
360        .collect::<Box<_>>();
361        self.0.draw_placed_components(target, &places)
362    }
363}
364
365impl<C: PixelColor, O: Orientation, LL: LinearLayout<C, O>> Deref
366    for LayoutableLinearLayout<C, O, LL>
367{
368    type Target = LL;
369
370    fn deref(&self) -> &Self::Target {
371        &self.0
372    }
373}
374
375/*
376impl<L1: Layoutable<C>, L2: Layoutable<C>, C: PixelColor, O: Orientation> From<(L1, L2)>
377for LinearPair<L1, L2, C, O>
378{
379fn from((l1, l2): (L1, L2)) -> Self {
380    Self {
381        l1,
382        l2,
383        weights: [1, 1],
384        p1: PhantomData,
385        p2: PhantomData,
386    }
387}
388}
389*/
390pub struct ChainingLinearLayout<
391    LL: LinearLayout<C, O>,
392    L: Layoutable<C>,
393    C: PixelColor,
394    O: Orientation,
395> {
396    base_layout: LL,
397    layoutable: L,
398    weight: u32,
399    p: PhantomData<C>,
400    o: PhantomData<O>,
401}
402
403impl<LL: LinearLayout<C, O>, L: Layoutable<C>, C: PixelColor, O: Orientation> LinearLayout<C, O>
404    for ChainingLinearLayout<LL, L, C, O>
405{
406    #[inline]
407    fn len() -> usize {
408        LL::len() + 1
409    }
410
411    #[inline]
412    fn fill_sizes(&self, sizes: &mut [ComponentSize]) {
413        let idx = Self::len() - 1;
414        self.base_layout.fill_sizes(&mut sizes[0..idx]);
415        sizes[idx] = self.layoutable.size();
416    }
417
418    #[inline]
419    fn fill_weights(&self, weights: &mut [u32]) {
420        let idx = Self::len() - 1;
421        self.base_layout.fill_weights(&mut weights[0..idx]);
422        weights[idx] = self.weight;
423    }
424
425    #[inline]
426    fn draw_placed_components<DrawError>(
427        &self,
428        target: &mut impl DrawTarget<Color = C, Error = DrawError>,
429        places: &[Rectangle],
430    ) -> Result<(), DrawError> {
431        let idx = Self::len() - 1;
432        self.base_layout
433            .draw_placed_components(target, &places[0..idx])?;
434        self.layoutable.draw_placed(target, places[idx])
435    }
436}
437
438///
439/// Stack multiple layout elements vertically
440///
441/// # Arguments
442///
443/// * `first_child`: First layout element to stack
444/// * `first_child_weight`: Weight of this element when expansion or shrinking is needed to fit elements vertically
445///
446/// returns: LayoutableLinearLayout<C, Vertical, SingleLinearLayout<L, C, Vertical>>
447///
448/// # Examples
449///
450/// ```
451/// use embedded_graphics::mono_font::iso_8859_1::FONT_6X12;
452/// use embedded_graphics::mono_font::MonoTextStyle;
453/// use embedded_graphics::pixelcolor::BinaryColor;
454/// use embedded_graphics::prelude::Point;
455/// use embedded_graphics::text::Text;
456/// use simple_layout::prelude::{bordered, center, expand, horizontal_layout, optional_placement, owned_text, padding, RoundedLine, scale, vertical_layout};
457/// const TEXT_STYLE: MonoTextStyle<BinaryColor> = MonoTextStyle::new(&FONT_6X12, BinaryColor::On);
458/// let mut minus_button_placement=None;
459/// let mut plus_button_placement=None;
460/// let value = 0.7;
461/// let data_visualization = vertical_layout(expand(center(owned_text(format!("{value:.1}"), TEXT_STYLE))),1,)
462///                 .append(scale(value, BinaryColor::On), 0);
463/// let numbered_scale = expand(center(
464///         horizontal_layout(
465///             center(optional_placement(
466///                 &mut minus_button_placement,
467///                 bordered(
468///                     padding(Text::new("-", Point::zero(), TEXT_STYLE), -2, 1, -1, 1),
469///                     RoundedLine::new(BinaryColor::On),
470///                 ),
471///             )),
472///             0,
473///         )
474///         .append(data_visualization, 1)
475///         .append(
476///             center(optional_placement(
477///                 &mut plus_button_placement,
478///                 bordered(
479///                     padding(Text::new("+", Point::zero(), TEXT_STYLE), -2, 1, -1, 1),
480///                     RoundedLine::new(BinaryColor::On),
481///                 ),
482///             )),
483///             0,
484///         ),
485///     ));
486/// ```
487pub fn vertical_layout<L: Layoutable<C>, C: PixelColor>(
488    first_child: L,
489    first_child_weight: u32,
490) -> LayoutableLinearLayout<C, Vertical, SingleLinearLayout<L, C, Vertical>> {
491    LayoutableLinearLayout(
492        SingleLinearLayout {
493            layout: first_child,
494            weight: first_child_weight,
495            p1: PhantomData,
496            p2: PhantomData,
497        },
498        PhantomData,
499        PhantomData,
500    )
501}
502
503///
504///
505/// # Arguments
506///
507/// * `first_child`: First layout element to stack
508/// * `first_child_weight`: Weight of this element when expansion or shrinking is needed to fit elements vertically
509///
510/// returns: LayoutableLinearLayout<C, Horizontal, SingleLinearLayout<L, C, Horizontal>>
511///
512pub fn horizontal_layout<L: Layoutable<C>, C: PixelColor>(
513    first_child: L,
514    first_child_weight: u32,
515) -> LayoutableLinearLayout<C, Horizontal, SingleLinearLayout<L, C, Horizontal>> {
516    LayoutableLinearLayout(
517        SingleLinearLayout {
518            layout: first_child,
519            weight: first_child_weight,
520            p1: PhantomData,
521            p2: PhantomData,
522        },
523        PhantomData,
524        PhantomData,
525    )
526}