use eframe::egui; use egui::{Color32, DragValue, Rect, Stroke, vec2}; use crate::font::BitmapFont; use kiln_core::game::FontSpec; /// State for the font-picker dialog. /// /// Holds the in-progress edit of font path and tile dimensions, and a loaded /// preview font so the user can see the tilesheet before applying. pub(crate) struct FontDialogState { /// Path string as edited by the user (may differ from the applied font). pub path: String, /// Tile width being edited. pub tile_w: u32, /// Tile height being edited. pub tile_h: u32, /// Loaded preview of the current path+dimensions, if valid. pub preview: Option, } impl FontDialogState { /// Creates dialog state pre-populated from an existing [`FontSpec`], or /// defaults (8×8, empty path) when there is no board-level font. pub(crate) fn from_spec(spec: Option<&FontSpec>) -> Self { match spec { Some(s) => Self { path: s.path.clone(), tile_w: s.tile_w, tile_h: s.tile_h, preview: None, }, None => Self { path: String::new(), tile_w: 8, tile_h: 8, preview: None, }, } } } /// Renders the floating font-picker dialog. /// /// Lets the user choose a PNG file, set tile dimensions, preview the tilesheet /// with grid lines, and apply the selection. When the user clicks "Apply", /// `font_spec` is written and `open` is set to `false`. pub(crate) fn show( ctx: &egui::Context, open: &mut bool, state: &mut FontDialogState, font_spec: &mut Option, ) { let mut should_close = false; egui::Window::new("Font") .collapsible(false) .resizable(true) .open(open) .show(ctx, |ui| { // --- File chooser --- ui.horizontal(|ui| { ui.label("File:"); ui.text_edit_singleline(&mut state.path); if ui.button("Browse…").clicked() { if let Some(path) = rfd::FileDialog::new() .add_filter("PNG image", &["png"]) .pick_file() { state.path = path.to_string_lossy().into_owned(); state.preview = None; // reload on next reload click } } }); // --- Tile dimensions --- ui.horizontal(|ui| { ui.label("Tile W:"); if ui .add(DragValue::new(&mut state.tile_w).range(1..=64)) .changed() { state.preview = None; } ui.label("Tile H:"); if ui .add(DragValue::new(&mut state.tile_h).range(1..=64)) .changed() { state.preview = None; } if ui.button("Load").clicked() { state.preview = None; if !state.path.is_empty() { match BitmapFont::load( ctx, std::path::Path::new(&state.path), state.tile_w, state.tile_h, ) { Ok(f) => state.preview = Some(f), Err(e) => eprintln!("font load error: {e}"), } } } }); ui.separator(); // --- Preview --- if let Some(font) = &state.preview { let tw = font.tile_w as f32; let th = font.tile_h as f32; let preview_size = vec2(font.img_w() as f32, font.img_h() as f32); egui::ScrollArea::both() .max_width(400.0) .max_height(200.0) .id_salt("font_preview") .show(ui, |ui| { let (rect, _) = ui.allocate_exact_size(preview_size, egui::Sense::hover()); // Draw the full font texture. ui.painter().image( font.texture.id(), rect, Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), Color32::WHITE, ); // Overlay grid lines at tile boundaries. let p = ui.painter(); let cols = font.tile_cols(); let rows = font.img_h() / font.tile_h; let stroke = Stroke::new(0.5, Color32::from_rgba_unmultiplied(255, 255, 0, 120)); for c in 0..=cols { let x = rect.min.x + c as f32 * tw; p.line_segment( [egui::pos2(x, rect.min.y), egui::pos2(x, rect.max.y)], stroke, ); } for r in 0..=rows { let y = rect.min.y + r as f32 * th; p.line_segment( [egui::pos2(rect.min.x, y), egui::pos2(rect.max.x, y)], stroke, ); } }); } else { ui.label("(no preview — click Load after setting path and tile size)"); } ui.separator(); // --- Apply / Clear --- ui.horizontal(|ui| { let can_apply = state.preview.is_some(); if ui .add_enabled(can_apply, egui::Button::new("Apply")) .clicked() { *font_spec = Some(FontSpec { path: state.path.clone(), tile_w: state.tile_w, tile_h: state.tile_h, }); should_close = true; } if ui.button("Use default").clicked() { *font_spec = None; should_close = true; } }); }); if should_close { *open = false; } }