Coverage Report

Created: 2026-08-05 20:21

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
//! `BoxStyle` does not word-wrap: it lays out already-broken lines (only
7
//! `'\n'` is treated specially).
8
//!
9
//! For word-wrapping text to a width first, use `Paragraph`, then hand the
10
//! wrapped result to `BoxStyle::render`. Keeping wrapping and box-model
11
//! layout separate avoids tying every consumer of this module to `Paragraph`
12
//! or the `egc` feature.
13
use alloc::vec::Vec;
14
15
use retroglyph_core::color::Style;
16
use retroglyph_core::grid::Grid;
17
use retroglyph_core::text::{char_width, width_usize as measured_width};
18
use retroglyph_core::tile::Tile;
19
// `Rect` and `HasSize` are only named by the `egc` content-measuring path below and by this
20
// module's tests.
21
#[cfg(feature = "egc")]
22
use retroglyph_core::grid::{HasSize, Rect};
23
24
use crate::Surface;
25
use crate::text::truncate;
26
use crate::widget::Widget;
27
use retroglyph_core::symbols::border::PLAIN;
28
29
/// CSS-style box-model sides: top/right/bottom/left, in terminal cells.
30
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
31
pub struct Sides {
32
    /// Cells above.
33
    pub top: u16,
34
    /// Cells to the right.
35
    pub right: u16,
36
    /// Cells below.
37
    pub bottom: u16,
38
    /// Cells to the left.
39
    pub left: u16,
40
}
41
42
impl Sides {
43
    /// No space on any side.
44
    pub const ZERO: Self = Self {
45
        top: 0,
46
        right: 0,
47
        bottom: 0,
48
        left: 0,
49
    };
50
51
    /// The same number of cells on all four sides.
52
    #[must_use]
53
6
    pub const fn all(n: u16) -> Self {
54
6
        Self {
55
6
            top: n,
56
6
            right: n,
57
6
            bottom: n,
58
6
            left: n,
59
6
        }
60
6
    }
61
62
    /// `vertical` cells top/bottom, `horizontal` cells left/right (CSS
63
    /// `padding: v h` shorthand).
64
    #[must_use]
65
4
    pub const fn symmetric(vertical: u16, horizontal: u16) -> Self {
66
4
        Self {
67
4
            top: vertical,
68
4
            right: horizontal,
69
4
            bottom: vertical,
70
4
            left: horizontal,
71
4
        }
72
4
    }
73
74
    /// Returns `self` with `top` replaced.
75
    #[must_use]
76
0
    pub const fn top(mut self, top: u16) -> Self {
77
0
        self.top = top;
78
0
        self
79
0
    }
80
81
    /// Returns `self` with `right` replaced.
82
    #[must_use]
83
0
    pub const fn right(mut self, right: u16) -> Self {
84
0
        self.right = right;
85
0
        self
86
0
    }
87
88
    /// Returns `self` with `bottom` replaced.
89
    #[must_use]
90
0
    pub const fn bottom(mut self, bottom: u16) -> Self {
91
0
        self.bottom = bottom;
92
0
        self
93
0
    }
94
95
    /// Returns `self` with `left` replaced.
96
    #[must_use]
97
0
    pub const fn left(mut self, left: u16) -> Self {
98
0
        self.left = left;
99
0
        self
100
0
    }
101
102
32
    const fn horizontal(self) -> u16 {
103
32
        self.left.saturating_add(self.right)
104
32
    }
105
106
32
    const fn vertical(self) -> u16 {
107
32
        self.top.saturating_add(self.bottom)
108
32
    }
109
}
110
111
/// A box-model wrapper: content, padding, an optional single-line border,
112
/// and margin, rendered into a standalone [`Grid`] via [`BoxStyle::render`].
113
///
114
/// Layers from the inside out: content -> padding -> border -> margin.
115
/// Margin cells are left empty (transparent, per [`Grid::new`]'s default
116
/// tiles), matching CSS margin being outside the box's own background.
117
///
118
/// # Examples
119
///
120
/// ```
121
/// use retroglyph_core::color::Style;
122
/// use retroglyph_core::grid::Pos;
123
/// use retroglyph_ui::{BoxStyle, Sides};
124
///
125
/// let grid = BoxStyle::new(Style::new())
126
///     .border(true)
127
///     .padding(Sides::all(1))
128
///     .render("hi");
129
/// assert_eq!(grid[Pos::new(2, 2)].glyph(), 'h'); // 1 border + 1 padding cell in from the corner
130
/// ```
131
#[derive(Clone, Copy, Debug)]
132
pub struct BoxStyle {
133
    style: Style,
134
    padding: Sides,
135
    margin: Sides,
136
    border: bool,
137
    width: Option<u16>,
138
    height: Option<u16>,
139
}
140
141
impl BoxStyle {
142
    /// A borderless box with no padding/margin, in `style`, sized to fit its
143
    /// content.
144
    #[must_use]
145
16
    pub const fn new(style: Style) -> Self {
146
16
        Self {
147
16
            style,
148
16
            padding: Sides::ZERO,
149
16
            margin: Sides::ZERO,
150
16
            border: false,
151
16
            width: None,
152
16
            height: None,
153
16
        }
154
16
    }
155
156
    /// Sets the padding, between the border (if any) and the content.
157
    #[must_use]
158
3
    pub const fn padding(mut self, padding: Sides) -> Self {
159
3
        self.padding = padding;
160
3
        self
161
3
    }
162
163
    /// Sets the margin, outside the border (if any); left transparent.
164
    #[must_use]
165
1
    pub const fn margin(mut self, margin: Sides) -> Self {
166
1
        self.margin = margin;
167
1
        self
168
1
    }
169
170
    /// Draws a single-line border, in `style`, around the padding.
171
    #[must_use]
172
4
    pub const fn border(mut self, border: bool) -> Self {
173
4
        self.border = border;
174
4
        self
175
4
    }
176
177
    /// Sets an explicit content width (excludes padding/border/margin).
178
    ///
179
    /// Lines wider than this are clipped; without this, the box sizes to
180
    /// its widest content line.
181
    #[must_use]
182
3
    pub const fn width(mut self, width: u16) -> Self {
183
3
        self.width = Some(width);
184
3
        self
185
3
    }
186
187
    /// Sets an explicit content height (excludes padding/border/margin).
188
    ///
189
    /// Lines past this are dropped; without this, the box sizes to the
190
    /// number of lines in the content.
191
    #[must_use]
192
1
    pub const fn height(mut self, height: u16) -> Self {
193
1
        self.height = Some(height);
194
1
        self
195
1
    }
196
197
    /// Renders `text` into a standalone [`Grid`]: content, padding, border,
198
    /// and margin, in that order from the inside out.
199
    ///
200
    /// `text` is split only on `'\n'`; it is not word-wrapped (see the
201
    /// module docs).
202
    ///
203
    /// Content is positioned by display column (via `retroglyph_core::text`), so a
204
    /// wide (2-column) character correctly pushes later characters on the
205
    /// same line over by 2 columns rather than 1, and gets a proper
206
    /// `WIDE_CHAR_SPACER` reservation on the cell to its right, courtesy of
207
    /// `retroglyph_core::grid::Grid::put_tile` (wide-char aware on every feature
208
    /// combination, not just `egc`; this module still does not depend on it).
209
    #[must_use]
210
13
    pub fn render(&self, text: &str) -> Grid {
211
13
        let lines: Vec<&str> = text.split('\n').collect();
212
13
        let content_w = self.width.unwrap_or_else(|| 
{12
213
16
            
u16::try_from12
(
lines.iter()12
.
map12
(|l| measured_width(l)).
max12
().
unwrap_or12
(0))
214
12
                .unwrap_or(u16::MAX)
215
12
        });
216
13
        let content_h = self
217
13
            .height
218
13
            .unwrap_or_else(|| 
u16::try_from12
(
lines12
.
len12
()).
unwrap_or12
(u16::MAX));
219
220
13
        let (mut grid, content_x, content_y) = self.scaffold(content_w, content_h);
221
15
        for (row, line) in 
lines.iter()13
.
take13
(
usize::from13
(
content_h13
)).
enumerate13
() {
222
15
            let Ok(row) = u16::try_from(row) else { 
break0
};
223
15
            let clipped = truncate(line, content_w);
224
15
            let mut col = 0u16;
225
65.5k
            for ch in 
clipped15
.
chars15
() {
226
65.5k
                let w = char_width(ch);
227
65.5k
                if col.saturating_add(w) > content_w {
228
0
                    break;
229
65.5k
                }
230
65.5k
                grid.put_tile(
231
                    0,
232
65.5k
                    (content_x.saturating_add(col), content_y.saturating_add(row)),
233
65.5k
                    Tile::new(ch, self.style),
234
                );
235
65.5k
                col = col.saturating_add(w);
236
            }
237
        }
238
13
        grid
239
13
    }
240
241
    /// Word-wraps `text` to this box's content width, then renders it the
242
    /// same way as [`render`](Self::render): content, padding, border, and
243
    /// margin, from the inside out.
244
    ///
245
    /// Requires the `egc` feature: wrapping is delegated to
246
    /// `retroglyph_core::layout::TextLayout`, which (unlike `render`) also
247
    /// places wide characters correctly, with a proper `WIDE_CHAR_SPACER`.
248
    /// If no explicit width was set via [`BoxStyle::width`], `text` is
249
    /// measured but not wrapped (there is no width to wrap to), matching
250
    /// `render`'s own natural-width fallback.
251
    #[cfg(feature = "egc")]
252
    #[must_use]
253
3
    pub fn render_wrapped(&self, text: &str) -> Grid {
254
        use retroglyph_core::layout::TextLayout;
255
        use retroglyph_core::text::{Line, Span};
256
257
3
        let content_w = self.width.unwrap_or_else(|| 
{1
258
1
            u16::try_from(text.split('\n').map(measured_width).max().unwrap_or(0))
259
1
                .unwrap_or(u16::MAX)
260
1
        });
261
3
        let line = Line::from(Span::styled(text, self.style));
262
3
        let content_h = self.height.unwrap_or_else(|| {
263
3
            TextLayout::new(&line)
264
3
                .rect(Rect::new(0, 0, content_w, u16::MAX))
265
3
                .measure()
266
3
                .height()
267
3
        });
268
269
3
        let (mut grid, content_x, content_y) = self.scaffold(content_w, content_h);
270
3
        TextLayout::new(&line)
271
3
            .rect(Rect::new(content_x, content_y, content_w, content_h))
272
3
            .render_to_grid(&mut grid, 0);
273
274
3
        grid
275
3
    }
276
277
    /// Builds the padding/border/margin scaffold for a `content_w`x`content_h`
278
    /// content area: a fresh [`Grid`] with the box's background (and border,
279
    /// if any) already drawn, plus the `(x, y)` offset where content should
280
    /// be written.
281
16
    fn scaffold(&self, content_w: u16, content_h: u16) -> (Grid, u16, u16) {
282
16
        let border_wh = u16::from(self.border) * 2;
283
16
        let inner_w = content_w
284
16
            .saturating_add(self.padding.horizontal())
285
16
            .saturating_add(border_wh);
286
16
        let inner_h = content_h
287
16
            .saturating_add(self.padding.vertical())
288
16
            .saturating_add(border_wh);
289
16
        let outer_w = inner_w.saturating_add(self.margin.horizontal()).max(1);
290
16
        let outer_h = inner_h.saturating_add(self.margin.vertical()).max(1);
291
292
16
        let mut grid = Grid::new(outer_w, outer_h);
293
16
        let box_x = self.margin.left;
294
16
        let box_y = self.margin.top;
295
296
16
        fill_rect(&mut grid, box_x, box_y, inner_w, inner_h, self.style);
297
16
        if self.border {
298
4
            // `inner_w`/`inner_h` already include the border's own 2 cells
299
4
            // (`border_wh` above), so both are always >= 2 here.
300
4
            draw_border(&mut grid, box_x, box_y, inner_w, inner_h, self.style);
301
12
        }
302
303
16
        let content_x = box_x
304
16
            .saturating_add(u16::from(self.border))
305
16
            .saturating_add(self.padding.left);
306
16
        let content_y = box_y
307
16
            .saturating_add(u16::from(self.border))
308
16
            .saturating_add(self.padding.top);
309
16
        (grid, content_x, content_y)
310
16
    }
311
}
312
313
/// Pairs a [`BoxStyle`] with the text it should render, so the pair can
314
/// implement [`Widget`] (which has no room for a text parameter). Build one
315
/// via [`BoxStyle::text`].
316
///
317
/// [`Widget::render`] places the box at `area`'s top-left corner, sized to
318
/// the style's own explicit-or-content-fit dimensions: it does not stretch
319
/// or clip to fill `area`. It always uses [`BoxStyle::render`] (not
320
/// `BoxStyle::render_wrapped`, behind the `egc` feature); for wrapped
321
/// content, call `render_wrapped` directly and
322
/// [`Surface::blit`](retroglyph_core::surface::Surface::blit) the result yourself.
323
#[derive(Clone, Copy, Debug)]
324
pub struct Boxed<'a> {
325
    style: BoxStyle,
