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