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 ixy::HasSize;
154
155 use super::*;
156 use crate::grid::Pos;
157 use alloc::string::String;
158
159 #[test]
160 fn test_measure_single_line() {
161 let line = Line::raw("hello");
162 let m = TextLayout::new(&line)
163 .rect(Rect::new(0, 0, 20, 5))
164 .measure();
165 assert_eq!(m.width(), 5);
166 assert_eq!(m.height(), 1);
167 }
168
169 #[test]
170 fn test_measure_wraps() {
171 let line = Line::raw("hello world");
172 let m = TextLayout::new(&line)
173 .rect(Rect::new(0, 0, 7, 10))
174 .measure();
175 assert_eq!(m.height(), 2);
176 assert_eq!(m.width(), 5);
177 }
178
179 #[test]
180 fn test_render_left_top() {
181 use crate::backend::Headless;
182 use crate::terminal::Terminal;
183
184 let mut term = Terminal::new(Headless::new(20, 5));
185 let line = Line::raw("hi");
186 TextLayout::new(&line)
187 .rect(Rect::new(2, 1, 10, 3))
188 .render_to_surface(&mut term.surface());
189
190 assert_eq!(term.grid()[Pos::new(2, 1)].glyph(), 'h');
191 assert_eq!(term.grid()[Pos::new(3, 1)].glyph(), 'i');
192 assert_eq!(term.grid()[Pos::new(4, 1)].glyph(), ' '); }
194
195 #[test]
196 fn test_render_center_h() {
197 use crate::backend::Headless;
198 use crate::terminal::Terminal;
199
200 let mut term = Terminal::new(Headless::new(20, 5));
202 let line = Line::raw("hi");
203 TextLayout::new(&line)
204 .rect(Rect::new(0, 0, 10, 3))
205 .h_align(HAlign::Center)
206 .render_to_surface(&mut term.surface());
207
208 assert_eq!(term.grid()[Pos::new(4, 0)].glyph(), 'h');
209 assert_eq!(term.grid()[Pos::new(5, 0)].glyph(), 'i');
210 }
211
212 #[test]
213 fn test_render_right_h() {
214 use crate::backend::Headless;
215 use crate::terminal::Terminal;
216
217 let mut term = Terminal::new(Headless::new(20, 5));
219 let line = Line::raw("hi");
220 TextLayout::new(&line)
221 .rect(Rect::new(0, 0, 10, 3))
222 .h_align(HAlign::Right)
223 .render_to_surface(&mut term.surface());
224
225 assert_eq!(term.grid()[Pos::new(8, 0)].glyph(), 'h');
226 assert_eq!(term.grid()[Pos::new(9, 0)].glyph(), 'i');
227 }
228
229 #[test]
230 fn test_render_middle_v() {
231 use crate::backend::Headless;
232 use crate::terminal::Terminal;
233
234 let mut term = Terminal::new(Headless::new(20, 10));
236 let line = Line::raw("hi");
237 TextLayout::new(&line)
238 .rect(Rect::new(0, 0, 10, 5))
239 .v_align(VAlign::Middle)
240 .render_to_surface(&mut term.surface());
241
242 assert_eq!(term.grid()[Pos::new(0, 2)].glyph(), 'h');
243 }
244
245 #[test]
246 fn test_render_bottom_v() {
247 use crate::backend::Headless;
248 use crate::terminal::Terminal;
249
250 let mut term = Terminal::new(Headless::new(20, 10));
252 let line = Line::raw("hi");
253 TextLayout::new(&line)
254 .rect(Rect::new(0, 0, 10, 5))
255 .v_align(VAlign::Bottom)
256 .render_to_surface(&mut term.surface());
257
258 assert_eq!(term.grid()[Pos::new(0, 4)].glyph(), 'h');
259 }
260
261 #[test]
262 fn test_render_clips_to_height() {
263 use crate::backend::Headless;
264 use crate::terminal::Terminal;
265
266 let mut term = Terminal::new(Headless::new(10, 10));
268 let line = Line::raw("a b c");
269 TextLayout::new(&line)
270 .rect(Rect::new(0, 0, 1, 2))
271 .render_to_surface(&mut term.surface());
272
273 assert_eq!(term.grid()[Pos::new(0, 0)].glyph(), 'a');
274 assert_eq!(term.grid()[Pos::new(0, 1)].glyph(), 'b');
275 assert_eq!(term.grid()[Pos::new(0, 2)].glyph(), ' '); }
277
278 #[test]
279 fn text_layout_render_to_surface_escapes_the_surface_clip() {
280 use crate::backend::Headless;
281 use crate::terminal::Terminal;
282
283 let mut term = Terminal::new(Headless::new(20, 5));
287 {
288 let mut surface = term.surface();
289 let mut bar = surface.clip(Rect::new(0, 0, 10, 1));
290 let line = Line::raw("hello world");
291 TextLayout::new(&line)
292 .rect(Rect::new(0, 0, 10, 4))
293 .render_to_surface(&mut bar);
294 }
295
296 let row0: String = (0..10)
297 .map(|x| term.grid()[Pos::new(x, 0)].glyph())
298 .collect();
299 assert_eq!(row0.trim_end(), "hello");
300 for x in 0..10 {
301 assert_eq!(term.grid()[Pos::new(x, 1)].glyph(), ' ');
302 }
303 }
304
305 #[test]
306 fn text_layout_wide_glyph_stays_inside_the_rect() {
307 use crate::backend::Headless;
308 use crate::terminal::Terminal;
309
310 let mut term = Terminal::new(Headless::new(10, 5));
314 let line = Line::raw("\u{3042}"); TextLayout::new(&line)
316 .rect(Rect::new(0, 0, 1, 1))
317 .render_to_surface(&mut term.surface());
318
319 assert_eq!(term.grid()[Pos::new(0, 0)].glyph(), ' ');
320 assert_eq!(term.grid()[Pos::new(1, 0)].glyph(), ' ');
321 }
322}