//! Rendering a kiln [`Board`] into a ratatui buffer as colored text. //! //! Each board cell becomes one terminal cell: the glyph's tile index is mapped //! to a character (see [`crate::cp437`]) and drawn with the glyph's foreground //! and background colors. Objects are drawn over their floor cell, and the //! player is drawn on top of everything. use crate::cp437::tile_to_char; use color::Rgba8; use kiln_core::game::{Board, Glyph}; use kiln_core::log::LogLine; use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::style::{Color, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::Widget; /// Converts a core [`Rgba8`] color into a ratatui truecolor [`Color`]. /// /// The alpha channel is dropped — terminals have no per-cell alpha. Truecolor /// requires terminal support; terminals limited to 256 colors approximate it. fn rgba8_to_color(c: Rgba8) -> Color { Color::Rgb(c.r, c.g, c.b) } /// Converts a core [`LogLine`] into a styled ratatui [`Line`] for display. /// /// Each [`LogSpan`](kiln_core::log::LogSpan) becomes a ratatui [`Span`]; a span's /// foreground/background color is applied only when present, so `None` colors /// inherit the surrounding (default) style. pub fn logline_to_line(line: &LogLine) -> Line<'static> { let spans = line .spans .iter() .map(|s| { let mut style = Style::default(); if let Some(fg) = s.fg { style = style.fg(rgba8_to_color(fg)); } if let Some(bg) = s.bg { style = style.bg(rgba8_to_color(bg)); } Span::styled(s.text.clone(), style) }) .collect::>(); Line::from(spans) } /// A ratatui widget that draws a [`Board`] (with objects and the player) into a /// rectangular area, scrolled so the player stays visible on larger boards. pub struct BoardWidget<'a> { /// The board to render. Borrowed for the duration of the draw. pub board: &'a Board, } impl<'a> BoardWidget<'a> { /// Creates a widget that renders `board`. pub fn new(board: &'a Board) -> Self { Self { board } } /// Lays out one axis of the board within a viewport `view` cells long. /// /// Returns `(off, pad, count)` where `off` is the first board cell to draw, /// `pad` is the blank margin (in screen cells) before the board starts, and /// `count` is how many board cells are visible: /// /// - When the board fits (`board_len <= view`) it is **centered**: `off` is /// 0, `pad` splits the leftover space, and the whole board is drawn. /// - When the board is larger it **scrolls** to follow the player: `pad` is /// 0, `off` centers on the player clamped into `[0, board_len - view]`, and /// exactly `view` cells are shown — never any empty space past the edge. fn axis(board_len: usize, view: usize, player: i32) -> (usize, u16, usize) { if board_len <= view { let pad = (view - board_len) / 2; (0, pad as u16, board_len) } else { let half = (view / 2) as i32; let off = player .saturating_sub(half) .clamp(0, (board_len - view) as i32) as usize; (off, 0, view) } } } impl Widget for BoardWidget<'_> { fn render(self, area: Rect, buf: &mut Buffer) { let board = self.board; // Resolve each axis independently: a small board is centered, a large // one scrolls to keep the player on screen. let (off_x, pad_x, cols) = Self::axis(board.width, area.width as usize, board.player.x); let (off_y, pad_y, rows) = Self::axis(board.height, area.height as usize, board.player.y); // Walk the visible board cells, placing each after the centering pad. for row in 0..rows { let by = off_y + row; let sy = area.y + pad_y + row as u16; for col in 0..cols { let bx = off_x + col; let sx = area.x + pad_x + col as u16; // Resolve which glyph wins this cell: player > object > grid floor. let glyph = if board.player.x == bx as i32 && board.player.y == by as i32 { Glyph::player() } else if let Some(obj) = board.object_at(bx, by) { obj.glyph } else { board.get(bx, by).0 }; // Paint the resolved glyph into the terminal cell. if let Some(cell) = buf.cell_mut((sx, sy)) { cell.set_char(tile_to_char(glyph.tile)) .set_fg(rgba8_to_color(glyph.fg)) .set_bg(rgba8_to_color(glyph.bg)); } } } } }