Coverage Report

Created: 2026-08-23 16:09

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/runner/work/retroglyph/retroglyph/crates/ui/src/style.rs
Line
Count
Source
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.
19
use alloc::vec::Vec;
20
21
use retroglyph_core::color::Style;
22
use retroglyph_core::grid::Grid;
23
use retroglyph_core::text::{char_width, width_usize as measured_width};
24
use 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")]
28
use retroglyph_core::grid::{HasSize, Rect};
29
30
use crate::Surface;
31
use crate::text::truncate;
32
use crate::widget::Widget;
33
use 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)]
37
pub 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
48
impl 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
6
    pub const fn all(n: u16) -> Self {
60
6
        Self {
61
6
            top: n,
62
6
            right: n,
63
6
            bottom: n,
64
6
            left: n,
65
6
        }
66
6
    }
67
68
    /// `vertical` cells top/bottom, `horizontal` cells left/right (CSS
69
    /// `padding: v h` shorthand).
70
    #[must_use]
71
4
    pub const fn symmetric(vertical: u16, horizontal: u16) -> Self {
72
4
        Self {
73
4
            top: vertical,
74
4
            right: horizontal,
75
4
            bottom: vertical,
76
4
            left: horizontal,
77
4
        }
78
4
    }
79
80
    /// Returns `self` with `top` replaced.
81
    #[must_use]
82
0
    pub const fn top(mut self, top: u16) -> Self {
83
0
        self.top = top;
84
0
        self
85
0
    }
86
87
    /// Returns `self` with `right` replaced.
88
    #[must_use]
89
0
    pub const fn right(mut self, right: u16) -> Self {
90
0
        self.right = right;
91
0
        self
92
0
    }
93
94
    /// Returns `self` with `bottom` replaced.
95
    #[must_use]
96
0
    pub const fn bottom(mut self, bottom: u16) -> Self {
97
0
        self.bottom = bottom;
98
0
        self
99
0
    }
100
101
    /// Returns `self` with `left` replaced.
102
    #[must_use]
103
0
    pub const fn left(mut self, left: u16) -> Self {
104
0
        self.left = left;
105
0
        self
106
0
    }
107
108
32
    const fn horizontal(self) -> u16 {
109
32
        self.left.saturating_add(self.right)
110
32
    }
