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}