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:
2026-05-17 19:09:16 -05:00
parent 60b04e6ef3
commit 03d6337f04
4 changed files with 344 additions and 297 deletions
+111
View File
@@ -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 0x200x7E.
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;
}
}
});
}