Skip to main content

ez_tui/utils/
layout.rs

1use crate::types::cpt_ids::EzCptIds;
2use crate::{Result, ViewError};
3use ratatui::buffer::Buffer;
4use ratatui::layout::{Layout, Rect};
5use std::collections::VecDeque;
6use std::fmt::Debug;
7use std::sync::atomic::{AtomicU32, Ordering};
8
9static LAYOUT_COUNT: AtomicU32 = AtomicU32::new(0);
10
11/// A layout wrapper that makes it easy to create nested layouts.
12#[derive(Debug, Clone)]
13pub struct CustomLayout<CID>
14where
15    CID: EzCptIds,
16{
17    /// The name of the layout. It is only used for debugging purposes.
18    #[allow(dead_code)]
19    name: String,
20    /// The actual [`Layout`] to be used.
21    layout: Layout,
22    /// The items to be displayed in the layout. They could be
23    ///  * [`LayoutItem::NestedLayout`] - a nested [`CustomLayout`]
24    ///  * [`LayoutItem::Component`] - a component id
25    items: VecDeque<LayoutItem<CID>>,
26}
27/// An item of a layout. It can be either a nested [`CustomLayout`] or a component id.
28#[derive(Debug, Clone)]
29pub enum LayoutItem<CID>
30where
31    CID: EzCptIds,
32{
33    /// describe a nested layout
34    NestedLayout(CustomLayout<CID>),
35    /// describe a component to be rendered
36    Component(CID),
37}
38impl<CID> CustomLayout<CID>
39where
40    CID: EzCptIds,
41{
42    /// Create a new [`CustomLayout`] with a name
43    ///
44    /// # Errors
45    /// * [`LayoutError::MismatchConstraintsAndCpts`] if the number of areas generated by the layout does not match the number of items in the layout.
46    pub fn named<S: Into<String>>(
47        name: S,
48        layout: Layout,
49        items: VecDeque<LayoutItem<CID>>,
50    ) -> Result<Self> {
51        let name = name.into();
52        Self::check(name.clone(), &layout, &items)?;
53        LAYOUT_COUNT.fetch_add(1, Ordering::Relaxed);
54        Ok(Self {
55            name,
56            layout,
57            items,
58        })
59    }
60    fn check(name: String, layout: &Layout, items: &VecDeque<LayoutItem<CID>>) -> Result<()> {
61        let dummy_rect = Rect::new(0, 0, 0, 0);
62        let splited = layout.split(dummy_rect);
63        if splited.len() != items.len() {
64            return Err(
65                ViewError::MismatchConstraintsAndCpts(name, splited.len(), items.len()).into(),
66            );
67        }
68        Ok(())
69    }
70
71    /// Splits the given area based on the underlying layout.
72    #[must_use]
73    pub fn split(&self, area: Rect) -> Vec<(Rect, &LayoutItem<CID>)> {
74        let areas = self.layout.split(area);
75        areas.iter().copied().zip(&self.items).collect()
76    }
77
78    /// Check if the layout contains a component with the given id
79    pub fn contains(&self, id: &CID) -> bool {
80        self.items.iter().any(|item| match item {
81            LayoutItem::Component(cpt_id) => cpt_id == id,
82            LayoutItem::NestedLayout(layout) => layout.contains(id),
83        })
84    }
85
86    pub(crate) fn draw<CALLBACK>(&self, area: Rect, buf: &mut Buffer, draw_id: &mut CALLBACK)
87    where
88        CALLBACK: for<'b> FnMut((CID, Rect, &'b mut Buffer)),
89    {
90        let tulples = self.split(area);
91        for (inner_area, item) in tulples {
92            match item {
93                LayoutItem::NestedLayout(custom_layout) => {
94                    custom_layout.draw(inner_area, buf, draw_id);
95                }
96                LayoutItem::Component(cid) => {
97                    draw_id((cid.clone(), inner_area, buf));
98                }
99            }
100        }
101    }
102}