Files
kiln/src/editor.rs
T

242 lines
11 KiB
Rust
Raw Normal View History

use eframe::egui;
use egui::{Sense, vec2};
2026-05-19 00:07:04 -05:00
use crate::font::BitmapFont;
use crate::font_dialog::{FontDialogState, show as show_font_dialog};
2026-05-30 18:48:41 -05:00
use crate::game::{ALL_ARCHETYPES, Archetype, Board, Glyph};
2026-05-19 00:07:04 -05:00
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,
2026-05-30 18:48:41 -05:00
/// Object placement and script assignment.
Objects,
/// Board-level properties (font, board script).
Board,
2026-05-30 18:48:41 -05:00
/// Named script library for this board.
Scripts,
/// World-level properties (placeholder).
World,
}
/// Transient editor state. Only meaningful when the app is in Edit mode.
pub(crate) struct EditorState {
/// The archetype that will be stamped when the user clicks a cell.
pub(crate) selected: Archetype,
2026-05-19 00:07:04 -05:00
/// The glyph (tile index + colors) to stamp. Independent of the archetype's default.
pub(crate) glyph: Glyph,
2026-05-30 18:48:41 -05:00
/// Whether the glyph picker dialog is currently open (Palette tab).
pub(crate) glyph_picker_open: bool,
2026-05-19 00:07:04 -05:00
/// 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,
2026-05-30 18:48:41 -05:00
/// Index into `board.objects` of the currently selected object, or `None`.
pub(crate) selected_object: Option<usize>,
/// Whether the glyph picker is open for editing the selected object's glyph.
pub(crate) object_glyph_picker_open: bool,
/// Local copy of the selected object's cell glyph for the picker to edit.
///
/// Kept separate from `board.cells` to avoid a borrow conflict: `glyph_picker::show`
/// takes both `&mut Glyph` (edit target) and `&[(Glyph, Archetype)]` (board palette),
/// and those can't both alias `board.cells`. This copy is written back to the board
/// each frame while an object is selected.
pub(crate) object_editing_glyph: Glyph,
}
impl EditorState {
2026-05-19 00:07:04 -05:00
/// 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,
2026-05-19 00:07:04 -05:00
font_dialog_open: false,
font_dialog_state: FontDialogState::from_spec(None),
tab: EditorTab::Palette,
2026-05-30 18:48:41 -05:00
selected_object: None,
object_glyph_picker_open: false,
object_editing_glyph: Glyph::player(), // overwritten on first selection
}
}
}
/// Renders the editor side panel (must be called before `CentralPanel`).
///
2026-05-30 18:48:41 -05:00
/// 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.
2026-05-19 00:07:04 -05:00
pub(crate) fn show_editor_panel(
ctx: &egui::Context,
editor: &mut EditorState,
2026-05-30 18:48:41 -05:00
board: &mut Board,
2026-05-19 00:07:04 -05:00
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| {
2026-05-30 18:48:41 -05:00
// Tab bar — wraps automatically if the panel is narrow.
ui.horizontal_wrapped(|ui| {
ui.selectable_value(&mut editor.tab, EditorTab::Palette, "Palette");
2026-05-30 18:48:41 -05:00
ui.selectable_value(&mut editor.tab, EditorTab::Objects, "Objects");
2026-05-19 00:07:04 -05:00
ui.selectable_value(&mut editor.tab, EditorTab::Board, "Board");
2026-05-30 18:48:41 -05:00
ui.selectable_value(&mut editor.tab, EditorTab::Scripts, "Scripts");
2026-05-19 00:07:04 -05:00
ui.selectable_value(&mut editor.tab, EditorTab::World, "World");
});
ui.separator();
match editor.tab {
EditorTab::Palette => {
ui.label("Element");
// Scrollable archetype list; shows at most 5 rows before scrolling.
2026-05-19 00:07:04 -05:00
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 = editor.selected == archetype;
let glyph = archetype.default_glyph();
ui.horizontal(|ui| {
2026-05-19 00:07:04 -05:00
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;
2026-05-19 00:07:04 -05:00
editor.glyph = archetype.default_glyph();
}
});
}
});
ui.separator();
ui.label("Glyph");
// Paint a preview cell showing the current glyph, sized like a board cell.
let (cell_rect, glyph_btn) =
2026-05-19 00:07:04 -05:00
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;
}
}
2026-05-19 00:07:04 -05:00
2026-05-30 18:48:41 -05:00
EditorTab::Objects => {
match editor.selected_object {
None => {
ui.label("Click an object on the board to select it.");
}
Some(i) => {
// Collect sorted script names before taking a mutable borrow on objects.
let mut script_names: Vec<String> =
board.scripts.keys().cloned().collect();
script_names.sort();
ui.label("Glyph");
// Preview button — same pattern as Palette tab.
let (cell_rect, glyph_btn) =
ui.allocate_exact_size(vec2(tw, th), Sense::click());
paint_glyph(
ui.painter(),
cell_rect,
&editor.object_editing_glyph,
active_font,
);
if glyph_btn.clicked() {
editor.object_glyph_picker_open = true;
}
let obj = &mut board.objects[i];
let current =
obj.script_name.as_deref().unwrap_or("(none)").to_string();
ui.label("Script");
egui::ComboBox::from_id_salt("obj_script")
.selected_text(&current)
.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() {
// TODO: open script editor
}
}
}
}
2026-05-19 00:07:04 -05:00
EditorTab::Board => {
2026-05-30 18:48:41 -05:00
let font_label = match &board.font {
2026-05-19 00:07:04 -05:00
Some(s) => s.path.clone(),
None => "(default)".to_owned(),
};
ui.label(format!("Font: {font_label}"));
if ui.button("Font…").clicked() {
2026-05-30 18:48:41 -05:00
editor.font_dialog_state =
FontDialogState::from_spec(board.font.as_ref());
2026-05-19 00:07:04 -05:00
editor.font_dialog_open = true;
}
}
2026-05-30 18:48:41 -05:00
EditorTab::Scripts => {
if board.scripts.is_empty() {
ui.label("No scripts defined.");
} else {
let mut names: Vec<&String> = board.scripts.keys().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() {
// TODO: open script editor
}
});
}
});
}
}
EditorTab::World => {}
}
});
2026-05-19 00:07:04 -05:00
// 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,
2026-05-30 18:48:41 -05:00
&mut board.font,
2026-05-19 00:07:04 -05:00
);
}
}