1use alloc::vec::Vec;
20
21use retroglyph_core::color::Style;
22use retroglyph_core::grid::Grid;
23use retroglyph_core::text::{char_width, width_usize as measured_width};
24use retroglyph_core::tile::Tile;
25#[cfg(feature = "egc")]
28use retroglyph_core::grid::{HasSize, Rect};
29
30use crate::Surface;
31use crate::text::truncate;
32use crate::widget::Widget;
33use retroglyph_core::symbols::border::PLAIN;
34
35#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
37pub struct Sides {
38 pub top: u16,
40 pub right: u16,
42 pub bottom: u16,
44 pub left: u16,
46}
47
48impl Sides {
49 pub const ZERO: Self = Self {
51 top: 0,
52 right: 0,
53 bottom: 0,
54 left: 0,
55 };
56
57 #[must_use]
59 pub const fn all(n: u16) -> Self {
60 Self {
61 top: n,
62 right: n,
63 bottom: n,
64 left: n,
65 }
66 }
67
68 #[must_use]
71 pub const fn symmetric(vertical: u16, horizontal: u16) -> Self {
72 Self {
73 top: vertical,
74 right: horizontal,
75 bottom: vertical,
76 left: horizontal,
77 }
78 }
79
80 #[must_use]
82 pub const fn top(mut self, top: u16) -> Self {
83 self.top = top;
84 self
85 }
86
87 #[must_use]
89 pub const fn right(mut self, right: u16) -> Self {
90 self.right = right;
91 self
92 }
93
94 #[must_use]
96 pub const fn bottom(mut self, bottom: u16) -> Self {
97 self.bottom = bottom;
98 self
99 }
100
101 #[must_use]
103 pub const fn left(mut self, left: u16) -> Self {
104 self.left = left;
105 self
106 }
107
108 const fn horizontal(self) -> u16 {
109 self.left.saturating_add(self.right)
110 }
111
112 const fn vertical(self) -> u16 {
113 self.top.saturating_add(self.bottom)
114 }
115}
116
117#[derive(Clone, Copy, Debug)]
141pub struct BoxStyle {
142 style: Style,
143 padding: Sides,
144 margin: Sides,
145 border: bool,
146 width: Option<u16>,
147 height: Option<u16>,
148}
149
150impl BoxStyle {
151 #[must_use]
154 pub const fn new(style: Style) -> Self {
155 Self {
156 style,
157 padding: Sides::ZERO,
158 margin: Sides::ZERO,
159 border: false,
160 width: None,
161 height: None,
162 }
163 }
164
165 #[must_use]
167 pub const fn padding(mut self, padding: Sides) -> Self {
168 self.padding = padding;
169 self
170 }
171
172 #[must_use]
174 pub const fn margin(mut self, margin: Sides) -> Self {
175 self.margin = margin;
176 self
177 }
178
179 #[must_use]
181 pub const fn border(mut self, border: bool) -> Self {
182 self.border = border;
183 self
184 }
185
186 #[must_use]
191 pub const fn width(mut self, width: u16) -> Self {
192 self.width = Some(width);
193 self
194 }
195
196 #[must_use]
201 pub const fn height(mut self, height: u16) -> Self {
202 self.height = Some(height);
203 self
204 }
205
206 #[must_use]
219 pub fn render(&self, text: &str) -> Grid {
220 let lines: Vec<&str> = text.split('\n').collect();
221 let content_w = self.width.unwrap_or_else(|| {
222 u16::try_from(lines.iter().map(|l| measured_width(l)).max().unwrap_or(0))
223 .unwrap_or(u16::MAX)
224 });
225 let content_h = self
226 .height
227 .unwrap_or_else(|| u16::try_from(lines.len()).unwrap_or(u16::MAX));
228
229 let (mut grid, content_x, content_y) = self.scaffold(content_w, content_h);
230 for (row, line) in lines.iter().take(usize::from(content_h)).enumerate() {
231 let Ok(row) = u16::try_from(row) else { break };
232 let clipped = truncate(line, content_w);
233 let mut col = 0u16;
234 for ch in clipped.chars() {
235 let w = char_width(ch);
236 if col.saturating_add(w) > content_w {
237 break;
238 }
239 grid.put_tile(
240 0,
241 (content_x.saturating_add(col), content_y.saturating_add(row)),
242 Tile::new(ch, self.style),
243 );
244 col = col.saturating_add(w);
245 }
246 }
247 grid
248 }
249
250 #[cfg(feature = "egc")]
261 #[must_use]
262 pub fn render_wrapped(&self, text: &str) -> Grid {
263 use retroglyph_core::layout::TextLayout;
264 use retroglyph_core::text::{Line, Span};
265
266 let content_w = self.width.unwrap_or_else(|| {
267 u16::try_from(text.split('\n').map(measured_width).max().unwrap_or(0))
268 .unwrap_or(u16::MAX)
269 });
270 let line = Line::from(Span::styled(text, self.style));
271 let content_h = self.height.unwrap_or_else(|| {
272 TextLayout::new(&line)
273 .rect(Rect::new(0, 0, content_w, u16::MAX))
274 .measure()
275 .height()
276 });
277
278 let (mut grid, content_x, content_y) = self.scaffold(content_w, content_h);
279 TextLayout::new(&line)
280 .rect(Rect::new(content_x, content_y, content_w, content_h))
281 .render_to_grid(&mut grid, 0);
282
283 grid
284 }
285
286 fn scaffold(&self, content_w: u16, content_h: u16) -> (Grid, u16, u16) {
291 let border_wh = u16::from(self.border) * 2;
292 let inner_w = content_w
293 .saturating_add(self.padding.horizontal())
294 .saturating_add(border_wh);
295 let inner_h = content_h
296 .saturating_add(self.padding.vertical())
297 .saturating_add(border_wh);
298 let outer_w = inner_w.saturating_add(self.margin.horizontal()).max(1);
299 let outer_h = inner_h.saturating_add(self.margin.vertical()).max(1);
300
301 let mut grid = Grid::new(outer_w, outer_h);
302 let box_x = self.margin.left;
303 let box_y = self.margin.top;
304
305 fill_rect(&mut grid, box_x, box_y, inner_w, inner_h, self.style);
306 if self.border {
307 draw_border(&mut grid, box_x, box_y, inner_w, inner_h, self.style);
310 }
311
312 let content_x = box_x
313 .saturating_add(u16::from(self.border))
314 .saturating_add(self.padding.left);
315 let content_y = box_y
316 .saturating_add(u16::from(self.border))
317 .saturating_add(self.padding.top);
318 (grid, content_x, content_y)
319 }
320}
321
322#[derive(Clone, Copy, Debug)]
333pub struct Boxed<'a> {
334 style: BoxStyle,
335 text: &'a str,
336}
337
338impl BoxStyle {
339 #[must_use]
341 pub const fn text(self, text: &str) -> Boxed<'_> {
342 Boxed { style: self, text }
343 }
344}
345
346impl Widget for Boxed<'_> {
347 fn render(&self, surface: &mut Surface<'_>) {
348 let grid = self.style.render(self.text);
349 surface.blit(&grid, 0, 0);
351 }
352}
353
354fn fill_rect(grid: &mut Grid, x: u16, y: u16, w: u16, h: u16, style: Style) {
356 for dy in 0..h {
357 for dx in 0..w {
358 grid.put_tile(0, (x + dx, y + dy), Tile::new(' ', style));
359 }
360 }
361}
362
363fn draw_border(grid: &mut Grid, x: u16, y: u16, w: u16, h: u16, style: Style) {
366 let right = x + w - 1;
367 let bottom = y + h - 1;
368
369 grid.put_tile(0, (x, y), Tile::new(PLAIN.top_left, style));
370 grid.put_tile(0, (right, y), Tile::new(PLAIN.top_right, style));
371 grid.put_tile(0, (x, bottom), Tile::new(PLAIN.bottom_left, style));
372 grid.put_tile(0, (right, bottom), Tile::new(PLAIN.bottom_right, style));
373 for cx in (x + 1)..right {
374 grid.put_tile(0, (cx, y), Tile::new(PLAIN.horizontal, style));
375 grid.put_tile(0, (cx, bottom), Tile::new(PLAIN.horizontal, style));
376 }
377 for cy in (y + 1)..bottom {
378 grid.put_tile(0, (x, cy), Tile::new(PLAIN.vertical, style));
379 grid.put_tile(0, (right, cy), Tile::new(PLAIN.vertical, style));
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use alloc::string::String;
386
387 use super::*;
388 use retroglyph_core::grid::{Pos, Rect};
389
390 fn glyphs(grid: &Grid) -> Vec<String> {
391 (0..grid.height())
392 .map(|y| {
393 (0..grid.width())
394 .map(|x| grid[Pos::new(x, y)].glyph())
395 .collect()
396 })
397 .collect()
398 }
399
400 #[test]
401 fn boxed_render_draws_on_a_surface_that_is_not_on_layer_zero() {
402 use retroglyph_core::surface::{Layer, Surface};
407
408 let mut grid = Grid::new(6, 3);
409 let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 6, 3), Layer::World.as_u8());
410 let boxed = BoxStyle::new(Style::default()).text("hi");
411 boxed.render(&mut surface.on_tier(Layer::Overlay));
412
413 assert_eq!(
414 grid.tile(Layer::Overlay.as_u8(), (0, 0)).map(Tile::glyph),
415 Some('h')
416 );
417 assert_eq!(
418 grid.tile(Layer::Overlay.as_u8(), (1, 0)).map(Tile::glyph),
419 Some('i')
420 );
421 }
422
423 #[test]
424 fn sides_helpers() {
425 assert_eq!(
426 Sides::all(2),
427 Sides {
428 top: 2,
429 right: 2,
430 bottom: 2,
431 left: 2
432 }
433 );
434 assert_eq!(
435 Sides::symmetric(1, 3),
436 Sides {
437 top: 1,
438 right: 3,
439 bottom: 1,
440 left: 3
441 }
442 );
443 }
444
445 #[test]
446 fn sizes_to_content_with_no_padding_or_border() {
447 let grid = BoxStyle::new(Style::default()).render("hi");
448 assert_eq!((grid.width(), grid.height()), (2, 1));
449 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'h');
450 assert_eq!(grid[Pos::new(1, 0)].glyph(), 'i');
451 }
452
453 #[test]
454 fn sizes_to_the_widest_of_multiple_lines() {
455 let grid = BoxStyle::new(Style::default()).render("a\nbcd\nef");
456 assert_eq!((grid.width(), grid.height()), (3, 3));
457 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'a');
458 assert_eq!(grid[Pos::new(1, 0)].glyph(), ' '); assert_eq!(grid[Pos::new(0, 1)].glyph(), 'b');
460 assert_eq!(grid[Pos::new(2, 1)].glyph(), 'd');
461 }
462
463 #[test]
464 fn explicit_width_clips_longer_lines_and_pads_shorter_ones() {
465 let grid = BoxStyle::new(Style::default()).width(3).render("hello");
466 assert_eq!(grid.width(), 3);
467 let row: String = (0..3).map(|x| grid[Pos::new(x, 0)].glyph()).collect();
468 assert_eq!(row, "hel");
469 }
470
471 #[test]
472 fn explicit_height_drops_extra_lines() {
473 let grid = BoxStyle::new(Style::default()).height(1).render("a\nb\nc");
474 assert_eq!(grid.height(), 1);
475 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'a');
476 }
477
478 #[test]
479 fn padding_surrounds_content_with_the_box_style() {
480 let grid = BoxStyle::new(Style::default())
481 .padding(Sides::all(1))
482 .render("x");
483 assert_eq!((grid.width(), grid.height()), (3, 3));
485 assert_eq!(grid[Pos::new(1, 1)].glyph(), 'x');
486 assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
487 }
488
489 #[test]
490 fn render_does_not_overflow_on_a_near_u16_max_line_with_padding() {
491 let text = "a".repeat(65_535);
494 let grid = BoxStyle::new(Style::default())
495 .padding(Sides::all(2))
496 .render(&text);
497 assert_eq!(grid[Pos::new(2, 2)].glyph(), 'a');
498 }
499
500 #[test]
501 fn border_draws_a_box_around_padding_and_content() {
502 let grid = BoxStyle::new(Style::default()).border(true).render("x");
503 assert_eq!((grid.width(), grid.height()), (3, 3));
505 let rows = glyphs(&grid);
506 assert_eq!(rows[0], "┌─┐");
507 assert_eq!(rows[1], "│x│");
508 assert_eq!(rows[2], "└─┘");
509 }
510
511 #[test]
512 fn margin_is_left_transparent_outside_the_border() {
513 let grid = BoxStyle::new(Style::default())
514 .margin(Sides::all(1))
515 .render("x");
516 assert_eq!((grid.width(), grid.height()), (3, 3));
520 assert!(grid[Pos::new(0, 0)].is_empty());
521 assert_eq!(grid[Pos::new(1, 1)].glyph(), 'x');
522 }
523
524 #[test]
525 fn wide_characters_push_later_columns_over_by_their_width() {
526 use retroglyph_core::tile::TileFlags;
527
528 let grid = BoxStyle::new(Style::default()).render("aあb");
532 assert_eq!(grid.width(), 4);
533 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'a');
534 assert_eq!(grid[Pos::new(1, 0)].glyph(), 'あ');
535 assert!(
537 grid[Pos::new(2, 0)]
538 .flags()
539 .contains(TileFlags::WIDE_CHAR_SPACER)
540 );
541 assert_eq!(grid[Pos::new(3, 0)].glyph(), 'b');
542 }
543
544 #[test]
545 fn control_characters_occupy_one_column_matching_core_text_char_width() {
546 let grid = BoxStyle::new(Style::default()).render("a\u{7}b");
552 assert_eq!(grid.width(), 3);
553 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'a');
554 assert_eq!(grid[Pos::new(2, 0)].glyph(), 'b');
555 }
556
557 #[test]
558 fn border_with_empty_content_is_still_at_least_a_2x2_box() {
559 let grid = BoxStyle::new(Style::default()).border(true).render("");
562 assert_eq!((grid.width(), grid.height()), (2, 3));
563 let rows = glyphs(&grid);
564 assert_eq!(rows[0], "┌┐");
565 assert_eq!(rows[2], "└┘");
566 }
567
568 #[test]
569 #[cfg(feature = "egc")]
570 fn render_wrapped_word_wraps_to_the_explicit_width() {
571 let grid = BoxStyle::new(Style::default())
574 .width(10)
575 .render_wrapped("the quick brown fox jumps");
576 assert_eq!(grid.width(), 10);
577 let rows = glyphs(&grid);
578 assert_eq!(rows[0].trim_end(), "the quick");
579 assert_eq!(rows[1].trim_end(), "brown fox");
580 assert_eq!(rows[2].trim_end(), "jumps");
581 }
582
583 #[test]
584 #[cfg(feature = "egc")]
585 fn render_wrapped_without_an_explicit_width_measures_but_does_not_wrap() {
586 let grid = BoxStyle::new(Style::default()).render_wrapped("hi");
589 assert_eq!((grid.width(), grid.height()), (2, 1));
590 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'h');
591 assert_eq!(grid[Pos::new(1, 0)].glyph(), 'i');
592 }
593
594 #[test]
595 #[cfg(feature = "egc")]
596 fn render_wrapped_respects_padding_and_border_like_render() {
597 let grid = BoxStyle::new(Style::default())
598 .border(true)
599 .padding(Sides::all(1))
600 .width(3)
601 .render_wrapped("hi");
602 assert_eq!((grid.width(), grid.height()), (7, 5));
605 assert_eq!(grid[Pos::new(2, 2)].glyph(), 'h');
606 assert_eq!(grid[Pos::new(3, 2)].glyph(), 'i');
607 }
608
609 #[test]
610 fn boxed_widget_places_the_box_at_the_areas_top_left() {
611 let styled = BoxStyle::new(Style::default()).border(true).text("hi");
612 let area = Rect::new(2, 1, 10, 6);
613 let mut grid = Grid::new(12, 7);
614 styled.render(&mut Surface::new(&mut grid, area, 0));
615
616 assert_eq!(grid[Pos::new(2, 1)].glyph(), '┌');
619 assert_eq!(grid[Pos::new(3, 2)].glyph(), 'h');
620 assert_eq!(grid[Pos::new(4, 2)].glyph(), 'i');
621 assert_eq!(grid[Pos::new(5, 3)].glyph(), '┘');
622 }
623}