111
112
32
    const fn vertical(self) -> u16 {
113
32
        self.top.saturating_add(self.bottom)
114
32
    }
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)]
141
pub struct BoxStyle {
142
    style: Style,
143
    padding: Sides,
144
    margin: Sides,
145
    border: bool,
146
    width: Option<u16>,
147
    height: Option<u16>,
148
}
149
150
impl BoxStyle {
151
    /// A borderless box with no padding/margin, in `style`, sized to fit its
152
    /// content.
153
    #[must_use]
154
16
    pub const fn new(style: Style) -> Self {
155
16
        Self {
156
16
            style,
157
16
            padding: Sides::ZERO,
158
16
            margin: Sides::ZERO,
159
16
            border: false,
160
16
            width: None,
161
16
            height: None,
162
16
        }
163
16
    }
164
165
    /// Sets the padding, between the border (if any) and the content.
166
    #[must_use]
167
3
    pub const fn padding(mut self, padding: Sides) -> Self {
168
3
        self.padding = padding;
169
3
        self
170
3
    }
171
172
    /// Sets the margin, outside the border (if any); left transparent.
173
    #[must_use]
174
1
    pub const fn margin(mut self, margin: Sides) -> Self {
175
1
        self.margin = margin;
176
1
        self
177
1
    }
178
179
    /// Draws a single-line border, in `style`, around the padding.
180
    #[must_use]
181
4
    pub const fn border(mut self, border: bool) -> Self {
182
4
        self.border = border;
183
4
        self
184
4
    }
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
3
    pub const fn width(mut self, width: u16) -> Self {
192
3
        self.width = Some(width);
193
3
        self
194
3
    }
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
1
    pub const fn height(mut self, height: u16) -> Self {
202
1
        self.height = Some(height);
203
1
        self
204
1
    }
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
13
    pub fn render(&self, text: &str) -> Grid {
220
13
        let lines: Vec<&str> = text.split('\n').collect();
221
13
        let content_w = self.width.unwrap_or_else(|| 
{12
222
16
            
u16::try_from12
(
lines.iter()12
.
map12
(|l| measured_width(l)).
max12
().
unwrap_or12
(0))
223
12
                .unwrap_or(u16::MAX)
224
12
        });
225
13
        let content_h = self
226
13
            .height
227
13
            .unwrap_or_else(|| 
u16::try_from12
(
lines12
.
len12
()).
unwrap_or12
(u16::MAX));
228
229
13
        let (mut grid, content_x, content_y) = self.scaffold(content_w, content_h);
230
15
        for (row, line) in 
lines.iter()13
.
take13
(
usize::from13
(
content_h13
)).
enumerate13
() {
231
15
            let Ok(row) = u16::try_from(row) else { 
break0
};
232
15
            let clipped = truncate(line, content_w);
233
15
            let mut col = 0u16;
234
65.5k
            for ch in 
clipped15
.
chars15
() {
235
65.5k
                let w = char_width(ch);
236
65.5k
                if col.saturating_add(w) > content_w {
237
0
                    break;
238
65.5k
                }
239
65.5k
                grid.put_tile(
240
                    0,
241
65.5k
                    (content_x.saturating_add(col), content_y.saturating_add(row)),
242
65.5k
                    Tile::new(ch, self.style),
243
                );
244
65.5k
                col = col.saturating_add(w);
245
            }
246
        }
247
13
        grid
248
13
    }
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
3
    pub fn render_wrapped(&self, text: &str) -> Grid {
263
        use retroglyph_core::layout::TextLayout;
264
        use retroglyph_core::text::{Line, Span};
265
266
3
        let content_w = self.width.unwrap_or_else(|| 
{1
267
1
            u16::try_from(text.split('\n').map(measured_width).max().unwrap_or(0))
268
1
                .unwrap_or(u16::MAX)
269
1
        });
270
3
        let line = Line::from(Span::styled(text, self.style));
271
3
        let content_h = self.height.unwrap_or_else(|| {
272
3
            TextLayout::new(&line)
273
3
                .rect(Rect::new(0, 0, content_w, u16::MAX))
274
3
                .measure()
275
3
                .height()
276
3
        });
277
278
3
        let (mut grid, content_x, content_y) = self.scaffold(content_w, content_h);
279
3
        TextLayout::new(&line)
280
3
            .rect(Rect::new(content_x, content_y, content_w, content_h))
281
3
            .render_to_grid(&mut grid, 0);
282
283
3
        grid
284
3
    }
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
16
    fn scaffold(&self, content_w: u16, content_h: u16) -> (Grid, u16, u16) {
291
16
        let border_wh = u16::from(self.border) * 2;
292
16
        let inner_w = content_w
293
16
            .saturating_add(self.padding.horizontal())
294
16
            .saturating_add(border_wh);
295
16
        let inner_h = content_h
296
16
            .saturating_add(self.padding.vertical())
297
16
            .saturating_add(border_wh);
298
16
        let outer_w = inner_w.saturating_add(self.margin.horizontal()).max(1);
299
16
        let outer_h = inner_h.saturating_add(self.margin.vertical()).max(1);
300
301
16
        let mut grid = Grid::new(outer_w, outer_h);
302
16
        let box_x = self.margin.left;
303
16
        let box_y = self.margin.top;
304
305
16
        fill_rect(&mut grid, box_x, box_y, inner_w, inner_h, self.style);
306
16
        if self.border {
307
4
            // `inner_w`/`inner_h` already include the border's own 2 cells
308
4
            // (`border_wh` above), so both are always >= 2 here.
309
4
            draw_border(&mut grid, box_x, box_y, inner_w, inner_h, self.style);
310
12
        }
311
312
16
        let content_x = box_x
313
16
            .saturating_add(u16::from(self.border))
314
16
            .saturating_add(self.padding.left);
315
16
        let content_y = box_y
316
16
            .saturating_add(u16::from(self.border))
317
16
            .saturating_add(self.padding.top);
318
16
        (grid, content_x, content_y)
319
16
    }
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)]
333
pub struct Boxed<'a> {
334
    style: BoxStyle,
