From 49297342128acc6fa0d95fd0c80cea331523ceda Mon Sep 17 00:00:00 2001 From: Ross Andrews Date: Mon, 15 Jun 2026 23:36:52 -0500 Subject: [PATCH] rm egui --- kiln-egui/Cargo.toml | 17 -- kiln-egui/assets/vga-font-8x16.png | Bin 1846 -> 0 bytes kiln-egui/src/editor.rs | 342 -------------------------- kiln-egui/src/font.rs | 274 --------------------- kiln-egui/src/font_dialog.rs | 180 -------------- kiln-egui/src/glyph_picker.rs | 136 ----------- kiln-egui/src/main.rs | 370 ----------------------------- kiln-egui/src/render.rs | 240 ------------------- kiln-egui/src/script_editor.rs | 62 ----- 9 files changed, 1621 deletions(-) delete mode 100644 kiln-egui/Cargo.toml delete mode 100644 kiln-egui/assets/vga-font-8x16.png delete mode 100644 kiln-egui/src/editor.rs delete mode 100644 kiln-egui/src/font.rs delete mode 100644 kiln-egui/src/font_dialog.rs delete mode 100644 kiln-egui/src/glyph_picker.rs delete mode 100644 kiln-egui/src/main.rs delete mode 100644 kiln-egui/src/render.rs delete mode 100644 kiln-egui/src/script_editor.rs diff --git a/kiln-egui/Cargo.toml b/kiln-egui/Cargo.toml deleted file mode 100644 index 921e11c..0000000 --- a/kiln-egui/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "kiln-egui" -version = "0.1.0" -edition = "2024" - -[[bin]] -name = "kiln" -path = "src/main.rs" - -[dependencies] -kiln-core = { path = "../kiln-core" } -eframe = "0.33.3" -color = "0.3.3" -image = { version = "0.25", default-features = false, features = ["png"] } -rfd = "0.15" -serde = { version = "1", features = ["derive"] } -egui_code_editor = "=0.2.20" diff --git a/kiln-egui/assets/vga-font-8x16.png b/kiln-egui/assets/vga-font-8x16.png deleted file mode 100644 index 336b7b4d064885d508741ebc1f05be2989daf306..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1846 zcmV-62g&$}P)DM!yhWvfWR~sMok2sLU zagYX?OtQ)GTUVq%x_Soql~RL+58xPLR-&;~ zC=^C07eGpdCIYx@a%Ts0;$F zlW#{+-pvm`3S8Ij*d4p#x~^+QdGc9=ow+pnn~8~0yvMUsnUUh9;-zA;y55>?mJr1( z7sF2HVq#JQzylVkQD6)~i3mlB00sI1oV$R=RYric^anu$6V%pPKZ6V|1KM(0WgZZi z|AHVGt^))C8w8&wb|iH`PzTy=A8IXV17Qd-j==^XA`o3RLXmJ&gT+k1yL=6Z4fs$= zCsij6-F-HK5^351w%f_KqhQR<&pe7;*ADDQcHY(|-F%c6`GeS1cMtc+V)x9!MdQ1P z@q+I!t}v2yPGGVwm36Z_rzOpRoSFtGqcTZZD&;-tFQxPn@}S+jVD^Zv31*~i*qv$B z98mbuv%P80ZZs^{bt8a`;6|%x6zAW5X}zs*{Da~LZ#%^-u*fGD{3m1uQ(;&(rwBl0 z4Ipp;t-&w;EMKA|I18-ov>UGV{M*YztJ{9M(-#3KD9O?;;Euv-CYuykQ?N8xU8>IC zp4FMv1FD0!=c|vFyvwD(n@_m#+=ALI$&`EfkZ9dT)b??t>>kcQl3&4acc#5>$+r`8 zZ#fd6YLrbh%E+>tm7q~J%DApHX%C2eL(MdrSu$?Z(wk!qgEum6^UbC^Hb&O^^kp%c zmD#M{AUozeqrO&O;K$i9|C~Q&yLua*^}^6SBBmM9NV=|TmGf7da6Z!@Ra3(=z&Q(k34g^IyjBN=5FCcz`db>zDPW_QuqW<(^4v$K?4$)NX8|YK zi;45*bFxG0G2+8yu$naWI;CyEDh`O%Bq~isNuskOM&4B*-CR;aAgqE8aC53$g^>OT z1_`%U>?;-B`LzSz60luIrVjZGGlzaTP^O=EUpfDolrjgo9J!n%En6zS<#y^&qX4g+ zcXq86Qk8OAH?jtRgon4R-2g)xhUF%G!F~$X{UrA!irQ^)79fB!7&-k$?%^$qk&%KqQlKD2|44E++n|6w!Gpx!Ykv~ehR~UF$@nt?lkcu4q$R8U?t>$Sg=R-YCuXP zQs#2x$dQvd$VtdaNlb7o{{Bza{}`Z;)Tw)MD!#W#41FhIb2ng%t*?%zwo*q^q+n|c zhI)X$&A2b#>`NW%i=XzzPvei{_-J3>sgF0;PZC2)e4ZNGN)2tr_u|y4ICUzO8hZWS z=UY!-AL`%wAG`T~1sp6pE{#Jqm~d=u3IsDzZ)zA$05AY#;n|G|5S*(M0TnZQoFhN5 z!BAiBskxAti0W9=PPXK8#3)A6S3xGXhG8S72`@VpEWJdlG6RB^d, - /// Whether the glyph picker is open for editing the selected object's glyph. - pub(crate) picker_open: bool, - /// Local copy of the selected object's glyph for the picker to edit. - /// - /// Kept separate from `board.objects` to avoid a borrow conflict while the - /// glyph picker holds `&mut Glyph` and the board is also borrowed. Written - /// back to `board.objects[i].glyph` each frame while an object is selected. - pub(crate) editing_glyph: Glyph, - /// When `true`, the next board click in the Objects tab places a new object. - /// Set by the "Add Object" button; cleared after placement or on tab switch. - pub(crate) placing: bool, -} - -/// Scripts-tab state: the script open in the code editor and the new-script draft. -pub(crate) struct ScriptEdit { - /// Name of the script currently open in the code editor, or `None` if closed. - pub(crate) editing: Option, - /// Working copy of the script body while the code editor is open. - /// Written back to `board.scripts` when the user clicks "Back". - pub(crate) content: String, - /// Draft name typed into the "new script" text field in the Scripts tab. - pub(crate) new_name: String, -} - -/// Transient editor state. Only meaningful when the app is in Edit mode. -/// -/// Fields are grouped by concern into sub-structs so each editor tab can take -/// the narrow borrow it needs rather than all of `EditorState`. -pub(crate) struct EditorState { - /// Which side-panel tab is currently shown. - pub(crate) tab: EditorTab, - /// Palette tab: archetype + glyph being painted. - pub(crate) palette: PaletteState, - /// The font picker dialog and its edit buffer. - pub(crate) font_dialog: FontDialog, - /// Objects tab: selection, placement, and object glyph editing. - pub(crate) objects: ObjectEdit, - /// Scripts tab: the open script and new-script draft. - pub(crate) scripts: ScriptEdit, -} - -impl EditorState { - /// Creates editor state with `Wall` selected and all dialogs closed. - pub(crate) fn new() -> Self { - Self { - tab: EditorTab::Palette, - palette: PaletteState { - selected: Archetype::Wall, - glyph: Archetype::Wall.default_glyph(), - picker_open: false, - }, - font_dialog: FontDialog { - open: false, - state: FontDialogState::from_spec(None), - }, - objects: ObjectEdit { - selected: None, - picker_open: false, - editing_glyph: Glyph::player(), // overwritten on first selection - placing: false, - }, - scripts: ScriptEdit { - editing: None, - content: String::new(), - new_name: String::new(), - }, - } - } -} - -/// Renders the editor side panel (must be called before `CentralPanel`). -/// -/// Shows tab bar (Palette / Objects / Board / Scripts / World) and the content -/// for the active tab. `board` is the current board, used by the Objects and -/// Scripts tabs and to read/write the board font. `active_font` is the resolved -/// font currently in use for rendering. -pub(crate) fn show_editor_panel( - ctx: &egui::Context, - editor: &mut EditorState, - board: &mut Board, - active_font: &BitmapFont, -) { - egui::SidePanel::right("palette_panel") - .resizable(true) - .default_width(PALETTE_PANEL_W) - .show(ctx, |ui| { - // Tab bar — wraps automatically if the panel is narrow. - ui.horizontal_wrapped(|ui| { - ui.selectable_value(&mut editor.tab, EditorTab::Palette, "Palette"); - ui.selectable_value(&mut editor.tab, EditorTab::Objects, "Objects"); - ui.selectable_value(&mut editor.tab, EditorTab::Board, "Board"); - ui.selectable_value(&mut editor.tab, EditorTab::Scripts, "Scripts"); - ui.selectable_value(&mut editor.tab, EditorTab::World, "World"); - }); - ui.separator(); - - // Dispatch to the active tab's renderer, passing only the sub-struct each - // tab needs. World is an empty placeholder. - match editor.tab { - EditorTab::Palette => palette_tab(ui, &mut editor.palette, active_font), - EditorTab::Objects => objects_tab( - ui, - &mut editor.objects, - &mut editor.scripts, - board, - active_font, - ), - EditorTab::Board => board_tab(ui, &mut editor.font_dialog, board), - EditorTab::Scripts => scripts_tab(ui, &mut editor.scripts, board), - 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, - &mut board.font, - ); - } -} - -/// Palette tab: scrollable archetype list plus the current-glyph preview button. -fn palette_tab(ui: &mut egui::Ui, palette: &mut PaletteState, active_font: &BitmapFont) { - let tw = active_font.tile_w as f32; - let th = active_font.tile_h as f32; - - ui.label("Element"); - - // Scrollable archetype list; shows at most 5 rows before scrolling. - let row_h = th + ui.spacing().item_spacing.y; - egui::ScrollArea::vertical() - .max_height(row_h * 5.0) - .id_salt("arch_list") - .show(ui, |ui| { - for &archetype in ALL_ARCHETYPES { - let is_selected = palette.selected == archetype; - let glyph = archetype.default_glyph(); - ui.horizontal(|ui| { - 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() { - palette.selected = archetype; - palette.glyph = archetype.default_glyph(); - } - }); - } - }); - - ui.separator(); - - ui.label("Glyph"); - // Preview swatch for the current glyph; clicking opens the glyph picker. - if glyph_preview_button(ui, &palette.glyph, active_font).clicked() { - palette.picker_open = true; - } -} - -/// Objects tab: add/select objects and edit the selected object's glyph, flags, and script. -fn objects_tab( - ui: &mut egui::Ui, - objects: &mut ObjectEdit, - scripts: &mut ScriptEdit, - board: &mut Board, - active_font: &BitmapFont, -) { - // "Add Object" toggles placement mode; next board click places a new object. - let add_label = if objects.placing { - "Cancel Add" - } else { - "Add Object" - }; - if ui.button(add_label).clicked() { - objects.placing = !objects.placing; - objects.selected = None; - } - if objects.placing { - ui.label("Click on the board to place."); - } - - ui.separator(); - - let Some(i) = objects.selected else { - if !objects.placing { - ui.label("Click an object on the board to select it."); - } - return; - }; - - // Collect sorted script names before taking a mutable borrow on board.objects. - let mut script_names: Vec = board.scripts.keys().cloned().collect(); - script_names.sort(); - - ui.label("Glyph"); - // Preview swatch for the object's glyph; clicking opens the glyph picker. - if glyph_preview_button(ui, &objects.editing_glyph, active_font).clicked() { - objects.picker_open = true; - } - - let obj = &mut board.objects[i]; - - ui.checkbox(&mut obj.passable, "Passable"); - ui.checkbox(&mut obj.opaque, "Opaque"); - - let current = obj.script_name.as_deref().unwrap_or("(none)").to_string(); - - ui.label("Script"); - egui::ComboBox::from_id_salt("obj_script") - .selected_text(¤t) - .show_ui(ui, |ui| { - // "(none)" clears the script assignment. - if ui - .selectable_label(obj.script_name.is_none(), "(none)") - .clicked() - { - obj.script_name = None; - } - for name in &script_names { - let is_sel = obj.script_name.as_deref() == Some(name.as_str()); - if ui.selectable_label(is_sel, name.as_str()).clicked() { - obj.script_name = Some(name.clone()); - } - } - // Placeholder for future "create and edit a new script" flow. - let _ = ui.selectable_label(false, "(create new...)"); - }); - - if ui.button("Edit Script").clicked() - && let Some(ref script_name) = obj.script_name.clone() - { - scripts.content = board.scripts.get(script_name).cloned().unwrap_or_default(); - scripts.editing = Some(script_name.clone()); - } -} - -/// Board tab: board-level font selection and zoom. -fn board_tab(ui: &mut egui::Ui, font_dialog: &mut FontDialog, board: &mut Board) { - let font_label = match &board.font { - Some(s) => s.path.clone(), - None => "(default)".to_owned(), - }; - ui.label(format!("Font: {font_label}")); - if ui.button("Font…").clicked() { - font_dialog.state = FontDialogState::from_spec(board.font.as_ref()); - font_dialog.open = true; - } - - ui.separator(); - ui.label("Zoom"); - ui.add(egui::Slider::new(&mut board.zoom, 1..=8).text("×")); -} - -/// Scripts tab: create a named script and edit existing ones. -fn scripts_tab(ui: &mut egui::Ui, scripts: &mut ScriptEdit, board: &mut Board) { - // New script: text field + Create button. - ui.horizontal(|ui| { - ui.text_edit_singleline(&mut scripts.new_name); - let can_create = - !scripts.new_name.is_empty() && !board.scripts.contains_key(&scripts.new_name); - if ui - .add_enabled(can_create, egui::Button::new("Create")) - .clicked() - { - let name = scripts.new_name.clone(); - board.scripts.insert(name.clone(), String::new()); - scripts.new_name.clear(); - scripts.content = String::new(); - scripts.editing = Some(name); - } - }); - - ui.separator(); - - if board.scripts.is_empty() { - ui.label("No scripts yet."); - return; - } - - let mut names: Vec = board.scripts.keys().cloned().collect(); - names.sort(); - egui::ScrollArea::vertical() - .id_salt("scripts_list") - .show(ui, |ui| { - for name in &names { - ui.horizontal(|ui| { - ui.label(name); - if ui.button("Edit").clicked() { - scripts.content = board.scripts.get(name).cloned().unwrap_or_default(); - scripts.editing = Some(name.clone()); - } - }); - } - }); -} diff --git a/kiln-egui/src/font.rs b/kiln-egui/src/font.rs deleted file mode 100644 index 35cc607..0000000 --- a/kiln-egui/src/font.rs +++ /dev/null @@ -1,274 +0,0 @@ -use eframe::egui; -use egui::{ColorImage, Rect, TextureHandle, TextureOptions, Vec2, vec2}; -use image::GenericImageView; -use std::path::Path; - -/// Computes a UV rectangle for `tile` within an image of known dimensions. -/// -/// This is the core tile-index-to-UV mapping; pulled out so tests can verify -/// it without needing an egui [`Context`] to construct a [`BitmapFont`]. -fn compute_tile_uv(tile: u32, tile_w: u32, tile_h: u32, img_w: u32, img_h: u32) -> Rect { - let tiles_per_row = (img_w / tile_w).max(1); - let col = tile % tiles_per_row; - let row = tile / tiles_per_row; - let u0 = col as f32 * tile_w as f32 / img_w as f32; - let v0 = row as f32 * tile_h as f32 / img_h as f32; - let u1 = u0 + tile_w as f32 / img_w as f32; - let v1 = v0 + tile_h as f32 / img_h as f32; - Rect::from_min_max(egui::pos2(u0, v0), egui::pos2(u1, v1)) -} - -/// A loaded bitmap font ready for rendering. -/// -/// Wraps an egui texture that has been pre-processed to a two-value RGBA -/// image: "background" pixels (matching the top-left pixel of the source -/// image) become fully transparent, and all other pixels become opaque white. -/// -/// At render time, each tile is drawn by: -/// 1. Filling the cell rect with the glyph's `bg` color. -/// 2. Calling `painter.image(…, tile_uv(tile), fg_color)` — egui tints the -/// opaque-white foreground pixels to `fg` and the transparent pixels stay -/// transparent, revealing the `bg` fill underneath. -pub struct BitmapFont { - /// The uploaded egui texture (preprocessed to opaque-white / transparent). - pub texture: TextureHandle, - /// Width of each tile in pixels. - pub tile_w: u32, - /// Height of each tile in pixels. - pub tile_h: u32, - /// Width of the full font image in pixels. - img_w: u32, - /// Height of the full font image in pixels. - img_h: u32, -} - -impl BitmapFont { - /// Loads a bitmap font from raw PNG bytes (e.g. via `include_bytes!`). - /// - /// `label` is passed to egui as the texture name for debugging. - /// Returns an error if the image cannot be decoded. - #[allow(dead_code)] - pub fn from_bytes( - ctx: &egui::Context, - bytes: &[u8], - tile_w: u32, - tile_h: u32, - label: &str, - ) -> Result> { - let img = image::load_from_memory(bytes)?; - Self::from_image(ctx, img, tile_w, tile_h, label) - } - - /// Loads a bitmap font from a PNG file on disk. - pub fn load( - ctx: &egui::Context, - path: &Path, - tile_w: u32, - tile_h: u32, - ) -> Result> { - let img = image::open(path)?; - let label = path.to_string_lossy(); - Self::from_image(ctx, img, tile_w, tile_h, &label) - } - - fn from_image( - ctx: &egui::Context, - img: image::DynamicImage, - tile_w: u32, - tile_h: u32, - label: &str, - ) -> Result> { - let img_w = img.width(); - let img_h = img.height(); - - // The top-left pixel defines the "background" color to treat as transparent. - let bg_pixel = img.get_pixel(0, 0); - let [br, bg, bb, _] = bg_pixel.0; - - // Convert to RGBA: bg-colored pixels → transparent, all others → opaque white. - let rgba = img.to_rgba8(); - let mut pixels: Vec = Vec::with_capacity((img_w * img_h) as usize); - for pixel in rgba.pixels() { - let [r, g, b, _] = pixel.0; - if r == br && g == bg && b == bb { - pixels.push(egui::Color32::TRANSPARENT); - } else { - pixels.push(egui::Color32::WHITE); - } - } - - let color_image = ColorImage { - size: [img_w as usize, img_h as usize], - pixels, - source_size: vec2(img_w as f32, img_h as f32), - }; - - let texture = ctx.load_texture(label, color_image, TextureOptions::NEAREST); - - Ok(Self { - texture, - tile_w, - tile_h, - img_w, - img_h, - }) - } - - /// Returns the UV rectangle (in 0.0–1.0 coordinates) for `tile` in the font image. - /// - /// Tiles are numbered left-to-right, top-to-bottom. For a 16-column font, - /// tile 0 is top-left, tile 15 is top-right, tile 16 is the first tile of - /// row 2, etc. - pub fn tile_uv(&self, tile: u32) -> Rect { - compute_tile_uv(tile, self.tile_w, self.tile_h, self.img_w, self.img_h) - } - - /// Returns the pixel size of one rendered cell at the given integer `zoom`. - /// - /// A cell is `tile_w × tile_h` scaled by `zoom`. Centralizes the - /// `tile_w as f32 * zoom as f32` math used throughout rendering and hit-testing. - pub fn cell_size(&self, zoom: u32) -> Vec2 { - vec2( - self.tile_w as f32 * zoom as f32, - self.tile_h as f32 * zoom as f32, - ) - } - - /// Returns the number of tile columns in the font image. - pub fn tile_cols(&self) -> u32 { - (self.img_w / self.tile_w).max(1) - } - - /// Returns the total number of tiles in this font image. - pub fn tile_count(&self) -> u32 { - let rows = (self.img_h / self.tile_h).max(1); - self.tile_cols() * rows - } - - /// Width of the full font image in pixels. - pub fn img_w(&self) -> u32 { - self.img_w - } - - /// Height of the full font image in pixels. - pub fn img_h(&self) -> u32 { - self.img_h - } - - /// Creates a procedural placeholder font (16×16 grid of 8×8 tiles). - /// - /// Each tile renders the 8 bits of its index as a vertical bar pattern, - /// making tiles visually distinct. Used as a fallback when no font file - /// is present. - pub fn create_placeholder(ctx: &egui::Context) -> Self { - let tile_w: u32 = 8; - let tile_h: u32 = 8; - let cols: u32 = 16; - let rows: u32 = 16; - let img_w = cols * tile_w; - let img_h = rows * tile_h; - - let mut pixels = vec![egui::Color32::TRANSPARENT; (img_w * img_h) as usize]; - - for tile in 0..256u32 { - let tc = tile % cols; - let tr = tile / cols; - // Render the 8 bits of the tile index as a column bar pattern (rows 1–6). - let byte = (tile & 0xFF) as u8; - for py in 1u32..7 { - for px in 0u32..8 { - let bit = (byte >> (7 - px)) & 1; - if bit == 1 { - let x = tc * tile_w + px; - let y = tr * tile_h + py; - pixels[(y * img_w + x) as usize] = egui::Color32::WHITE; - } - } - } - } - - let color_image = ColorImage { - size: [img_w as usize, img_h as usize], - pixels, - source_size: vec2(img_w as f32, img_h as f32), - }; - let texture = ctx.load_texture( - "default_font_placeholder", - color_image, - TextureOptions::NEAREST, - ); - Self { - texture, - tile_w, - tile_h, - img_w, - img_h, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn approx_eq(a: f32, b: f32) -> bool { - (a - b).abs() < 1e-5 - } - - fn rect_approx_eq(a: Rect, b: Rect) -> bool { - approx_eq(a.min.x, b.min.x) - && approx_eq(a.min.y, b.min.y) - && approx_eq(a.max.x, b.max.x) - && approx_eq(a.max.y, b.max.y) - } - - #[test] - fn tile_zero_is_top_left() { - let uv = compute_tile_uv(0, 8, 8, 128, 128); - assert!(rect_approx_eq( - uv, - Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0 / 16.0, 1.0 / 16.0)) - )); - } - - #[test] - fn tile_at_end_of_first_row() { - // 16 cols, so tile 15 is top-right. - let uv = compute_tile_uv(15, 8, 8, 128, 128); - assert!(approx_eq(uv.min.x, 15.0 / 16.0)); - assert!(approx_eq(uv.min.y, 0.0)); - } - - #[test] - fn tile_at_start_of_second_row() { - // Tile 16 should be col 0, row 1. - let uv = compute_tile_uv(16, 8, 8, 128, 128); - assert!(approx_eq(uv.min.x, 0.0)); - assert!(approx_eq(uv.min.y, 1.0 / 16.0)); - } - - #[test] - fn tile_64_is_at_position() { - // '@' = ASCII 64; in a 16-col font: col = 64%16 = 0, row = 64/16 = 4. - let uv = compute_tile_uv(64, 8, 8, 128, 128); - assert!(approx_eq(uv.min.x, 0.0)); - assert!(approx_eq(uv.min.y, 4.0 / 16.0)); - } - - #[test] - fn non_square_font_layout() { - // 32 columns, 8 rows = 256 tiles; 6×8 tiles; image 192×64. - // tile 32 = col 0, row 1. - let uv = compute_tile_uv(32, 6, 8, 192, 64); - assert!(approx_eq(uv.min.x, 0.0)); - assert!(approx_eq(uv.min.y, 1.0 / 8.0)); - } - - #[test] - fn uv_width_matches_tile_fraction() { - let uv = compute_tile_uv(0, 8, 8, 128, 128); - // Each tile is 1/16 of the image width. - assert!(approx_eq(uv.width(), 1.0 / 16.0)); - assert!(approx_eq(uv.height(), 1.0 / 16.0)); - } -} diff --git a/kiln-egui/src/font_dialog.rs b/kiln-egui/src/font_dialog.rs deleted file mode 100644 index be335b5..0000000 --- a/kiln-egui/src/font_dialog.rs +++ /dev/null @@ -1,180 +0,0 @@ -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; - } -} diff --git a/kiln-egui/src/glyph_picker.rs b/kiln-egui/src/glyph_picker.rs deleted file mode 100644 index 93e2700..0000000 --- a/kiln-egui/src/glyph_picker.rs +++ /dev/null @@ -1,136 +0,0 @@ -use std::collections::HashSet; - -use eframe::egui; -use egui::{Color32, Rect, Sense, Stroke, vec2}; - -use crate::font::BitmapFont; -use crate::render::{color32_to_rgba8, paint_glyph, pos_to_cell, rgba8_to_color32}; -use kiln_core::game::{Archetype, Glyph}; - -/// Renders the floating glyph-picker dialog and updates `glyph` in place. -/// -/// Shows three sections: board palette (unique glyphs on the current board), -/// FG/BG color pickers, and a scrollable grid of all tiles in the active font. -/// Closes when the window's X button is clicked, writing `false` to `open`. -pub(crate) fn show( - ctx: &egui::Context, - open: &mut bool, - glyph: &mut Glyph, - board_cells: &[(Glyph, Archetype)], - font: &BitmapFont, -) { - // Collect unique glyphs from the board before the window closure borrows - // `glyph` mutably, to avoid a borrow conflict with `board_cells`. - let board_glyphs: Vec = { - let mut seen: HashSet = HashSet::new(); - board_cells - .iter() - .map(|(g, _)| *g) - .filter(|g| seen.insert(*g)) - .collect() - }; - - let tw = font.tile_w as f32; - let th = font.tile_h as f32; - - egui::Window::new("Glyph") - .collapsible(false) - .resizable(false) - .open(open) - .show(ctx, |ui| { - ui.label("Board palette"); - ui.horizontal_wrapped(|ui| { - for &g in &board_glyphs { - let (r, resp) = ui.allocate_exact_size(vec2(tw, th), Sense::click()); - let p = ui.painter(); - paint_glyph(p, r, &g, font); - if g == *glyph { - p.rect_stroke( - r, - 0.0, - Stroke::new(1.0, Color32::WHITE), - egui::epaint::StrokeKind::Middle, - ); - } - if resp.clicked() { - *glyph = g; - } - } - }); - - ui.separator(); - - ui.label("Colors"); - ui.horizontal(|ui| { - // The color picker works with Color32; convert to/from Rgba8 around the call. - let mut fg = rgba8_to_color32(glyph.fg); - ui.label("FG"); - egui::color_picker::color_edit_button_srgba( - ui, - &mut fg, - egui::color_picker::Alpha::Opaque, - ); - glyph.fg = color32_to_rgba8(fg); - - let mut bg = rgba8_to_color32(glyph.bg); - ui.label("BG"); - egui::color_picker::color_edit_button_srgba( - ui, - &mut bg, - egui::color_picker::Alpha::Opaque, - ); - glyph.bg = color32_to_rgba8(bg); - }); - - ui.separator(); - - // Full tile grid: column count matches the font image layout so the - // picker visually mirrors the tilesheet. - ui.label("Tile"); - let tile_cols = font.tile_cols() as usize; - let tile_count = font.tile_count() as usize; - let rows = tile_count.div_ceil(tile_cols); - let grid_size = vec2(tile_cols as f32 * tw, rows as f32 * th); - - egui::ScrollArea::vertical() - .max_height(th * 16.0) - .id_salt("tile_grid") - .show(ui, |ui| { - let (grid_rect, grid_resp) = ui.allocate_exact_size(grid_size, Sense::click()); - { - let p = ui.painter(); - for i in 0..tile_count { - let col = i % tile_cols; - let row = i / tile_cols; - let tl = grid_rect.min + vec2(col as f32 * tw, row as f32 * th); - let cell = Rect::from_min_size(tl, vec2(tw, th)); - let tile_glyph = Glyph { - tile: i as u32, - fg: glyph.fg, - bg: glyph.bg, - }; - paint_glyph(p, cell, &tile_glyph, font); - if i as u32 == glyph.tile { - p.rect_stroke( - cell, - 0.0, - Stroke::new(1.0, Color32::WHITE), - egui::epaint::StrokeKind::Middle, - ); - } - } - } - if grid_resp.clicked() - && let Some(pos) = grid_resp.interact_pointer_pos() - { - let (col, row) = pos_to_cell(grid_rect.min, pos, tw, th); - if col >= 0 && row >= 0 { - let idx = row as usize * tile_cols + col as usize; - if idx < tile_count { - glyph.tile = idx as u32; - } - } - } - }); - }); -} diff --git a/kiln-egui/src/main.rs b/kiln-egui/src/main.rs deleted file mode 100644 index 09a99ad..0000000 --- a/kiln-egui/src/main.rs +++ /dev/null @@ -1,370 +0,0 @@ -use eframe::egui; -use egui::{Key, Sense, vec2}; -use std::path::{Path, PathBuf}; - -mod editor; -mod font; -mod font_dialog; -mod glyph_picker; -mod render; -mod script_editor; - -use editor::{EditorState, EditorTab, show_editor_panel}; -use font::BitmapFont; -use kiln_core::game::{FontSpec, GameState, ObjectDef}; -use kiln_core::map_file; -use render::{ - DEFAULT_WINDOW_H, DEFAULT_WINDOW_W, MIN_WINDOW_H, MIN_WINDOW_W, board_origin, draw_board, - draw_object_overlays, pos_to_cell, -}; - -/// Whether the application is in play mode or edit mode. -/// -/// In `Play` mode the game runs normally: arrow keys move the player. -/// In `Edit` mode arrow keys are inactive, a side panel shows the archetype -/// palette, and clicking on the viewport paints cells. -#[derive(Copy, Clone, PartialEq, Eq)] -enum AppMode { - /// Normal game play. Arrow keys control the player. - Play, - /// Board editing. Side panel shows archetypes; click to paint cells. - Edit, -} - -fn main() -> eframe::Result<()> { - let start_path = PathBuf::from("maps/start.toml"); - let board = - map_file::load(start_path.to_str().unwrap()).expect("failed to load maps/start.toml"); - - let options = eframe::NativeOptions { - viewport: egui::ViewportBuilder::default() - .with_inner_size([DEFAULT_WINDOW_W, DEFAULT_WINDOW_H]) - .with_min_inner_size([MIN_WINDOW_W, MIN_WINDOW_H]), - ..Default::default() - }; - eframe::run_native( - "Kiln", - options, - Box::new(move |cc| Ok(Box::new(App::new(board, &cc.egui_ctx, start_path)))), - ) -} - -/// The eframe application. Owns the game state and drives the frame loop. -/// -/// eframe calls [`App::update`] every frame. All input handling, game logic -/// updates, and rendering happen there. There is no separate game loop thread. -struct App { - state: GameState, - mode: AppMode, - editor: EditorState, - /// The default font, embedded in the binary and loaded at startup. - default_font: BitmapFont, - /// A per-board font loaded from `board.font`, if one is specified. - board_font: Option, - /// Path of the file currently open. `None` until the user saves for the first time. - /// Used by Save to write without prompting; Save As always prompts and updates this. - current_file_path: Option, -} - -impl App { - /// Creates the app with the given board, starting in Play mode. - /// - /// `egui_ctx` is needed to upload font textures before the first frame. - /// `initial_path` is the file the board was loaded from and becomes the - /// default Save target. - fn new(board: kiln_core::game::Board, egui_ctx: &egui::Context, initial_path: PathBuf) -> Self { - // Default font is embedded in the binary; no runtime file I/O needed. - let font_bytes = include_bytes!("../assets/vga-font-8x16.png"); - let default_font = BitmapFont::from_bytes(egui_ctx, font_bytes, 8, 16, "vga-font-8x16") - .unwrap_or_else(|_| BitmapFont::create_placeholder(egui_ctx)); - - // If the board specifies its own font, try to load it now. - let board_font = board.font.as_ref().and_then(|spec| { - BitmapFont::load(egui_ctx, Path::new(&spec.path), spec.tile_w, spec.tile_h) - .map_err(|e| eprintln!("board font load error: {e}")) - .ok() - }); - - Self { - state: GameState::new(board), - mode: AppMode::Play, - editor: EditorState::new(), - default_font, - board_font, - current_file_path: Some(initial_path), - } - } - - /// Reloads `board_font` from `font_spec`, or clears it if `font_spec` is `None`. - /// - /// Called after the font dialog applies a change. - fn apply_font_spec(&mut self, egui_ctx: &egui::Context, font_spec: Option<&FontSpec>) { - self.board_font = font_spec.and_then(|spec| { - BitmapFont::load(egui_ctx, Path::new(&spec.path), spec.tile_w, spec.tile_h) - .map_err(|e| eprintln!("font apply error: {e}")) - .ok() - }); - } - - /// The font currently used for rendering: the per-board font if loaded, else the default. - fn active_font(&self) -> &BitmapFont { - self.board_font.as_ref().unwrap_or(&self.default_font) - } - - /// Writes the board to `path`, logging any error. Does not change `current_file_path`. - fn save_to(&self, path: &Path) { - if let Err(e) = map_file::save(&self.state.board, path) { - eprintln!("save error: {e}"); - } - } - - /// Prompts for a destination and saves there, recording it as the current file on success. - fn save_as(&mut self) { - if let Some(path) = rfd::FileDialog::new() - .add_filter("Map file", &["toml"]) - .save_file() - { - if let Err(e) = map_file::save(&self.state.board, &path) { - eprintln!("save error: {e}"); - } else { - self.current_file_path = Some(path); - } - } - } - - /// Play-mode input: arrow keys move the player one cell. - fn handle_input(&mut self, ctx: &egui::Context) { - if self.mode != AppMode::Play { - return; - } - // (key, dx, dy) movement table — iterated instead of four near-identical ifs. - const MOVES: [(Key, i32, i32); 4] = [ - (Key::ArrowUp, 0, -1), - (Key::ArrowDown, 0, 1), - (Key::ArrowLeft, -1, 0), - (Key::ArrowRight, 1, 0), - ]; - ctx.input(|i| { - for (key, dx, dy) in MOVES { - if i.key_pressed(key) { - self.state.try_move(dx, dy); - } - } - }); - } - - /// Top menu bar: File (Save / Save As / Exit) and the Play/Edit mode toggle. - fn menu_bar(&mut self, ctx: &egui::Context) { - egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { - egui::MenuBar::new().ui(ui, |ui| { - ui.menu_button("File", |ui| { - if ui.button("Save").clicked() { - ui.close(); - // Save to the known path if we have one; otherwise behave like Save As. - match self.current_file_path.clone() { - Some(path) => self.save_to(&path), - None => self.save_as(), - } - } - if ui.button("Save As…").clicked() { - ui.close(); - self.save_as(); - } - ui.separator(); - if ui.button("Exit").clicked() { - ctx.send_viewport_cmd(egui::ViewportCommand::Close); - } - }); - // Mode toggle: Play keeps the game running; Edit shows the palette panel. - ui.separator(); - ui.selectable_value(&mut self.mode, AppMode::Play, "Play"); - ui.selectable_value(&mut self.mode, AppMode::Edit, "Edit"); - }); - }); - } - - /// If a script is open for editing, fills the central panel with the code editor. - /// - /// Returns `true` when it consumed the frame (the caller should early-return so - /// the side panel and board are skipped). On "Back", writes the edited body back - /// to `board.scripts` and closes the editor. - fn try_show_script_editor(&mut self, ctx: &egui::Context) -> bool { - if self.mode != AppMode::Edit { - return false; - } - let Some(name) = self.editor.scripts.editing.clone() else { - return false; - }; - egui::CentralPanel::default().show(ctx, |ui| { - if script_editor::show(ui, &name, &mut self.editor.scripts.content) { - self.state - .board - .scripts - .insert(name, self.editor.scripts.content.clone()); - self.editor.scripts.editing = None; - } - }); - true - } - - /// Draws the board viewport and returns the clicked cell in Edit mode, if any. - /// - /// Play mode centers/player-tracks the viewport and never returns a click. - /// Edit mode wraps the board in a `ScrollArea`, draws the object overlay on the - /// Objects tab, and returns validated `(cx, cy)` cell coordinates on a click so - /// the caller can dispatch to [`App::handle_board_click`] without holding the - /// font borrow. - fn show_board(&self, ctx: &egui::Context) -> Option<(usize, usize)> { - let font = self.active_font(); - let zoom = self.state.board.zoom; - let cell = font.cell_size(zoom); - let board_w = self.state.board.width; - let board_h = self.state.board.height; - - let mut clicked_cell = None; - egui::CentralPanel::default().show(ctx, |ui| { - if self.mode == AppMode::Edit { - // Edit mode: ScrollArea lets the user scroll when the board overflows. - let board_px = vec2(board_w as f32 * cell.x, board_h as f32 * cell.y); - egui::ScrollArea::new([true, true]).show(ui, |ui| { - let (rect, response) = ui.allocate_exact_size(board_px, Sense::click()); - draw_board(ui.painter(), rect.min, &self.state.board, font, zoom); - - // When the Objects tab is active, overlay the object highlights. - if self.editor.tab == EditorTab::Objects { - draw_object_overlays( - ui.painter(), - rect.min, - &self.state.board, - font, - self.editor.objects.selected, - zoom, - ); - } - - if response.clicked() - && let Some(pos) = response.interact_pointer_pos() - { - let (cx, cy) = pos_to_cell(rect.min, pos, cell.x, cell.y); - if cx >= 0 && cy >= 0 { - let (cx, cy) = (cx as usize, cy as usize); - if cx < board_w && cy < board_h { - clicked_cell = Some((cx, cy)); - } - } - } - }); - } else { - // Play mode: player-centered viewport, no scroll bars. - let available = ui.available_rect_before_wrap(); - let player = self.state.board.player; - let origin = board_origin(available, board_w, board_h, player, font, zoom); - draw_board(ui.painter(), origin, &self.state.board, font, zoom); - } - }); - clicked_cell - } - - /// Applies an Edit-mode board click at validated cell `(cx, cy)`. - /// - /// On the Objects tab the click places a new object (placement mode) or selects - /// an existing one; on any other tab it paints the grid cell (floor only), - /// leaving objects that sit on top untouched. - fn handle_board_click(&mut self, cx: usize, cy: usize) { - let board = &mut self.state.board; - if self.editor.tab != EditorTab::Objects { - *board.get_mut(cx, cy) = (self.editor.palette.glyph, self.editor.palette.selected); - return; - } - if self.editor.objects.placing { - // Placement mode: create a new object here if none exists. - if board.object_index_at(cx, cy).is_none() { - board.objects.push(ObjectDef::new(cx, cy)); - } - self.editor.objects.placing = false; - } else { - // Selection mode: click to select an existing object. - let found = board.object_index_at(cx, cy); - self.editor.objects.selected = found; - self.editor.objects.picker_open = false; - if let Some(i) = found { - // Snapshot the object's glyph for the picker. - self.editor.objects.editing_glyph = board.objects[i].glyph; - } - } - } - - /// Renders the floating glyph pickers (Palette and Objects tabs) on top of the board. - fn show_glyph_pickers(&mut self, ctx: &egui::Context) { - // Disjoint-field borrow: `font` reads board_font/default_font while the pickers - // mutate editor fields and read board cells — all distinct fields, so allowed. - // (`active_font()` can't be used here: it borrows all of `self`.) - let font = self.board_font.as_ref().unwrap_or(&self.default_font); - - if self.editor.palette.picker_open { - glyph_picker::show( - ctx, - &mut self.editor.palette.picker_open, - &mut self.editor.palette.glyph, - self.state.board.cells(), - font, - ); - } - - // Uses objects.editing_glyph as the target to avoid aliasing board.cells. - if self.editor.objects.picker_open && self.editor.tab == EditorTab::Objects { - glyph_picker::show( - ctx, - &mut self.editor.objects.picker_open, - &mut self.editor.objects.editing_glyph, - self.state.board.cells(), - font, - ); - } - } -} - -impl eframe::App for App { - fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - // Each phase is a focused method; this reads as the sequence of frame steps. - self.handle_input(ctx); - self.menu_bar(ctx); - - // When a script is open, the code editor takes over the frame entirely. - if self.try_show_script_editor(ctx) { - return; - } - - // --- Editor side panel (Edit mode) --- - // Must be shown before CentralPanel so egui allocates its space first. - // Snapshot the font spec so we can reload the board font if the panel changed it. - let font_spec_before = self.state.board.font.clone(); - if self.mode == AppMode::Edit { - // Disjoint-field borrow: active_font reads board_font/default_font while - // show_editor_panel takes &mut editor and &mut board (distinct fields). - let active_font = self.board_font.as_ref().unwrap_or(&self.default_font); - show_editor_panel(ctx, &mut self.editor, &mut self.state.board, active_font); - } - if self.state.board.font != font_spec_before { - let spec = self.state.board.font.clone(); - self.apply_font_spec(ctx, spec.as_ref()); - } - - // --- Board viewport --- - // show_board draws and reports the clicked cell; the click is dispatched - // afterward so the &mut handler doesn't conflict with the font borrow. - if let Some((cx, cy)) = self.show_board(ctx) { - self.handle_board_click(cx, cy); - } - - // --- Floating glyph pickers, drawn last so they sit on top --- - self.show_glyph_pickers(ctx); - - // Flush any picker edit of the selected object's glyph back to the board. - if let Some(i) = self.editor.objects.selected - && i < self.state.board.objects.len() - { - self.state.board.objects[i].glyph = self.editor.objects.editing_glyph; - } - } -} diff --git a/kiln-egui/src/render.rs b/kiln-egui/src/render.rs deleted file mode 100644 index 8f95831..0000000 --- a/kiln-egui/src/render.rs +++ /dev/null @@ -1,240 +0,0 @@ -// Layout constants — which value controls which part of the window: -// -// One board cell (size varies with the active font's tile dimensions): -// -// ◄── tile_w ──► -// ╔═════════════╗ ▲ -// ║ @ ║ │ tile_h -// ╚═════════════╝ ▼ -// -// Full window (Edit mode shown; side panel absent in Play mode): -// -// ◄────────────────── DEFAULT_WINDOW_W (840 px) ───────────────────► -// ┌────────────────────────────────────────────────────────────────┐ ▲ -// │ File │ Play Edit │ │ MENU_H (24 px) -// ├────────────────────────────────────────────────┬───────────────┤ ▼ -// │ PANEL_MARGIN (8 px) on all sides │ PALETTE_ │ ▲ -// │ ┌──────────────────────────────────────────┐ │ PANEL_W │ │ -// │ │ │ │ (200 px) │ │ DEFAULT_ -// │ │ board cells (tile_w × tile_h each) │ │ Edit mode │ │ WINDOW_H -// │ │ │ │ only │ │ (524 px) -// │ └──────────────────────────────────────────┘ │ │ │ -// │ │ │ ▼ -// └────────────────────────────────────────────────┴───────────────┘ -// -// MIN_WINDOW_W and MIN_WINDOW_H are derived: 10 cells wide / 5 cells tall plus margins. - -use eframe::egui; -use egui::{Color32, Pos2, Rect, vec2}; - -use crate::font::BitmapFont; -use kiln_core::game::{Board, Glyph, Player}; - -/// Converts a core [`Glyph`] color ([`color::Rgba8`]) to egui [`Color32`]. -pub(crate) fn rgba8_to_color32(c: color::Rgba8) -> Color32 { - Color32::from_rgba_unmultiplied(c.r, c.g, c.b, c.a) -} - -/// Converts an egui [`Color32`] back to a core [`color::Rgba8`]. -/// -/// Inverse of [`rgba8_to_color32`]; used when reading colors out of egui's -/// color-picker widgets back into a [`Glyph`]. -pub(crate) fn color32_to_rgba8(c: Color32) -> color::Rgba8 { - color::Rgba8 { - r: c.r(), - g: c.g(), - b: c.b(), - a: c.a(), - } -} - -/// Approximate height of the egui menu bar in pixels, used for window sizing. -pub(crate) const MENU_H: f32 = 24.0; -/// Default inner margin that egui's CentralPanel adds on all sides, in pixels. -pub(crate) const PANEL_MARGIN: f32 = 8.0; -/// Default width of the editor side panel in pixels. The panel is resizable. -pub(crate) const PALETTE_PANEL_W: f32 = 200.0; -/// Default window width in pixels. Independent of map size. -pub(crate) const DEFAULT_WINDOW_W: f32 = 840.0; -/// Default window height in pixels. Independent of map size. -pub(crate) const DEFAULT_WINDOW_H: f32 = 524.0; -/// Minimum window width: enough room for 10 cells plus margins (using 8 px tile width). -pub(crate) const MIN_WINDOW_W: f32 = 8.0 * 10.0 + 2.0 * PANEL_MARGIN; -/// Minimum window height: enough room for 5 cells plus menu and margins (using 8 px tile height). -pub(crate) const MIN_WINDOW_H: f32 = 8.0 * 5.0 + MENU_H + 2.0 * PANEL_MARGIN; - -/// Paints a glyph into an already-allocated `rect` using a bitmap font. -/// -/// Fills `rect` with `glyph.bg`, then draws the tile from `font` tinted with -/// `glyph.fg`. Unlike [`draw_glyph`], the caller owns the rect and handles -/// interaction — use this when you need the click [`egui::Response`]. -pub(crate) fn paint_glyph(painter: &egui::Painter, rect: Rect, glyph: &Glyph, font: &BitmapFont) { - painter.rect_filled(rect, 0.0, rgba8_to_color32(glyph.bg)); - let uv = font.tile_uv(glyph.tile); - painter.image(font.texture.id(), rect, uv, rgba8_to_color32(glyph.fg)); -} - -/// Allocates a one-cell preview swatch showing `glyph` and returns its click [`Response`]. -/// -/// The cell is sized to the font's unzoomed tile dimensions and painted with -/// `glyph`. Editor tabs use this for the editable glyph swatch; the caller -/// inspects the returned [`Response`] to open a glyph picker on click. -pub(crate) fn glyph_preview_button( - ui: &mut egui::Ui, - glyph: &Glyph, - font: &BitmapFont, -) -> egui::Response { - let (rect, response) = ui.allocate_exact_size(font.cell_size(1), egui::Sense::click()); - paint_glyph(ui.painter(), rect, glyph, font); - response -} - -/// Draws a single cell at grid position `(x, y)` using the given glyph and font. -/// -/// `origin` is the pixel position of cell `(0, 0)` — the top-left corner of -/// the board within the panel. Cell positions are computed from `origin` using -/// the font's tile dimensions multiplied by `zoom`. -pub(crate) fn draw_glyph( - painter: &egui::Painter, - origin: Pos2, - x: usize, - y: usize, - glyph: &Glyph, - font: &BitmapFont, - zoom: u32, -) { - let cell = font.cell_size(zoom); - let tl = origin + vec2(x as f32 * cell.x, y as f32 * cell.y); - let rect = Rect::from_min_size(tl, cell); - paint_glyph(painter, rect, glyph, font); -} - -/// Draws all board cells, then the object overlay, then the player at `origin`. -/// -/// Rendering order: grid (floor) → objects → player. Objects are drawn on top -/// of their grid cell's background, which is revealed if the object moves away. -/// `zoom` scales each tile by an integer factor. -pub(crate) fn draw_board( - painter: &egui::Painter, - origin: Pos2, - board: &Board, - font: &BitmapFont, - zoom: u32, -) { - // Pass 1: grid floor. - for y in 0..board.height { - for x in 0..board.width { - let (glyph, _) = board.get(x, y); - draw_glyph(painter, origin, x, y, glyph, font, zoom); - } - } - // Pass 2: objects drawn on top of the floor. - for obj in &board.objects { - draw_glyph(painter, origin, obj.x, obj.y, &obj.glyph, font, zoom); - } - // Pass 3: player overlay. Will be removed once the player becomes a scripted object. - let player_glyph = Glyph::player(); - draw_glyph( - painter, - origin, - board.player.x as usize, - board.player.y as usize, - &player_glyph, - font, - zoom, - ); -} - -/// Draws the objects-mode overlay over an already-drawn board. -/// -/// Dims the entire board with a semi-transparent fill, then for each -/// [`ObjectDef`](crate::game::ObjectDef) redraws that cell at full brightness -/// and outlines it in white. The selected object (by index into -/// `board.objects`) gets a yellow outline instead. `zoom` must match the zoom -/// used to draw the board. -pub(crate) fn draw_object_overlays( - painter: &egui::Painter, - origin: Pos2, - board: &Board, - font: &BitmapFont, - selected: Option, - zoom: u32, -) { - let cell = font.cell_size(zoom); - - // Dim the whole board so non-object cells recede visually. - let board_rect = Rect::from_min_size( - origin, - vec2(board.width as f32 * cell.x, board.height as f32 * cell.y), - ); - painter.rect_filled(board_rect, 0.0, Color32::from_black_alpha(160)); - - // Redraw each object cell at full brightness and surround it with an outline. - for (i, obj) in board.objects.iter().enumerate() { - let tl = origin + vec2(obj.x as f32 * cell.x, obj.y as f32 * cell.y); - let rect = Rect::from_min_size(tl, cell); - - // Redraw on top of the dim overlay so this cell remains bright. - paint_glyph(painter, rect, &obj.glyph, font); - - // Yellow outline for the selected object, white for all others. - let outline = if selected == Some(i) { - Color32::YELLOW - } else { - Color32::WHITE - }; - painter.rect_stroke(rect, 0.0, (1.0, outline), egui::StrokeKind::Outside); - } -} - -/// Converts a pixel position to cell coordinates relative to `origin`. -/// -/// Uses floor so positions above or left of `origin` produce negative values — -/// callers must guard `>= 0` before treating the result as a valid cell index. -pub(crate) fn pos_to_cell(origin: Pos2, pos: Pos2, tile_w: f32, tile_h: f32) -> (i32, i32) { - let rel = pos - origin; - ( - (rel.x / tile_w).floor() as i32, - (rel.y / tile_h).floor() as i32, - ) -} - -/// Computes the pixel position of board cell (0, 0) within `available`. -/// -/// If the board fits along an axis, it is centered. If it overflows, the -/// viewport is centered on the player and clamped so no empty space appears -/// at the board edges. `zoom` scales tile dimensions before computing pixel sizes. -pub(crate) fn board_origin( - available: Rect, - board_w: usize, - board_h: usize, - player: Player, - font: &BitmapFont, - zoom: u32, -) -> Pos2 { - let cell = font.cell_size(zoom); - let board_px_w = board_w as f32 * cell.x; - let board_px_h = board_h as f32 * cell.y; - - let origin_x = if board_px_w <= available.width() { - // Board fits: center it horizontally. - available.min.x + (available.width() - board_px_w) / 2.0 - } else { - // Board overflows: keep player at viewport center, clamp to board edges. - let player_px_x = player.x as f32 * cell.x + cell.x / 2.0; - (available.center().x - player_px_x) - .max(available.max.x - board_px_w) // right edge must reach viewport right - .min(available.min.x) // left edge must reach viewport left - }; - - let origin_y = if board_px_h <= available.height() { - available.min.y + (available.height() - board_px_h) / 2.0 - } else { - let player_px_y = player.y as f32 * cell.y + cell.y / 2.0; - (available.center().y - player_px_y) - .max(available.max.y - board_px_h) - .min(available.min.y) - }; - - Pos2::new(origin_x, origin_y) -} diff --git a/kiln-egui/src/script_editor.rs b/kiln-egui/src/script_editor.rs deleted file mode 100644 index 5e1fc5a..0000000 --- a/kiln-egui/src/script_editor.rs +++ /dev/null @@ -1,62 +0,0 @@ -use eframe::egui; -use egui_code_editor::{CodeEditor, ColorTheme, Syntax}; -use std::collections::BTreeSet; - -/// Returns a [`Syntax`] definition for the Rhai scripting language. -/// -/// Keyword sets are sourced from the official -/// [rhaiscript/sublime-rhai](https://github.com/rhaiscript/sublime-rhai) grammar (MPL-2.0). -pub(crate) fn rhai_syntax() -> Syntax { - Syntax { - language: "Rhai", - case_sensitive: true, - comment: "//", - comment_multiline: ["/*", "*/"], - hyperlinks: BTreeSet::new(), - keywords: BTreeSet::from([ - "if", "else", "switch", "for", "in", "loop", "do", "while", "until", "break", - "continue", "return", "throw", "try", "catch", "let", "const", "fn", "import", - "export", "as", "private", - ]), - // Boolean literals and context keywords get a distinct type color. - types: BTreeSet::from(["true", "false", "this", "global"]), - // Built-in Rhai functions highlighted as special identifiers. - special: BTreeSet::from([ - "print", - "debug", - "type_of", - "is_def_var", - "is_def_fn", - "is_shared", - "call", - "curry", - "eval", - ]), - } -} - -/// Renders the script code editor and header bar inside `ui`. -/// -/// Returns `true` when the user clicks "Back", signalling the caller to save -/// `content` back to `board.scripts` and close the editor. -pub(crate) fn show(ui: &mut egui::Ui, name: &str, content: &mut String) -> bool { - let mut done = false; - - ui.horizontal(|ui| { - if ui.button("◄ Back").clicked() { - done = true; - } - ui.label(format!("Editing: {name}")); - }); - ui.separator(); - - CodeEditor::default() - .id_source("script_editor") - .with_rows(30) - .with_fontsize(14.0) - .with_theme(ColorTheme::GRUVBOX) - .with_syntax(rhai_syntax()) - .show(ui, content); - - done -}