glyph serialization
This commit is contained in:
@@ -15,7 +15,7 @@ use crate::log::{LogWidget, log_preview_line};
|
||||
use crate::render::{BoardWidget, board_to_screen, screen_to_board};
|
||||
use crate::ui::Ui;
|
||||
use crate::utils::{glyph_to_span, rgba8_to_color};
|
||||
use kiln_core::cp437::tile_to_char;
|
||||
use crate::utils::glyph_char;
|
||||
use kiln_core::game::GameState;
|
||||
use kiln_core::glyph::Glyph;
|
||||
use kiln_core::log::LogLine;
|
||||
@@ -510,7 +510,7 @@ fn draw_footer_lines<'a>(
|
||||
) -> Vec<Line<'a>> {
|
||||
// A one-cell preview of the current glyph in its own fg/bg colors.
|
||||
let preview = Span::styled(
|
||||
tile_to_char(ed.current_glyph.tile).to_string(),
|
||||
glyph_char(ed.current_glyph).to_string(),
|
||||
Style::default()
|
||||
.fg(rgba8_to_color(ed.current_glyph.fg))
|
||||
.bg(rgba8_to_color(ed.current_glyph.bg)),
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
use color::Rgba8;
|
||||
use kiln_core::colors::NAMED_COLORS;
|
||||
use kiln_core::cp437::tile_to_char;
|
||||
use kiln_core::cp437::{char_to_tile, tile_to_char};
|
||||
use kiln_core::glyph::Glyph;
|
||||
use kiln_ui::dialog::DialogResult;
|
||||
use kiln_ui::text_field::TextField;
|
||||
@@ -55,6 +55,17 @@ const GRID_ROWS: u32 = 8;
|
||||
/// The strip slot index that means "custom color" (one past the named colors).
|
||||
const CUSTOM: usize = NAMED_COLORS.len();
|
||||
|
||||
/// The character drawn for CP437 slot `index` in the picker grid and preview.
|
||||
///
|
||||
/// Slot 0 holds `'\0'`, the transparent sentinel, which has no printable form —
|
||||
/// it shows as a blank, matching how a transparent cell renders on the board (see
|
||||
/// [`crate::utils::glyph_char`]). The slot is still selectable: picking it is how
|
||||
/// you author an invisible glyph.
|
||||
fn glyph_char_for(index: u32) -> char {
|
||||
let ch = tile_to_char(index);
|
||||
if ch == '\0' { ' ' } else { ch }
|
||||
}
|
||||
|
||||
/// A single fg/bg color selector: a strip of the named swatches plus a custom slot,
|
||||
/// with an editable hex field used when the custom slot is selected.
|
||||
///
|
||||
@@ -126,8 +137,13 @@ type GlyphCallback<Ctx> = Box<dyn FnOnce(Option<Glyph>, &mut Ctx)>;
|
||||
pub struct GlyphDialog<Ctx> {
|
||||
/// Title shown in the window's top border.
|
||||
title: String,
|
||||
/// The currently selected tile index (`0..256`).
|
||||
tile: u32,
|
||||
/// The currently selected CP437 index (`0..256`).
|
||||
///
|
||||
/// Kept as an index, not the `char` a [`Glyph`] stores, because the picker is
|
||||
/// a 32x8 grid over the CP437 table and navigates by row/column arithmetic.
|
||||
/// Conversion happens only when seeding from ([`char_to_tile`]) and emitting
|
||||
/// to ([`tile_to_char`]) a `Glyph`.
|
||||
index: u32,
|
||||
/// Foreground color selector.
|
||||
fg: ColorPicker,
|
||||
/// Background color selector.
|
||||
@@ -139,10 +155,14 @@ pub struct GlyphDialog<Ctx> {
|
||||
}
|
||||
|
||||
impl<Ctx> GlyphDialog<Ctx> {
|
||||
/// Builds a glyph picker seeded from `initial`: tile selection starts at
|
||||
/// `initial.tile`, and each color picker is seeded from `initial.fg`/`initial.bg`
|
||||
/// (a matching named swatch if any, else the custom slot). `on_done` receives
|
||||
/// `Some(glyph)` on OK or `None` on cancel, plus `&mut Ctx`.
|
||||
/// Builds a glyph picker seeded from `initial`: the grid selects the CP437
|
||||
/// slot holding `initial.tile`, and each color picker is seeded from
|
||||
/// `initial.fg`/`initial.bg` (a matching named swatch if any, else the custom
|
||||
/// slot). `on_done` receives `Some(glyph)` on OK or `None` on cancel, plus
|
||||
/// `&mut Ctx`.
|
||||
///
|
||||
/// A glyph whose character has no CP437 slot (a map may carry any character)
|
||||
/// falls back to slot 0, the transparent sentinel.
|
||||
pub fn new(
|
||||
title: impl Into<String>,
|
||||
initial: Glyph,
|
||||
@@ -150,7 +170,7 @@ impl<Ctx> GlyphDialog<Ctx> {
|
||||
) -> Self {
|
||||
Self {
|
||||
title: title.into(),
|
||||
tile: initial.tile,
|
||||
index: char_to_tile(initial.tile).unwrap_or(0),
|
||||
fg: ColorPicker::new(initial.fg),
|
||||
bg: ColorPicker::new(initial.bg),
|
||||
focus: Focus::Grid,
|
||||
@@ -211,7 +231,7 @@ impl<Ctx> GlyphDialog<Ctx> {
|
||||
/// The chosen [`Glyph`], or `None` if either color is currently unresolved.
|
||||
fn glyph(&self) -> Option<Glyph> {
|
||||
Some(Glyph {
|
||||
tile: self.tile,
|
||||
tile: tile_to_char(self.index),
|
||||
fg: self.fg.color()?,
|
||||
bg: self.bg.color()?,
|
||||
})
|
||||
@@ -280,7 +300,7 @@ impl<Ctx> GlyphDialog<Ctx> {
|
||||
|
||||
/// Moves the grid selection one cell for an arrow key, clamped at the edges.
|
||||
fn move_grid(&mut self, code: KeyCode) {
|
||||
let (col, row) = (self.tile % GRID_COLS, self.tile / GRID_COLS);
|
||||
let (col, row) = (self.index % GRID_COLS, self.index / GRID_COLS);
|
||||
let (col, row) = match code {
|
||||
KeyCode::Left => (col.saturating_sub(1), row),
|
||||
KeyCode::Right => ((col + 1).min(GRID_COLS - 1), row),
|
||||
@@ -288,7 +308,7 @@ impl<Ctx> GlyphDialog<Ctx> {
|
||||
KeyCode::Down => (col, (row + 1).min(GRID_ROWS - 1)),
|
||||
_ => (col, row),
|
||||
};
|
||||
self.tile = row * GRID_COLS + col;
|
||||
self.index = row * GRID_COLS + col;
|
||||
}
|
||||
|
||||
/// Draws the 32×8 character grid into `area`, centered horizontally. When `colors`
|
||||
@@ -308,7 +328,7 @@ impl<Ctx> GlyphDialog<Ctx> {
|
||||
if x >= area.right() || y >= area.bottom() {
|
||||
continue;
|
||||
}
|
||||
let style = if tile == self.tile {
|
||||
let style = if tile == self.index {
|
||||
if self.focus == Focus::Grid {
|
||||
// Bright cursor so the selection reads even against the colored grid.
|
||||
Style::default().fg(Color::Black).bg(CURSOR_BG)
|
||||
@@ -319,7 +339,7 @@ impl<Ctx> GlyphDialog<Ctx> {
|
||||
base
|
||||
};
|
||||
if let Some(cell) = buf.cell_mut((x, y)) {
|
||||
cell.set_symbol(&tile_to_char(tile).to_string());
|
||||
cell.set_symbol(&glyph_char_for(tile).to_string());
|
||||
cell.set_style(style);
|
||||
}
|
||||
}
|
||||
@@ -430,7 +450,7 @@ impl<Ctx> GlyphDialog<Ctx> {
|
||||
let footer = Line::from(vec![
|
||||
Span::styled("[enter] ", key_style),
|
||||
Span::styled("OK ", ok_style),
|
||||
Span::styled(tile_to_char(self.tile).to_string(), preview_style),
|
||||
Span::styled(glyph_char_for(self.index).to_string(), preview_style),
|
||||
Span::styled(" [esc] ", key_style),
|
||||
Span::styled("Cancel", Style::default().fg(Color::White).bg(BG)),
|
||||
]);
|
||||
@@ -567,10 +587,11 @@ mod tests {
|
||||
use super::*;
|
||||
use ratatui::crossterm::event::{KeyEvent, KeyModifiers};
|
||||
|
||||
/// A glyph with custom (non-named) colors for seeding tests.
|
||||
/// A glyph with custom (non-named) colors for seeding tests. Its character
|
||||
/// is CP437 slot 5 (`♣`), so the picker seeds its grid selection there.
|
||||
fn sample() -> Glyph {
|
||||
Glyph {
|
||||
tile: 5,
|
||||
tile: '♣',
|
||||
fg: Rgba8 {
|
||||
r: 0xFF,
|
||||
g: 0x00,
|
||||
@@ -594,7 +615,7 @@ mod tests {
|
||||
#[test]
|
||||
fn seed_custom_colors_go_to_custom_slot() {
|
||||
let d: GlyphDialog<()> = GlyphDialog::new("t", sample(), |_, _| {});
|
||||
assert_eq!(d.tile, 5);
|
||||
assert_eq!(d.index, 5, "seeded to the CP437 slot holding the glyph's char");
|
||||
assert!(d.fg.is_custom() && d.bg.is_custom());
|
||||
assert_eq!(d.fg.field.value(), "FF0000");
|
||||
assert_eq!(d.bg.field.value(), "0000FF");
|
||||
@@ -604,7 +625,7 @@ mod tests {
|
||||
#[test]
|
||||
fn seed_matching_named_color_selects_its_swatch() {
|
||||
let g = Glyph {
|
||||
tile: 1,
|
||||
tile: '☺',
|
||||
fg: NAMED_COLORS[4].1, // Red
|
||||
bg: NAMED_COLORS[0].1, // Black
|
||||
};
|
||||
@@ -634,7 +655,7 @@ mod tests {
|
||||
fn tab_cycles_focus_skipping_unreachable_custom_fields() {
|
||||
// A named-only glyph: neither color is custom, so the field stops are skipped.
|
||||
let g = Glyph {
|
||||
tile: 0,
|
||||
tile: '\0',
|
||||
fg: NAMED_COLORS[1].1,
|
||||
bg: NAMED_COLORS[2].1,
|
||||
};
|
||||
@@ -667,20 +688,20 @@ mod tests {
|
||||
#[test]
|
||||
fn arrows_move_grid_selection_and_clamp() {
|
||||
let mut g = sample();
|
||||
g.tile = 0;
|
||||
g.tile = '\0'; // CP437 slot 0, the grid's top-left
|
||||
let mut d: GlyphDialog<()> = GlyphDialog::new("t", g, |_, _| {});
|
||||
d.handle_event(&key(KeyCode::Right));
|
||||
assert_eq!(d.tile, 1);
|
||||
assert_eq!(d.index, 1);
|
||||
d.handle_event(&key(KeyCode::Down));
|
||||
assert_eq!(d.tile, 1 + GRID_COLS);
|
||||
assert_eq!(d.index, 1 + GRID_COLS);
|
||||
d.handle_event(&key(KeyCode::Left));
|
||||
assert_eq!(d.tile, GRID_COLS);
|
||||
assert_eq!(d.index, GRID_COLS);
|
||||
d.handle_event(&key(KeyCode::Up));
|
||||
assert_eq!(d.tile, 0);
|
||||
assert_eq!(d.index, 0);
|
||||
// Clamps at the top-left corner.
|
||||
d.handle_event(&key(KeyCode::Up));
|
||||
d.handle_event(&key(KeyCode::Left));
|
||||
assert_eq!(d.tile, 0);
|
||||
assert_eq!(d.index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -707,11 +728,11 @@ mod tests {
|
||||
let mut out: Option<Option<Glyph>> = None;
|
||||
let mut d: GlyphDialog<Option<Option<Glyph>>> =
|
||||
GlyphDialog::new("t", sample(), |g, c| *c = Some(g));
|
||||
d.handle_event(&key(KeyCode::Right)); // tile 5 -> 6
|
||||
d.handle_event(&key(KeyCode::Right)); // CP437 slot 5 -> 6
|
||||
let res = d.handle_event(&key(KeyCode::Enter));
|
||||
d.finish(res, &mut out);
|
||||
let chosen = out.unwrap().unwrap();
|
||||
assert_eq!(chosen.tile, 6);
|
||||
assert_eq!(chosen.tile, tile_to_char(6));
|
||||
assert_eq!(chosen.fg, sample().fg);
|
||||
assert_eq!(chosen.bg, sample().bg);
|
||||
|
||||
|
||||
@@ -5,11 +5,10 @@
|
||||
//! and background colors. Objects are drawn over their floor cell, and the
|
||||
//! player is drawn on top of everything.
|
||||
|
||||
use crate::utils::rgba8_to_color;
|
||||
use crate::utils::{glyph_char, rgba8_to_color};
|
||||
use color::Rgba8;
|
||||
use kiln_core::Board;
|
||||
use kiln_core::Lighting;
|
||||
use kiln_core::cp437::tile_to_char;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::widgets::Widget;
|
||||
@@ -162,7 +161,7 @@ impl Widget for BoardWidget<'_> {
|
||||
Some(l) => (l.tint(bx, by, glyph.fg), l.tint(bx, by, glyph.bg)),
|
||||
None => (glyph.fg, glyph.bg),
|
||||
};
|
||||
cell.set_char(tile_to_char(glyph.tile))
|
||||
cell.set_char(glyph_char(glyph))
|
||||
.set_fg(rgba8_to_color(fg))
|
||||
.set_bg(rgba8_to_color(bg));
|
||||
} else {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
use std::collections::VecDeque;
|
||||
use crate::utils::rgba8_to_color;
|
||||
use kiln_core::Builtin;
|
||||
use kiln_core::cp437::tile_to_char;
|
||||
use crate::utils::glyph_char;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Color, Style};
|
||||
@@ -45,7 +45,7 @@ impl Widget for StatusSidebarWidget {
|
||||
// Draw the gem indicator from the gem archetype's default glyph, so the
|
||||
// sidebar matches what gems look like on the board (no hardcoded ♦/color).
|
||||
let gem_glyph = Builtin::Gem.default_glyph_for("gem");
|
||||
let gem_char = tile_to_char(gem_glyph.tile).to_string();
|
||||
let gem_char = glyph_char(gem_glyph).to_string();
|
||||
let gem_style = Style::default().fg(rgba8_to_color(gem_glyph.fg));
|
||||
|
||||
// Build the key row: 8 ♀ glyphs, colored when held, near-black when absent.
|
||||
|
||||
+16
-5
@@ -1,5 +1,4 @@
|
||||
use color::Rgba8;
|
||||
use kiln_core::cp437::tile_to_char;
|
||||
use kiln_core::glyph::Glyph;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::prelude::Color;
|
||||
@@ -14,15 +13,27 @@ pub fn rgba8_to_color(c: Rgba8) -> Color {
|
||||
Color::Rgb(c.r, c.g, c.b)
|
||||
}
|
||||
|
||||
/// Converts a [`Glyph`] to a single-character styled [`Span`].
|
||||
/// The character this terminal front-end draws for `glyph`.
|
||||
///
|
||||
/// The tile index is mapped to a CP437 character; fg and bg are both applied.
|
||||
/// A glyph carries its character directly, so this is almost the identity — the
|
||||
/// one case that needs handling is the transparent sentinel `'\0'`, which means
|
||||
/// "draw nothing". By the time a glyph reaches a renderer, `Board::glyph_at` has
|
||||
/// already exhausted its precedence chain, so there is nothing underneath left to
|
||||
/// reveal and the cell is simply blank. Writing the NUL through to the terminal
|
||||
/// buffer would emit an unprintable character instead.
|
||||
///
|
||||
/// How a sentinel looks on screen is a front-end decision, which is why this
|
||||
/// lives here rather than in kiln-core.
|
||||
pub fn glyph_char(glyph: Glyph) -> char {
|
||||
if glyph.is_visible() { glyph.tile } else { ' ' }
|
||||
}
|
||||
|
||||
/// Converts a [`Glyph`] to a single-character styled [`Span`], applying fg and bg.
|
||||
pub fn glyph_to_span(glyph: Glyph) -> Span<'static> {
|
||||
let ch = tile_to_char(glyph.tile).to_string();
|
||||
let style = Style::default()
|
||||
.fg(rgba8_to_color(glyph.fg))
|
||||
.bg(rgba8_to_color(glyph.bg));
|
||||
Span::styled(ch, style)
|
||||
Span::styled(glyph_char(glyph).to_string(), style)
|
||||
}
|
||||
|
||||
/// Returns true if two `Rect`s share at least one cell.
|
||||
|
||||
Reference in New Issue
Block a user