1use retroglyph_core::{Grid, Rect, Style, Tile};
15use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
16
17use crate::Surface;
18use crate::draw::{BL, BR, H, TL, TR, V};
19use crate::text::truncate;
20use crate::widget::Widget;
21
22#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
24pub struct Sides {
25 pub top: u16,
27 pub right: u16,
29 pub bottom: u16,
31 pub left: u16,
33}
34
35impl Sides {
36 pub const ZERO: Self = Self {
38 top: 0,
39 right: 0,
40 bottom: 0,
41 left: 0,
42 };
43
44 #[must_use]
46 pub const fn all(n: u16) -> Self {
47 Self {
48 top: n,
49 right: n,
50 bottom: n,
51 left: n,
52 }
53 }
54
55 #[must_use]
58 pub const fn symmetric(vertical: u16, horizontal: u16) -> Self {
59 Self {
60 top: vertical,
61 right: horizontal,
62 bottom: vertical,
63 left: horizontal,
64 }
65 }
66
67 #[must_use]
69 pub const fn top(mut self, top: u16) -> Self {
70 self.top = top;
71 self
72 }
73
74 #[must_use]
76 pub const fn right(mut self, right: u16) -> Self {
77 self.right = right;
78 self
79 }
80
81 #[must_use]
83 pub const fn bottom(mut self, bottom: u16) -> Self {
84 self.bottom = bottom;
85 self
86 }
87
88 #[must_use]
90 pub const fn left(mut self, left: u16) -> Self {
91 self.left = left;
92 self
93 }
94
95 const fn horizontal(self) -> u16 {
96 self.left.saturating_add(self.right)
97 }
98
99 const fn vertical(self) -> u16 {
100 self.top.saturating_add(self.bottom)
101 }
102}
103
104#[derive(Clone, Copy, Debug)]
124pub struct BoxStyle {
125 style: Style,
126 padding: Sides,
127 margin: Sides,
128 border: bool,
129 width: Option<u16>,
130 height: Option<u16>,
131}
132
133impl BoxStyle {
134 #[must_use]
137 pub const fn new(style: Style) -> Self {
138 Self {
139 style,
140 padding: Sides::ZERO,
141 margin: Sides::ZERO,
142 border: false,
143 width: None,
144 height: None,
145 }
146 }
147
148 #[must_use]
150 pub const fn padding(mut self, padding: Sides) -> Self {
151 self.padding = padding;
152 self
153 }
154
155 #[must_use]
157 pub const fn margin(mut self, margin: Sides) -> Self {
158 self.margin = margin;
159 self
160 }
161
162 #[must_use]
164 pub const fn border(mut self, border: bool) -> Self {
165 self.border = border;
166 self
167 }
168
169 #[must_use]
174 pub const fn width(mut self, width: u16) -> Self {
175 self.width = Some(width);
176 self
177 }
178
179 #[must_use]
184 pub const fn height(mut self, height: u16) -> Self {
185 self.height = Some(height);
186 self
187 }
188
189 #[must_use]
205 pub fn render(&self, text: &str) -> Grid {
206 let lines: Vec<&str> = text.split('\n').collect();
207 let content_w = self.width.unwrap_or_else(|| {
208 u16::try_from(lines.iter().map(|l| l.width()).max().unwrap_or(0)).unwrap_or(u16::MAX)
209 });
210 let content_h = self
211 .height
212 .unwrap_or_else(|| u16::try_from(lines.len()).unwrap_or(u16::MAX));
213
214 let (mut grid, content_x, content_y) = self.scaffold(content_w, content_h);
215 for (row, line) in lines.iter().take(usize::from(content_h)).enumerate() {
216 let Ok(row) = u16::try_from(row) else { break };
217 let clipped = truncate(line, usize::from(content_w));
218 let mut col = 0u16;
219 for ch in clipped.chars() {
220 let w = u16::try_from(ch.width().unwrap_or(0)).unwrap_or(u16::MAX);
221 if col.saturating_add(w) > content_w {
222 break;
223 }
224 grid.put_tile(
225 0,
226 (content_x + col, content_y + row),
227 Tile::new(ch, self.style),
228 );
229 col = col.saturating_add(w);
230 }
231 }
232 grid
233 }
234
235 #[cfg(feature = "egc")]
246 #[must_use]
247 pub fn render_wrapped(&self, text: &str) -> Grid {
248 use retroglyph_core::layout::TextLayout;
249 use retroglyph_core::text::{Line, Span};
250
251 let content_w = self.width.unwrap_or_else(|| {
252 u16::try_from(
253 text.split('\n')
254 .map(UnicodeWidthStr::width)
255 .max()
256 .unwrap_or(0),
257 )
258 .unwrap_or(u16::MAX)
259 });
260 let line = Line::from(Span::styled(text, self.style));
261 let content_h = self.height.unwrap_or_else(|| {
262 TextLayout::new(&line)
263 .rect(Rect::new(0, 0, content_w, u16::MAX))
264 .measure()
265 .height
266 });
267
268 let (mut grid, content_x, content_y) = self.scaffold(content_w, content_h);
269 TextLayout::new(&line)
270 .rect(Rect::new(content_x, content_y, content_w, content_h))
271 .render_to_grid(&mut grid, 0);
272
273 grid
274 }
275
276 fn scaffold(&self, content_w: u16, content_h: u16) -> (Grid, u16, u16) {
281 let border_wh = u16::from(self.border) * 2;
282 let inner_w = content_w
283 .saturating_add(self.padding.horizontal())
284 .saturating_add(border_wh);
285 let inner_h = content_h
286 .saturating_add(self.padding.vertical())
287 .saturating_add(border_wh);
288 let outer_w = inner_w.saturating_add(self.margin.horizontal()).max(1);
289 let outer_h = inner_h.saturating_add(self.margin.vertical()).max(1);
290
291 let mut grid = Grid::new(outer_w, outer_h);
292 let box_x = self.margin.left;
293 let box_y = self.margin.top;
294
295 fill_rect(&mut grid, box_x, box_y, inner_w, inner_h, self.style);
296 if self.border {
297 draw_border(&mut grid, box_x, box_y, inner_w, inner_h, self.style);
300 }
301
302 let content_x = box_x
303 .saturating_add(u16::from(self.border))
304 .saturating_add(self.padding.left);
305 let content_y = box_y
306 .saturating_add(u16::from(self.border))
307 .saturating_add(self.padding.top);
308 (grid, content_x, content_y)
309 }
310}
311
312#[derive(Clone, Copy, Debug)]
323pub struct Boxed<'a> {
324 style: BoxStyle,
325 text: &'a str,
326}
327
328impl BoxStyle {
329 #[must_use]
331 pub const fn text(self, text: &str) -> Boxed<'_> {
332 Boxed { style: self, text }
333 }
334}
335
336impl Widget for Boxed<'_> {
337 fn render(&self, area: Rect, surface: &mut Surface<'_>) {
338 let grid = self.style.render(self.text);
339 crate::block::blit_into(surface, &grid, area.left(), area.top());
340 }
341}
342
343fn fill_rect(grid: &mut Grid, x: u16, y: u16, w: u16, h: u16, style: Style) {
345 for dy in 0..h {
346 for dx in 0..w {
347 grid.put_tile(0, (x + dx, y + dy), Tile::new(' ', style));
348 }
349 }
350}
351
352fn draw_border(grid: &mut Grid, x: u16, y: u16, w: u16, h: u16, style: Style) {
355 let right = x + w - 1;
356 let bottom = y + h - 1;
357
358 grid.put_tile(0, (x, y), Tile::new(TL, style));
359 grid.put_tile(0, (right, y), Tile::new(TR, style));
360 grid.put_tile(0, (x, bottom), Tile::new(BL, style));
361 grid.put_tile(0, (right, bottom), Tile::new(BR, style));
362 for cx in (x + 1)..right {
363 grid.put_tile(0, (cx, y), Tile::new(H, style));
364 grid.put_tile(0, (cx, bottom), Tile::new(H, style));
365 }
366 for cy in (y + 1)..bottom {
367 grid.put_tile(0, (x, cy), Tile::new(V, style));
368 grid.put_tile(0, (right, cy), Tile::new(V, style));
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375 use retroglyph_core::Pos;
376
377 fn glyphs(grid: &Grid) -> Vec<String> {
378 (0..grid.height())
379 .map(|y| {
380 (0..grid.width())
381 .map(|x| grid[Pos::new(x, y)].glyph())
382 .collect()
383 })
384 .collect()
385 }
386
387 #[test]
388 fn sides_helpers() {
389 assert_eq!(
390 Sides::all(2),
391 Sides {
392 top: 2,
393 right: 2,
394 bottom: 2,
395 left: 2
396 }
397 );
398 assert_eq!(
399 Sides::symmetric(1, 3),
400 Sides {
401 top: 1,
402 right: 3,
403 bottom: 1,
404 left: 3
405 }
406 );
407 }
408
409 #[test]
410 fn sizes_to_content_with_no_padding_or_border() {
411 let grid = BoxStyle::new(Style::default()).render("hi");
412 assert_eq!((grid.width(), grid.height()), (2, 1));
413 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'h');
414 assert_eq!(grid[Pos::new(1, 0)].glyph(), 'i');
415 }
416
417 #[test]
418 fn sizes_to_the_widest_of_multiple_lines() {
419 let grid = BoxStyle::new(Style::default()).render("a\nbcd\nef");
420 assert_eq!((grid.width(), grid.height()), (3, 3));
421 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'a');
422 assert_eq!(grid[Pos::new(1, 0)].glyph(), ' '); assert_eq!(grid[Pos::new(0, 1)].glyph(), 'b');
424 assert_eq!(grid[Pos::new(2, 1)].glyph(), 'd');
425 }
426
427 #[test]
428 fn explicit_width_clips_longer_lines_and_pads_shorter_ones() {
429 let grid = BoxStyle::new(Style::default()).width(3).render("hello");
430 assert_eq!(grid.width(), 3);
431 let row: String = (0..3).map(|x| grid[Pos::new(x, 0)].glyph()).collect();
432 assert_eq!(row, "hel");
433 }
434
435 #[test]
436 fn explicit_height_drops_extra_lines() {
437 let grid = BoxStyle::new(Style::default()).height(1).render("a\nb\nc");
438 assert_eq!(grid.height(), 1);
439 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'a');
440 }
441
442 #[test]
443 fn padding_surrounds_content_with_the_box_style() {
444 let grid = BoxStyle::new(Style::default())
445 .padding(Sides::all(1))
446 .render("x");
447 assert_eq!((grid.width(), grid.height()), (3, 3));
449 assert_eq!(grid[Pos::new(1, 1)].glyph(), 'x');
450 assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
451 }
452
453 #[test]
454 fn border_draws_a_box_around_padding_and_content() {
455 let grid = BoxStyle::new(Style::default()).border(true).render("x");
456 assert_eq!((grid.width(), grid.height()), (3, 3));
458 let rows = glyphs(&grid);
459 assert_eq!(rows[0], "┌─┐");
460 assert_eq!(rows[1], "│x│");
461 assert_eq!(rows[2], "└─┘");
462 }
463
464 #[test]
465 fn margin_is_left_transparent_outside_the_border() {
466 let grid = BoxStyle::new(Style::default())
467 .margin(Sides::all(1))
468 .render("x");
469 assert_eq!((grid.width(), grid.height()), (3, 3));
473 assert!(grid[Pos::new(0, 0)].is_empty());
474 assert_eq!(grid[Pos::new(1, 1)].glyph(), 'x');
475 }
476
477 #[test]
478 fn wide_characters_push_later_columns_over_by_their_width() {
479 let grid = BoxStyle::new(Style::default()).render("aあb");
488 assert_eq!(grid.width(), 4);
489 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'a');
490 assert_eq!(grid[Pos::new(1, 0)].glyph(), 'あ');
491 assert_eq!(grid[Pos::new(3, 0)].glyph(), 'b');
492 }
493
494 #[test]
495 fn border_with_empty_content_is_still_at_least_a_2x2_box() {
496 let grid = BoxStyle::new(Style::default()).border(true).render("");
499 assert_eq!((grid.width(), grid.height()), (2, 3));
500 let rows = glyphs(&grid);
501 assert_eq!(rows[0], "┌┐");
502 assert_eq!(rows[2], "└┘");
503 }
504
505 #[test]
506 #[cfg(feature = "egc")]
507 fn render_wrapped_word_wraps_to_the_explicit_width() {
508 let grid = BoxStyle::new(Style::default())
511 .width(10)
512 .render_wrapped("the quick brown fox jumps");
513 assert_eq!(grid.width(), 10);
514 let rows = glyphs(&grid);
515 assert_eq!(rows[0].trim_end(), "the quick");
516 assert_eq!(rows[1].trim_end(), "brown fox");
517 assert_eq!(rows[2].trim_end(), "jumps");
518 }
519
520 #[test]
521 #[cfg(feature = "egc")]
522 fn render_wrapped_without_an_explicit_width_measures_but_does_not_wrap() {
523 let grid = BoxStyle::new(Style::default()).render_wrapped("hi");
526 assert_eq!((grid.width(), grid.height()), (2, 1));
527 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'h');
528 assert_eq!(grid[Pos::new(1, 0)].glyph(), 'i');
529 }
530
531 #[test]
532 #[cfg(feature = "egc")]
533 fn render_wrapped_respects_padding_and_border_like_render() {
534 let grid = BoxStyle::new(Style::default())
535 .border(true)
536 .padding(Sides::all(1))
537 .width(3)
538 .render_wrapped("hi");
539 assert_eq!((grid.width(), grid.height()), (7, 5));
542 assert_eq!(grid[Pos::new(2, 2)].glyph(), 'h');
543 assert_eq!(grid[Pos::new(3, 2)].glyph(), 'i');
544 }
545
546 #[test]
547 fn boxed_widget_places_the_box_at_the_areas_top_left() {
548 let styled = BoxStyle::new(Style::default()).border(true).text("hi");
549 let area = Rect::new(2, 1, 10, 6);
550 let mut grid = Grid::new(12, 7);
551 styled.render(area, &mut Surface::new(&mut grid, area, 0));
552
553 assert_eq!(grid[Pos::new(2, 1)].glyph(), '┌');
556 assert_eq!(grid[Pos::new(3, 2)].glyph(), 'h');
557 assert_eq!(grid[Pos::new(4, 2)].glyph(), 'i');
558 assert_eq!(grid[Pos::new(5, 3)].glyph(), '┘');
559 }
560}