326
    text: &'a str,
327
}
328
329
impl BoxStyle {
330
    /// Pairs this style with `text`, ready to draw via [`Widget::render`].
331
    #[must_use]
332
2
    pub const fn text(self, text: &str) -> Boxed<'_> {
333
2
        Boxed { style: self, text }
334
2
    }
335
}
336
337
impl Widget for Boxed<'_> {
338
2
    fn render(&self, surface: &mut Surface<'_>) {
339
2
        let grid = self.style.render(self.text);
340
        // `(0, 0)` in this surface's own local coordinates is its area's own top-left corner.
341
2
        surface.blit(&grid, 0, 0);
342
2
    }
343
}
344
345
/// Fill `w`×`h` starting at `(x, y)` with a `style`d space.
346
16
fn fill_rect(grid: &mut Grid, x: u16, y: u16, w: u16, h: u16, style: Style) {
347
36
    for dy in 
0..h16
{
348
327k
        for dx in 
0..w36
{
349
327k
            grid.put_tile(0, (x + dx, y + dy), Tile::new(' ', style));
350
327k
        }
351
    }
352
16
}
353
354
/// Draw a single-line border around the `w`×`h` rect at `(x, y)`, in
355
/// `style`. Caller must ensure `w >= 2 && h >= 2`.
356
4
fn draw_border(grid: &mut Grid, x: u16, y: u16, w: u16, h: u16, style: Style) {
357
4
    let right = x + w - 1;
358
4
    let bottom = y + h - 1;
359
360
4
    grid.put_tile(0, (x, y), Tile::new(PLAIN.top_left, style));
361
4
    grid.put_tile(0, (right, y), Tile::new(PLAIN.top_right, style));
362
4
    grid.put_tile(0, (x, bottom), Tile::new(PLAIN.bottom_left, style));
363
4
    grid.put_tile(0, (right, bottom), Tile::new(PLAIN.bottom_right, style));
364
8
    for cx in 
(x + 1)..right4
{
365
8
        grid.put_tile(0, (cx, y), Tile::new(PLAIN.horizontal, style));
366
8
        grid.put_tile(0, (cx, bottom), Tile::new(PLAIN.horizontal, style));
367
8
    }
368
6
    for cy in 
(y + 1)..bottom4
{
369
6
        grid.put_tile(0, (x, cy), Tile::new(PLAIN.vertical, style));
370
6
        grid.put_tile(0, (right, cy), Tile::new(PLAIN.vertical, style));
371
6
    }
372
4
}
373
374
#[cfg(test)]
375
mod tests {
376
    use alloc::string::String;
377
378
    use super::*;
379
    use retroglyph_core::grid::{Pos, Rect};
380
381
3
    fn glyphs(grid: &Grid) -> Vec<String> {
382
3
        (0..grid.height())
383
9
            .
map3
(|y| {
384
9
                (0..grid.width())
385
45
                    .
map9
(|x| grid[Pos::new(x, y)].glyph())
386
9
                    .collect()
387
9
            })
388
3
            .collect()
389
3
    }
390
391
    #[test]
392
1
    fn boxed_render_draws_on_a_surface_that_is_not_on_layer_zero() {
393
        // The retroglyph#824 regression: `Boxed` renders its `BoxStyle` into a standalone,
394
        // layer-0-only `Grid` and stamps it onto the caller's surface, which must still work
395
        // when that surface is on a non-zero layer (e.g. `surface.on_tier(Layer::Overlay)`, as
396
        // `Modal`'s own docs recommend for overlay content).
397
        use retroglyph_core::surface::{Layer, Surface};
398
399
1
        let mut grid = Grid::new(6, 3);
400
1
        let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 6, 3), Layer::World.as_u8());
