cursive_flexbox/
lib.rs

1//! A [flexbox layout](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Flexbox)
2//! implementation for the [Rust Cursive TUI library](https://crates.io/crates/cursive) that tries
3//! to adhere to the [CSS3 specification](https://w3c.github.io/csswg-drafts/css-flexbox/#intro)
4//! as much as possible and where it makes sense for a TUI. Users who are already
5//! familiar with it should feel right at home working with this library.
6
7#![warn(
8    missing_docs,
9    future_incompatible,
10    rust_2018_idioms,
11    let_underscore,
12    clippy::missing_docs_in_private_items
13)]
14
15mod layout;
16#[allow(missing_docs, clippy::missing_docs_in_private_items)]
17pub mod prelude;
18
19use std::{
20    cell::RefCell,
21    fmt::Display,
22    rc::{Rc, Weak},
23};
24
25use cursive_core::{event::EventResult, view::IntoBoxedView, Rect, Vec2, View, XY};
26use layout::{Layout, PlacedElement};
27
28/// A container that can be used to display a list of items in a flexible way.
29pub struct Flexbox {
30    /// The content of the flexbox. Unlike some flexboxes, order is always dictated by the order of
31    /// the items in `content`. There is no way to overwrite this.
32    content: Vec<Rc<RefCell<FlexItem>>>,
33    /// Options to alter the behavior.
34    options: FlexBoxOptions,
35    /// The currently active view.
36    focused: Option<usize>,
37    /// The actual layout of the items.
38    layout: Option<Layout<Rc<RefCell<FlexItem>>>>,
39    /// Whether the layout needs to be regenerated for the current state.
40    needs_relayout: bool,
41}
42
43impl Default for Flexbox {
44    fn default() -> Self {
45        Self {
46            content: Default::default(),
47            options: Default::default(),
48            focused: Default::default(),
49            layout: Default::default(),
50            needs_relayout: true,
51        }
52    }
53}
54
55/// A single item in a Flexbox.
56pub struct FlexItem {
57    /// The actual view represented by this flex item.
58    view: Box<dyn View>,
59    /// A relative amount of free space in the main axis this item is in that should be given to
60    /// this item. The amount is relative as it's proportional to the total amount of free space
61    /// requested by all items in the same main axis.
62    flex_grow: u8,
63}
64
65/// Options that can alter the behavior of a flexbox.
66#[derive(Default, Clone, Copy)]
67struct FlexBoxOptions {
68    /// The direction of the main axis.
69    direction: FlexDirection,
70    /// Algorithm that assigns extra space on the main axis. This does nothing if any of the items
71    /// on a main axis request extra space with flex-grow.
72    justification: JustifyContent,
73    /// How to place items on the cross axis.
74    item_alignment: AlignItems,
75    /// How to place the main axes in the container.
76    axes_alignment: AlignContent,
77    /// Gap between items on the main axis. The gap doesn't get added to the sides.
78    main_axis_gap: u32,
79    /// Gap between the main axes.
80    cross_axis_gap: u32,
81    /// Wrapping behavior of the main axes.
82    wrap: FlexWrap,
83}
84
85// https://developer.mozilla.org/en-US/docs/Web/CSS/flex-direction
86// https://w3c.github.io/csswg-drafts/css-flexbox/#flex-direction-property
87/// Direction of a flex container's main axis.
88#[non_exhaustive] // TODO: Implement RowReverse and ColumnReverse!
89#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
90pub enum FlexDirection {
91    /// Flex items are layed out in a row.
92    #[default]
93    Row,
94    // /// Flex items are layed out in a row, in reverse order.
95    // RowReverse,
96    /// Flex items are layed out in a column.
97    Column,
98    // /// Flex items are layed out in a column, in reverse order.
99    // ColumnReverse,
100}
101
102impl Display for FlexDirection {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        write!(
105            f,
106            "{}",
107            match self {
108                Self::Row => "row",
109                Self::Column => "column",
110            }
111        )
112    }
113}
114
115// https://developer.mozilla.org/en-US/docs/Web/CSS/flex-wrap
116// https://w3c.github.io/csswg-drafts/css-flexbox/#flex-wrap-property
117/// Wrapping behavior and direction of a flexbox container's main axis.
118#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
119pub enum FlexWrap {
120    /// Don't wrap the main axis.
121    #[default]
122    NoWrap,
123    /// Wrap the main axis.
124    Wrap,
125    /// Wrap the main axis in the opposite direction.
126    WrapReverse,
127}
128
129impl Display for FlexWrap {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        write!(
132            f,
133            "{}",
134            match self {
135                Self::NoWrap => "nowrap",
136                Self::Wrap => "wrap",
137                Self::WrapReverse => "wrap-reverse",
138            }
139        )
140    }
141}
142
143// https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content
144// https://w3c.github.io/csswg-drafts/css-flexbox/#propdef-justify-content
145/// Alignment of items in a flexbox along the main axis.
146#[non_exhaustive] // Specification lists more options. Might be added later.
147#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
148pub enum JustifyContent {
149    /// Flex items are packed against the start of the container.
150    #[default] // Following w3c specification as there is no 'normal' option.
151    FlexStart,
152    /// Flex items are packed against the end of the container.
153    FlexEnd,
154    /// Flex items are packed in the center, with equal space to either side.
155    Center,
156    /// Flex items are packed with equal space between them.
157    SpaceBetween,
158    /// Flex items are packed with equal space around each item.
159    SpaceAround,
160    /// Flex items are packed with equal space between all items (including the sides).
161    SpaceEvenly, // Included although not in w3c specification.
162}
163
164impl Display for JustifyContent {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        write!(
167            f,
168            "{}",
169            match self {
170                Self::FlexStart => "flex-start",
171                Self::FlexEnd => "flex-end",
172                Self::Center => "center",
173                Self::SpaceBetween => "space-between",
174                Self::SpaceAround => "space-around",
175                Self::SpaceEvenly => "space-evenly",
176            }
177        )
178    }
179}
180
181// https://developer.mozilla.org/en-US/docs/Web/CSS/align-items
182// https://w3c.github.io/csswg-drafts/css-flexbox/#align-items-property
183// Baseline isn't included as Cursive doesn't support it, and it makes little sense in a TUI.
184/// Alignment of items in a flexbox along the cross axis.
185#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
186pub enum AlignItems {
187    /// Align flex items at the start of the cross axis.
188    FlexStart,
189    /// Align flex items at the end of the cross axis.
190    FlexEnd,
191    /// Align flex items at the center of the cross axis.
192    Center,
193    /// Stretch flex items to fill all the space along the cross axis.
194    #[default] // Following w3c specification as there is no 'normal' option.
195    Stretch,
196}
197
198impl Display for AlignItems {
199    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200        write!(
201            f,
202            "{}",
203            match self {
204                Self::FlexStart => "flex-start",
205                Self::FlexEnd => "flex-end",
206                Self::Center => "center",
207                Self::Stretch => "stretch",
208            }
209        )
210    }
211}
212
213// https://developer.mozilla.org/en-US/docs/Web/CSS/align-content
214// https://w3c.github.io/csswg-drafts/css-flexbox/#align-content-property
215/// Alignment of the main axes in a flexbox.
216#[non_exhaustive] // Might add space-evenly, even though not in w3c specification.
217#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
218pub enum AlignContent {
219    /// Align content to the start of the container.
220    #[default]
221    FlexStart,
222    /// Align content to the end of the container.
223    FlexEnd,
224    /// Align content to the center of the container.
225    Center,
226    /// Stretch content along the cross axis.
227    Stretch,
228    /// Align main axis with an equal amount of space between them.
229    SpaceBetween,
230    /// Align main axis with an equal of margin per axis.
231    SpaceAround,
232}
233
234impl Display for AlignContent {
235    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236        write!(
237            f,
238            "{}",
239            match self {
240                Self::FlexStart => "flex-start",
241                Self::FlexEnd => "flex-end",
242                Self::Center => "center",
243                Self::Stretch => "stretch",
244                Self::SpaceBetween => "space-between",
245                Self::SpaceAround => "space-around",
246            }
247        )
248    }
249}
250
251/// An actual layout of a flexbox with real dimensions.
252/// <https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Flexbox#the_flex_model>
253#[derive(Default)]
254struct FlexboxLayout {
255    /// The dimensions of the container.
256    size: XY<usize>,
257    /// Options for this particular layout of the flexbox.
258    options: FlexBoxOptions,
259    /// Parts that together form the entire main axis of this flexbox.
260    main_axes: Vec<MainAxis>,
261}
262
263/// Any error that can arise from operations on a flexbox.
264#[derive(Debug)]
265enum FlexboxError {
266    /// Error when trying to add too many items to one axis.
267    AxisFull,
268}
269
270impl FlexboxLayout {
271    /// Return all the child items along with their absolute position. This makes drawing the
272    /// flexbox very simple.
273    pub fn windows(&mut self) -> Vec<(Rc<RefCell<FlexItem>>, Rect)> {
274        let mut windows = Vec::new();
275        let mut cross_offset = 0;
276        let mut assignable_free_space = self.cross_axis_free_space();
277
278        for (axis_index, axis) in self.main_axes.iter().enumerate() {
279            match self.options.axes_alignment {
280                AlignContent::FlexEnd => {
281                    if assignable_free_space > 0 {
282                        cross_offset += assignable_free_space;
283                    }
284                    assignable_free_space = 0;
285                },
286                AlignContent::Center => {
287                    if assignable_free_space > 0 {
288                        cross_offset += assignable_free_space / 2;
289                    }
290                    assignable_free_space = 0;
291                },
292                AlignContent::SpaceAround => {
293                    let assigned_space =
294                        assignable_free_space / (self.main_axes.len() * 2 - axis_index * 2);
295                    if assignable_free_space > 0 {
296                        cross_offset += assigned_space;
297                    }
298                    assignable_free_space -= assigned_space;
299                },
300                _ => {},
301            }
302            for mut combo in axis.windows(self) {
303                match self.options.direction {
304                    FlexDirection::Row => combo.1.offset(XY::from((0, cross_offset))),
305                    FlexDirection::Column => combo.1.offset(XY::from((cross_offset, 0))),
306                }
307                windows.push(combo);
308            }
309            match self.options.axes_alignment {
310                AlignContent::SpaceBetween => {
311                    if assignable_free_space > 0 {
312                        let assigned_space =
313                            assignable_free_space / (self.main_axes.len() - axis_index - 1);
314                        if assignable_free_space > 0 {
315                            cross_offset += assigned_space;
316                        }
317                        assignable_free_space -= assigned_space;
318                    }
319                },
320                AlignContent::Stretch => {
321                    let assigned_space =
322                        assignable_free_space / (self.main_axes.len() - axis_index);
323                    if assignable_free_space > 0 {
324                        cross_offset += assigned_space;
325                    }
326                    assignable_free_space -= assigned_space;
327                },
328                AlignContent::SpaceAround => {
329                    let assigned_space =
330                        assignable_free_space / (self.main_axes.len() * 2 - (axis_index * 2 + 1));
331                    if assignable_free_space > 0 {
332                        cross_offset += assigned_space;
333                    }
334                    assignable_free_space -= assigned_space;
335                },
336                _ => {},
337            }
338            cross_offset += axis.cross_axis_size(self) + self.options.cross_axis_gap as usize;
339        }
340
341        windows
342    }
343
344    /// Return the amount of left over space on the cross axis.
345    pub fn cross_axis_free_space(&self) -> usize {
346        let mut used_space = 0;
347
348        for axis in &self.main_axes {
349            used_space += axis.cross_axis_size(self);
350        }
351
352        used_space += (self.main_axis_count() - 1) * self.options.cross_axis_gap as usize;
353
354        match self.options.direction {
355            FlexDirection::Row => self.size.y.saturating_sub(used_space),
356            FlexDirection::Column => self.size.x.saturating_sub(used_space),
357        }
358    }
359
360    /// Generate the actual layout for the flexbox with `content` and given `width` and `height`.
361    pub fn generate(
362        content: &[Weak<RefCell<FlexItem>>],
363        width: usize,
364        height: usize,
365        options: FlexBoxOptions,
366    ) -> Rc<RefCell<Self>> {
367        let layout = Rc::new(RefCell::new(FlexboxLayout {
368            size: XY::from((width, height)),
369            options,
370            main_axes: Vec::new(),
371        }));
372
373        // TODO: This is a bit (very much) anti-idiomatic Rust...
374
375        let mut added = 0;
376        let length = content.len();
377
378        while added < length {
379            let mut main_axis = MainAxis::new(Rc::downgrade(&layout));
380
381            loop {
382                let result =
383                    main_axis.add_item(content[added].clone(), &mut RefCell::borrow_mut(&layout));
384                if result.is_err() {
385                    // If the current main axis couldn't hold the item anymore.
386                    break;
387                } else if added + 1 == length {
388                    // If this was the last element to add to the flexbox.
389                    added += 1;
390                    break;
391                } else {
392                    // If the current main axis could still hold the item.
393                    added += 1;
394                }
395            }
396
397            // PERF: Inserting elements at the front isn't ideal for performance.
398            match options.wrap {
399                FlexWrap::NoWrap | FlexWrap::Wrap => {
400                    RefCell::borrow_mut(&layout).main_axes.push(main_axis)
401                },
402                FlexWrap::WrapReverse => {
403                    RefCell::borrow_mut(&layout).main_axes.insert(0, main_axis)
404                },
405            }
406        }
407
408        layout
409    }
410
411    /// Return the size of a [FlexItem] along the main axis.
412    pub fn flexitem_main_axis_size(&self, item: &mut FlexItem) -> usize {
413        match self.options.direction {
414            FlexDirection::Row => item.view.required_size(self.size).x,
415            FlexDirection::Column => item.view.required_size(self.size).y,
416        }
417    }
418
419    /// Return the amount of main axes in this layout.
420    pub fn main_axis_count(&self) -> usize {
421        self.main_axes.len()
422    }
423}
424
425/// A single main axis of a flexbox. In a flexbox without wrap, this will be the only main axis and
426/// contain all the items. In a flexbox with wrap, this axis will only hold as many items as it can
427/// accomodate given the size of the main axis and the gapsize of the main axis.
428struct MainAxis {
429    /// The items in this main axis.
430    items: Vec<Weak<RefCell<FlexItem>>>,
431    /// Cache value for the remaining free space in this axis.
432    free_space: usize,
433}
434
435impl MainAxis {
436    /// Create a new main axis part for the given layout.
437    pub fn new(layout: Weak<RefCell<FlexboxLayout>>) -> Self {
438        let layout_upgraded = layout.upgrade().unwrap();
439        let free_space = match RefCell::borrow(&layout_upgraded).options.direction {
440            FlexDirection::Row => RefCell::borrow(&layout_upgraded).size.x,
441            FlexDirection::Column => RefCell::borrow(&layout_upgraded).size.y,
442        };
443        MainAxis {
444            items: Vec::new(),
445            free_space,
446        }
447    }
448
449    /// Return the cross axis size. The size of the cross axis is the maximum size of its elements
450    /// along the cross axis.
451    pub fn cross_axis_size(&self, layout: &FlexboxLayout) -> usize {
452        let mut maximum_item_cross_axis_size = 0;
453        match layout.options.direction {
454            FlexDirection::Row => {
455                for item in &self.items {
456                    maximum_item_cross_axis_size = maximum_item_cross_axis_size.max(
457                        RefCell::borrow_mut(&item.upgrade().unwrap())
458                            .view
459                            .required_size(layout.size)
460                            .y,
461                    );
462                }
463            },
464            FlexDirection::Column => {
465                for item in &self.items {
466                    maximum_item_cross_axis_size = maximum_item_cross_axis_size.max(
467                        RefCell::borrow_mut(&item.upgrade().unwrap())
468                            .view
469                            .required_size(layout.size)
470                            .x,
471                    );
472                }
473            },
474        }
475
476        maximum_item_cross_axis_size
477    }
478
479    /// Returns the flexitems and their corresponding windows in the local coordinates (relative to
480    /// the topleft of the bounding box of this axis.
481    pub fn windows(&self, layout: &FlexboxLayout) -> Vec<(Rc<RefCell<FlexItem>>, Rect)> {
482        let mut windows = Vec::new();
483        let mut offset = 0;
484        let mut assignable_free_space = self.free_space;
485        let combined_grow_factor = self.combined_grow_factor();
486        let mut remaining_grow_factor = combined_grow_factor;
487        let cross_axis_size = self.cross_axis_size(layout);
488
489        for (item_index, item) in self
490            .items
491            .iter()
492            .map(|item| item.upgrade().unwrap())
493            .enumerate()
494        {
495            let mut start_x = 0;
496            let mut start_y = 0;
497            let mut width = 1;
498            let mut height = 1;
499
500            if combined_grow_factor > 0 {
501                // Axis contains elements that want the free space. Give it to them, don't use
502                // justify-content.
503
504                let mut current_item_assigned_space = 0;
505                let item_main_axis_size =
506                    layout.flexitem_main_axis_size(&mut RefCell::borrow_mut(&item));
507                if remaining_grow_factor > 0 {
508                    current_item_assigned_space =
509                        ((RefCell::borrow(&item).flex_grow as f64 / remaining_grow_factor as f64)
510                            * assignable_free_space as f64) as usize;
511                }
512
513                match layout.options.direction {
514                    FlexDirection::Row => {
515                        start_x = offset;
516                        width = item_main_axis_size + current_item_assigned_space;
517                    },
518                    FlexDirection::Column => {
519                        start_y = offset;
520                        height = item_main_axis_size + current_item_assigned_space;
521                    },
522                }
523                offset += item_main_axis_size
524                    + layout.options.main_axis_gap as usize
525                    + current_item_assigned_space;
526                assignable_free_space -= current_item_assigned_space;
527                remaining_grow_factor -= RefCell::borrow(&item).flex_grow as usize;
528            } else {
529                // Axis doesn't contain elements that want free space. Use justify-content property
530                // to decide positioning.
531
532                match layout.options.direction {
533                    FlexDirection::Row => {
534                        width = RefCell::borrow_mut(&item).view.required_size(layout.size).x;
535                    },
536                    FlexDirection::Column => {
537                        height = RefCell::borrow_mut(&item).view.required_size(layout.size).y;
538                    },
539                }
540
541                // Decides `start_x`, `width` is item's preferred width.
542                match layout.options.justification {
543                    JustifyContent::FlexStart => {
544                        match layout.options.direction {
545                            FlexDirection::Row => {
546                                start_x = offset;
547                            },
548                            FlexDirection::Column => {
549                                start_y = offset;
550                            },
551                        }
552
553                        offset += layout.flexitem_main_axis_size(&mut RefCell::borrow_mut(&item))
554                            + layout.options.main_axis_gap as usize;
555                    },
556                    JustifyContent::FlexEnd => {
557                        if assignable_free_space > 0 {
558                            offset = assignable_free_space;
559                            assignable_free_space = 0;
560                        }
561                        match layout.options.direction {
562                            FlexDirection::Row => {
563                                start_x = offset;
564                            },
565                            FlexDirection::Column => {
566                                start_y = offset;
567                            },
568                        }
569
570                        offset += layout.flexitem_main_axis_size(&mut RefCell::borrow_mut(&item))
571                            + layout.options.main_axis_gap as usize;
572                    },
573                    JustifyContent::Center => {
574                        if assignable_free_space > 0 {
575                            offset = assignable_free_space / 2;
576                            assignable_free_space = 0;
577                        }
578
579                        match layout.options.direction {
580                            FlexDirection::Row => {
581                                start_x = offset;
582                            },
583                            FlexDirection::Column => {
584                                start_y = offset;
585                            },
586                        }
587
588                        offset += layout.flexitem_main_axis_size(&mut RefCell::borrow_mut(&item))
589                            + layout.options.main_axis_gap as usize;
590                    },
591                    JustifyContent::SpaceBetween => {
592                        match layout.options.direction {
593                            FlexDirection::Row => {
594                                start_x = offset;
595                            },
596                            FlexDirection::Column => {
597                                start_y = offset;
598                            },
599                        }
600
601                        if assignable_free_space > 0 && item_index + 1 < self.number_of_items() {
602                            let extra_free_space = assignable_free_space
603                                / (self.number_of_items().saturating_sub(1 + item_index));
604                            assignable_free_space -= extra_free_space;
605                            offset += extra_free_space;
606                        }
607                        offset += layout.flexitem_main_axis_size(&mut RefCell::borrow_mut(&item))
608                            + layout.options.main_axis_gap as usize;
609                    },
610                    JustifyContent::SpaceAround => {
611                        let mut extra_free_space =
612                            assignable_free_space / (self.number_of_items() * 2 - item_index * 2);
613                        if assignable_free_space > 0 {
614                            offset += extra_free_space;
615                        }
616                        assignable_free_space -= extra_free_space;
617
618                        match layout.options.direction {
619                            FlexDirection::Row => {
620                                start_x = offset;
621                            },
622                            FlexDirection::Column => {
623                                start_y = offset;
624                            },
625                        }
626
627                        extra_free_space = assignable_free_space
628                            / (self.number_of_items() * 2 - (item_index * 2 + 1));
629                        if assignable_free_space > 0 {
630                            offset += extra_free_space;
631                        }
632                        assignable_free_space -= extra_free_space;
633
634                        offset += layout.flexitem_main_axis_size(&mut RefCell::borrow_mut(&item))
635                            + layout.options.main_axis_gap as usize;
636                    },
637                    JustifyContent::SpaceEvenly => {
638                        let extra_free_space =
639                            assignable_free_space / (self.number_of_items() + 1 - item_index);
640                        if assignable_free_space > 0 {
641                            offset += extra_free_space;
642                        }
643                        assignable_free_space -= extra_free_space;
644
645                        match layout.options.direction {
646                            FlexDirection::Row => {
647                                start_x = offset;
648                            },
649                            FlexDirection::Column => {
650                                start_y = offset;
651                            },
652                        }
653
654                        offset += layout.flexitem_main_axis_size(&mut RefCell::borrow_mut(&item))
655                            + layout.options.main_axis_gap as usize;
656                    },
657                }
658            }
659
660            // Decides `start_y` and `height`. Item's `layout()` called with this calculated height
661            // later.
662            match layout.options.item_alignment {
663                AlignItems::FlexStart => match layout.options.direction {
664                    FlexDirection::Row => {
665                        start_y = 0;
666                        height = RefCell::borrow_mut(&item).view.required_size(layout.size).y;
667                    },
668                    FlexDirection::Column => {
669                        start_x = 0;
670                        width = RefCell::borrow_mut(&item).view.required_size(layout.size).x;
671                    },
672                },
673                AlignItems::FlexEnd => match layout.options.direction {
674                    FlexDirection::Row => {
675                        height = RefCell::borrow_mut(&item).view.required_size(layout.size).y;
676                        start_y = cross_axis_size - height;
677                    },
678                    FlexDirection::Column => {
679                        width = RefCell::borrow_mut(&item).view.required_size(layout.size).x;
680                        start_x = cross_axis_size - width;
681                    },
682                },
683                AlignItems::Center => match layout.options.direction {
684                    FlexDirection::Row => {
685                        height = RefCell::borrow_mut(&item).view.required_size(layout.size).y;
686                        start_y = (cross_axis_size - height) / 2;
687                    },
688                    FlexDirection::Column => {
689                        width = RefCell::borrow_mut(&item).view.required_size(layout.size).x;
690                        start_x = (cross_axis_size - width) / 2;
691                    },
692                },
693                AlignItems::Stretch => match layout.options.direction {
694                    FlexDirection::Row => {
695                        height = cross_axis_size;
696                        start_y = 0;
697                    },
698                    FlexDirection::Column => {
699                        width = cross_axis_size;
700                        start_x = 0;
701                    },
702                },
703            }
704
705            RefCell::borrow_mut(&item)
706                .view
707                .layout((width, height).into());
708            windows.push((item, Rect::from_size((start_x, start_y), (width, height))));
709        }
710
711        windows
712    }
713
714    /// Try to add `item` to this main axis, fail if this axis can't accomodate the item.
715    pub fn add_item(
716        &mut self,
717        item: Weak<RefCell<FlexItem>>,
718        layout: &mut FlexboxLayout,
719    ) -> Result<(), FlexboxError> {
720        let upgraded_item = item.upgrade().unwrap();
721        if self.can_accomodate(&mut RefCell::borrow_mut(&upgraded_item), layout) {
722            self.free_space = self.free_space.saturating_sub(
723                layout.flexitem_main_axis_size(&mut RefCell::borrow_mut(&upgraded_item)),
724            );
725
726            // Only add gaps if there is already an item.
727            if self.number_of_items() >= 1 {
728                self.free_space = self
729                    .free_space
730                    .saturating_sub(layout.options.main_axis_gap as usize);
731            }
732
733            self.items.push(item);
734
735            Ok(())
736        } else {
737            Err(FlexboxError::AxisFull)
738        }
739    }
740
741    /// Return whether this axis can accomodate `item` with the amount of free space it has left. A
742    /// main axis can accomodate an item if either it is the first axis in a non-wrapped flexbox,
743    /// or it has enough space for the item and possible gap that would be added.
744    pub fn can_accomodate(&self, item: &mut FlexItem, layout: &mut FlexboxLayout) -> bool {
745        if let FlexWrap::NoWrap = layout.options.wrap {
746            // There can only be one main axis in a non-wrapping layout.
747            layout.main_axes.len() == 1
748        } else if self.items.is_empty() {
749            // Each main axis must be able to hold at least one item!
750            true
751        } else {
752            let extra_used_space = if self.number_of_items() >= 1 {
753                layout.flexitem_main_axis_size(item) + layout.options.main_axis_gap as usize
754            } else {
755                layout.flexitem_main_axis_size(item)
756            };
757            extra_used_space <= self.free_space
758        }
759    }
760
761    /// Return the number of items on this axis.
762    pub fn number_of_items(&self) -> usize {
763        self.items.len()
764    }
765
766    /// Sum of the flex-grow of all the [FlexItem]s in this axis.
767    pub fn combined_grow_factor(&self) -> usize {
768        let mut total_grow_factor = 0usize;
769        self.items.iter().for_each(|item| {
770            total_grow_factor += RefCell::borrow(&item.upgrade().unwrap()).flex_grow as usize;
771        });
772        total_grow_factor
773    }
774}
775
776impl<T: Into<FlexItem>> From<Vec<T>> for Flexbox {
777    fn from(value: Vec<T>) -> Self {
778        let content: Vec<Rc<RefCell<FlexItem>>> = value
779            .into_iter()
780            .map(|item| Rc::new(RefCell::new(item.into())))
781            .collect();
782        Self {
783            content,
784            ..Default::default()
785        }
786    }
787}
788
789impl Flexbox {
790    /// Create a new Flexbox with default options.
791    pub fn new() -> Self {
792        Self::default()
793    }
794
795    /// Add a view to the end.
796    pub fn push(&mut self, item: impl Into<FlexItem>) {
797        self.content.push(Rc::new(RefCell::new(item.into())));
798        self.needs_relayout = true;
799    }
800
801    /// Remove all items.
802    pub fn clear(&mut self) {
803        self.content.clear();
804        self.needs_relayout = true;
805    }
806
807    /// Insert a view at `index`.
808    ///
809    /// # Panics
810    /// Panics if `index > self.len()`.
811    pub fn insert(&mut self, index: usize, item: impl Into<FlexItem>) {
812        self.content
813            .insert(index, Rc::new(RefCell::new(item.into())));
814        self.needs_relayout = true;
815    }
816
817    /// Set the grow factor of an item.
818    ///
819    /// # Panics
820    /// Panics if `index >= self.len()`.
821    pub fn set_flex_grow(&mut self, index: usize, flex_grow: u8) {
822        Rc::as_ref(&self.content[index]).borrow_mut().flex_grow = flex_grow;
823        self.needs_relayout = true;
824    }
825
826    /// Returns the number of items in the flexbox.
827    pub fn len(&self) -> usize {
828        self.content.len()
829    }
830
831    /// Returns whether the flexbox is empty.
832    pub fn is_empty(&self) -> bool {
833        self.content.is_empty()
834    }
835
836    /// Remove an item from the flexbox.
837    ///
838    /// # Panics
839    /// Panics if `index >= self.len()`.
840    pub fn remove(&mut self, index: usize) {
841        self.content.remove(index);
842    }
843
844    /// Gap between items on the main axis.
845    pub fn main_axis_gap(&self) -> u32 {
846        self.options.main_axis_gap
847    }
848
849    /// Set the fixed gap between elements on the main axis.
850    pub fn set_main_axis_gap(&mut self, gap: u32) {
851        self.options.main_axis_gap = gap;
852        self.needs_relayout = true;
853    }
854
855    /// Gap between the main axes.
856    pub fn cross_axis_gap(&self) -> u32 {
857        self.options.cross_axis_gap
858    }
859
860    /// Set the fixed gap between the main axes.
861    pub fn set_cross_axis_gap(&mut self, gap: u32) {
862        self.options.cross_axis_gap = gap;
863        self.needs_relayout = true;
864    }
865
866    /// Get the justify-content option.
867    pub fn justify_content(&self) -> JustifyContent {
868        self.options.justification
869    }
870
871    /// Set the justify-content option.
872    pub fn set_justify_content(&mut self, justify_content: JustifyContent) {
873        self.options.justification = justify_content;
874        self.needs_relayout = true;
875    }
876
877    /// Get the align-items option.
878    pub fn align_items(&self) -> AlignItems {
879        self.options.item_alignment
880    }
881
882    /// Set the align-items option.
883    pub fn set_align_items(&mut self, item_alignment: AlignItems) {
884        self.options.item_alignment = item_alignment;
885        self.needs_relayout = true;
886    }
887
888    /// Get the align-content option.
889    pub fn align_content(&self) -> AlignContent {
890        self.options.axes_alignment
891    }
892
893    /// Set the align-content option.
894    pub fn set_align_content(&mut self, axes_alignment: AlignContent) {
895        self.options.axes_alignment = axes_alignment;
896        self.needs_relayout = true;
897    }
898
899    /// Get the flex-direction option.
900    pub fn flex_direction(&self) -> FlexDirection {
901        self.options.direction
902    }
903
904    /// Set the direction of the main axis.
905    pub fn set_flex_direction(&mut self, direction: FlexDirection) {
906        self.options.direction = direction;
907        self.needs_relayout = true;
908    }
909
910    /// Get the flex-wrap option.
911    pub fn flex_wrap(&self) -> FlexWrap {
912        self.options.wrap
913    }
914
915    /// Set the wrapping behavior.
916    pub fn set_flex_wrap(&mut self, wrap: FlexWrap) {
917        self.options.wrap = wrap;
918        self.needs_relayout = true;
919    }
920
921    /// Generate the concrete layout of this flexbox with the given constraints.
922    fn generate_layout(&self, constraints: XY<usize>) -> Layout<Rc<RefCell<FlexItem>>> {
923        let layout = FlexboxLayout::generate(
924            &self.content.iter().map(Rc::downgrade).collect::<Vec<_>>(),
925            constraints.x,
926            constraints.y,
927            self.options,
928        );
929        let mut result = Layout {
930            elements: Vec::new(),
931        };
932        RefCell::borrow_mut(&layout)
933            .windows()
934            .into_iter()
935            .for_each(|item| {
936                result.elements.push(PlacedElement {
937                    element: item.0,
938                    position: item.1,
939                })
940            });
941        result
942    }
943}
944
945impl View for Flexbox {
946    /// Draw this view using the printer.
947    fn draw(&self, printer: &cursive_core::Printer<'_, '_>) {
948        if let Some(ref layout) = self.layout {
949            for placed_element in layout {
950                RefCell::borrow(&placed_element.element)
951                    .view
952                    .draw(&printer.windowed(placed_element.position));
953            }
954        }
955    }
956
957    /// Called when the final size has been determined. `printer_size` will be the actual size of
958    /// the printer given to `draw()`. This should call layout on all child items with their
959    /// respective sizes.
960    fn layout(&mut self, printer_size: Vec2) {
961        // Generate the concrete layout for this flexbox.
962        self.layout = Some(self.generate_layout(printer_size));
963
964        // Use the layout to lay out the child views.
965        for placed_element in self.layout.as_ref().unwrap() {
966            RefCell::borrow_mut(&placed_element.element)
967                .view
968                .layout(placed_element.position.size());
969        }
970
971        self.needs_relayout = false;
972    }
973
974    /// Return true if this view needs a relayout before the next call to `draw()`. If the view's
975    /// layout is somehow cached, returning true here will cause `layout()` to be called so the new
976    /// layout can be computed.
977    fn needs_relayout(&self) -> bool {
978        // TODO: Reimplement proper detection of relayout requirements. Returning true always works
979        // but isn't efficient!
980        self.needs_relayout
981    }
982
983    /// Given `constraint`, return the minimal required size the printer for this view should be.
984    /// `constraint` is the maximum possible size for the printer.
985    fn required_size(&mut self, constraint: cursive_core::Vec2) -> cursive_core::Vec2 {
986        // PERF: Cache the values that the previous layout was generated with and regenerate if
987        // cached version is outdated.
988        constraint
989    }
990
991    fn on_event(
992        &mut self,
993        mut event: cursive_core::event::Event,
994    ) -> cursive_core::event::EventResult {
995        if let cursive_core::event::Event::Mouse {
996            ref mut offset,
997            ref mut position,
998            ..
999        } = event
1000        {
1001            if let Some(ref layout) = self.layout {
1002                if let Some(placed_element) =
1003                    layout.element_at(global_to_view_coordinates(*position, *offset))
1004                {
1005                    *offset = *offset + placed_element.position.top_left();
1006                    RefCell::borrow_mut(&placed_element.element)
1007                        .view
1008                        .on_event(event)
1009                } else {
1010                    EventResult::Ignored
1011                }
1012            } else {
1013                EventResult::Ignored
1014            }
1015        } else if let Some(active_child) = self.focused {
1016            RefCell::borrow_mut(&self.content[active_child])
1017                .view
1018                .on_event(event)
1019        } else {
1020            EventResult::Ignored
1021        }
1022    }
1023
1024    fn focus_view(
1025        &mut self,
1026        selector: &cursive_core::view::Selector<'_>,
1027    ) -> Result<EventResult, cursive_core::view::ViewNotFound> {
1028        for (index, view) in self.content.iter_mut().enumerate() {
1029            if let Ok(event_result) = RefCell::borrow_mut(view).view.focus_view(selector) {
1030                self.focused = Some(index);
1031                return Ok(event_result);
1032            }
1033        }
1034        Err(cursive_core::view::ViewNotFound)
1035    }
1036
1037    fn call_on_any(
1038        &mut self,
1039        selector: &cursive_core::view::Selector<'_>,
1040        callback: cursive_core::event::AnyCb<'_>,
1041    ) {
1042        for view in self.content.iter_mut() {
1043            RefCell::borrow_mut(view)
1044                .view
1045                .call_on_any(selector, callback);
1046        }
1047    }
1048
1049    fn take_focus(
1050        &mut self,
1051        _source: cursive_core::direction::Direction,
1052    ) -> Result<EventResult, cursive_core::view::CannotFocus> {
1053        Ok(EventResult::Consumed(None))
1054    }
1055
1056    fn important_area(&self, _view_size: Vec2) -> Rect {
1057        if let Some(ref layout) = self.layout {
1058            if let Some(focused) = self.focused {
1059                layout.elements[focused].position
1060            } else {
1061                Rect::from_size((0, 0), (1, 1))
1062            }
1063        } else {
1064            Rect::from_size((0, 0), (1, 1))
1065        }
1066    }
1067}
1068
1069impl FlexItem {
1070    /// Create a flex item with the given grow factor.
1071    pub fn with_flex_grow(view: impl IntoBoxedView, flex_grow: u8) -> Self {
1072        Self {
1073            view: view.into_boxed_view(),
1074            flex_grow,
1075        }
1076    }
1077
1078    /// Set the flex-grow.
1079    pub fn set_flex_grow(&mut self, flex_grow: u8) {
1080        self.flex_grow = flex_grow;
1081    }
1082
1083    /// Returns the flex-grow.
1084    pub fn flex_grow(&self) -> u8 {
1085        self.flex_grow
1086    }
1087}
1088
1089impl<T: IntoBoxedView> From<T> for FlexItem {
1090    fn from(value: T) -> Self {
1091        Self {
1092            view: value.into_boxed_view(),
1093            flex_grow: 0,
1094        }
1095    }
1096}
1097
1098/// Convert `global_coordinates` to coordinates within a View, using `view_offset` as the top-left
1099/// point of the view to convert to.
1100fn global_to_view_coordinates(global_coordinates: XY<usize>, view_offset: XY<usize>) -> XY<usize> {
1101    global_coordinates - view_offset
1102}