Skip to main content

dotzuki_renderer/layout_engine/elements/
border.rs

1//! Game Boy-style tile border rendering.
2//!
3//! A [`Border`] draws a bordered rectangle using configurable tile indices.
4//! Each of the 4 corners, 4 edge segments, and the fill area can use
5//! independent tile IDs. When no tile style is set (`tiles` is `None`),
6//! the entire rectangle is filled with a solid background color.
7
8use dotzuki_engine::render::{Rgba, Painter, TilePos, TileRect};
9
10// ── BorderTiles ──────────────────────────────────────────────────────────
11
12/// Tile indices for a Game Boy-style box border.
13///
14/// Each field corresponds to a specific position in the border grid:
15///
16/// ```text
17/// ┌──────┬──────┬──────┬──────┬──────┐
18/// │ tl   │ top  │ top  │ top  │ tr   │
19/// ├──────┼──────┼──────┼──────┼──────┤
20/// │ left │ fill │ fill │ fill │ right│
21/// ├──────┼──────┼──────┼──────┼──────┤
22/// │ left │ fill │ fill │ fill │ right│
23/// ├──────┼──────┼──────┼──────┼──────┤
24/// │ bl   │ bot  │ bot  │ bot  │ br   │
25/// └──────┴──────┴──────┴──────┴──────┘
26/// ```
27///
28/// Defaults match the box-drawing tile IDs from the embedded font
29/// (0x79–0x7F).
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct BorderTiles {
32    /// Top-left corner tile.
33    pub top_left: u8,
34    /// Top edge tile (repeated across the top).
35    pub top: u8,
36    /// Top-right corner tile.
37    pub top_right: u8,
38    /// Left edge tile (repeated down the left side).
39    pub left: u8,
40    /// Right edge tile (repeated down the right side).
41    pub right: u8,
42    /// Bottom-left corner tile.
43    pub bottom_left: u8,
44    /// Bottom edge tile (repeated across the bottom).
45    pub bottom: u8,
46    /// Bottom-right corner tile.
47    pub bottom_right: u8,
48    /// Fill tile for the interior.
49    pub fill: u8,
50}
51
52impl Default for BorderTiles {
53    fn default() -> Self {
54        Self {
55            top_left: 0x79,     // ┌
56            top: 0x7A,          // ─
57            top_right: 0x7B,    // ┐
58            left: 0x7C,         // │
59            right: 0x7C,        // │
60            bottom_left: 0x7D,  // └
61            bottom: 0x7A,       // ─
62            bottom_right: 0x7E, // ┘
63            fill: 0x7F,         // space
64        }
65    }
66}
67
68// ── Border ───────────────────────────────────────────────────────────────
69
70/// A Game Boy-style bordered rectangle.
71///
72/// # Examples
73///
74/// ```
75/// use dotzuki_renderer::layout_engine::elements::border::{Border, BorderTiles};
76/// use dotzuki_engine::render::{TileRect, Rgba};
77///
78/// let rect = TileRect::new(1, 1, 10, 5);
79/// let border = Border::new(rect, Rgba::INK_BLACK);
80///
81/// // Use custom tiles if needed
82/// let custom_tiles = BorderTiles {
83///     top_left: 0x01,
84///     top_right: 0x02,
85///     bottom_left: 0x03,
86///     bottom_right: 0x04,
87///     ..BorderTiles::default()
88/// };
89/// let styled = Border::with_tiles(rect, custom_tiles, Rgba::INK_DARK_GRAY);
90/// ```
91#[derive(Debug, Clone)]
92pub struct Border {
93    /// The area this border occupies (in tiles).
94    pub rect: TileRect,
95    /// Optional tile indices for the border and fill.
96    /// When `None`, the border renders as a solid color fill.
97    pub tiles: Option<BorderTiles>,
98    /// The ink color used for drawing tiles or the solid fill.
99    pub color: Rgba,
100}
101
102impl Border {
103    /// Create a border with default tile indices.
104    #[inline]
105    pub fn new(rect: TileRect, color: Rgba) -> Self {
106        Self {
107            rect,
108            tiles: Some(BorderTiles::default()),
109            color,
110        }
111    }
112
113    /// Create a border with custom tile indices.
114    #[inline]
115    pub fn with_tiles(rect: TileRect, tiles: BorderTiles, color: Rgba) -> Self {
116        Self {
117            rect,
118            tiles: Some(tiles),
119            color,
120        }
121    }
122
123    /// Create a borderless fill (no tiles, just a solid color rectangle).
124    ///
125    /// Useful as a background fill behind other elements.
126    #[inline]
127    pub fn fill(rect: TileRect, color: Rgba) -> Self {
128        Self {
129            rect,
130            tiles: None,
131            color,
132        }
133    }
134
135    /// Whether this border has a tile style set.
136    #[inline]
137    pub fn has_style(&self) -> bool {
138        self.tiles.is_some()
139    }
140
141    // ── Rendering ──────────────────────────────────────────────────────
142
143    /// Render this border into the given [`Painter`].
144    ///
145    /// When `tiles` is `Some`, draws the border using tile IDs:
146    /// corners, edges, and fill. When `tiles` is `None`, fills the
147    /// entire rectangle with `color` using a single pixel-rect call.
148    pub fn render(&self, painter: &mut dyn Painter) {
149        let rect = self.rect;
150
151        match &self.tiles {
152            Some(tiles) => {
153                // Corners (drawn even for 1×1 rects)
154                painter.draw_gb_tile(
155                    TilePos::new(rect.tx, rect.ty),
156                    tiles.top_left,
157                    " ",
158                    self.color,
159                );
160                if rect.tw > 1 {
161                    painter.draw_gb_tile(
162                        TilePos::new(rect.tx + rect.tw - 1, rect.ty),
163                        tiles.top_right,
164                        " ",
165                        self.color,
166                    );
167                }
168                if rect.th > 1 {
169                    painter.draw_gb_tile(
170                        TilePos::new(rect.tx, rect.ty + rect.th - 1),
171                        tiles.bottom_left,
172                        " ",
173                        self.color,
174                    );
175                }
176                if rect.tw > 1 && rect.th > 1 {
177                    painter.draw_gb_tile(
178                        TilePos::new(rect.tx + rect.tw - 1, rect.ty + rect.th - 1),
179                        tiles.bottom_right,
180                        " ",
181                        self.color,
182                    );
183                }
184
185                // Top edge
186                for x in 1..rect.tw.saturating_sub(1) {
187                    painter.draw_gb_tile(
188                        TilePos::new(rect.tx + x, rect.ty),
189                        tiles.top,
190                        " ",
191                        self.color,
192                    );
193                }
194
195                // Bottom edge
196                if rect.th > 1 {
197                    for x in 1..rect.tw.saturating_sub(1) {
198                        painter.draw_gb_tile(
199                            TilePos::new(rect.tx + x, rect.ty + rect.th - 1),
200                            tiles.bottom,
201                            " ",
202                            self.color,
203                        );
204                    }
205                }
206
207                // Left edge
208                for y in 1..rect.th.saturating_sub(1) {
209                    painter.draw_gb_tile(
210                        TilePos::new(rect.tx, rect.ty + y),
211                        tiles.left,
212                        " ",
213                        self.color,
214                    );
215                }
216
217                // Right edge
218                if rect.tw > 1 {
219                    for y in 1..rect.th.saturating_sub(1) {
220                        painter.draw_gb_tile(
221                            TilePos::new(rect.tx + rect.tw - 1, rect.ty + y),
222                            tiles.right,
223                            " ",
224                            self.color,
225                        );
226                    }
227                }
228
229                // Fill interior
230                for y in 1..rect.th.saturating_sub(1) {
231                    for x in 1..rect.tw.saturating_sub(1) {
232                        painter.draw_gb_tile(
233                            TilePos::new(rect.tx + x, rect.ty + y),
234                            tiles.fill,
235                            " ",
236                            self.color,
237                        );
238                    }
239                }
240            }
241            None => {
242                // Solid color fill — no border tiles
243                painter.draw_pixel_rect(
244                    rect.tx * 8,
245                    rect.ty * 8,
246                    rect.tw * 8,
247                    rect.th * 8,
248                    self.color,
249                );
250            }
251        }
252    }
253}
254
255// ── Tests ────────────────────────────────────────────────────────────────
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use dotzuki_engine::render::TileRect;
261
262    // ── BorderTiles tests ──────────────────────────────────────────────
263
264    #[test]
265    fn default_tiles_match_textbox_constants() {
266        let t = BorderTiles::default();
267        assert_eq!(t.top_left, 0x79);
268        assert_eq!(t.top, 0x7A);
269        assert_eq!(t.top_right, 0x7B);
270        assert_eq!(t.left, 0x7C);
271        assert_eq!(t.right, 0x7C);
272        assert_eq!(t.bottom_left, 0x7D);
273        assert_eq!(t.bottom, 0x7A);
274        assert_eq!(t.bottom_right, 0x7E);
275        assert_eq!(t.fill, 0x7F);
276    }
277
278    #[test]
279    fn custom_tiles_support() {
280        let t = BorderTiles {
281            top_left: 0x01,
282            top: 0x02,
283            top_right: 0x03,
284            left: 0x04,
285            right: 0x05,
286            bottom_left: 0x06,
287            bottom: 0x07,
288            bottom_right: 0x08,
289            fill: 0x09,
290        };
291        assert_eq!(t.top_left, 0x01);
292        assert_eq!(t.right, 0x05);
293        assert_eq!(t.fill, 0x09);
294    }
295
296    #[test]
297    fn border_tiles_copy_and_eq() {
298        let a = BorderTiles::default();
299        let b = a;
300        assert_eq!(a, b);
301
302        let mut c = a;
303        c.top_left = 0x00;
304        assert_ne!(a, c);
305    }
306
307    // ── Border construction tests ─────────────────────────────────────
308
309    #[test]
310    fn new_border_has_default_tiles() {
311        let rect = TileRect::new(0, 0, 10, 5);
312        let b = Border::new(rect, Rgba::INK_BLACK);
313        assert!(b.has_style());
314        assert_eq!(b.tiles, Some(BorderTiles::default()));
315        assert_eq!(b.rect, rect);
316    }
317
318    #[test]
319    fn with_tiles_sets_custom_tiles() {
320        let rect = TileRect::new(2, 3, 8, 4);
321        let tiles = BorderTiles {
322            top_left: 0x10,
323            ..BorderTiles::default()
324        };
325        let b = Border::with_tiles(rect, tiles, Rgba::INK_DARK_GRAY);
326        assert!(b.has_style());
327        assert_eq!(b.tiles.unwrap().top_left, 0x10);
328    }
329
330    #[test]
331    fn fill_border_has_no_style() {
332        let rect = TileRect::new(0, 0, 20, 18);
333        let b = Border::fill(rect, Rgba::INK_WHITE);
334        assert!(!b.has_style());
335        assert_eq!(b.tiles, None);
336        assert_eq!(b.color, Rgba::INK_WHITE);
337    }
338
339    #[test]
340    fn has_style_reflects_tiles_presence() {
341        let rect = TileRect::new(0, 0, 4, 4);
342
343        let styled = Border::new(rect, Rgba::INK_BLACK);
344        assert!(styled.has_style());
345
346        let fill_only = Border::fill(rect, Rgba::INK_WHITE);
347        assert!(!fill_only.has_style());
348    }
349
350    #[test]
351    fn minimal_1x1_border() {
352        // A 1×1 border should just draw the top-left corner
353        let rect = TileRect::new(5, 5, 1, 1);
354        let b = Border::new(rect, Rgba::INK_BLACK);
355        assert!(b.has_style());
356        assert_eq!(b.rect.tw, 1);
357        assert_eq!(b.rect.th, 1);
358    }
359
360    #[test]
361    fn thin_horizontal_border() {
362        // 10-wide × 1-high: just top edge between two corners
363        let rect = TileRect::new(0, 0, 10, 1);
364        let b = Border::new(rect, Rgba::INK_BLACK);
365        assert_eq!(b.rect.tw, 10);
366        assert_eq!(b.rect.th, 1);
367    }
368
369    #[test]
370    fn thin_vertical_border() {
371        // 1-wide × 5-high: just left edge between two corners
372        let rect = TileRect::new(0, 0, 1, 5);
373        let b = Border::new(rect, Rgba::INK_BLACK);
374        assert_eq!(b.rect.tw, 1);
375        assert_eq!(b.rect.th, 5);
376    }
377}