401
1
        let boxed = BoxStyle::new(Style::default()).text("hi");
402
1
        boxed.render(&mut surface.on_tier(Layer::Overlay));
403
404
1
        assert_eq!(
405
1
            grid.tile(Layer::Overlay.as_u8(), (0, 0)).map(Tile::glyph),
406
            Some('h')
407
        );
408
1
        assert_eq!(
409
1
            grid.tile(Layer::Overlay.as_u8(), (1, 0)).map(Tile::glyph),
410
            Some('i')
411
        );
412
1
    }
413
414
    #[test]
415
1
    fn sides_helpers() {
416
1
        assert_eq!(
417
1
            Sides::all(2),
418
            Sides {
419
                top: 2,
420
                right: 2,
421
                bottom: 2,
422
                left: 2
423
            }
424
        );
425
1
        assert_eq!(
426
1
            Sides::symmetric(1, 3),
427
            Sides {
428
                top: 1,
429
                right: 3,
430
                bottom: 1,
431
                left: 3
432
            }
433
        );
434
1
    }
435
436
    #[test]
437
1
    fn sizes_to_content_with_no_padding_or_border() {
438
1
        let grid = BoxStyle::new(Style::default()).render("hi");
439
1
        assert_eq!((grid.width(), grid.height()), (2, 1));
440
1
        assert_eq!(grid[Pos::new(0, 0)].glyph(), 'h');
441
1
        assert_eq!(grid[Pos::new(1, 0)].glyph(), 'i');
442
1
    }
