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}