glyph picker
This commit is contained in:
@@ -1,89 +0,0 @@
|
||||
//! CP437 → Unicode mapping for rendering glyph tile indices as terminal text.
|
||||
//!
|
||||
//! kiln stores each cell's visual as a `tile: u32` index into a bitmap font.
|
||||
//! The default kiln font is IBM CP437, where the tile index equals the CP437
|
||||
//! code point. A terminal can't draw the bitmap font, so kiln-tui reinterprets
|
||||
//! the tile index as a character via this table and prints that character with
|
||||
//! the cell's foreground/background colors.
|
||||
|
||||
/// CP437 code point → Unicode scalar value.
|
||||
///
|
||||
/// Index this array by a byte value (0–255) to get the displayable character.
|
||||
/// The low control range (0x00–0x1F) maps to CP437's graphic glyphs (hearts,
|
||||
/// arrows, musical notes, etc.) rather than ASCII control codes, matching how
|
||||
/// these byte values render in a DOS/ZZT-style font. Code 0x00 maps to a blank
|
||||
/// space since a literal NUL is not displayable.
|
||||
const CP437: [char; 256] = [
|
||||
// 0x00–0x0F
|
||||
' ', '☺', '☻', '♥', '♦', '♣', '♠', '•', '◘', '○', '◙', '♂', '♀', '♪', '♫', '☼',
|
||||
// 0x10–0x1F
|
||||
'►', '◄', '↕', '‼', '¶', '§', '▬', '↨', '↑', '↓', '→', '←', '∟', '↔', '▲', '▼',
|
||||
// 0x20–0x2F (ASCII space onward)
|
||||
' ', '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/',
|
||||
// 0x30–0x3F
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?',
|
||||
// 0x40–0x4F
|
||||
'@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
|
||||
// 0x50–0x5F
|
||||
'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_',
|
||||
// 0x60–0x6F
|
||||
'`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
|
||||
// 0x70–0x7F
|
||||
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', '⌂',
|
||||
// 0x80–0x8F
|
||||
'Ç', 'ü', 'é', 'â', 'ä', 'à', 'å', 'ç', 'ê', 'ë', 'è', 'ï', 'î', 'ì', 'Ä', 'Å',
|
||||
// 0x90–0x9F
|
||||
'É', 'æ', 'Æ', 'ô', 'ö', 'ò', 'û', 'ù', 'ÿ', 'Ö', 'Ü', '¢', '£', '¥', '₧', 'ƒ',
|
||||
// 0xA0–0xAF
|
||||
'á', 'í', 'ó', 'ú', 'ñ', 'Ñ', 'ª', 'º', '¿', '⌐', '¬', '½', '¼', '¡', '«', '»',
|
||||
// 0xB0–0xBF (shading + box drawing)
|
||||
'░', '▒', '▓', '│', '┤', '╡', '╢', '╖', '╕', '╣', '║', '╗', '╝', '╜', '╛', '┐',
|
||||
// 0xC0–0xCF
|
||||
'└', '┴', '┬', '├', '─', '┼', '╞', '╟', '╚', '╔', '╩', '╦', '╠', '═', '╬', '╧',
|
||||
// 0xD0–0xDF
|
||||
'╨', '╤', '╥', '╙', '╘', '╒', '╓', '╫', '╪', '┘', '┌', '█', '▄', '▌', '▐', '▀',
|
||||
// 0xE0–0xEF
|
||||
'α', 'ß', 'Γ', 'π', 'Σ', 'σ', 'µ', 'τ', 'Φ', 'Θ', 'Ω', 'δ', '∞', 'φ', 'ε', '∩',
|
||||
// 0xF0–0xFF
|
||||
'≡', '±', '≥', '≤', '⌠', '⌡', '÷', '≈', '°', '∙', '·', '√', 'ⁿ', '²', '■', '\u{00A0}',
|
||||
];
|
||||
|
||||
/// Converts a glyph tile index into a displayable character.
|
||||
///
|
||||
/// Tile indices in `0..256` use the [`CP437`] table. Larger indices (which a
|
||||
/// non-default font could in principle reference) fall back to interpreting the
|
||||
/// index as a raw Unicode scalar, then to a blank space if that is not a valid
|
||||
/// or printable character.
|
||||
pub fn tile_to_char(tile: u32) -> char {
|
||||
if tile < 256 {
|
||||
CP437[tile as usize]
|
||||
} else {
|
||||
char::from_u32(tile).unwrap_or(' ')
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ascii_passthrough() {
|
||||
// The printable ASCII range must map to itself.
|
||||
assert_eq!(tile_to_char('@' as u32), '@'); // player tile 64
|
||||
assert_eq!(tile_to_char('#' as u32), '#'); // wall tile 35
|
||||
assert_eq!(tile_to_char(' ' as u32), ' '); // empty tile 32
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn box_drawing_and_blocks() {
|
||||
assert_eq!(tile_to_char(0xB0), '░');
|
||||
assert_eq!(tile_to_char(0xDB), '█');
|
||||
assert_eq!(tile_to_char(0xC4), '─');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_range_falls_back_to_space() {
|
||||
// A surrogate code point is not a valid char → blank.
|
||||
assert_eq!(tile_to_char(0xD800), ' ');
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
use kiln_ui::code_editor::{CodeEditor, CodeEditorOutcome};
|
||||
use kiln_ui::dialog::{Dialog, DialogResult, ListDialogResponse};
|
||||
use kiln_ui::glyph_dialog::GlyphDialog;
|
||||
use crate::log::{log_preview_line, LogWidget};
|
||||
use crate::render::{board_to_screen, screen_to_board, BoardWidget};
|
||||
use crate::ui::Ui;
|
||||
@@ -55,6 +56,9 @@ pub(crate) struct EditorState {
|
||||
/// The open script code editor, if any. While `Some` it replaces the board view and
|
||||
/// the editor is in [`InputMode::CodeEditor`](crate::input::InputMode::CodeEditor).
|
||||
pub(crate) code_editor: Option<CodeEditor>,
|
||||
/// The open glyph picker overlay, if any. While `Some` the editor is in
|
||||
/// [`InputMode::GlyphDialog`](crate::input::InputMode::GlyphDialog).
|
||||
pub(crate) glyph_dialog: Option<GlyphDialog<EditorState>>,
|
||||
/// Cursor position in board cells, clamped to the board bounds.
|
||||
cursor: (i32, i32),
|
||||
/// Width of the right-hand sidebar in terminal columns (mouse-resizable).
|
||||
@@ -81,6 +85,7 @@ impl EditorState {
|
||||
menu: vec![MenuLevel::Main],
|
||||
dialog: None,
|
||||
code_editor: None,
|
||||
glyph_dialog: None,
|
||||
cursor: (0, 0),
|
||||
sidebar_width: DEFAULT_SIDEBAR_WIDTH,
|
||||
blink_on: true,
|
||||
@@ -176,6 +181,27 @@ impl EditorState {
|
||||
self.dialog = Some(Dialog::list("Boards", names, false, |_resp, _ed: &mut EditorState| {}));
|
||||
}
|
||||
|
||||
/// Opens the glyph picker seeded from the glyph currently under the cursor.
|
||||
/// On OK the chosen glyph is logged (there is no paint tool yet).
|
||||
pub(crate) fn open_glyph_picker(&mut self) {
|
||||
let glyph = {
|
||||
let board = self.board();
|
||||
board.glyph_at(self.cursor.0 as usize, self.cursor.1 as usize)
|
||||
};
|
||||
self.glyph_dialog = Some(GlyphDialog::new(
|
||||
"Glyph",
|
||||
glyph,
|
||||
|chosen, ed: &mut EditorState| {
|
||||
if let Some(g) = chosen {
|
||||
ed.log.push(LogLine::raw(format!(
|
||||
"glyph tile={} fg=#{:02X}{:02X}{:02X} bg=#{:02X}{:02X}{:02X}",
|
||||
g.tile, g.fg.r, g.fg.g, g.fg.b, g.bg.r, g.bg.g, g.bg.b
|
||||
)));
|
||||
}
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
/// Borrows the board currently being edited.
|
||||
pub(crate) fn board(&self) -> Ref<'_, Board> {
|
||||
self.world.boards[&self.board_name].borrow()
|
||||
@@ -235,6 +261,24 @@ pub(crate) fn handle_dialog_input(event: Event, ed: &mut EditorState) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles an input event while the glyph picker is open ([`InputMode::GlyphDialog`](crate::input::InputMode::GlyphDialog)).
|
||||
///
|
||||
/// Mirrors [`handle_dialog_input`]: forwards to the picker and, on `Submit`/`Cancel`,
|
||||
/// takes it out of [`EditorState::glyph_dialog`] and fires its callback via
|
||||
/// [`GlyphDialog::finish`].
|
||||
pub(crate) fn handle_glyph_dialog_input(event: Event, ed: &mut EditorState) {
|
||||
let Some(dialog) = ed.glyph_dialog.as_mut() else {
|
||||
return;
|
||||
};
|
||||
match dialog.handle_event(&event) {
|
||||
DialogResult::Continue => {}
|
||||
result => {
|
||||
let dialog = ed.glyph_dialog.take().expect("glyph dialog present");
|
||||
dialog.finish(result, ed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles an input event while the code editor is open ([`InputMode::CodeEditor`](crate::input::InputMode::CodeEditor)).
|
||||
///
|
||||
/// Forwards key/mouse events to the editor; on `Exit` (Esc) it closes and saves the script.
|
||||
|
||||
@@ -33,6 +33,8 @@ pub enum InputMode {
|
||||
Dialog,
|
||||
/// The script code editor is open; all input drives the editor.
|
||||
CodeEditor,
|
||||
/// The glyph picker is open over the editor; all input drives the picker.
|
||||
GlyphDialog,
|
||||
/// An animation is running and has not claimed any input mode; all input is discarded.
|
||||
Ignore,
|
||||
/// Reserved for a post-transition freeze; nothing currently produces this mode.
|
||||
@@ -54,7 +56,9 @@ pub(crate) fn current_input_mode(mode: &Mode, ui: &Ui) -> InputMode {
|
||||
}
|
||||
match mode {
|
||||
Mode::Edit(ed) => {
|
||||
if ed.dialog.is_some() {
|
||||
if ed.glyph_dialog.is_some() {
|
||||
InputMode::GlyphDialog
|
||||
} else if ed.dialog.is_some() {
|
||||
InputMode::Dialog
|
||||
} else if ed.code_editor.is_some() {
|
||||
InputMode::CodeEditor
|
||||
@@ -122,6 +126,8 @@ pub(crate) fn handle_editor_input(event: Event, term_w: u16, ed: &mut EditorStat
|
||||
KeyCode::Left => ed.move_cursor(-1, 0),
|
||||
KeyCode::Right => ed.move_cursor(1, 0),
|
||||
KeyCode::Char('l') => ui.log.toggle(),
|
||||
// `g` opens the glyph picker for the cell under the cursor.
|
||||
KeyCode::Char('g') => ed.open_glyph_picker(),
|
||||
// A menu letter opens its dialog; unmatched letters are ignored.
|
||||
KeyCode::Char(c) => {
|
||||
ed.menu_key(c);
|
||||
|
||||
+10
-5
@@ -5,10 +5,9 @@
|
||||
//! (or HJKL). There is no editor — this is a play-only front-end.
|
||||
//!
|
||||
//! Rendering is text-based: kiln's bitmap-font tile indices are reinterpreted
|
||||
//! as characters (see [`cp437`]) and drawn with each cell's RGB colors.
|
||||
//! as characters (see [`kiln_core::cp437`]) and drawn with each cell's RGB colors.
|
||||
|
||||
mod animation;
|
||||
mod cp437;
|
||||
mod editor;
|
||||
mod input;
|
||||
mod log;
|
||||
@@ -25,7 +24,10 @@ mod utils;
|
||||
mod editor_menu;
|
||||
|
||||
use crate::animation::{AnimWidget, AnimationLayer};
|
||||
use crate::editor::{EditorState, draw_editor, handle_code_editor_input, handle_dialog_input};
|
||||
use crate::editor::{
|
||||
EditorState, draw_editor, handle_code_editor_input, handle_dialog_input,
|
||||
handle_glyph_dialog_input,
|
||||
};
|
||||
use crate::input::{
|
||||
InputMode, current_input_mode, handle_board_input, handle_editor_input, handle_menu_input,
|
||||
handle_scroll_input,
|
||||
@@ -170,6 +172,7 @@ fn run(terminal: &mut ratatui::DefaultTerminal, mode: &mut Mode, ui: &mut Ui) ->
|
||||
Mode::Edit(ed) => match input_mode {
|
||||
InputMode::Editor => handle_editor_input(event, size.width, ed, ui),
|
||||
InputMode::Dialog => handle_dialog_input(event, ed),
|
||||
InputMode::GlyphDialog => handle_glyph_dialog_input(event, ed),
|
||||
InputMode::CodeEditor => handle_code_editor_input(event, ed),
|
||||
_ => unreachable!(
|
||||
"Menu and Ignore handled above; Board/Scroll/Frozen input modes only occur in Play mode"
|
||||
@@ -263,9 +266,11 @@ fn draw(frame: &mut Frame, mode: &mut Mode, ui: &mut Ui) {
|
||||
);
|
||||
}
|
||||
}
|
||||
// The dialog overlay only exists while editing.
|
||||
// The dialog / glyph-picker overlays only exist while editing.
|
||||
Mode::Edit(ed) => {
|
||||
if let Some(d) = &ed.dialog {
|
||||
if let Some(g) = &ed.glyph_dialog {
|
||||
g.draw(frame);
|
||||
} else if let Some(d) = &ed.dialog {
|
||||
d.draw(frame);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
//! 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
|
||||
//! to a character (see [`kiln_core::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 kiln_core::cp437::tile_to_char;
|
||||
use kiln_core::Board;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
|
||||
Reference in New Issue
Block a user