Add bitmap font rendering via PNG tilesheet

Replace egui text rendering with a BitmapFont system:
- Glyph.tile: u32 replaces ch: char; top-left pixel of PNG is background sentinel
- BitmapFont loads/preprocesses PNG to opaque-white + transparent, tinted at render time
- Default font: assets/vga-font-8x16.png (CP437, 8×16 tiles); placeholder generated if missing
- Boards can specify a per-board font via [font] in their .toml file
- Editor Board tab: "Font…" button opens font dialog (rfd file picker, tile dims, tilesheet preview)
- Glyph picker: tile grid from active font replaces hardcoded ASCII char grid
- map_file: TileIndex untagged enum accepts char or integer in palette entries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-19 00:07:04 -05:00
parent 12ebe77932
commit 212297ecf9
14 changed files with 1174 additions and 260 deletions
+57 -19
View File
@@ -1,15 +1,17 @@
use eframe::egui;
use egui::{Sense, vec2};
use crate::game::{Archetype, Glyph, ALL_ARCHETYPES};
use crate::render::{CELL_H, CELL_W, PALETTE_PANEL_W, paint_glyph};
use crate::font::BitmapFont;
use crate::font_dialog::{FontDialogState, show as show_font_dialog};
use crate::game::{ALL_ARCHETYPES, Archetype, FontSpec, Glyph};
use crate::render::{PALETTE_PANEL_W, paint_glyph};
/// Which tab is active in the editor side panel.
#[derive(Copy, Clone, PartialEq, Eq)]
pub(crate) enum EditorTab {
/// Archetype + glyph palette for painting cells.
Palette,
/// Board-level properties (placeholder).
/// Board-level properties.
Board,
/// World-level properties (placeholder).
World,
@@ -19,21 +21,27 @@ pub(crate) enum EditorTab {
pub(crate) struct EditorState {
/// The archetype that will be stamped when the user clicks a cell.
pub(crate) selected: Archetype,
/// The glyph (character + colors) to stamp. Independent of the archetype's default.
/// The glyph (tile index + colors) to stamp. Independent of the archetype's default.
pub(crate) glyph: Glyph,
/// Whether the glyph picker dialog is currently open.
pub(crate) glyph_picker_open: bool,
/// Whether the font picker dialog is currently open.
pub(crate) font_dialog_open: bool,
/// In-progress state for the font dialog while it is open.
pub(crate) font_dialog_state: FontDialogState,
/// Which side-panel tab is currently shown.
pub(crate) tab: EditorTab,
}
impl EditorState {
/// Creates editor state with `Wall` selected and the picker closed.
/// Creates editor state with `Wall` selected and all dialogs closed.
pub(crate) fn new() -> Self {
Self {
selected: Archetype::Wall,
glyph: Archetype::Wall.default_glyph(),
glyph_picker_open: false,
font_dialog_open: false,
font_dialog_state: FontDialogState::from_spec(None),
tab: EditorTab::Palette,
}
}
@@ -41,17 +49,26 @@ impl EditorState {
/// Renders the editor side panel (must be called before `CentralPanel`).
///
/// Shows tab bar (Palette / Board / World) and, on the Palette tab, the
/// scrollable archetype list and current-glyph preview button.
pub(crate) fn show_editor_panel(ctx: &egui::Context, editor: &mut EditorState) {
/// Shows tab bar (Palette / Board / World) and the content for the active tab.
/// `font_spec` is the board's current font override (may be `None` for default).
/// `active_font` is the resolved font currently in use for rendering.
pub(crate) fn show_editor_panel(
ctx: &egui::Context,
editor: &mut EditorState,
font_spec: &mut Option<FontSpec>,
active_font: &BitmapFont,
) {
let tw = active_font.tile_w as f32;
let th = active_font.tile_h as f32;
egui::SidePanel::right("palette_panel")
.resizable(true)
.default_width(PALETTE_PANEL_W)
.show(ctx, |ui| {
ui.horizontal(|ui| {
ui.selectable_value(&mut editor.tab, EditorTab::Palette, "Palette");
ui.selectable_value(&mut editor.tab, EditorTab::Board, "Board");
ui.selectable_value(&mut editor.tab, EditorTab::World, "World");
ui.selectable_value(&mut editor.tab, EditorTab::Board, "Board");
ui.selectable_value(&mut editor.tab, EditorTab::World, "World");
});
ui.separator();
@@ -60,7 +77,7 @@ pub(crate) fn show_editor_panel(ctx: &egui::Context, editor: &mut EditorState) {
ui.label("Element");
// Scrollable archetype list; shows at most 5 rows before scrolling.
let row_h = CELL_H + ui.spacing().item_spacing.y;
let row_h = th + ui.spacing().item_spacing.y;
egui::ScrollArea::vertical()
.max_height(row_h * 5.0)
.id_salt("arch_list")
@@ -69,14 +86,14 @@ pub(crate) fn show_editor_panel(ctx: &egui::Context, editor: &mut EditorState) {
let is_selected = editor.selected == archetype;
let glyph = archetype.default_glyph();
ui.horizontal(|ui| {
let (r, cell_resp) = ui.allocate_exact_size(
vec2(CELL_W, CELL_H), Sense::click());
paint_glyph(ui.painter(), r, &glyph);
let (r, cell_resp) =
ui.allocate_exact_size(vec2(tw, th), Sense::click());
paint_glyph(ui.painter(), r, &glyph, active_font);
let label_resp =
ui.selectable_label(is_selected, archetype.name());
if cell_resp.clicked() || label_resp.clicked() {
editor.selected = archetype;
editor.glyph = archetype.default_glyph();
editor.glyph = archetype.default_glyph();
}
});
}
@@ -87,15 +104,36 @@ pub(crate) fn show_editor_panel(ctx: &egui::Context, editor: &mut EditorState) {
ui.label("Glyph");
// Paint a preview cell showing the current glyph, sized like a board cell.
let (cell_rect, glyph_btn) =
ui.allocate_exact_size(vec2(CELL_W, CELL_H), Sense::click());
paint_glyph(ui.painter(), cell_rect, &editor.glyph);
ui.allocate_exact_size(vec2(tw, th), Sense::click());
paint_glyph(ui.painter(), cell_rect, &editor.glyph, active_font);
if glyph_btn.clicked() {
editor.glyph_picker_open = true;
}
}
EditorTab::Board => {}
EditorTab::Board => {
let font_label = match font_spec {
Some(s) => s.path.clone(),
None => "(default)".to_owned(),
};
ui.label(format!("Font: {font_label}"));
if ui.button("Font…").clicked() {
editor.font_dialog_state = FontDialogState::from_spec(font_spec.as_ref());
editor.font_dialog_open = true;
}
}
EditorTab::World => {}
}
});
}
// Font dialog floats outside the side panel; show it here so it renders on top.
if editor.font_dialog_open {
show_font_dialog(
ctx,
&mut editor.font_dialog_open,
&mut editor.font_dialog_state,
font_spec,
);
}
}