443
444
    #[test]
445
1
    fn sizes_to_the_widest_of_multiple_lines() {
446
1
        let grid = BoxStyle::new(Style::default()).render("a\nbcd\nef");
447
1
        assert_eq!((grid.width(), grid.height()), (3, 3));
448
1
        assert_eq!(grid[Pos::new(0, 0)].glyph(), 'a');
449
1
        assert_eq!(grid[Pos::new(1, 0)].glyph(), ' '); // shorter line padded with blanks
450
1
        assert_eq!(grid[Pos::new(0, 1)].glyph(), 'b');
451
1
        assert_eq!(grid[Pos::new(2, 1)].glyph(), 'd');
452
1
    }
453
454
    #[test]
455
1
    fn explicit_width_clips_longer_lines_and_pads_shorter_ones() {
456
1
        let grid = BoxStyle::new(Style::default()).width(3).render("hello");
457
1
        assert_eq!(grid.width(), 3);
458
3
        let 
row1
:
String1
= (
0..31
).
map1
(|x| grid[Pos::new(x, 0)].glyph()).
collect1
();
459
1
        assert_eq!(row, "hel");
460
1
    }
461
462
    #[test]
463
1
    fn explicit_height_drops_extra_lines() {
464
1
        let grid = BoxStyle::new(Style::default()).height(1).render("a\nb\nc");
465
1
        assert_eq!(grid.height(), 1);
466
1
        assert_eq!(grid[Pos::new(0, 0)].glyph(), 'a');
467
1
    }
