181 lines
6.4 KiB
Rust
181 lines
6.4 KiB
Rust
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<BitmapFont>,
|
||
}
|
||
|
||
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<FontSpec>,
|
||
) {
|
||
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;
|
||
}
|
||
}
|