use eframe::egui; use egui::{Sense, vec2}; use crate::font::BitmapFont; use crate::font_dialog::{FontDialogState, show as show_font_dialog}; use crate::render::{PALETTE_PANEL_W, glyph_preview_button, paint_glyph}; use kiln_core::game::{ALL_ARCHETYPES, Archetype, Board, 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, /// Object placement and script assignment. Objects, /// Board-level properties (font, board script). Board, /// Named script library for this board. Scripts, /// World-level properties (placeholder). World, } /// Palette-tab state: what gets stamped when painting cells. pub(crate) struct PaletteState { /// The archetype that will be stamped when the user clicks a cell. pub(crate) selected: Archetype, /// 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 (Palette tab). pub(crate) picker_open: bool, } /// Font-dialog state: the floating font picker and its in-progress edit. pub(crate) struct FontDialog { /// Whether the font picker dialog is currently open. pub(crate) open: bool, /// In-progress state for the font dialog while it is open. pub(crate) state: FontDialogState, } /// Objects-tab state: selection, placement, and the glyph being edited. pub(crate) struct ObjectEdit { /// Index into `board.objects` of the currently selected object, or `None`. pub(crate) selected: Option, /// 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()); } }); } }); }