468
469
    #[test]
470
1
    fn padding_surrounds_content_with_the_box_style() {
471
1
        let grid = BoxStyle::new(Style::default())
472
1
            .padding(Sides::all(1))
473
1
            .render("x");
474
        // 1 content col/row + 1 padding on each side = 3x3.
475
1
        assert_eq!((grid.width(), grid.height()), (3, 3));
476
1
        assert_eq!(grid[Pos::new(1, 1)].glyph(), 'x');
477
1
        assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
478
1
    }
479
480
    #[test]
481
1
    fn render_does_not_overflow_on_a_near_u16_max_line_with_padding() {
482
        // retroglyph#729: `content_x + col` used to overflow `u16` once padding pushed the write
483
        // position past `u16::MAX` for a line wide enough to fill the content area to its edge.
484
1
        let text = "a".repeat(65_535);
485
1
        let grid = BoxStyle::new(Style::default())
486
1
            .padding(Sides::all(2))
487
1
            .render(&text);
488
1
        assert_eq!(grid[Pos::new(2, 2)].glyph(), 'a');
489
1
    }
490
491
    #[test]
492
1
    fn border_draws_a_box_around_padding_and_content() {
493
1
        let grid = BoxStyle::new(Style::default()).border(true).render("x");
494
        // 1 content col/row + 2 border = 3x3.
495
1
        assert_eq!((grid.width(), grid.height()), (3, 3));
496
1
        let rows = glyphs(&grid);
497
1
        assert_eq!(rows[0], "┌─┐");
498
1
        assert_eq!(rows[1], "│x│");
499
1
        assert_eq!(rows[2], "└─┘");
500
1
    }
