basic drawing
This commit is contained in:
@@ -329,6 +329,50 @@ impl Board {
|
|||||||
self.objects.insert(id, object);
|
self.objects.insert(id, object);
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Editor primitive: stamps `arch` (with visual `glyph`) into the cell at
|
||||||
|
/// `(x, y)`, applying the editor's placement/removal rules.
|
||||||
|
///
|
||||||
|
/// Two cases, keyed only on the archetype (the floor is **never** touched — the
|
||||||
|
/// drawing tools cannot place, remove, or alter a floor):
|
||||||
|
///
|
||||||
|
/// - **Terrain** (`arch != Empty`, always solid today): removes any solid object
|
||||||
|
/// already in the cell, then writes `(glyph, arch)` into the cell's terrain.
|
||||||
|
/// - **Erase** (`arch == Empty`): removes the cell's terrain *and* every object in
|
||||||
|
/// it, leaving the floor (a visible `Empty` cell on a lower layer) in place.
|
||||||
|
///
|
||||||
|
/// Terrain is written to the cell's existing terrain layer (the single non-`Empty`
|
||||||
|
/// archetype across layers, if any) or else the top layer; a vacated terrain cell
|
||||||
|
/// becomes a transparent `Empty` so a lower floor shows through. Panics if `(x, y)`
|
||||||
|
/// is out of bounds.
|
||||||
|
pub fn place_archetype(&mut self, x: usize, y: usize, arch: Archetype, glyph: Glyph) {
|
||||||
|
if arch == Archetype::Empty {
|
||||||
|
// Erase: drop every object in the cell and clear its terrain (keep floor).
|
||||||
|
for id in self.object_ids_at(x, y) {
|
||||||
|
self.objects.remove(&id);
|
||||||
|
}
|
||||||
|
if let Some(z) = self.terrain_layer_at(x, y) {
|
||||||
|
*self.get_mut(z, x, y) = (Glyph::transparent(), Archetype::Empty);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placing solid terrain: a solid object can't share the cell, so drop it.
|
||||||
|
if let Some(id) = self.solid_object_id_at(x, y) {
|
||||||
|
self.objects.remove(&id);
|
||||||
|
}
|
||||||
|
// Reuse the existing terrain layer if the cell already has terrain, else the
|
||||||
|
// top layer (so the new wall draws above any floor on a lower layer).
|
||||||
|
let z = self.terrain_layer_at(x, y).unwrap_or(self.layers.len() - 1);
|
||||||
|
*self.get_mut(z, x, y) = (glyph, arch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the index of the layer whose terrain cell at `(x, y)` is non-`Empty`
|
||||||
|
/// (the cell's single terrain archetype, if any). By the one-solid-per-cell
|
||||||
|
/// invariant there is at most one such layer.
|
||||||
|
fn terrain_layer_at(&self, x: usize, y: usize) -> Option<usize> {
|
||||||
|
(0..self.layers.len()).find(|&z| self.get(z, x, y).1 != Archetype::Empty)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -559,6 +603,62 @@ pub(crate) mod tests {
|
|||||||
assert_eq!(board.glyph_at(0, 0).tile, '*' as u32);
|
assert_eq!(board.glyph_at(0, 0).tile, '*' as u32);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn place_wall_keeps_floor_and_removes_solid_object() {
|
||||||
|
// Floor on layer 0, terrain layer on top; a solid object sits at (1,0).
|
||||||
|
let mut board = open_board(3, 1, (2, 0), vec![ObjectDef::new(1, 0)]);
|
||||||
|
let floor = Glyph {
|
||||||
|
tile: '.' as u32,
|
||||||
|
..Glyph::transparent()
|
||||||
|
};
|
||||||
|
add_floor(&mut board, floor);
|
||||||
|
|
||||||
|
let wall = Archetype::Wall.default_glyph();
|
||||||
|
board.place_archetype(1, 0, Archetype::Wall, wall);
|
||||||
|
|
||||||
|
// The wall landed on the terrain (top) layer; the floor below is untouched.
|
||||||
|
assert_eq!(board.get(1, 1, 0), &(wall, Archetype::Wall));
|
||||||
|
assert_eq!(board.get(0, 1, 0).0, floor);
|
||||||
|
// The solid object that was there is gone.
|
||||||
|
assert!(board.object_ids_at(1, 0).is_empty());
|
||||||
|
assert_eq!(board.glyph_at(1, 0), wall);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn place_wall_overwrites_existing_terrain_in_place() {
|
||||||
|
// A crate already occupies the top layer at (1,0).
|
||||||
|
let mut board = open_board(3, 1, (2, 0), vec![]);
|
||||||
|
crate_at(&mut board, 1, 0);
|
||||||
|
let wall = Archetype::Wall.default_glyph();
|
||||||
|
board.place_archetype(1, 0, Archetype::Wall, wall);
|
||||||
|
assert_eq!(board.get(0, 1, 0), &(wall, Archetype::Wall));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn erase_removes_terrain_and_objects_but_keeps_floor() {
|
||||||
|
// Floor, a wall on the terrain layer, and a (non-solid) object all at (1,0).
|
||||||
|
let mut obj = ObjectDef::new(1, 0);
|
||||||
|
obj.solid = false;
|
||||||
|
let mut board = open_board(3, 1, (2, 0), vec![obj]);
|
||||||
|
let floor = Glyph {
|
||||||
|
tile: '.' as u32,
|
||||||
|
..Glyph::transparent()
|
||||||
|
};
|
||||||
|
add_floor(&mut board, floor);
|
||||||
|
wall_at(&mut board, 1, 0);
|
||||||
|
|
||||||
|
board.place_archetype(1, 0, Archetype::Empty, Glyph::transparent());
|
||||||
|
|
||||||
|
// Terrain cleared to transparent Empty; object removed; floor still there.
|
||||||
|
assert_eq!(
|
||||||
|
board.get(1, 1, 0),
|
||||||
|
&(Glyph::transparent(), Archetype::Empty)
|
||||||
|
);
|
||||||
|
assert!(board.object_ids_at(1, 0).is_empty());
|
||||||
|
assert_eq!(board.get(0, 1, 0).0, floor);
|
||||||
|
assert_eq!(board.glyph_at(1, 0), floor);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fresh_board_is_valid_and_reports_errors() {
|
fn fresh_board_is_valid_and_reports_errors() {
|
||||||
let mut board = open_board(1, 1, (0, 0), vec![]);
|
let mut board = open_board(1, 1, (0, 0), vec![]);
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ mod action;
|
|||||||
mod archetype;
|
mod archetype;
|
||||||
mod board;
|
mod board;
|
||||||
mod builtin_scripts;
|
mod builtin_scripts;
|
||||||
/// CP437 tile-index → character mapping ([`cp437::tile_to_char`]) for the default font.
|
|
||||||
pub mod cp437;
|
|
||||||
/// The 16 EGA/VGA named colors ([`colors::NAMED_COLORS`]), shared by scripts and the editor.
|
/// The 16 EGA/VGA named colors ([`colors::NAMED_COLORS`]), shared by scripts and the editor.
|
||||||
pub mod colors;
|
pub mod colors;
|
||||||
|
/// CP437 tile-index → character mapping ([`cp437::tile_to_char`]) for the default font.
|
||||||
|
pub mod cp437;
|
||||||
/// Procedural floor generators ([`floor::FloorGenerator`]).
|
/// Procedural floor generators ([`floor::FloorGenerator`]).
|
||||||
pub mod floor;
|
pub mod floor;
|
||||||
/// Core game types: [`board::Board`], [`glyph::Glyph`], [`archetype::Archetype`], etc.
|
/// Core game types: [`board::Board`], [`glyph::Glyph`], [`archetype::Archetype`], etc.
|
||||||
@@ -23,6 +23,7 @@ mod utils;
|
|||||||
/// World type: a named collection of boards in a single `.toml` file ([`world::World`], [`world::load`]).
|
/// World type: a named collection of boards in a single `.toml` file ([`world::World`], [`world::load`]).
|
||||||
pub mod world;
|
pub mod world;
|
||||||
|
|
||||||
|
pub use archetype::Archetype;
|
||||||
pub use board::Board;
|
pub use board::Board;
|
||||||
pub use utils::Direction;
|
pub use utils::Direction;
|
||||||
|
|
||||||
|
|||||||
+144
-37
@@ -8,15 +8,20 @@
|
|||||||
//! sub-menus. `Esc` walks back up the tree, opening the overlay
|
//! sub-menus. `Esc` walks back up the tree, opening the overlay
|
||||||
//! [editor menu](editor_menu::editor_menu_items) at the root.
|
//! [editor menu](editor_menu::editor_menu_items) at the root.
|
||||||
|
|
||||||
|
use crate::editor_menu;
|
||||||
|
use crate::editor_menu::{MenuEntry, MenuLevel};
|
||||||
|
use crate::log::{LogWidget, log_preview_line};
|
||||||
|
use crate::render::{BoardWidget, board_to_screen, screen_to_board};
|
||||||
|
use crate::ui::Ui;
|
||||||
|
use crate::utils::rgba8_to_color;
|
||||||
|
use kiln_core::cp437::tile_to_char;
|
||||||
|
use kiln_core::glyph::Glyph;
|
||||||
|
use kiln_core::log::LogLine;
|
||||||
|
use kiln_core::world::World;
|
||||||
|
use kiln_core::{Archetype, Board};
|
||||||
use kiln_ui::code_editor::{CodeEditor, CodeEditorOutcome};
|
use kiln_ui::code_editor::{CodeEditor, CodeEditorOutcome};
|
||||||
use kiln_ui::dialog::{Dialog, DialogResult, ListDialogResponse};
|
use kiln_ui::dialog::{Dialog, DialogResult, ListDialogResponse};
|
||||||
use kiln_ui::glyph_dialog::GlyphDialog;
|
use kiln_ui::glyph_dialog::GlyphDialog;
|
||||||
use crate::log::{log_preview_line, LogWidget};
|
|
||||||
use crate::render::{board_to_screen, screen_to_board, BoardWidget};
|
|
||||||
use crate::ui::Ui;
|
|
||||||
use kiln_core::Board;
|
|
||||||
use kiln_core::log::LogLine;
|
|
||||||
use kiln_core::world::World;
|
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
use ratatui::crossterm::event::Event;
|
use ratatui::crossterm::event::Event;
|
||||||
use ratatui::layout::{Constraint, Layout, Rect, Spacing};
|
use ratatui::layout::{Constraint, Layout, Rect, Spacing};
|
||||||
@@ -24,9 +29,7 @@ use ratatui::style::{Color, Style};
|
|||||||
use ratatui::symbols::merge::MergeStrategy;
|
use ratatui::symbols::merge::MergeStrategy;
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::{Block, Paragraph};
|
use ratatui::widgets::{Block, Paragraph};
|
||||||
use std::cell::Ref;
|
use std::cell::{Ref, RefMut};
|
||||||
use crate::editor_menu;
|
|
||||||
use crate::editor_menu::{MenuEntry, MenuLevel};
|
|
||||||
|
|
||||||
/// How long each half of the cursor blink lasts, in seconds.
|
/// How long each half of the cursor blink lasts, in seconds.
|
||||||
const BLINK_SECS: f32 = 0.5;
|
const BLINK_SECS: f32 = 0.5;
|
||||||
@@ -36,6 +39,8 @@ const DEFAULT_SIDEBAR_WIDTH: u16 = 24;
|
|||||||
const MIN_PANEL_WIDTH: u16 = 8;
|
const MIN_PANEL_WIDTH: u16 = 8;
|
||||||
/// The cursor glyph: a solid full block (CP437 219) drawn in light gray.
|
/// The cursor glyph: a solid full block (CP437 219) drawn in light gray.
|
||||||
const CURSOR_CHAR: char = '█';
|
const CURSOR_CHAR: char = '█';
|
||||||
|
/// Height (in rows) of the drawing-controls footer pinned to the sidebar bottom.
|
||||||
|
const DRAW_FOOTER_HEIGHT: u16 = 5;
|
||||||
|
|
||||||
/// All state for an active editing session.
|
/// All state for an active editing session.
|
||||||
///
|
///
|
||||||
@@ -61,6 +66,15 @@ pub(crate) struct EditorState {
|
|||||||
pub(crate) glyph_dialog: Option<GlyphDialog<EditorState>>,
|
pub(crate) glyph_dialog: Option<GlyphDialog<EditorState>>,
|
||||||
/// Cursor position in board cells, clamped to the board bounds.
|
/// Cursor position in board cells, clamped to the board bounds.
|
||||||
cursor: (i32, i32),
|
cursor: (i32, i32),
|
||||||
|
/// The archetype the drawing tools stamp on [`place_current`](EditorState::place_current).
|
||||||
|
/// Defaults to [`Archetype::Wall`]; the UI to change it comes later.
|
||||||
|
current_archetype: Archetype,
|
||||||
|
/// The glyph the drawing tools stamp. Starts as the current archetype's default
|
||||||
|
/// glyph and is edited via the glyph picker (`g`).
|
||||||
|
current_glyph: Glyph,
|
||||||
|
/// Whether draw mode is on: when set, moving the cursor stamps the current thing
|
||||||
|
/// into each newly-entered cell (as if `space` were pressed after the move).
|
||||||
|
draw_mode: bool,
|
||||||
/// Width of the right-hand sidebar in terminal columns (mouse-resizable).
|
/// Width of the right-hand sidebar in terminal columns (mouse-resizable).
|
||||||
sidebar_width: u16,
|
sidebar_width: u16,
|
||||||
/// Whether the blinking cursor is currently in its visible phase.
|
/// Whether the blinking cursor is currently in its visible phase.
|
||||||
@@ -87,6 +101,9 @@ impl EditorState {
|
|||||||
code_editor: None,
|
code_editor: None,
|
||||||
glyph_dialog: None,
|
glyph_dialog: None,
|
||||||
cursor: (0, 0),
|
cursor: (0, 0),
|
||||||
|
current_archetype: Archetype::Wall,
|
||||||
|
current_glyph: Archetype::Wall.default_glyph(),
|
||||||
|
draw_mode: false,
|
||||||
sidebar_width: DEFAULT_SIDEBAR_WIDTH,
|
sidebar_width: DEFAULT_SIDEBAR_WIDTH,
|
||||||
blink_on: true,
|
blink_on: true,
|
||||||
blink_timer: 0.0,
|
blink_timer: 0.0,
|
||||||
@@ -128,11 +145,15 @@ impl EditorState {
|
|||||||
/// Opens a text dialog to rename the world; OK writes [`World::name`].
|
/// Opens a text dialog to rename the world; OK writes [`World::name`].
|
||||||
pub(crate) fn open_name_dialog(&mut self) {
|
pub(crate) fn open_name_dialog(&mut self) {
|
||||||
let current = self.world.name.clone();
|
let current = self.world.name.clone();
|
||||||
self.dialog = Some(Dialog::text("World Name", current, |opt, ed: &mut EditorState| {
|
self.dialog = Some(Dialog::text(
|
||||||
if let Some(s) = opt {
|
"World Name",
|
||||||
ed.world.name = s;
|
current,
|
||||||
}
|
|opt, ed: &mut EditorState| {
|
||||||
}));
|
if let Some(s) = opt {
|
||||||
|
ed.world.name = s;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens a list dialog to choose the entry board; selecting one writes [`World::start`].
|
/// Opens a list dialog to choose the entry board; selecting one writes [`World::start`].
|
||||||
@@ -155,22 +176,27 @@ impl EditorState {
|
|||||||
pub(crate) fn open_scripts_dialog(&mut self) {
|
pub(crate) fn open_scripts_dialog(&mut self) {
|
||||||
let mut names: Vec<String> = self.world.scripts.keys().cloned().collect();
|
let mut names: Vec<String> = self.world.scripts.keys().cloned().collect();
|
||||||
names.sort();
|
names.sort();
|
||||||
self.dialog = Some(Dialog::list("Scripts", names, true, |resp, ed: &mut EditorState| {
|
self.dialog = Some(Dialog::list(
|
||||||
match resp {
|
"Scripts",
|
||||||
|
names,
|
||||||
|
true,
|
||||||
|
|resp, ed: &mut EditorState| match resp {
|
||||||
ListDialogResponse::Select(name) | ListDialogResponse::Create(name) => {
|
ListDialogResponse::Select(name) | ListDialogResponse::Create(name) => {
|
||||||
let source = ed.world.scripts.get(&name).cloned().unwrap_or_default();
|
let source = ed.world.scripts.get(&name).cloned().unwrap_or_default();
|
||||||
ed.code_editor = Some(CodeEditor::new(&name, &source));
|
ed.code_editor = Some(CodeEditor::new(&name, &source));
|
||||||
}
|
}
|
||||||
ListDialogResponse::Cancel => {}
|
ListDialogResponse::Cancel => {}
|
||||||
}
|
},
|
||||||
}));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Closes the code editor, saving its text back into the in-memory script pool
|
/// Closes the code editor, saving its text back into the in-memory script pool
|
||||||
/// (mirrors how the Name/Entry-board dialogs mutate the loaded `World`).
|
/// (mirrors how the Name/Entry-board dialogs mutate the loaded `World`).
|
||||||
pub(crate) fn close_script_editor(&mut self) {
|
pub(crate) fn close_script_editor(&mut self) {
|
||||||
if let Some(editor) = self.code_editor.take() {
|
if let Some(editor) = self.code_editor.take() {
|
||||||
self.world.scripts.insert(editor.name().to_string(), editor.text());
|
self.world
|
||||||
|
.scripts
|
||||||
|
.insert(editor.name().to_string(), editor.text());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,35 +204,54 @@ impl EditorState {
|
|||||||
pub(crate) fn open_boards_dialog(&mut self) {
|
pub(crate) fn open_boards_dialog(&mut self) {
|
||||||
let mut names: Vec<String> = self.world.boards.keys().cloned().collect();
|
let mut names: Vec<String> = self.world.boards.keys().cloned().collect();
|
||||||
names.sort();
|
names.sort();
|
||||||
self.dialog = Some(Dialog::list("Boards", names, false, |_resp, _ed: &mut EditorState| {}));
|
self.dialog = Some(Dialog::list(
|
||||||
|
"Boards",
|
||||||
|
names,
|
||||||
|
false,
|
||||||
|
|_resp, _ed: &mut EditorState| {},
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens the glyph picker seeded from the glyph currently under the cursor.
|
/// Opens the glyph picker seeded from the current drawing glyph. On OK the chosen
|
||||||
/// On OK the chosen glyph is logged (there is no paint tool yet).
|
/// glyph becomes the new drawing glyph (used by subsequent placements).
|
||||||
pub(crate) fn open_glyph_picker(&mut self) {
|
pub(crate) fn open_glyph_picker(&mut self) {
|
||||||
let glyph = {
|
|
||||||
let board = self.board();
|
|
||||||
board.glyph_at(self.cursor.0 as usize, self.cursor.1 as usize)
|
|
||||||
};
|
|
||||||
self.glyph_dialog = Some(GlyphDialog::new(
|
self.glyph_dialog = Some(GlyphDialog::new(
|
||||||
"Glyph",
|
"Glyph",
|
||||||
glyph,
|
self.current_glyph,
|
||||||
|chosen, ed: &mut EditorState| {
|
|chosen, ed: &mut EditorState| {
|
||||||
if let Some(g) = chosen {
|
if let Some(g) = chosen {
|
||||||
ed.log.push(LogLine::raw(format!(
|
ed.current_glyph = g;
|
||||||
"glyph tile={} fg=#{:02X}{:02X}{:02X} bg=#{:02X}{:02X}{:02X}",
|
|
||||||
g.tile, g.fg.r, g.fg.g, g.fg.b, g.bg.r, g.bg.g, g.bg.b
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stamps the current drawing thing (archetype + glyph) into the cell under the
|
||||||
|
/// cursor, applying the placement rules (see [`Board::place_archetype`]).
|
||||||
|
pub(crate) fn place_current(&mut self) {
|
||||||
|
let (x, y) = (self.cursor.0 as usize, self.cursor.1 as usize);
|
||||||
|
let (arch, glyph) = (self.current_archetype, self.current_glyph);
|
||||||
|
self.board_mut().place_archetype(x, y, arch, glyph);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggles draw mode (whether cursor movement auto-stamps the current thing).
|
||||||
|
pub(crate) fn toggle_draw_mode(&mut self) {
|
||||||
|
self.draw_mode = !self.draw_mode;
|
||||||
|
if self.draw_mode {
|
||||||
|
self.place_current()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Borrows the board currently being edited.
|
/// Borrows the board currently being edited.
|
||||||
pub(crate) fn board(&self) -> Ref<'_, Board> {
|
pub(crate) fn board(&self) -> Ref<'_, Board> {
|
||||||
self.world.boards[&self.board_name].borrow()
|
self.world.boards[&self.board_name].borrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mutably borrows the board currently being edited.
|
||||||
|
pub(crate) fn board_mut(&self) -> RefMut<'_, Board> {
|
||||||
|
self.world.boards[&self.board_name].borrow_mut()
|
||||||
|
}
|
||||||
|
|
||||||
/// Advances the cursor blink by `dt` seconds (the editor's only animation).
|
/// Advances the cursor blink by `dt` seconds (the editor's only animation).
|
||||||
pub(crate) fn tick(&mut self, dt: f32) {
|
pub(crate) fn tick(&mut self, dt: f32) {
|
||||||
self.blink_timer += dt;
|
self.blink_timer += dt;
|
||||||
@@ -216,22 +261,33 @@ impl EditorState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Moves the cursor by `(dx, dy)`, clamped to the board bounds.
|
/// Moves the cursor by `(dx, dy)`, clamped to the board bounds. When draw mode is
|
||||||
|
/// on and the cursor lands on a new cell, stamps the current thing there.
|
||||||
pub(crate) fn move_cursor(&mut self, dx: i32, dy: i32) {
|
pub(crate) fn move_cursor(&mut self, dx: i32, dy: i32) {
|
||||||
let (w, h) = {
|
let (w, h) = {
|
||||||
let b = self.board();
|
let b = self.board();
|
||||||
(b.width as i32, b.height as i32)
|
(b.width as i32, b.height as i32)
|
||||||
};
|
};
|
||||||
|
let before = self.cursor;
|
||||||
self.cursor.0 = (self.cursor.0 + dx).clamp(0, w - 1);
|
self.cursor.0 = (self.cursor.0 + dx).clamp(0, w - 1);
|
||||||
self.cursor.1 = (self.cursor.1 + dy).clamp(0, h - 1);
|
self.cursor.1 = (self.cursor.1 + dy).clamp(0, h - 1);
|
||||||
|
if self.draw_mode && self.cursor != before {
|
||||||
|
self.place_current();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Moves the cursor to the cell under a screen position, if it lands on the
|
/// Moves the cursor to the cell under a screen position, if it lands on the
|
||||||
/// board (right-click handling). No-op when the click is off the board.
|
/// board (right-click handling). No-op when the click is off the board. When draw
|
||||||
|
/// mode is on and the cursor lands on a new cell, stamps the current thing there.
|
||||||
pub(crate) fn set_cursor_at_screen(&mut self, sx: u16, sy: u16) {
|
pub(crate) fn set_cursor_at_screen(&mut self, sx: u16, sy: u16) {
|
||||||
let cell = screen_to_board(self.last_board_area, &self.board(), sx, sy);
|
let cell = screen_to_board(self.last_board_area, &self.board(), sx, sy);
|
||||||
if let Some((bx, by)) = cell {
|
if let Some((bx, by)) = cell {
|
||||||
self.cursor = (bx as i32, by as i32);
|
let target = (bx as i32, by as i32);
|
||||||
|
let moved = target != self.cursor;
|
||||||
|
self.cursor = target;
|
||||||
|
if self.draw_mode && moved {
|
||||||
|
self.place_current();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,7 +339,9 @@ pub(crate) fn handle_glyph_dialog_input(event: Event, ed: &mut EditorState) {
|
|||||||
///
|
///
|
||||||
/// Forwards key/mouse events to the editor; on `Exit` (Esc) it closes and saves the script.
|
/// Forwards key/mouse events to the editor; on `Exit` (Esc) it closes and saves the script.
|
||||||
pub(crate) fn handle_code_editor_input(event: Event, ed: &mut EditorState) {
|
pub(crate) fn handle_code_editor_input(event: Event, ed: &mut EditorState) {
|
||||||
let Some(editor) = ed.code_editor.as_mut() else { return };
|
let Some(editor) = ed.code_editor.as_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
if let CodeEditorOutcome::Exit = editor.handle_event(&event) {
|
if let CodeEditorOutcome::Exit = editor.handle_event(&event) {
|
||||||
ed.close_script_editor();
|
ed.close_script_editor();
|
||||||
}
|
}
|
||||||
@@ -350,7 +408,7 @@ pub(crate) fn draw_editor(frame: &mut Frame, ed: &mut EditorState, ui: &mut Ui)
|
|||||||
let mut lines: Vec<Line> = Vec::new();
|
let mut lines: Vec<Line> = Vec::new();
|
||||||
for entry in ed.current_menu().entries() {
|
for entry in ed.current_menu().entries() {
|
||||||
match entry {
|
match entry {
|
||||||
MenuEntry::Item{ key, label, .. } => {
|
MenuEntry::Item { key, label, .. } => {
|
||||||
lines.push(Line::from(vec![
|
lines.push(Line::from(vec![
|
||||||
Span::styled(format!("[{}] ", key), key_style),
|
Span::styled(format!("[{}] ", key), key_style),
|
||||||
Span::styled(label, label_style),
|
Span::styled(label, label_style),
|
||||||
@@ -373,7 +431,16 @@ pub(crate) fn draw_editor(frame: &mut Frame, ed: &mut EditorState, ui: &mut Ui)
|
|||||||
"[esc] menu",
|
"[esc] menu",
|
||||||
Style::default().fg(Color::DarkGray),
|
Style::default().fg(Color::DarkGray),
|
||||||
)));
|
)));
|
||||||
frame.render_widget(Paragraph::new(lines), sb_inner);
|
// Split the sidebar: the menu fills the top, the drawing-controls footer (the
|
||||||
|
// current thing being drawn) is pinned to the bottom regardless of menu level.
|
||||||
|
let [menu_area, footer_area] =
|
||||||
|
Layout::vertical([Constraint::Min(0), Constraint::Length(DRAW_FOOTER_HEIGHT)])
|
||||||
|
.areas(sb_inner);
|
||||||
|
frame.render_widget(Paragraph::new(lines), menu_area);
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(draw_footer_lines(ed, key_style, label_style, sep_style)),
|
||||||
|
footer_area,
|
||||||
|
);
|
||||||
|
|
||||||
// ── Log panel (full width, below board + sidebar) ─────────────────────────
|
// ── Log panel (full width, below board + sidebar) ─────────────────────────
|
||||||
if let Some(log_area) = log_area {
|
if let Some(log_area) = log_area {
|
||||||
@@ -381,6 +448,46 @@ pub(crate) fn draw_editor(frame: &mut Frame, ed: &mut EditorState, ui: &mut Ui)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds the drawing-controls footer: the current archetype name, the glyph control
|
||||||
|
/// (label + a live preview of the current drawing glyph), and the place / draw-mode
|
||||||
|
/// key hints. Shown at the bottom of the sidebar regardless of the open menu level.
|
||||||
|
fn draw_footer_lines<'a>(
|
||||||
|
ed: &EditorState,
|
||||||
|
key_style: Style,
|
||||||
|
label_style: Style,
|
||||||
|
sep_style: Style,
|
||||||
|
) -> Vec<Line<'a>> {
|
||||||
|
// A one-cell preview of the current glyph in its own fg/bg colors.
|
||||||
|
let preview = Span::styled(
|
||||||
|
tile_to_char(ed.current_glyph.tile).to_string(),
|
||||||
|
Style::default()
|
||||||
|
.fg(rgba8_to_color(ed.current_glyph.fg))
|
||||||
|
.bg(rgba8_to_color(ed.current_glyph.bg)),
|
||||||
|
);
|
||||||
|
vec![
|
||||||
|
// Separator marking off the footer from the menu above.
|
||||||
|
Line::from(Span::styled("─".repeat(64), sep_style)),
|
||||||
|
// The archetype currently being drawn (no UI to change it yet).
|
||||||
|
Line::from(Span::styled(ed.current_archetype.name(), label_style)),
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled("[g] ", key_style),
|
||||||
|
Span::styled("Glyph ", label_style),
|
||||||
|
preview,
|
||||||
|
]),
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled("[spc] ", key_style),
|
||||||
|
Span::styled("Place", label_style),
|
||||||
|
]),
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled("[tab] ", key_style),
|
||||||
|
Span::styled(
|
||||||
|
format!("Draw mode ({})", if ed.draw_mode { "on" } else { "off" }),
|
||||||
|
label_style,
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
/// Computes the board / sidebar / optional-log rects for the editor.
|
/// Computes the board / sidebar / optional-log rects for the editor.
|
||||||
///
|
///
|
||||||
/// When the log is open the screen splits vertically first (so the log spans the
|
/// When the log is open the screen splits vertically first (so the log spans the
|
||||||
|
|||||||
@@ -126,8 +126,11 @@ pub(crate) fn handle_editor_input(event: Event, term_w: u16, ed: &mut EditorStat
|
|||||||
KeyCode::Left => ed.move_cursor(-1, 0),
|
KeyCode::Left => ed.move_cursor(-1, 0),
|
||||||
KeyCode::Right => ed.move_cursor(1, 0),
|
KeyCode::Right => ed.move_cursor(1, 0),
|
||||||
KeyCode::Char('l') => ui.log.toggle(),
|
KeyCode::Char('l') => ui.log.toggle(),
|
||||||
// `g` opens the glyph picker for the cell under the cursor.
|
// `g` opens the glyph picker to edit the current drawing glyph.
|
||||||
KeyCode::Char('g') => ed.open_glyph_picker(),
|
KeyCode::Char('g') => ed.open_glyph_picker(),
|
||||||
|
// Space stamps the current thing; Tab toggles draw mode.
|
||||||
|
KeyCode::Char(' ') => ed.place_current(),
|
||||||
|
KeyCode::Tab => ed.toggle_draw_mode(),
|
||||||
// A menu letter opens its dialog; unmatched letters are ignored.
|
// A menu letter opens its dialog; unmatched letters are ignored.
|
||||||
KeyCode::Char(c) => {
|
KeyCode::Char(c) => {
|
||||||
ed.menu_key(c);
|
ed.menu_key(c);
|
||||||
|
|||||||
Reference in New Issue
Block a user