335
    text: &'a str,
336
}
337
338
impl BoxStyle {
339
    /// Pairs this style with `text`, ready to draw via [`Widget::render`].
340
    #[must_use]
341
2
    pub const fn text(self, text: &str) -> Boxed<'_> {
342
2
        Boxed { style: self, text }
343
2
    }
344
}
345
346
impl Widget for Boxed<'_> {
347
2
    fn render(&self, surface: &mut Surface<'_>) {
348
2
        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
2
        surface.blit(&grid, 0, 0);
351
2
    }
352
}
353
354
/// Fill `w`×`h` starting at `(x, y)` with a `style`d space.
355
16
fn fill_rect(grid: &mut Grid, x: u16, y: u16, w: u16, h: u16, style: Style) {
356
36
    for dy in 
0..h16
{
357
327k
        for dx in 
0..w36
{
358
327k
            grid.put_tile(0, (x + dx, y + dy), Tile::new(' ', style));
359
327k
        }
360
    }
361
16
}
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`.
365
4
fn draw_border(grid: &mut Grid, x: u16, y: u16, w: u16, h: u16, style: Style) {
366
4
    let right = x + w - 1;
367
4
    let bottom = y + h - 1;
368
369
4
    grid.put_tile(0, (x, y), Tile::new(PLAIN.top_left, style));
370
4
    grid.put_tile(0, (right, y), Tile::new(PLAIN.top_right, style));
371
4
    grid.put_tile(0, (x, bottom), Tile::new(PLAIN.bottom_left, style));
372
4
    grid.put_tile(0, (right, bottom), Tile::new(PLAIN.bottom_right, style));
373
8
    for cx in 
(x + 1)..right4
{
374
8
        grid.put_tile(0, (cx, y), Tile::new(PLAIN.horizontal, style));
375
8
        grid.put_tile(0, (cx, bottom), Tile::new(PLAIN.horizontal, style));
376
8
    }
377
6
    for cy in 
(y + 1)..bottom4
{
378
6
        grid.put_tile(0, (x, cy), Tile::new(PLAIN.vertical, style));
379
6
        grid.put_tile(0, (right, cy), Tile::new(PLAIN.vertical, style));
380
6
    }
381
4
}
382
383
#[cfg(test)]
384
mod tests {
385
    use alloc::string::String;
386
387
    use super::*;
388
    use retroglyph_core::grid::{Pos, Rect};
389
390
3
    fn glyphs(grid: &Grid) -> Vec<String> {
391
3
        (0..grid.height())
392
9
            .
map3
(|y| {
393
9
                (0..grid.width())
394
45
                    .
map9
(|x| grid[Pos::new(x, y)].glyph())
395
9
                    .collect()
396
9
            })
397
3
            .collect()
398
3
    }
399
400
    #[test]
401
1
    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
1
        let mut grid = Grid::new(6, 3);
409
1
        let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 6, 3), Layer::World.as_u8());
410
1
        let boxed = BoxStyle::new(Style::default()).text("hi");
411
1
        boxed.render(&mut surface.on_tier(Layer::Overlay));
412
413
1
        assert_eq!(
414
1
            grid.tile(Layer::Overlay.as_u8(), (0, 0)).map(Tile::glyph),
415
            Some('h')
416
        );
417
1
        assert_eq!(
418
1
            grid.tile(Layer::Overlay.as_u8(), (1, 0)).map(Tile::glyph),
419
            Some('i')
420
        );
421
1
    }
422
423
    #[test]
424
1
    fn sides_helpers() {
425
1
        assert_eq!(
426
1
            Sides::all(2),
427
            Sides {
428
                top: 2,
429
                right: 2,
430
                bottom: 2,
431
                left: 2
432
            }
433
        );
434
1
        assert_eq!(
435
1
            Sides::symmetric(1, 3),
436
            Sides {
437
                top: 1,
438
                right: 3,
439
                bottom: 1,
440
                left: 3
441
            }
442
        );
443
1
    }
444
445
    #[test]
446
1
    fn sizes_to_content_with_no_padding_or_border() {
447
1
        let grid = BoxStyle::new(Style::default()).render("hi");
448
1
        assert_eq!((grid.width(), grid.height()), (2, 1));
449
1
        assert_eq!(grid[Pos::new(0, 0)].glyph(), 'h');
450
1
        assert_eq!(grid[Pos::new(1, 0)].glyph(), 'i');
451
1
    }
452
453
    #[test]
454
1
    fn sizes_to_the_widest_of_multiple_lines() {
455
1
        let grid = BoxStyle::new(Style::default()).render("a\nbcd\nef");
456
1
        assert_eq!((grid.width(), grid.height()), (3, 3));
457
1
        assert_eq!(grid[Pos::new(0, 0)].glyph(), 'a');
458
1
        assert_eq!(grid[Pos::new(1, 0)].glyph(), ' '); // shorter line padded with blanks
459
1
        assert_eq!(grid[Pos::new(0, 1)].glyph(), 'b');
460
1
        assert_eq!(grid[Pos::new(2, 1)].glyph(), 'd');
461
1
    }
462
463
    #[test]
464
1
    fn explicit_width_clips_longer_lines_and_pads_shorter_ones() {
465
1
        let grid = BoxStyle::new(Style::default()).width(3).render("hello");
466
1
        assert_eq!(grid.width(), 3);
467
3
        let 
row1
:
String1
= (
0..31
).
map1
(|x| grid[Pos::new(x, 0)].glyph()).
collect1
();
468
1
        assert_eq!(row, "hel");
469
1
    }
470
471
    #[test]
472
1
    fn explicit_height_drops_extra_lines() {
473
1
        let grid = BoxStyle::new(Style::default()).height(1).render("a\nb\nc");
474
1
        assert_eq!(grid.height(), 1);
475
1
        assert_eq!(grid[Pos::new(0, 0)].glyph(), 'a');
476
1
    }
477
478
    #[test]
479
1
    fn padding_surrounds_content_with_the_box_style() {
480
1
        let grid = BoxStyle::new(Style::default())
481
1
            .padding(Sides::all(1))
482
1
            .render("x");
483
        // 1 content col/row + 1 padding on each side = 3x3.
484
1
        assert_eq!((grid.width(), grid.height()), (3, 3));
485
1
        assert_eq!(grid[Pos::new(1, 1)].glyph(), 'x');
486
1
        assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
487
1
    }
488
489
    #[test]
490
1
    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
1
        let text = "a".repeat(65_535);
494
1
        let grid = BoxStyle::new(Style::default())
495
1
            .padding(Sides::all(2))
496
1
            .render(&text);
497
1
        assert_eq!(grid[Pos::new(2, 2)].glyph(), 'a');
498
1
    }
499
500
    #[test]
501
1
    fn border_draws_a_box_around_padding_and_content() {
502
1
        let grid = BoxStyle::new(Style::default()).border(true).render("x");
503
        // 1 content col/row + 2 border = 3x3.
504
1
        assert_eq!((grid.width(), grid.height()), (3, 3));
505
1
        let rows = glyphs(&grid);
506
1
        assert_eq!(rows[0], "┌─┐");
507
1
        assert_eq!(rows[1], "│x│");
508
1
        assert_eq!(rows[2], "└─┘");
509
1
    }
510
511
    #[test]
512
1
    fn margin_is_left_transparent_outside_the_border() {
513
1
        let grid = BoxStyle::new(Style::default())
514
1
            .margin(Sides::all(1))
515
1
            .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
1
        assert_eq!((grid.width(), grid.height()), (3, 3));
520
1
        assert!(grid[Pos::new(0, 0)].is_empty());
521
1
        assert_eq!(grid[Pos::new(1, 1)].glyph(), 'x');
522
1
    }
523
524
    #[test]
525
1
    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
1
        let grid = BoxStyle::new(Style::default()).render("aあb");
532
1
        assert_eq!(grid.width(), 4);
533
1
        assert_eq!(grid[Pos::new(0, 0)].glyph(), 'a');
534
1
        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
1
        assert!(
537
1
            grid[Pos::new(2, 0)]
538
1
                .flags()
539
1
                .contains(TileFlags::WIDE_CHAR_SPACER)
540
        );
541
1
        assert_eq!(grid[Pos::new(3, 0)].glyph(), 'b');
542
1
    }
543
544
    #[test]
545
1
    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
1
        let grid = BoxStyle::new(Style::default()).render("a\u{7}b");
552
1
        assert_eq!(grid.width(), 3);
553
1
        assert_eq!(grid[Pos::new(0, 0)].glyph(), 'a');
554
1
        assert_eq!(grid[Pos::new(2, 0)].glyph(), 'b');
555
1
    }
556
557
    #[test]
558
1
    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
1
        let grid = BoxStyle::new(Style::default()).border(true).render("");
562
1
        assert_eq!((grid.width(), grid.height()), (2, 3));
563
1
        let rows = glyphs(&grid);
564
1
        assert_eq!(rows[0], "┌┐");
565
1
        assert_eq!(rows[2], "└┘");
566
1
    }
567
568
    #[test]
569
    #[cfg(feature = "egc")]
570
1
    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
1
        let grid = BoxStyle::new(Style::default())
574
1
            .width(10)
575
1
            .render_wrapped("the quick brown fox jumps");
576
1
        assert_eq!(grid.width(), 10);
577
1
        let rows = glyphs(&grid);
578
1
        assert_eq!(rows[0].trim_end(), "the quick");
579
1
        assert_eq!(rows[1].trim_end(), "brown fox");
580
1
        assert_eq!(rows[2].trim_end(), "jumps");
581
1
    }
582
583
    #[test]
584
    #[cfg(feature = "egc")]
585
1
    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
1
        let grid = BoxStyle::new(Style::default()).render_wrapped("hi");
589
1
        assert_eq!((grid.width(), grid.height()), (2, 1));
590
1
        assert_eq!(grid[Pos::new(0, 0)].glyph(), 'h');
591
1
        assert_eq!(grid[Pos::new(1, 0)].glyph(), 'i');
592
1
    }
593
594
    #[test]
595
    #[cfg(feature = "egc")]
596
1
    fn render_wrapped_respects_padding_and_border_like_render() {
597
1
        let grid = BoxStyle::new(Style::default())
598
1
            .border(true)
599
1
            .padding(Sides::all(1))
600
1
            .width(3)
601
1
            .render_wrapped("hi");
602
        // 3 content cols + 2 padding + 2 border = 7; 1 content row + 2
603
        // padding + 2 border = 5.
604
1
        assert_eq!((grid.width(), grid.height()), (7, 5));
605
1
        assert_eq!(grid[Pos::new(2, 2)].glyph(), 'h');
606
1
        assert_eq!(grid[Pos::new(3, 2)].glyph(), 'i');
607
1
    }
608
609
    #[test]
610
1
    fn boxed_widget_places_the_box_at_the_areas_top_left() {
611
1
        let styled = BoxStyle::new(Style::default()).border(true).text("hi");
612
1
        let area = Rect::new(2, 1, 10, 6);
613
1
        let mut grid = Grid::new(12, 7);
614
1
        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
1
        assert_eq!(grid[Pos::new(2, 1)].glyph(), '┌');
619
1
        assert_eq!(grid[Pos::new(3, 2)].glyph(), 'h');
620
1
        assert_eq!(grid[Pos::new(4, 2)].glyph(), 'i');
621
1
        assert_eq!(grid[Pos::new(5, 3)].glyph(), '┘');
622
1
    }
623
}