1use alloc::vec::Vec;
14
15use retroglyph_core::color::Style;
16use retroglyph_core::grid::Grid;
17use retroglyph_core::text::{char_width, width_usize as measured_width};
18use retroglyph_core::tile::Tile;
19#[cfg(feature = "egc")]
22use retroglyph_core::grid::{HasSize, Rect};
23
24use crate::Surface;
25use crate::text::truncate;
26use crate::widget::Widget;
27use retroglyph_core::symbols::border::PLAIN;
28
29#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
31pub struct Sides {
32 pub top: u16,
34 pub right: u16,
36 pub bottom: u16,
38 pub left: u16,
40}
41
42impl Sides {
43 pub const ZERO: Self = Self {
45 top: 0,
46 right: 0,
47 bottom: 0,
48 left: 0,
49 };
50
51 #[must_use]
53 pub const fn all(n: u16) -> Self {
54 Self {
55 top: n,
56 right: n,
57 bottom: n,
58 left: n,
59 }
60 }
61
62 #[must_use]
65 pub const fn symmetric(vertical: u16, horizontal: u16) -> Self {
66 Self {
67 top: vertical,
68 right: horizontal,
69 bottom: vertical,
70 left: horizontal,
71 }
72 }
73
74 #[must_use]
76 pub const fn top(mut self, top: u16) -> Self {
77 self.top = top;
78 self
79 }
80
81 #[must_use]
83 pub const fn right(mut self, right: u16) -> Self {
84 self.right = right;
85 self
86 }
87
88 #[must_use]
90 pub const fn bottom(mut self, bottom: u16) -> Self {
91 self.bottom = bottom;
92 self
93 }
94
95 #[must_use]
97 pub const fn left(mut self, left: u16) -> Self {
98 self.left = left;
99 self
100 }
101
102 const fn horizontal(self) -> u16 {
103 self.left.saturating_add(self.right)
104 }
105
106 const fn vertical(self) -> u16 {
107 self.top.saturating_add(self.bottom)
108 }
109}
110
111#[derive(Clone, Copy, Debug)]
132pub struct BoxStyle {
133 style: Style,
134 padding: Sides,
135 margin: Sides,
136 border: bool,
137 width: Option<u16>,
138 height: Option<u16>,
139}
140
141impl BoxStyle {
142 #[must_use]
145 pub const fn new(style: Style) -> Self {
146 Self {
147 style,
148 padding: Sides::ZERO,
149 margin: Sides::ZERO,
150 border: false,
151 width: None,
152 height: None,
153 }
154 }
155
156 #[must_use]
158 pub const fn padding(mut self, padding: Sides) -> Self {
159 self.padding = padding;
160 self
161 }
162
163 #[must_use]
165 pub const fn margin(mut self, margin: Sides) -> Self {
166 self.margin = margin;
167 self
168 }
169
170 #[must_use]
172 pub const fn border(mut self, border: bool) -> Self {
173 self.border = border;
174 self
175 }
176
177 #[must_use]
182 pub const fn width(mut self, width: u16) -> Self {
183 self.width = Some(width);
184 self
185 }
186
187 #[must_use]
192 pub const fn height(mut self, height: u16) -> Self {
193 self.height = Some(height);
194 self
195 }
196
197 #[must_use]
210 pub fn render(&self, text: &str) -> Grid {
211 let lines: Vec<&str> = text.split('\n').collect();
212 let content_w = self.width.unwrap_or_else(|| {
213 u16::try_from(lines.iter().map(|l| measured_width(l)).max().unwrap_or(0))
214 .unwrap_or(u16::MAX)
215 });
216 let content_h = self
217 .height
218 .unwrap_or_else(|| u16::try_from(lines.len()).unwrap_or(u16::MAX));
219
220 let (mut grid, content_x, content_y) = self.scaffold(content_w, content_h);
221 for (row, line) in lines.iter().take(usize::from(content_h)).enumerate() {
222 let Ok(row) = u16::try_from(row) else { break };
223 let clipped = truncate(line, content_w);
224 let mut col = 0u16;
225 for ch in clipped.chars() {
226 let w = char_width(ch);
227 if col.saturating_add(w) > content_w {
228 break;
229 }
230 grid.put_tile(
231 0,
232 (content_x.saturating_add(col), content_y.saturating_add(row)),
233 Tile::new(ch, self.style),
234 );
235 col = col.saturating_add(w);
236 }
237 }
238 grid
239 }
240
241 #[cfg(feature = "egc")]
252 #[must_use]
253 pub fn render_wrapped(&self, text: &str) -> Grid {
254 use retroglyph_core::layout::TextLayout;
255 use retroglyph_core::text::{Line, Span};
256
257 let content_w = self.width.unwrap_or_else(|| {
258 u16::try_from(text.split('\n').map(measured_width).max().unwrap_or(0))
259 .unwrap_or(u16::MAX)
260 });
261 let line = Line::from(Span::styled(text, self.style));
262 let content_h = self.height.unwrap_or_else(|| {
263 TextLayout::new(&line)
264 .rect(Rect::new(0, 0, content_w, u16::MAX))
265 .measure()
266 .height()
267 });
268
269 let (mut grid, content_x, content_y) = self.scaffold(content_w, content_h);
270 TextLayout::new(&line)
271 .rect(Rect::new(content_x, content_y, content_w, content_h))
272 .render_to_grid(&mut grid, 0);
273
274 grid
275 }
276
277 fn scaffold(&self, content_w: u16, content_h: u16) -> (Grid, u16, u16) {
282 let border_wh = u16::from(self.border) * 2;
283 let inner_w = content_w
284 .saturating_add(self.padding.horizontal())
285 .saturating_add(border_wh);
286 let inner_h = content_h
287 .saturating_add(self.padding.vertical())
288 .saturating_add(border_wh);
289 let outer_w = inner_w.saturating_add(self.margin.horizontal()).max(1);
290 let outer_h = inner_h.saturating_add(self.margin.vertical()).max(1);
291
292 let mut grid = Grid::new(outer_w, outer_h);
293 let box_x = self.margin.left;
294 let box_y = self.margin.top;
295
296 fill_rect(&mut grid, box_x, box_y, inner_w, inner_h, self.style);
297 if self.border {
298 draw_border(&mut grid, box_x, box_y, inner_w, inner_h, self.style);
301 }
302
303 let content_x = box_x
304 .saturating_add(u16::from(self.border))
305 .saturating_add(self.padding.left);
306 let content_y = box_y
307 .saturating_add(u16::from(self.border))
308 .saturating_add(self.padding.top);
309 (grid, content_x, content_y)
310 }
311}
312
313#[derive(Clone, Copy, Debug)]
324pub struct Boxed<'a> {
325 style: BoxStyle,
326 text: &'a str,
327}
328
329impl BoxStyle {
330 #[must_use]
332 pub const fn text(self, text: &str) -> Boxed<'_> {
333 Boxed { style: self, text }
334 }
335}
336
337impl Widget for Boxed<'_> {
338 fn render(&self, surface: &mut Surface<'_>) {
339 let grid = self.style.render(self.text);
340 surface.blit(&grid, 0, 0);
342 }
343}
344
345fn fill_rect(grid: &mut Grid, x: u16, y: u16, w: u16, h: u16, style: Style) {
347 for dy in 0..h {
348 for dx in 0..w {
349 grid.put_tile(0, (x + dx, y + dy), Tile::new(' ', style));
350 }
351 }
352}
353
354fn draw_border(grid: &mut Grid, x: u16, y: u16, w: u16, h: u16, style: Style) {
357 let right = x + w - 1;
358 let bottom = y + h - 1;
359
360 grid.put_tile(0, (x, y), Tile::new(PLAIN.top_left, style));
361 grid.put_tile(0, (right, y), Tile::new(PLAIN.top_right, style));
362 grid.put_tile(0, (x, bottom), Tile::new(PLAIN.bottom_left, style));
363 grid.put_tile(0, (right, bottom), Tile::new(PLAIN.bottom_right, style));
364 for cx in (x + 1)..right {
365 grid.put_tile(0, (cx, y), Tile::new(PLAIN.horizontal, style));
366 grid.put_tile(0, (cx, bottom), Tile::new(PLAIN.horizontal, style));
367 }
368 for cy in (y + 1)..bottom {
369 grid.put_tile(0, (x, cy), Tile::new(PLAIN.vertical, style));
370 grid.put_tile(0, (right, cy), Tile::new(PLAIN.vertical, style));
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use alloc::string::String;
377
378 use super::*;
379 use retroglyph_core::grid::{Pos, Rect};
380
381 fn glyphs(grid: &Grid) -> Vec<String> {
382 (0..grid.height())
383 .map(|y| {
384 (0..grid.width())
385 .map(|x| grid[Pos::new(x, y)].glyph())
386 .collect()
387 })
388 .collect()
389 }
390
391 #[test]
392 fn boxed_render_draws_on_a_surface_that_is_not_on_layer_zero() {
393 use retroglyph_core::surface::{Layer, Surface};
398
399 let mut grid = Grid::new(6, 3);
400 let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 6, 3), Layer::World.as_u8());
401 let boxed = BoxStyle::new(Style::default()).text("hi");
402 boxed.render(&mut surface.on_tier(Layer::Overlay));
403
404 assert_eq!(
405 grid.tile(Layer::Overlay.as_u8(), (0, 0)).map(Tile::glyph),
406 Some('h')
407 );
408 assert_eq!(
409 grid.tile(Layer::Overlay.as_u8(), (1, 0)).map(Tile::glyph),
410 Some('i')
411 );
412 }
413
414 #[test]
415 fn sides_helpers() {
416 assert_eq!(
417 Sides::all(2),
418 Sides {
419 top: 2,
420 right: 2,
421 bottom: 2,
422 left: 2
423 }
424 );
425 assert_eq!(
426 Sides::symmetric(1, 3),
427 Sides {
428 top: 1,
429 right: 3,
430 bottom: 1,
431 left: 3
432 }
433 );
434 }
435
436 #[test]
437 fn sizes_to_content_with_no_padding_or_border() {
438 let grid = BoxStyle::new(Style::default()).render("hi");
439 assert_eq!((grid.width(), grid.height()), (2, 1));
440 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'h');
441 assert_eq!(grid[Pos::new(1, 0)].glyph(), 'i');
442 }
443
444 #[test]
445 fn sizes_to_the_widest_of_multiple_lines() {
446 let grid = BoxStyle::new(Style::default()).render("a\nbcd\nef");
447 assert_eq!((grid.width(), grid.height()), (3, 3));
448 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'a');
449 assert_eq!(grid[Pos::new(1, 0)].glyph(), ' '); assert_eq!(grid[Pos::new(0, 1)].glyph(), 'b');
451 assert_eq!(grid[Pos::new(2, 1)].glyph(), 'd');
452 }
453
454 #[test]
455 fn explicit_width_clips_longer_lines_and_pads_shorter_ones() {
456 let grid = BoxStyle::new(Style::default()).width(3).render("hello");
457 assert_eq!(grid.width(), 3);
458 let row: String = (0..3).map(|x| grid[Pos::new(x, 0)].glyph()).collect();
459 assert_eq!(row, "hel");
460 }
461
462 #[test]
463 fn explicit_height_drops_extra_lines() {
464 let grid = BoxStyle::new(Style::default()).height(1).render("a\nb\nc");
465 assert_eq!(grid.height(), 1);
466 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'a');
467 }
468
469 #[test]
470 fn padding_surrounds_content_with_the_box_style() {
471 let grid = BoxStyle::new(Style::default())
472 .padding(Sides::all(1))
473 .render("x");
474 assert_eq!((grid.width(), grid.height()), (3, 3));
476 assert_eq!(grid[Pos::new(1, 1)].glyph(), 'x');
477 assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
478 }
479
480 #[test]
481 fn render_does_not_overflow_on_a_near_u16_max_line_with_padding() {
482 let text = "a".repeat(65_535);
485 let grid = BoxStyle::new(Style::default())
486 .padding(Sides::all(2))
487 .render(&text);
488 assert_eq!(grid[Pos::new(2, 2)].glyph(), 'a');
489 }
490
491 #[test]
492 fn border_draws_a_box_around_padding_and_content() {
493 let grid = BoxStyle::new(Style::default()).border(true).render("x");
494 assert_eq!((grid.width(), grid.height()), (3, 3));
496 let rows = glyphs(&grid);
497 assert_eq!(rows[0], "┌─┐");
498 assert_eq!(rows[1], "│x│");
499 assert_eq!(rows[2], "└─┘");
500 }
501
502 #[test]
503 fn margin_is_left_transparent_outside_the_border() {
504 let grid = BoxStyle::new(Style::default())
505 .margin(Sides::all(1))
506 .render("x");
507 assert_eq!((grid.width(), grid.height()), (3, 3));
511 assert!(grid[Pos::new(0, 0)].is_empty());
512 assert_eq!(grid[Pos::new(1, 1)].glyph(), 'x');
513 }
514
515 #[test]
516 fn wide_characters_push_later_columns_over_by_their_width() {
517 use retroglyph_core::tile::TileFlags;
518
519 let grid = BoxStyle::new(Style::default()).render("aあb");
523 assert_eq!(grid.width(), 4);
524 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'a');
525 assert_eq!(grid[Pos::new(1, 0)].glyph(), 'あ');
526 assert!(
528 grid[Pos::new(2, 0)]
529 .flags()
530 .contains(TileFlags::WIDE_CHAR_SPACER)
531 );
532 assert_eq!(grid[Pos::new(3, 0)].glyph(), 'b');
533 }
534
535 #[test]
536 fn control_characters_occupy_one_column_matching_core_text_char_width() {
537 let grid = BoxStyle::new(Style::default()).render("a\u{7}b");
543 assert_eq!(grid.width(), 3);
544 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'a');
545 assert_eq!(grid[Pos::new(2, 0)].glyph(), 'b');
546 }
547
548 #[test]
549 fn border_with_empty_content_is_still_at_least_a_2x2_box() {
550 let grid = BoxStyle::new(Style::default()).border(true).render("");
553 assert_eq!((grid.width(), grid.height()), (2, 3));
554 let rows = glyphs(&grid);
555 assert_eq!(rows[0], "┌┐");
556 assert_eq!(rows[2], "└┘");
557 }
558
559 #[test]
560 #[cfg(feature = "egc")]
561 fn render_wrapped_word_wraps_to_the_explicit_width() {
562 let grid = BoxStyle::new(Style::default())
565 .width(10)
566 .render_wrapped("the quick brown fox jumps");
567 assert_eq!(grid.width(), 10);
568 let rows = glyphs(&grid);
569 assert_eq!(rows[0].trim_end(), "the quick");
570 assert_eq!(rows[1].trim_end(), "brown fox");
571 assert_eq!(rows[2].trim_end(), "jumps");
572 }
573
574 #[test]
575 #[cfg(feature = "egc")]
576 fn render_wrapped_without_an_explicit_width_measures_but_does_not_wrap() {
577 let grid = BoxStyle::new(Style::default()).render_wrapped("hi");
580 assert_eq!((grid.width(), grid.height()), (2, 1));
581 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'h');
582 assert_eq!(grid[Pos::new(1, 0)].glyph(), 'i');
583 }
584
585 #[test]
586 #[cfg(feature = "egc")]
587 fn render_wrapped_respects_padding_and_border_like_render() {
588 let grid = BoxStyle::new(Style::default())
589 .border(true)
590 .padding(Sides::all(1))
591 .width(3)
592 .render_wrapped("hi");
593 assert_eq!((grid.width(), grid.height()), (7, 5));
596 assert_eq!(grid[Pos::new(2, 2)].glyph(), 'h');
597 assert_eq!(grid[Pos::new(3, 2)].glyph(), 'i');
598 }
599
600 #[test]
601 fn boxed_widget_places_the_box_at_the_areas_top_left() {
602 let styled = BoxStyle::new(Style::default()).border(true).text("hi");
603 let area = Rect::new(2, 1, 10, 6);
604 let mut grid = Grid::new(12, 7);
605 styled.render(&mut Surface::new(&mut grid, area, 0));
606
607 assert_eq!(grid[Pos::new(2, 1)].glyph(), '┌');
610 assert_eq!(grid[Pos::new(3, 2)].glyph(), 'h');
611 assert_eq!(grid[Pos::new(4, 2)].glyph(), 'i');
612 assert_eq!(grid[Pos::new(5, 3)].glyph(), '┘');
613 }
614}