1use super::align::{HAlign, VAlign};
5use super::word_wrap::wrap_line;
6use crate::grid::{Grid, Rect, Size};
7use crate::surface::Surface;
8use crate::text::Line;
9
10pub struct TextLayout<'a> {
35 line: &'a Line,
36 rect: Rect,
37 h_align: HAlign,
38 v_align: VAlign,
39}
40
41impl<'a> TextLayout<'a> {
42 #[must_use]
48 pub const fn new(line: &'a Line) -> Self {
49 Self {
50 line,
51 rect: Rect::EMPTY,
52 h_align: HAlign::Left,
53 v_align: VAlign::Top,
54 }
55 }
56
57 #[must_use]
59 pub const fn rect(mut self, rect: Rect) -> Self {
60 self.rect = rect;
61 self
62 }
63
64 #[must_use]
66 pub const fn h_align(mut self, align: HAlign) -> Self {
67 self.h_align = align;
68 self
69 }
70
71 #[must_use]
73 pub const fn v_align(mut self, align: VAlign) -> Self {
74 self.v_align = align;
75 self
76 }
77
78 #[must_use]
83 pub fn measure(&self) -> Size {
84 let lines = wrap_line(self.line, self.rect.width());
85 let width = lines.iter().map(|l| l.width).max().unwrap_or(0);
86 #[allow(clippy::cast_possible_truncation)]
87 let height = lines.len().min(u16::MAX as usize) as u16;
88 Size::new(width, height)
89 }
90
91 pub fn render_to_surface(&self, surface: &mut Surface<'_>) {
113 let clipped = Self {
114 line: self.line,
115 rect: self.rect.intersect(surface.clip_rect()),
116 h_align: self.h_align,
117 v_align: self.v_align,
118 };
119 let layer = surface.layer();
120 clipped.render_to_grid(surface.grid_mut(), layer);
121 }
122
123 pub fn render_to_grid(&self, grid: &mut Grid, layer: u8) {
125 let lines = wrap_line(self.line, self.rect.width());
126 let rect = self.rect;
127
128 #[allow(clippy::cast_possible_truncation)]
129 let total_lines = lines.len().min(usize::from(rect.height())) as u16;
130
131 let y_offset = self.v_align.offset(rect.height(), total_lines);
132
133 for (line_idx, wrapped) in lines.into_iter().take(total_lines as usize).enumerate() {
134 let x_offset = self.h_align.offset(rect.width(), wrapped.width);
135
136 #[allow(clippy::cast_possible_truncation)]
137 let row = rect.top() + y_offset + line_idx as u16;
138 let mut cx = rect.left() + x_offset;
139
140 for glyph in wrapped.glyphs {
141 if cx + glyph.width > rect.right() {
142 break;
143 }
144 grid.write_grapheme(layer, cx, row, &glyph.grapheme, glyph.style);
145 cx += glyph.width;
146 }
147 }
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::grid::{HasSize, Pos};
155 use alloc::string::String;
156
157 #[test]
158 fn test_measure_single_line() {
159 let line = Line::raw("hello");
160 let m = TextLayout::new(&line)
161 .rect(Rect::new(0, 0, 20, 5))
162 .measure();
163 assert_eq!(m.width(), 5);
164 assert_eq!(m.height(), 1);
165 }
166
167 #[test]
168 fn test_measure_wraps() {
169 let line = Line::raw("hello world");
170 let m = TextLayout::new(&line)
171 .rect(Rect::new(0, 0, 7, 10))
172 .measure();
173 assert_eq!(m.height(), 2);
174 assert_eq!(m.width(), 5);
175 }
176
177 #[test]
178 fn test_render_left_top() {
179 use crate::backend::Headless;
180 use crate::terminal::Terminal;
181
182 let mut term = Terminal::new(Headless::new(20, 5));
183 let line = Line::raw("hi");
184 TextLayout::new(&line)
185 .rect(Rect::new(2, 1, 10, 3))
186 .render_to_surface(&mut term.surface());
187
188 assert_eq!(term.grid()[Pos::new(2, 1)].glyph(), 'h');
189 assert_eq!(term.grid()[Pos::new(3, 1)].glyph(), 'i');
190 assert_eq!(term.grid()[Pos::new(4, 1)].glyph(), ' '); }
192
193 #[test]
194 fn test_render_center_h() {
195 use crate::backend::Headless;
196 use crate::terminal::Terminal;
197
198 let mut term = Terminal::new(Headless::new(20, 5));
200 let line = Line::raw("hi");
201 TextLayout::new(&line)
202 .rect(Rect::new(0, 0, 10, 3))
203 .h_align(HAlign::Center)
204 .render_to_surface(&mut term.surface());
205
206 assert_eq!(term.grid()[Pos::new(4, 0)].glyph(), 'h');
207 assert_eq!(term.grid()[Pos::new(5, 0)].glyph(), 'i');
208 }
209
210 #[test]
211 fn test_render_right_h() {
212 use crate::backend::Headless;
213 use crate::terminal::Terminal;
214
215 let mut term = Terminal::new(Headless::new(20, 5));
217 let line = Line::raw("hi");
218 TextLayout::new(&line)
219 .rect(Rect::new(0, 0, 10, 3))
220 .h_align(HAlign::Right)
221 .render_to_surface(&mut term.surface());
222
223 assert_eq!(term.grid()[Pos::new(8, 0)].glyph(), 'h');
224 assert_eq!(term.grid()[Pos::new(9, 0)].glyph(), 'i');
225 }
226
227 #[test]
228 fn test_render_middle_v() {
229 use crate::backend::Headless;
230 use crate::terminal::Terminal;
231
232 let mut term = Terminal::new(Headless::new(20, 10));
234 let line = Line::raw("hi");
235 TextLayout::new(&line)
236 .rect(Rect::new(0, 0, 10, 5))
237 .v_align(VAlign::Middle)
238 .render_to_surface(&mut term.surface());
239
240 assert_eq!(term.grid()[Pos::new(0, 2)].glyph(), 'h');
241 }
242
243 #[test]
244 fn test_render_bottom_v() {
245 use crate::backend::Headless;
246 use crate::terminal::Terminal;
247
248 let mut term = Terminal::new(Headless::new(20, 10));
250 let line = Line::raw("hi");
251 TextLayout::new(&line)
252 .rect(Rect::new(0, 0, 10, 5))
253 .v_align(VAlign::Bottom)
254 .render_to_surface(&mut term.surface());
255
256 assert_eq!(term.grid()[Pos::new(0, 4)].glyph(), 'h');
257 }
258
259 #[test]
260 fn test_render_clips_to_height() {
261 use crate::backend::Headless;
262 use crate::terminal::Terminal;
263
264 let mut term = Terminal::new(Headless::new(10, 10));
266 let line = Line::raw("a b c");
267 TextLayout::new(&line)
268 .rect(Rect::new(0, 0, 1, 2))
269 .render_to_surface(&mut term.surface());
270
271 assert_eq!(term.grid()[Pos::new(0, 0)].glyph(), 'a');
272 assert_eq!(term.grid()[Pos::new(0, 1)].glyph(), 'b');
273 assert_eq!(term.grid()[Pos::new(0, 2)].glyph(), ' '); }
275
276 #[test]
277 fn text_layout_render_to_surface_escapes_the_surface_clip() {
278 use crate::backend::Headless;
279 use crate::terminal::Terminal;
280
281 let mut term = Terminal::new(Headless::new(20, 5));
285 {
286 let mut surface = term.surface();
287 let mut bar = surface.clip(Rect::new(0, 0, 10, 1));
288 let line = Line::raw("hello world");
289 TextLayout::new(&line)
290 .rect(Rect::new(0, 0, 10, 4))
291 .render_to_surface(&mut bar);
292 }
293
294 let row0: String = (0..10)
295 .map(|x| term.grid()[Pos::new(x, 0)].glyph())
296 .collect();
297 assert_eq!(row0.trim_end(), "hello");
298 for x in 0..10 {
299 assert_eq!(term.grid()[Pos::new(x, 1)].glyph(), ' ');
300 }
301 }
302
303 #[test]
304 fn text_layout_wide_glyph_stays_inside_the_rect() {
305 use crate::backend::Headless;
306 use crate::terminal::Terminal;
307
308 let mut term = Terminal::new(Headless::new(10, 5));
312 let line = Line::raw("\u{3042}"); TextLayout::new(&line)
314 .rect(Rect::new(0, 0, 1, 1))
315 .render_to_surface(&mut term.surface());
316
317 assert_eq!(term.grid()[Pos::new(0, 0)].glyph(), ' ');
318 assert_eq!(term.grid()[Pos::new(1, 0)].glyph(), ' ');
319 }
320}