requestty_ui/
layout.rs

1//! A module to describe regions of the screen that can be rendered to.
2
3/// The part of the text to render if the full text cannot be rendered
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5#[allow(missing_docs)]
6pub enum RenderRegion {
7    Top,
8    Middle,
9    Bottom,
10}
11
12impl Default for RenderRegion {
13    fn default() -> Self {
14        RenderRegion::Middle
15    }
16}
17
18/// `Layout` represents a portion of the screen that is available to be rendered to.
19///
20/// Assume the highlighted part of the block below is the place available for rendering
21/// in the given box
22/// ```text
23///  ____________
24/// |            |
25/// |     ███████|
26/// |  ██████████|
27/// |  ██████████|
28/// '------------'
29/// ```
30#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, Default)]
31pub struct Layout {
32    /// ```text
33    ///  ____________
34    /// |  vvv-- line_offset
35    /// |     ███████|
36    /// |  ██████████|
37    /// |  ██████████|
38    /// '------------'
39    /// ```
40    pub line_offset: u16,
41    /// ```text
42    ///  ____________
43    /// |vv-- offset_x
44    /// |     ███████|
45    /// |  ██████████|
46    /// |  ██████████|
47    /// '------------'
48    /// ```
49    pub offset_x: u16,
50    /// ```text
51    ///  .-- offset_y
52    /// |'>          |
53    /// |     ███████|
54    /// |  ██████████|
55    /// |  ██████████|
56    /// '------------'
57    /// ```
58    pub offset_y: u16,
59    /// ```text
60    ///  ____________
61    /// |            |
62    /// |     ███████|
63    /// |  ██████████|
64    /// |  ██████████|
65    /// '------------'
66    ///  ^^^^^^^^^^^^-- width
67    /// ```
68    pub width: u16,
69    /// ```text
70    ///  _____ height --.
71    /// |            | <'
72    /// |     ███████| <'
73    /// |  ██████████| <'
74    /// |  ██████████| <'
75    /// '------------'
76    /// ```
77    pub height: u16,
78    /// ```text
79    ///  ____________
80    /// |.-- max_height
81    /// |'>   ███████|
82    /// |'>██████████|
83    /// |'>██████████|
84    /// '------------'
85    /// ```
86    pub max_height: u16,
87    /// The region to render if full text cannot be rendered
88    pub render_region: RenderRegion,
89}
90
91impl Layout {
92    /// Creates a new `Layout`.
93    pub fn new(line_offset: u16, size: crate::backend::Size) -> Self {
94        Self {
95            line_offset,
96            offset_x: 0,
97            offset_y: 0,
98            width: size.width,
99            height: size.height,
100            max_height: size.height,
101            render_region: RenderRegion::Top,
102        }
103    }
104
105    /// Creates a new `Layout` with given `line_offset`.
106    pub fn with_line_offset(mut self, line_offset: u16) -> Self {
107        self.line_offset = line_offset;
108        self
109    }
110
111    /// Creates a new `Layout` with given `width` and `height`.
112    pub fn with_size(mut self, size: crate::backend::Size) -> Self {
113        self.set_size(size);
114        self
115    }
116
117    /// Creates a new `Layout` with new `offset_x` and `offset_y`.
118    pub fn with_offset(mut self, offset_x: u16, offset_y: u16) -> Self {
119        self.offset_x = offset_x;
120        self.offset_y = offset_y;
121        self
122    }
123
124    /// Creates a new `Layout` with new `render_region`.
125    pub fn with_render_region(mut self, region: RenderRegion) -> Self {
126        self.render_region = region;
127        self
128    }
129
130    /// Creates a new `Layout` with new `max_height`.
131    pub fn with_max_height(mut self, max_height: u16) -> Self {
132        self.max_height = max_height;
133        self
134    }
135
136    /// Creates a new `Layout` that represents a region past the `cursor_pos`. `cursor_pos` is
137    /// relative to `offset_x` and `offset_y`.
138    pub fn with_cursor_pos(mut self, cursor_pos: (u16, u16)) -> Self {
139        self.line_offset = cursor_pos.0;
140        self.offset_y = cursor_pos.1;
141        self
142    }
143
144    /// Sets the `width` and `height` of the layout.
145    pub fn set_size(&mut self, terminal_size: crate::backend::Size) {
146        self.width = terminal_size.width;
147        self.height = terminal_size.height;
148    }
149
150    /// Converts a `cursor_pos` relative to (`offset_x`, `offset_y`) to be relative to (0, 0)
151    pub fn offset_cursor(&self, cursor_pos: (u16, u16)) -> (u16, u16) {
152        (self.offset_x + cursor_pos.0, self.offset_y + cursor_pos.1)
153    }
154
155    /// Gets the width of renderable space on the first line.
156    ///
157    /// ```text
158    ///  ____________
159    /// |     vvvvvvv-- line_width
160    /// |     ███████|
161    /// |  ██████████|
162    /// |  ██████████|
163    /// '------------'
164    /// ```
165    pub fn line_width(&self) -> u16 {
166        self.available_width() - self.line_offset
167    }
168
169    /// Gets the width of renderable space on subsequent lines.
170    ///
171    /// ```text
172    ///  ____________
173    /// |  vvvvvvvvvv-- available_width
174    /// |     ███████|
175    /// |  ██████████|
176    /// |  ██████████|
177    /// '------------'
178    /// ```
179    pub fn available_width(&self) -> u16 {
180        self.width - self.offset_x
181    }
182
183    /// Gets the starting line number for the given `height` taking into account the `max_height`
184    /// and the `render_region`.
185    ///
186    /// If the height of the widget to render is 5 and the max_height is 2, then the start would be:
187    /// - `RenderRegion::Top`: 0
188    /// - `RenderRegion::Middle`: 1
189    /// - `RenderRegion::Top`: 3
190    pub fn get_start(&self, height: u16) -> u16 {
191        if height > self.max_height {
192            match self.render_region {
193                RenderRegion::Top => 0,
194                RenderRegion::Middle => (height - self.max_height) / 2,
195                RenderRegion::Bottom => height - self.max_height,
196            }
197        } else {
198            0
199        }
200    }
201}
202
203#[test]
204fn test_layout() {
205    let layout = Layout::new(0, (100, 5).into());
206    assert_eq!(
207        layout.with_render_region(RenderRegion::Top).get_start(10),
208        0
209    );
210    assert_eq!(
211        layout
212            .with_render_region(RenderRegion::Middle)
213            .get_start(10),
214        2
215    );
216    assert_eq!(
217        layout
218            .with_render_region(RenderRegion::Bottom)
219            .get_start(10),
220        5
221    );
222}