501
502
    #[test]
503
1
    fn margin_is_left_transparent_outside_the_border() {
504
1
        let grid = BoxStyle::new(Style::default())
505
1
            .margin(Sides::all(1))
506
1
            .render("x");
507
        // 1x1 content, 1 margin on each side = 3x3; margin cells are never
508
        // written, so they keep Grid::new's default "empty" tile, which
509
        // Grid::blit treats as transparent.
510
1
        assert_eq!((grid.width(), grid.height()), (3, 3));
511
1
        assert!(grid[Pos::new(0, 0)].is_empty());
512
1
        assert_eq!(grid[Pos::new(1, 1)].glyph(), 'x');
513
1
    }
514
515
    #[test]
516
1
    fn wide_characters_push_later_columns_over_by_their_width() {
517
        use retroglyph_core::tile::TileFlags;
518
519
        // "あ" (HIRAGANA A) is 2 columns wide: width("aあb") == 4, and 'b'
520
        // must land at column 3, not column 2 (its char index), or it would
521
        // collide with あ's second visual column.
522
1
        let grid = BoxStyle::new(Style::default()).render("aあb");
523
1
        assert_eq!(grid.width(), 4);
524
1
        assert_eq!(grid[Pos::new(0, 0)].glyph(), 'a');
525
1
        assert_eq!(grid[Pos::new(1, 0)].glyph(), 'あ');
526
        // `put_tile` reserves a proper `WIDE_CHAR_SPACER` at the wide glyph's right half.
527
1
        assert!(
528
1
            grid[Pos::new(2, 0)]
529
1
                .flags()
530
1
                .contains(TileFlags::WIDE_CHAR_SPACER)
531
        );
532
1
        assert_eq!(grid[Pos::new(3, 0)].glyph(), 'b');
533
1
    }
534
535
    #[test]
536
1
    fn control_characters_occupy_one_column_matching_core_text_char_width() {
537
        // retroglyph#760: this crate's own char-width loop used to answer `0` columns for a
538
        // control character (`ch.width().unwrap_or(0)`), disagreeing with `Surface`/`Tile`, which
539
        // both already advance one column when a control character is drawn. Routing through
540
        // `retroglyph_core::text::char_width` (now `unwrap_or(1)`) makes a BEL take up a column
541
        // here too, so "a\u{7}b" is 3 columns wide, not 2.
542
1
        let grid = BoxStyle::new(Style::default()).render("a\u{7}b");
543
1
        assert_eq!(grid.width(), 3);
544
1
        assert_eq!(grid[Pos::new(0, 0)].glyph(), 'a');
545
1
        assert_eq!(grid[Pos::new(2, 0)].glyph(), 'b');
546
1
    }
