Skip to main content

retroglyph_ui/
style.rs

1//! [`BoxStyle`]: a Lip-Gloss-style box model (padding, border, margin).
2//!
3//! Renders content into a standalone [`Grid`], independent of any
4//! [`Backend`](retroglyph_core::backend::Backend)/[`Terminal`](retroglyph_core::terminal::Terminal).
5//!
6//! For a box drawn straight into a [`Surface`](crate::Surface) you already own -- with titles,
7//! `BorderType` glyph sets, and theming -- use [`Panel`](crate::widget::Panel) instead. Reach
8//! for `BoxStyle` when composing a box outside a frame, laying boxes out with
9//! [`join_h`](crate::block::join_h)/[`join_v`](crate::block::join_v), or needing `margin` (space
10//! outside the border, which `Panel` has no concept of).
11//!
12//! `BoxStyle` does not word-wrap: it lays out already-broken lines (only
13//! `'\n'` is treated specially).
14//!
15//! For word-wrapping text to a width first, use `Paragraph`, then hand the
16//! wrapped result to `BoxStyle::render`. Keeping wrapping and box-model
17//! layout separate avoids tying every consumer of this module to `Paragraph`
18//! or the `egc` feature.
19use 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// `Rect` and `HasSize` are only named by the `egc` content-measuring path below and by this
26// module's tests.
27#[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/// CSS-style box-model sides: top/right/bottom/left, in terminal cells.
36#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
37pub struct Sides {
38    /// Cells above.
39    pub top: u16,
40    /// Cells to the right.
41    pub right: u16,
42    /// Cells below.
43    pub bottom: u16,
44    /// Cells to the left.
45    pub left: u16,
46}
47
48impl Sides {
49    /// No space on any side.
50    pub const ZERO: Self = Self {
51        top: 0,
52        right: 0,
53        bottom: 0,
54        left: 0,
55    };
56
57    /// The same number of cells on all four sides.
58    #[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    /// `vertical` cells top/bottom, `horizontal` cells left/right (CSS
69    /// `padding: v h` shorthand).
70    #[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    /// Returns `self` with `top` replaced.
81    #[must_use]
82    pub const fn top(mut self, top: u16) -> Self {
83        self.top = top;
84        self
85    }
86
87    /// Returns `self` with `right` replaced.
88    #[must_use]
89    pub const fn right(mut self, right: u16) -> Self {
90        self.right = right;
91        self
92    }
93
94    /// Returns `self` with `bottom` replaced.
95    #[must_use]
96    pub const fn bottom(mut self, bottom: u16) -> Self {
97        self.bottom = bottom;
98        self
99    }
100
101    /// Returns `self` with `left` replaced.
102    #[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/// A box-model wrapper: content, padding, an optional single-line border,
118/// and margin, rendered into a standalone [`Grid`] via [`BoxStyle::render`].
119///
120/// Layers from the inside out: content -> padding -> border -> margin.
121/// Margin cells are left empty (transparent, per [`Grid::new`]'s default
122/// tiles), matching CSS margin being outside the box's own background.
123///
124/// See [`Panel`](crate::widget::Panel) for drawing a bordered, titled box directly into a
125/// [`Surface`](crate::Surface) instead.
126///
127/// # Examples
128///
129/// ```
130/// use retroglyph_core::color::Style;
131/// use retroglyph_core::grid::Pos;
132/// use retroglyph_ui::style::{BoxStyle, Sides};
133///
134/// let grid = BoxStyle::new(Style::new())
135///     .border(true)
136///     .padding(Sides::all(1))
137///     .render("hi");
138/// assert_eq!(grid[Pos::new(2, 2)].glyph(), 'h'); // 1 border + 1 padding cell in from the corner
139/// ```
140#[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    /// A borderless box with no padding/margin, in `style`, sized to fit its
152    /// content.
153    #[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    /// Sets the padding, between the border (if any) and the content.
166    #[must_use]
167    pub const fn padding(mut self, padding: Sides) -> Self {
168        self.padding = padding;
169        self
170    }
171
172    /// Sets the margin, outside the border (if any); left transparent.
173    #[must_use]
174    pub const fn margin(mut self, margin: Sides) -> Self {
175        self.margin = margin;
176        self
177    }
178
179    /// Draws a single-line border, in `style`, around the padding.
180    #[must_use]
181    pub const fn border(mut self, border: bool) -> Self {
182        self.border = border;
183        self
184    }
185
186    /// Sets an explicit content width (excludes padding/border/margin).
187    ///
188    /// Lines wider than this are clipped; without this, the box sizes to
189    /// its widest content line.
190    #[must_use]
191    pub const fn width(mut self, width: u16) -> Self {
192        self.width = Some(width);
193        self
194    }
195
196    /// Sets an explicit content height (excludes padding/border/margin).
197    ///
198    /// Lines past this are dropped; without this, the box sizes to the
199    /// number of lines in the content.
200    #[must_use]
201    pub const fn height(mut self, height: u16) -> Self {
202        self.height = Some(height);
203        self
204    }
205
206    /// Renders `text` into a standalone [`Grid`]: content, padding, border,
207    /// and margin, in that order from the inside out.
208    ///
209    /// `text` is split only on `'\n'`; it is not word-wrapped (see the
210    /// module docs).
211    ///
212    /// Content is positioned by display column (via `retroglyph_core::text`), so a
213    /// wide (2-column) character correctly pushes later characters on the
214    /// same line over by 2 columns rather than 1, and gets a proper
215    /// `WIDE_CHAR_SPACER` reservation on the cell to its right, courtesy of
216    /// `retroglyph_core::grid::Grid::put_tile` (wide-char aware on every feature
217    /// combination, not just `egc`; this module still does not depend on it).
218    #[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    /// Word-wraps `text` to this box's content width, then renders it the
251    /// same way as [`render`](Self::render): content, padding, border, and
252    /// margin, from the inside out.
253    ///
254    /// Requires the `egc` feature: wrapping is delegated to
255    /// `retroglyph_core::layout::TextLayout`, which (unlike `render`) also
256    /// places wide characters correctly, with a proper `WIDE_CHAR_SPACER`.
257    /// If no explicit width was set via [`BoxStyle::width`], `text` is
258    /// measured but not wrapped (there is no width to wrap to), matching
259    /// `render`'s own natural-width fallback.
260    #[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    /// Builds the padding/border/margin scaffold for a `content_w`x`content_h`
287    /// content area: a fresh [`Grid`] with the box's background (and border,
288    /// if any) already drawn, plus the `(x, y)` offset where content should
289    /// be written.
290    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            // `inner_w`/`inner_h` already include the border's own 2 cells
308            // (`border_wh` above), so both are always >= 2 here.
309            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/// Pairs a [`BoxStyle`] with the text it should render, so the pair can
323/// implement [`Widget`] (which has no room for a text parameter). Build one
324/// via [`BoxStyle::text`].
325///
326/// [`Widget::render`] places the box at `area`'s top-left corner, sized to
327/// the style's own explicit-or-content-fit dimensions: it does not stretch
328/// or clip to fill `area`. It always uses [`BoxStyle::render`] (not
329/// `BoxStyle::render_wrapped`, behind the `egc` feature); for wrapped
330/// content, call `render_wrapped` directly and
331/// [`Surface::blit`](retroglyph_core::surface::Surface::blit) the result yourself.
332#[derive(Clone, Copy, Debug)]
333pub struct Boxed<'a> {
334    style: BoxStyle,
335    text: &'a str,
336}
337
338impl BoxStyle {
339    /// Pairs this style with `text`, ready to draw via [`Widget::render`].
340    #[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        // `(0, 0)` in this surface's own local coordinates is its area's own top-left corner.
350        surface.blit(&grid, 0, 0);
351    }
352}
353
354/// Fill `w`×`h` starting at `(x, y)` with a `style`d space.
355fn 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
363/// Draw a single-line border around the `w`×`h` rect at `(x, y)`, in
364/// `style`. Caller must ensure `w >= 2 && h >= 2`.
365fn 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        // The retroglyph#824 regression: `Boxed` renders its `BoxStyle` into a standalone,
403        // layer-0-only `Grid` and stamps it onto the caller's surface, which must still work
404        // when that surface is on a non-zero layer (e.g. `surface.on_tier(Layer::Overlay)`, as
405        // `Modal`'s own docs recommend for overlay content).
406        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(), ' '); // shorter line padded with blanks
459        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        // 1 content col/row + 1 padding on each side = 3x3.
484        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        // retroglyph#729: `content_x + col` used to overflow `u16` once padding pushed the write
492        // position past `u16::MAX` for a line wide enough to fill the content area to its edge.
493        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        // 1 content col/row + 2 border = 3x3.
504        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        // 1x1 content, 1 margin on each side = 3x3; margin cells are never
517        // written, so they keep Grid::new's default "empty" tile, which
518        // Grid::blit treats as transparent.
519        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        // "あ" (HIRAGANA A) is 2 columns wide: width("aあb") == 4, and 'b'
529        // must land at column 3, not column 2 (its char index), or it would
530        // collide with あ's second visual column.
531        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        // `put_tile` reserves a proper `WIDE_CHAR_SPACER` at the wide glyph's right half.
536        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        // retroglyph#760: this crate's own char-width loop used to answer `0` columns for a
547        // control character (`ch.width().unwrap_or(0)`), disagreeing with `Surface`/`Tile`, which
548        // both already advance one column when a control character is drawn. Routing through
549        // `retroglyph_core::text::char_width` (now `unwrap_or(1)`) makes a BEL take up a column
550        // here too, so "a\u{7}b" is 3 columns wide, not 2.
551        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        // No content, no padding: inner size is exactly the border's own 2
560        // cells in each axis (content_w = 0, content_h = 1 line of "").
561        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        // Same text/width Paragraph's own tests use (see widget/paragraph.rs),
572        // so this is exercising the same, already-verified TextLayout wrap.
573        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        // No width set: same natural-width fallback as `render`, so nothing
587        // is short enough to need wrapping.
588        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        // 3 content cols + 2 padding + 2 border = 7; 1 content row + 2
603        // padding + 2 border = 5.
604        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        // 2 content cols + 2 border = 4 wide, 1 content row + 2 border = 3
617        // tall, anchored at (2, 1) regardless of the much larger area.
618        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}