ui crate, editor
This commit is contained in:
+70
-32
@@ -7,6 +7,7 @@
|
||||
//! title and letter-keyed choices that open [dialogs](kiln_ui::dialog). `Esc` walks
|
||||
//! back up the tree, opening the overlay [editor menu](editor_menu_items) at the root.
|
||||
|
||||
use kiln_ui::code_editor::{CodeEditor, CodeEditorOutcome};
|
||||
use kiln_ui::dialog::{Dialog, DialogResult, ListDialogResponse};
|
||||
use crate::log::{LogWidget, log_preview_line};
|
||||
use crate::menu::{MenuItem, MenuKey};
|
||||
@@ -84,6 +85,9 @@ pub(crate) struct EditorState {
|
||||
/// The currently open dialog overlay, if any. While `Some`, the editor is in
|
||||
/// [`InputMode::Dialog`](crate::input::InputMode::Dialog).
|
||||
pub(crate) dialog: Option<Dialog<EditorState>>,
|
||||
/// The open script code editor, if any. While `Some` it replaces the board view and
|
||||
/// the editor is in [`InputMode::CodeEditor`](crate::input::InputMode::CodeEditor).
|
||||
pub(crate) code_editor: Option<CodeEditor>,
|
||||
/// Cursor position in board cells, clamped to the board bounds.
|
||||
cursor: (i32, i32),
|
||||
/// Width of the right-hand sidebar in terminal columns (mouse-resizable).
|
||||
@@ -109,6 +113,7 @@ impl EditorState {
|
||||
board_name,
|
||||
menu: vec![MenuLevel::World],
|
||||
dialog: None,
|
||||
code_editor: None,
|
||||
cursor: (0, 0),
|
||||
sidebar_width: DEFAULT_SIDEBAR_WIDTH,
|
||||
blink_on: true,
|
||||
@@ -202,15 +207,31 @@ impl EditorState {
|
||||
));
|
||||
}
|
||||
|
||||
/// Opens a list dialog of the world's scripts. Select-only for now (no side effect).
|
||||
/// Opens a list dialog of the world's scripts; selecting one opens it in the code editor.
|
||||
fn open_scripts_dialog(&mut self) {
|
||||
let mut names: Vec<String> = self.world.scripts.keys().cloned().collect();
|
||||
names.sort();
|
||||
self.dialog = Some(Dialog::list("Scripts", names, false, |resp, ed: &mut EditorState| {
|
||||
ed.log.push(LogLine::raw(format!("Script dialog: {:?}", resp)))
|
||||
if let ListDialogResponse::Select(name) = resp {
|
||||
ed.open_script_editor(&name);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/// Opens the code editor on the named script (empty if the name is unknown).
|
||||
fn open_script_editor(&mut self, name: &str) {
|
||||
let source = self.world.scripts.get(name).cloned().unwrap_or_default();
|
||||
self.code_editor = Some(CodeEditor::new(name, &source));
|
||||
}
|
||||
|
||||
/// 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`).
|
||||
pub(crate) fn close_script_editor(&mut self) {
|
||||
if let Some(editor) = self.code_editor.take() {
|
||||
self.world.scripts.insert(editor.name().to_string(), editor.text());
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens a list dialog of the world's boards. Select-only for now (no side effect).
|
||||
fn open_boards_dialog(&mut self) {
|
||||
let mut names: Vec<String> = self.world.boards.keys().cloned().collect();
|
||||
@@ -298,43 +319,60 @@ pub(crate) fn handle_dialog_input(event: Event, ed: &mut EditorState) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles an input event while the code editor is open ([`InputMode::CodeEditor`](crate::input::InputMode::CodeEditor)).
|
||||
///
|
||||
/// 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) {
|
||||
let Some(editor) = ed.code_editor.as_mut() else { return };
|
||||
if let CodeEditorOutcome::Exit = editor.handle_event(&event) {
|
||||
ed.close_script_editor();
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the editor: the board (with a blinking cursor and coordinate readout),
|
||||
/// the right-hand sidebar, and — when open — the bottom log panel spanning the
|
||||
/// full width (board and sidebar sit side-by-side above it).
|
||||
pub(crate) fn draw_editor(frame: &mut Frame, ed: &mut EditorState, ui: &mut Ui) {
|
||||
let (board_area, sidebar_area, log_area) = editor_layout(frame.area(), ui, ed.sidebar_width);
|
||||
|
||||
// ── Board, cursor, coordinate readout ────────────────────────────────────
|
||||
let inner = {
|
||||
let board = ed.board();
|
||||
// Title carries the board name (top-left) and live cursor coords (top-right).
|
||||
let mut block = Block::bordered()
|
||||
.title(format!(" {} ", board.name))
|
||||
.title_top(Line::from(format!(" ({}, {}) ", ed.cursor.0, ed.cursor.1)).right_aligned())
|
||||
.merge_borders(MergeStrategy::Exact);
|
||||
// When the log is closed, preview the latest line in the bottom border.
|
||||
if log_area.is_none() {
|
||||
block = block.title_bottom(log_preview_line(&ed.log).left_aligned());
|
||||
}
|
||||
let inner = block.inner(board_area);
|
||||
frame.render_widget(block, board_area);
|
||||
frame.render_widget(BoardWidget::new(&board), inner);
|
||||
|
||||
// Overlay the cursor during its visible phase (a light-gray solid block).
|
||||
if ed.blink_on {
|
||||
let (cx, cy) = (ed.cursor.0 as usize, ed.cursor.1 as usize);
|
||||
if let Some((sx, sy)) = board_to_screen(inner, &board, cx, cy)
|
||||
&& let Some(cell) = frame.buffer_mut().cell_mut((sx, sy))
|
||||
{
|
||||
cell.set_char(CURSOR_CHAR)
|
||||
.set_fg(Color::Rgb(180, 180, 180))
|
||||
.set_bg(Color::Black);
|
||||
// ── Board area: the open script editor, else the board with its cursor ────
|
||||
if let Some(editor) = ed.code_editor.as_mut() {
|
||||
// A script is being edited: the code editor takes the board's place.
|
||||
editor.draw(frame, board_area);
|
||||
} else {
|
||||
let inner = {
|
||||
let board = ed.board();
|
||||
// Title carries the board name (top-left) and live cursor coords (top-right).
|
||||
let mut block = Block::bordered()
|
||||
.title(format!(" {} ", board.name))
|
||||
.title_top(
|
||||
Line::from(format!(" ({}, {}) ", ed.cursor.0, ed.cursor.1)).right_aligned(),
|
||||
)
|
||||
.merge_borders(MergeStrategy::Exact);
|
||||
// When the log is closed, preview the latest line in the bottom border.
|
||||
if log_area.is_none() {
|
||||
block = block.title_bottom(log_preview_line(&ed.log).left_aligned());
|
||||
}
|
||||
}
|
||||
inner
|
||||
};
|
||||
// Record the board rect for right-click hit-testing (board borrow now dropped).
|
||||
ed.last_board_area = inner;
|
||||
let inner = block.inner(board_area);
|
||||
frame.render_widget(block, board_area);
|
||||
frame.render_widget(BoardWidget::new(&board), inner);
|
||||
|
||||
// Overlay the cursor during its visible phase (a light-gray solid block).
|
||||
if ed.blink_on {
|
||||
let (cx, cy) = (ed.cursor.0 as usize, ed.cursor.1 as usize);
|
||||
if let Some((sx, sy)) = board_to_screen(inner, &board, cx, cy)
|
||||
&& let Some(cell) = frame.buffer_mut().cell_mut((sx, sy))
|
||||
{
|
||||
cell.set_char(CURSOR_CHAR)
|
||||
.set_fg(Color::Rgb(180, 180, 180))
|
||||
.set_bg(Color::Black);
|
||||
}
|
||||
}
|
||||
inner
|
||||
};
|
||||
// Record the board rect for right-click hit-testing (board borrow now dropped).
|
||||
ed.last_board_area = inner;
|
||||
}
|
||||
|
||||
// ── Sidebar: the current menu (breadcrumb title + letter-keyed choices) ───
|
||||
let sb_block = Block::bordered()
|
||||
|
||||
@@ -31,6 +31,8 @@ pub enum InputMode {
|
||||
Editor,
|
||||
/// A dialog overlay is open over the editor; all input drives the dialog.
|
||||
Dialog,
|
||||
/// The script code editor is open; all input drives the editor.
|
||||
CodeEditor,
|
||||
/// An animation is running and has not claimed any input mode; all input is discarded.
|
||||
Ignore,
|
||||
/// Reserved for a post-transition freeze; nothing currently produces this mode.
|
||||
@@ -54,6 +56,8 @@ pub(crate) fn current_input_mode(mode: &Mode, ui: &Ui) -> InputMode {
|
||||
Mode::Edit(ed) => {
|
||||
if ed.dialog.is_some() {
|
||||
InputMode::Dialog
|
||||
} else if ed.code_editor.is_some() {
|
||||
InputMode::CodeEditor
|
||||
} else {
|
||||
InputMode::Editor
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ mod ui;
|
||||
mod utils;
|
||||
|
||||
use crate::animation::{AnimWidget, AnimationLayer};
|
||||
use crate::editor::{EditorState, draw_editor, handle_dialog_input};
|
||||
use crate::editor::{EditorState, draw_editor, handle_code_editor_input, handle_dialog_input};
|
||||
use crate::input::{
|
||||
InputMode, current_input_mode, handle_board_input, handle_editor_input, handle_menu_input,
|
||||
handle_scroll_input,
|
||||
@@ -40,7 +40,8 @@ use kiln_core::log::LogLine;
|
||||
use kiln_core::world;
|
||||
use ratatui::Frame;
|
||||
use ratatui::crossterm::event::{
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyEventKind,
|
||||
self, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
|
||||
Event, KeyEventKind,
|
||||
};
|
||||
use ratatui::crossterm::execute;
|
||||
use ratatui::layout::{Constraint, Layout, Rect, Spacing};
|
||||
@@ -77,6 +78,9 @@ fn main() -> ExitCode {
|
||||
// resized by dragging its divider. Disabled again before restoring.
|
||||
let _ = execute!(io::stdout(), EnableMouseCapture);
|
||||
|
||||
// Enable bracketed paste so the code editor receives terminal pastes as one event.
|
||||
let _ = execute!(io::stdout(), EnableBracketedPaste);
|
||||
|
||||
// Detect terminal capabilities (now that we're in raw mode) and enable the
|
||||
// Kitty keyboard protocol when it's available, so scripts can rely on the
|
||||
// richer bindings it provides. Disabled again before restoring the terminal.
|
||||
@@ -105,6 +109,7 @@ fn main() -> ExitCode {
|
||||
if caps.keyboard_enhancement {
|
||||
let _ = term::pop_kitty_flags();
|
||||
}
|
||||
let _ = execute!(io::stdout(), DisableBracketedPaste);
|
||||
let _ = execute!(io::stdout(), DisableMouseCapture);
|
||||
ratatui::restore();
|
||||
|
||||
@@ -164,6 +169,7 @@ fn run(terminal: &mut ratatui::DefaultTerminal, mode: &mut Mode, ui: &mut Ui) ->
|
||||
Mode::Edit(ed) => match input_mode {
|
||||
InputMode::Editor => handle_editor_input(event, size.width, ed, ui),
|
||||
InputMode::Dialog => handle_dialog_input(event, ed),
|
||||
InputMode::CodeEditor => handle_code_editor_input(event, ed),
|
||||
_ => unreachable!(
|
||||
"Menu and Ignore handled above; Board/Scroll/Frozen input modes only occur in Play mode"
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user