547
548
    #[test]
549
1
    fn border_with_empty_content_is_still_at_least_a_2x2_box() {
550
        // No content, no padding: inner size is exactly the border's own 2
551
        // cells in each axis (content_w = 0, content_h = 1 line of "").
552
1
        let grid = BoxStyle::new(Style::default()).border(true).render("");
553
1
        assert_eq!((grid.width(), grid.height()), (2, 3));
554
1
        let rows = glyphs(&grid);
555
1
        assert_eq!(rows[0], "┌┐");
556
1
        assert_eq!(rows[2], "└┘");
557
1
    }
558
559
    #[test]
560
    #[cfg(feature = "egc")]
561
1
    fn render_wrapped_word_wraps_to_the_explicit_width() {
562
        // Same text/width Paragraph's own tests use (see widget/paragraph.rs),
563
        // so this is exercising the same, already-verified TextLayout wrap.
564
1
        let grid = BoxStyle::new(Style::default())
565
1
            .width(10)
566
1
            .render_wrapped("the quick brown fox jumps");
567
1
        assert_eq!(grid.width(), 10);
568
1
        let rows = glyphs(&grid);
569
1
        assert_eq!(rows[0].trim_end(), "the quick");
570
1
        assert_eq!(rows[1].trim_end(), "brown fox");
571
1
        assert_eq!(rows[2].trim_end(), "jumps");
572
1
    }
573
574
    #[test]
575
    #[cfg(feature = "egc")]
576
1
    fn render_wrapped_without_an_explicit_width_measures_but_does_not_wrap() {
577
        // No width set: same natural-width fallback as `render`, so nothing
578
        // is short enough to need wrapping.
579
1
        let grid = BoxStyle::new(Style::default()).render_wrapped("hi");
580
1
        assert_eq!((grid.width(), grid.height()), (2, 1));
581
1
        assert_eq!(grid[Pos::new(0, 0)].glyph(), 'h');
582
1
        assert_eq!(grid[Pos::new(1, 0)].glyph(), 'i');
583
1
    }
584
585
    #[test]
586
    #[cfg(feature = "egc")]
587
1
    fn render_wrapped_respects_padding_and_border_like_render() {
588
1
        let grid = BoxStyle::new(Style::default())
589
1
            .border(true)
590
1
            .padding(Sides::all(1))
591
1
            .width(3)
592
1
            .render_wrapped("hi");
593
        // 3 content cols + 2 padding + 2 border = 7; 1 content row + 2
594
        // padding + 2 border = 5.
595
1
        assert_eq!((grid.width(), grid.height()), (7, 5));
596
1
        assert_eq!(grid[Pos::new(2, 2)].glyph(), 'h');
597
1
        assert_eq!(grid[Pos::new(3, 2)].glyph(), 'i');
598
1
    }
599
600
    #[test]
601
1
    fn boxed_widget_places_the_box_at_the_areas_top_left() {
602
1
        let styled = BoxStyle::new(Style::default()).border(true).text("hi");
603
1
        let area = Rect::new(2, 1, 10, 6);
604
1
        let mut grid = Grid::new(12, 7);
605
1
        styled.render(&mut Surface::new(&mut grid, area, 0));
606
607
        // 2 content cols + 2 border = 4 wide, 1 content row + 2 border = 3
608
        // tall, anchored at (2, 1) regardless of the much larger area.
609
1
        assert_eq!(grid[Pos::new(2, 1)].glyph(), '┌');
610
1
        assert_eq!(grid[Pos::new(3, 2)].glyph(), 'h');
611
1
        assert_eq!(grid[Pos::new(4, 2)].glyph(), 'i');
612
1
        assert_eq!(grid[Pos::new(5, 3)].glyph(), '┘');
613
1
    }
614
}