Add glyph chooser and redesign editor side panel
Glyph chooser: a painted preview button in the Palette tab opens a floating dialog with a board-palette picker, fg/bg color pickers (egui built-in), and a 16×6 printable ASCII character grid. The stamped glyph is now independent of the archetype's default. Side panel: widened to 200 px (resizable), tabbed (Palette / Board / World), archetype rows show their default glyph cell, list scrolls after 5 entries, labels use ui.label() for consistent sizing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -12,7 +12,7 @@ use serde::Deserialize;
|
|||||||
/// `Glyph` values come from the map file palette and are set at load time.
|
/// `Glyph` values come from the map file palette and are set at load time.
|
||||||
/// The player is the only entity whose glyph is hardcoded at runtime
|
/// The player is the only entity whose glyph is hardcoded at runtime
|
||||||
/// (see [`Glyph::player`]).
|
/// (see [`Glyph::player`]).
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
pub struct Glyph {
|
pub struct Glyph {
|
||||||
/// The character to display in this cell.
|
/// The character to display in this cell.
|
||||||
pub ch: char,
|
pub ch: char,
|
||||||
|
|||||||
+204
-15
@@ -1,5 +1,5 @@
|
|||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use egui::{Align2, FontId, Key, Pos2, Rect, Sense, vec2};
|
use egui::{Align2, Color32, FontId, Key, Pos2, Rect, Sense, Stroke, vec2};
|
||||||
|
|
||||||
mod game;
|
mod game;
|
||||||
mod map_file;
|
mod map_file;
|
||||||
@@ -14,8 +14,8 @@ const CELL_H: f32 = 20.0;
|
|||||||
const MENU_H: f32 = 24.0;
|
const MENU_H: f32 = 24.0;
|
||||||
/// Default inner margin that egui's CentralPanel adds on all sides, in pixels.
|
/// Default inner margin that egui's CentralPanel adds on all sides, in pixels.
|
||||||
const PANEL_MARGIN: f32 = 8.0;
|
const PANEL_MARGIN: f32 = 8.0;
|
||||||
/// Width of the editor palette side panel in pixels.
|
/// Default width of the editor side panel in pixels. The panel is resizable.
|
||||||
const PALETTE_PANEL_W: f32 = 120.0;
|
const PALETTE_PANEL_W: f32 = 200.0;
|
||||||
/// Default window width in pixels. Independent of map size.
|
/// Default window width in pixels. Independent of map size.
|
||||||
const DEFAULT_WINDOW_W: f32 = 840.0;
|
const DEFAULT_WINDOW_W: f32 = 840.0;
|
||||||
/// Default window height in pixels. Independent of map size.
|
/// Default window height in pixels. Independent of map size.
|
||||||
@@ -38,15 +38,37 @@ enum AppMode {
|
|||||||
Edit,
|
Edit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Which tab is active in the editor side panel.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||||
|
enum EditorTab {
|
||||||
|
/// Archetype + glyph palette for painting cells.
|
||||||
|
Palette,
|
||||||
|
/// Board-level properties (placeholder).
|
||||||
|
Board,
|
||||||
|
/// World-level properties (placeholder).
|
||||||
|
World,
|
||||||
|
}
|
||||||
|
|
||||||
/// Transient editor state. Only meaningful when [`AppMode::Edit`] is active.
|
/// Transient editor state. Only meaningful when [`AppMode::Edit`] is active.
|
||||||
struct EditorState {
|
struct EditorState {
|
||||||
/// The archetype that will be stamped when the user clicks a cell.
|
/// The archetype that will be stamped when the user clicks a cell.
|
||||||
selected: Archetype,
|
selected: Archetype,
|
||||||
|
/// The glyph (character + colors) to stamp. Independent of the archetype's default.
|
||||||
|
glyph: Glyph,
|
||||||
|
/// Whether the glyph picker dialog is currently open.
|
||||||
|
glyph_picker_open: bool,
|
||||||
|
/// Which side-panel tab is currently shown.
|
||||||
|
tab: EditorTab,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EditorState {
|
impl EditorState {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self { selected: Archetype::Wall }
|
Self {
|
||||||
|
selected: Archetype::Wall,
|
||||||
|
glyph: Archetype::Wall.default_glyph(),
|
||||||
|
glyph_picker_open: false,
|
||||||
|
tab: EditorTab::Palette,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,21 +136,78 @@ impl eframe::App for App {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Editor palette panel ---
|
// --- Editor side panel ---
|
||||||
// Must be declared before CentralPanel so egui allocates its space first.
|
// Must be declared before CentralPanel so egui allocates its space first.
|
||||||
if self.mode == AppMode::Edit {
|
if self.mode == AppMode::Edit {
|
||||||
egui::SidePanel::right("palette_panel")
|
egui::SidePanel::right("palette_panel")
|
||||||
.resizable(false)
|
.resizable(true)
|
||||||
.exact_width(PALETTE_PANEL_W)
|
.default_width(PALETTE_PANEL_W)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
ui.heading("Elements");
|
// Tab bar
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.selectable_value(&mut self.editor.tab, EditorTab::Palette, "Palette");
|
||||||
|
ui.selectable_value(&mut self.editor.tab, EditorTab::Board, "Board");
|
||||||
|
ui.selectable_value(&mut self.editor.tab, EditorTab::World, "World");
|
||||||
|
});
|
||||||
ui.separator();
|
ui.separator();
|
||||||
for archetype in ALL_ARCHETYPES {
|
|
||||||
ui.selectable_value(
|
match self.editor.tab {
|
||||||
&mut self.editor.selected,
|
EditorTab::Palette => {
|
||||||
*archetype,
|
ui.label("Element");
|
||||||
archetype.name(),
|
|
||||||
);
|
// Scrollable archetype list; shows at most 5 rows before scrolling.
|
||||||
|
let row_h = CELL_H + 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 = self.editor.selected == archetype;
|
||||||
|
let glyph = archetype.default_glyph();
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let (r, cell_resp) = ui.allocate_exact_size(
|
||||||
|
vec2(CELL_W, CELL_H), Sense::click());
|
||||||
|
{
|
||||||
|
let p = ui.painter();
|
||||||
|
p.rect_filled(r, 0.0, glyph.bg);
|
||||||
|
p.text(r.center(), Align2::CENTER_CENTER,
|
||||||
|
glyph.ch, FontId::monospace(CELL_H), glyph.fg);
|
||||||
|
}
|
||||||
|
if cell_resp.clicked() {
|
||||||
|
self.editor.selected = archetype;
|
||||||
|
}
|
||||||
|
if ui.selectable_label(is_selected, archetype.name())
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
self.editor.selected = archetype;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
ui.label("Glyph");
|
||||||
|
// Paint a preview cell showing the current glyph, sized like a board cell.
|
||||||
|
let (cell_rect, glyph_btn) =
|
||||||
|
ui.allocate_exact_size(vec2(CELL_W, CELL_H), Sense::click());
|
||||||
|
{
|
||||||
|
let p = ui.painter();
|
||||||
|
p.rect_filled(cell_rect, 0.0, self.editor.glyph.bg);
|
||||||
|
p.text(
|
||||||
|
cell_rect.center(),
|
||||||
|
Align2::CENTER_CENTER,
|
||||||
|
self.editor.glyph.ch,
|
||||||
|
FontId::monospace(CELL_H),
|
||||||
|
self.editor.glyph.fg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if glyph_btn.clicked() {
|
||||||
|
self.editor.glyph_picker_open = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EditorTab::Board => {}
|
||||||
|
EditorTab::World => {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -176,7 +255,7 @@ impl eframe::App for App {
|
|||||||
let board = &mut self.state.board;
|
let board = &mut self.state.board;
|
||||||
if cx < board.width && cy < board.height {
|
if cx < board.width && cy < board.height {
|
||||||
let arch = self.editor.selected;
|
let arch = self.editor.selected;
|
||||||
*board.get_mut(cx, cy) = (arch.default_glyph(), arch);
|
*board.get_mut(cx, cy) = (self.editor.glyph, arch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,6 +290,116 @@ impl eframe::App for App {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Glyph picker dialog ---
|
||||||
|
// Rendered after all panels so it floats on top.
|
||||||
|
if self.editor.glyph_picker_open {
|
||||||
|
// Collect unique glyphs from the board before the window closure
|
||||||
|
// borrows self mutably, to avoid a conflict with self.state.board.
|
||||||
|
let board_glyphs: Vec<Glyph> = {
|
||||||
|
let mut seen: Vec<(u32, [u8; 4], [u8; 4])> = Vec::new();
|
||||||
|
let mut out: Vec<Glyph> = Vec::new();
|
||||||
|
for (glyph, _) in &self.state.board.cells {
|
||||||
|
let key = (glyph.ch as u32, glyph.fg.to_array(), glyph.bg.to_array());
|
||||||
|
if !seen.contains(&key) {
|
||||||
|
seen.push(key);
|
||||||
|
out.push(*glyph);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use a local bool so the window's close button can write to it
|
||||||
|
// without conflicting with the closure's mutable borrow of self.
|
||||||
|
let mut picker_open = true;
|
||||||
|
egui::Window::new("Glyph")
|
||||||
|
.collapsible(false)
|
||||||
|
.resizable(false)
|
||||||
|
.open(&mut picker_open)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
// ----- Board palette -----
|
||||||
|
ui.label("Board palette");
|
||||||
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
for &g in &board_glyphs {
|
||||||
|
let (r, resp) =
|
||||||
|
ui.allocate_exact_size(vec2(CELL_W, CELL_H), Sense::click());
|
||||||
|
{
|
||||||
|
let p = ui.painter();
|
||||||
|
p.rect_filled(r, 0.0, g.bg);
|
||||||
|
p.text(r.center(), Align2::CENTER_CENTER, g.ch,
|
||||||
|
FontId::monospace(CELL_H), g.fg);
|
||||||
|
if g == self.editor.glyph {
|
||||||
|
p.rect_stroke(r, 0.0, Stroke::new(1.0, Color32::WHITE),
|
||||||
|
egui::epaint::StrokeKind::Middle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if resp.clicked() {
|
||||||
|
self.editor.glyph = g;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
// ----- Colors -----
|
||||||
|
ui.label("Colors");
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("FG");
|
||||||
|
egui::color_picker::color_edit_button_srgba(
|
||||||
|
ui, &mut self.editor.glyph.fg, egui::color_picker::Alpha::Opaque);
|
||||||
|
ui.label("BG");
|
||||||
|
egui::color_picker::color_edit_button_srgba(
|
||||||
|
ui, &mut self.editor.glyph.bg, egui::color_picker::Alpha::Opaque);
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
// ----- Character grid -----
|
||||||
|
// 16 columns × 6 rows covering printable ASCII 0x20–0x7E.
|
||||||
|
ui.label("Character");
|
||||||
|
const CHAR_COLS: usize = 16;
|
||||||
|
let chars: Vec<char> =
|
||||||
|
(0x20u32..=0x7eu32).filter_map(char::from_u32).collect();
|
||||||
|
let rows = (chars.len() + CHAR_COLS - 1) / CHAR_COLS;
|
||||||
|
let grid_size = vec2(CHAR_COLS as f32 * CELL_W, rows as f32 * CELL_H);
|
||||||
|
let (grid_rect, grid_resp) =
|
||||||
|
ui.allocate_exact_size(grid_size, Sense::click());
|
||||||
|
// Copy colors and selected char before painter borrow.
|
||||||
|
let fg = self.editor.glyph.fg;
|
||||||
|
let bg = self.editor.glyph.bg;
|
||||||
|
let cur_ch = self.editor.glyph.ch;
|
||||||
|
{
|
||||||
|
let p = ui.painter();
|
||||||
|
for (i, &ch) in chars.iter().enumerate() {
|
||||||
|
let col = i % CHAR_COLS;
|
||||||
|
let row = i / CHAR_COLS;
|
||||||
|
let tl = grid_rect.min
|
||||||
|
+ vec2(col as f32 * CELL_W, row as f32 * CELL_H);
|
||||||
|
let cell = Rect::from_min_size(tl, vec2(CELL_W, CELL_H));
|
||||||
|
p.rect_filled(cell, 0.0, bg);
|
||||||
|
p.text(cell.center(), Align2::CENTER_CENTER, ch,
|
||||||
|
FontId::monospace(CELL_H), fg);
|
||||||
|
if ch == cur_ch {
|
||||||
|
p.rect_stroke(cell, 0.0, Stroke::new(1.0, Color32::WHITE),
|
||||||
|
egui::epaint::StrokeKind::Middle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if grid_resp.clicked() {
|
||||||
|
if let Some(pos) = grid_resp.interact_pointer_pos() {
|
||||||
|
let rel = pos - grid_rect.min;
|
||||||
|
let col = (rel.x / CELL_W).floor() as usize;
|
||||||
|
let row = (rel.y / CELL_H).floor() as usize;
|
||||||
|
let idx = row * CHAR_COLS + col;
|
||||||
|
if let Some(&ch) = chars.get(idx) {
|
||||||
|
self.editor.glyph.ch = ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Window's close button sets picker_open to false.
|
||||||
|
self.editor.glyph_picker_open = picker_open;
|
||||||
|
}
|
||||||
|
|
||||||
// egui repaints automatically on input events, so no explicit
|
// egui repaints automatically on input events, so no explicit
|
||||||
// request_repaint() is needed while the game is purely event-driven.
|
// request_repaint() is needed while the game is purely event-driven.
|
||||||
// Add it back (or call it from game logic) when continuous animation is needed.
|
// Add it back (or call it from game logic) when continuous animation is needed.
|
||||||
|
|||||||
Reference in New Issue
Block a user