Refactor main.rs into focused modules: render, editor, glyph_picker
Splits the flat 432-line main.rs into four modules so each file has one clear job: render.rs owns layout constants and drawing primitives, editor.rs owns EditorTab/EditorState and the side panel, glyph_picker.rs owns the floating picker dialog with a decoupled (open, glyph) interface, and main.rs is reduced to AppMode, App, and the frame loop (~150 lines). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+117
@@ -0,0 +1,117 @@
|
|||||||
|
use eframe::egui;
|
||||||
|
use egui::{Align2, FontId, Sense, vec2};
|
||||||
|
|
||||||
|
use crate::game::{Archetype, Board, Glyph, ALL_ARCHETYPES};
|
||||||
|
use crate::render::{CELL_H, CELL_W, PALETTE_PANEL_W};
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
/// Board-level properties (placeholder).
|
||||||
|
Board,
|
||||||
|
/// 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,
|
||||||
|
/// The glyph (character + colors) to stamp. Independent of the archetype's default.
|
||||||
|
pub(crate) glyph: Glyph,
|
||||||
|
/// Whether the glyph picker dialog is currently open.
|
||||||
|
pub(crate) glyph_picker_open: bool,
|
||||||
|
/// Which side-panel tab is currently shown.
|
||||||
|
pub(crate) tab: EditorTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EditorState {
|
||||||
|
/// Creates editor state with `Wall` selected and the picker closed.
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
selected: Archetype::Wall,
|
||||||
|
glyph: Archetype::Wall.default_glyph(),
|
||||||
|
glyph_picker_open: false,
|
||||||
|
tab: EditorTab::Palette,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders the editor side panel (must be called before `CentralPanel`).
|
||||||
|
///
|
||||||
|
/// Shows tab bar (Palette / Board / World) and, on the Palette tab, the
|
||||||
|
/// scrollable archetype list and current-glyph preview button.
|
||||||
|
pub(crate) fn show_editor_panel(ctx: &egui::Context, editor: &mut EditorState, _board: &Board) {
|
||||||
|
egui::SidePanel::right("palette_panel")
|
||||||
|
.resizable(true)
|
||||||
|
.default_width(PALETTE_PANEL_W)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
// Tab bar
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.selectable_value(&mut editor.tab, EditorTab::Palette, "Palette");
|
||||||
|
ui.selectable_value(&mut editor.tab, EditorTab::Board, "Board");
|
||||||
|
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.
|
||||||
|
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 = 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);
|
||||||
|
}
|
||||||
|
let label_resp =
|
||||||
|
ui.selectable_label(is_selected, archetype.name());
|
||||||
|
if cell_resp.clicked() || label_resp.clicked() {
|
||||||
|
editor.selected = archetype;
|
||||||
|
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) =
|
||||||
|
ui.allocate_exact_size(vec2(CELL_W, CELL_H), Sense::click());
|
||||||
|
{
|
||||||
|
let p = ui.painter();
|
||||||
|
p.rect_filled(cell_rect, 0.0, editor.glyph.bg);
|
||||||
|
p.text(
|
||||||
|
cell_rect.center(),
|
||||||
|
Align2::CENTER_CENTER,
|
||||||
|
editor.glyph.ch,
|
||||||
|
FontId::monospace(CELL_H),
|
||||||
|
editor.glyph.fg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if glyph_btn.clicked() {
|
||||||
|
editor.glyph_picker_open = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EditorTab::Board => {}
|
||||||
|
EditorTab::World => {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
use eframe::egui;
|
||||||
|
use egui::{Align2, Color32, FontId, Rect, Sense, Stroke, vec2};
|
||||||
|
|
||||||
|
use crate::game::{Archetype, Glyph};
|
||||||
|
use crate::render::{CELL_H, CELL_W, pos_to_cell};
|
||||||
|
|
||||||
|
/// 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 16×6 printable-ASCII character grid.
|
||||||
|
/// 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)],
|
||||||
|
) {
|
||||||
|
// 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<Glyph> = {
|
||||||
|
let mut seen: Vec<Glyph> = Vec::new();
|
||||||
|
for (g, _) in board_cells {
|
||||||
|
if !seen.contains(g) {
|
||||||
|
seen.push(*g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
seen
|
||||||
|
};
|
||||||
|
|
||||||
|
egui::Window::new("Glyph")
|
||||||
|
.collapsible(false)
|
||||||
|
.resizable(false)
|
||||||
|
.open(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 == *glyph {
|
||||||
|
p.rect_stroke(r, 0.0, Stroke::new(1.0, Color32::WHITE),
|
||||||
|
egui::epaint::StrokeKind::Middle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if resp.clicked() {
|
||||||
|
*glyph = g;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
// ----- Colors -----
|
||||||
|
ui.label("Colors");
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("FG");
|
||||||
|
egui::color_picker::color_edit_button_srgba(
|
||||||
|
ui, &mut glyph.fg, egui::color_picker::Alpha::Opaque);
|
||||||
|
ui.label("BG");
|
||||||
|
egui::color_picker::color_edit_button_srgba(
|
||||||
|
ui, &mut 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().div_ceil(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());
|
||||||
|
let fg = glyph.fg;
|
||||||
|
let bg = glyph.bg;
|
||||||
|
let cur_ch = 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()
|
||||||
|
&& let Some(pos) = grid_resp.interact_pointer_pos() {
|
||||||
|
let (col, row) = pos_to_cell(grid_rect.min, pos);
|
||||||
|
let idx = row as usize * CHAR_COLS + col as usize;
|
||||||
|
if let Some(&ch) = chars.get(idx) {
|
||||||
|
glyph.ch = ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
+20
-297
@@ -1,29 +1,18 @@
|
|||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use egui::{Align2, Color32, FontId, Key, Pos2, Rect, Sense, Stroke, vec2};
|
use egui::{Key, Sense, vec2};
|
||||||
|
|
||||||
|
mod editor;
|
||||||
mod game;
|
mod game;
|
||||||
|
mod glyph_picker;
|
||||||
mod map_file;
|
mod map_file;
|
||||||
use game::{Archetype, GameState, Glyph, ALL_ARCHETYPES};
|
mod render;
|
||||||
|
|
||||||
/// Width of one board cell in pixels.
|
use editor::{EditorState, show_editor_panel};
|
||||||
const CELL_W: f32 = 14.0;
|
use game::GameState;
|
||||||
/// Height of one board cell in pixels.
|
use render::{
|
||||||
const CELL_H: f32 = 20.0;
|
DEFAULT_WINDOW_H, DEFAULT_WINDOW_W, MIN_WINDOW_H, MIN_WINDOW_W,
|
||||||
|
board_origin, draw_board, pos_to_cell, CELL_H, CELL_W,
|
||||||
/// Approximate height of the egui menu bar in pixels, used for window sizing.
|
};
|
||||||
const MENU_H: f32 = 24.0;
|
|
||||||
/// Default inner margin that egui's CentralPanel adds on all sides, in pixels.
|
|
||||||
const PANEL_MARGIN: f32 = 8.0;
|
|
||||||
/// Default width of the editor side panel in pixels. The panel is resizable.
|
|
||||||
const PALETTE_PANEL_W: f32 = 200.0;
|
|
||||||
/// Default window width in pixels. Independent of map size.
|
|
||||||
const DEFAULT_WINDOW_W: f32 = 840.0;
|
|
||||||
/// Default window height in pixels. Independent of map size.
|
|
||||||
const DEFAULT_WINDOW_H: f32 = 524.0;
|
|
||||||
/// Minimum window width: enough room for 10 cells plus margins.
|
|
||||||
const MIN_WINDOW_W: f32 = CELL_W * 10.0 + 2.0 * PANEL_MARGIN;
|
|
||||||
/// Minimum window height: enough room for 5 cells plus menu and margins.
|
|
||||||
const MIN_WINDOW_H: f32 = CELL_H * 5.0 + MENU_H + 2.0 * PANEL_MARGIN;
|
|
||||||
|
|
||||||
/// Whether the application is in play mode or edit mode.
|
/// Whether the application is in play mode or edit mode.
|
||||||
///
|
///
|
||||||
@@ -38,40 +27,6 @@ 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.
|
|
||||||
struct EditorState {
|
|
||||||
/// The archetype that will be stamped when the user clicks a cell.
|
|
||||||
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 {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
selected: Archetype::Wall,
|
|
||||||
glyph: Archetype::Wall.default_glyph(),
|
|
||||||
glyph_picker_open: false,
|
|
||||||
tab: EditorTab::Palette,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> eframe::Result<()> {
|
fn main() -> eframe::Result<()> {
|
||||||
let board = map_file::load("maps/start.toml").expect("failed to load maps/start.toml");
|
let board = map_file::load("maps/start.toml").expect("failed to load maps/start.toml");
|
||||||
|
|
||||||
@@ -99,6 +54,7 @@ struct App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
|
/// Creates the app with the given board, starting in Play mode.
|
||||||
fn new(board: game::Board) -> Self {
|
fn new(board: game::Board) -> Self {
|
||||||
Self {
|
Self {
|
||||||
state: GameState::new(board),
|
state: GameState::new(board),
|
||||||
@@ -139,75 +95,7 @@ impl eframe::App for App {
|
|||||||
// --- Editor side 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")
|
show_editor_panel(ctx, &mut self.editor, &self.state.board);
|
||||||
.resizable(true)
|
|
||||||
.default_width(PALETTE_PANEL_W)
|
|
||||||
.show(ctx, |ui| {
|
|
||||||
// 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();
|
|
||||||
|
|
||||||
match self.editor.tab {
|
|
||||||
EditorTab::Palette => {
|
|
||||||
ui.label("Element");
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
let label_resp =
|
|
||||||
ui.selectable_label(is_selected, archetype.name());
|
|
||||||
if cell_resp.clicked() || label_resp.clicked() {
|
|
||||||
self.editor.selected = archetype;
|
|
||||||
self.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) =
|
|
||||||
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 => {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Board rendering ---
|
// --- Board rendering ---
|
||||||
@@ -224,8 +112,8 @@ impl eframe::App for App {
|
|||||||
egui::ScrollArea::new([true, true]).show(ui, |ui| {
|
egui::ScrollArea::new([true, true]).show(ui, |ui| {
|
||||||
let (rect, response) = ui.allocate_exact_size(board_px, Sense::click());
|
let (rect, response) = ui.allocate_exact_size(board_px, Sense::click());
|
||||||
draw_board(ui.painter(), rect.min, &self.state.board);
|
draw_board(ui.painter(), rect.min, &self.state.board);
|
||||||
if response.clicked() {
|
if response.clicked()
|
||||||
if let Some(pos) = response.interact_pointer_pos() {
|
&& let Some(pos) = response.interact_pointer_pos() {
|
||||||
let (cx, cy) = pos_to_cell(rect.min, pos);
|
let (cx, cy) = pos_to_cell(rect.min, pos);
|
||||||
// floor() keeps out-of-bounds negatives negative; caught here.
|
// floor() keeps out-of-bounds negatives negative; caught here.
|
||||||
if cx >= 0 && cy >= 0 {
|
if cx >= 0 && cy >= 0 {
|
||||||
@@ -238,7 +126,6 @@ impl eframe::App for App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Play mode: player-centered viewport, no scroll bars.
|
// Play mode: player-centered viewport, no scroll bars.
|
||||||
@@ -252,104 +139,12 @@ impl eframe::App for App {
|
|||||||
// --- Glyph picker dialog ---
|
// --- Glyph picker dialog ---
|
||||||
// Rendered after all panels so it floats on top.
|
// Rendered after all panels so it floats on top.
|
||||||
if self.editor.glyph_picker_open {
|
if self.editor.glyph_picker_open {
|
||||||
// Collect unique glyphs from the board before the window closure
|
glyph_picker::show(
|
||||||
// borrows self mutably, to avoid a conflict with self.state.board.
|
ctx,
|
||||||
let board_glyphs: Vec<Glyph> = {
|
&mut self.editor.glyph_picker_open,
|
||||||
let mut seen: Vec<Glyph> = Vec::new();
|
&mut self.editor.glyph,
|
||||||
for (glyph, _) in &self.state.board.cells {
|
&self.state.board.cells,
|
||||||
if !seen.contains(glyph) {
|
);
|
||||||
seen.push(*glyph);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
seen
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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());
|
|
||||||
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 (col, row) = pos_to_cell(grid_rect.min, pos);
|
|
||||||
let idx = row as usize * CHAR_COLS + col as usize;
|
|
||||||
if let Some(&ch) = chars.get(idx) {
|
|
||||||
self.editor.glyph.ch = ch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
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
|
||||||
@@ -357,75 +152,3 @@ impl eframe::App for App {
|
|||||||
// 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.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws a single cell at grid position `(x, y)` using the given glyph.
|
|
||||||
///
|
|
||||||
/// Each cell is rendered as two layers:
|
|
||||||
/// 1. A filled rectangle in `glyph.bg` covering the full cell area.
|
|
||||||
/// 2. A monospace character (`glyph.ch`) in `glyph.fg`, centered in the cell.
|
|
||||||
///
|
|
||||||
/// `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
|
|
||||||
/// [`CELL_W`] and [`CELL_H`].
|
|
||||||
fn draw_glyph(painter: &egui::Painter, origin: Pos2, x: usize, y: usize, glyph: &Glyph) {
|
|
||||||
let tl = origin + vec2(x as f32 * CELL_W, y as f32 * CELL_H);
|
|
||||||
let rect = Rect::from_min_size(tl, vec2(CELL_W, CELL_H));
|
|
||||||
let center = rect.center();
|
|
||||||
painter.rect_filled(rect, 0.0, glyph.bg);
|
|
||||||
painter.text(center, Align2::CENTER_CENTER, glyph.ch, FontId::monospace(CELL_H), glyph.fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draws all board cells then the player overlay at `origin`.
|
|
||||||
fn draw_board(painter: &egui::Painter, origin: Pos2, board: &game::Board) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The player is not a board cell; it is rendered on top as an overlay.
|
|
||||||
// This separate draw 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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.
|
|
||||||
fn pos_to_cell(origin: Pos2, pos: Pos2) -> (i32, i32) {
|
|
||||||
let rel = pos - origin;
|
|
||||||
((rel.x / CELL_W).floor() as i32, (rel.y / CELL_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.
|
|
||||||
fn board_origin(available: Rect, board_w: usize, board_h: usize, player: game::Player) -> Pos2 {
|
|
||||||
let board_px_w = board_w as f32 * CELL_W;
|
|
||||||
let board_px_h = board_h as f32 * CELL_H;
|
|
||||||
|
|
||||||
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_W + CELL_W / 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_H + CELL_H / 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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
use eframe::egui;
|
||||||
|
use egui::{Align2, FontId, Pos2, Rect, vec2};
|
||||||
|
|
||||||
|
use crate::game::{Board, Glyph, Player};
|
||||||
|
|
||||||
|
/// Width of one board cell in pixels.
|
||||||
|
pub(crate) const CELL_W: f32 = 14.0;
|
||||||
|
/// Height of one board cell in pixels.
|
||||||
|
pub(crate) const CELL_H: f32 = 20.0;
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
pub(crate) const MIN_WINDOW_W: f32 = CELL_W * 10.0 + 2.0 * PANEL_MARGIN;
|
||||||
|
/// Minimum window height: enough room for 5 cells plus menu and margins.
|
||||||
|
pub(crate) const MIN_WINDOW_H: f32 = CELL_H * 5.0 + MENU_H + 2.0 * PANEL_MARGIN;
|
||||||
|
|
||||||
|
/// Draws a single cell at grid position `(x, y)` using the given glyph.
|
||||||
|
///
|
||||||
|
/// Each cell is rendered as two layers:
|
||||||
|
/// 1. A filled rectangle in `glyph.bg` covering the full cell area.
|
||||||
|
/// 2. A monospace character (`glyph.ch`) in `glyph.fg`, centered in the cell.
|
||||||
|
///
|
||||||
|
/// `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
|
||||||
|
/// [`CELL_W`] and [`CELL_H`].
|
||||||
|
pub(crate) fn draw_glyph(painter: &egui::Painter, origin: Pos2, x: usize, y: usize, glyph: &Glyph) {
|
||||||
|
let tl = origin + vec2(x as f32 * CELL_W, y as f32 * CELL_H);
|
||||||
|
let rect = Rect::from_min_size(tl, vec2(CELL_W, CELL_H));
|
||||||
|
let center = rect.center();
|
||||||
|
painter.rect_filled(rect, 0.0, glyph.bg);
|
||||||
|
painter.text(center, Align2::CENTER_CENTER, glyph.ch, FontId::monospace(CELL_H), glyph.fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws all board cells then the player overlay at `origin`.
|
||||||
|
pub(crate) fn draw_board(painter: &egui::Painter, origin: Pos2, board: &Board) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The player is not a board cell; it is rendered on top as an overlay.
|
||||||
|
// This separate draw 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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) -> (i32, i32) {
|
||||||
|
let rel = pos - origin;
|
||||||
|
((rel.x / CELL_W).floor() as i32, (rel.y / CELL_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.
|
||||||
|
pub(crate) fn board_origin(available: Rect, board_w: usize, board_h: usize, player: Player) -> Pos2 {
|
||||||
|
let board_px_w = board_w as f32 * CELL_W;
|
||||||
|
let board_px_h = board_h as f32 * CELL_H;
|
||||||
|
|
||||||
|
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_W + CELL_W / 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_H + CELL_H / 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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user