515 lines
21 KiB
Rust
515 lines
21 KiB
Rust
//! World editor mode: state, rendering, and the editor pause menu.
|
|
//!
|
|
//! The editor is a separate top-level [`Mode`](crate::mode::Mode): it holds its
|
|
//! own freshly reloaded [`World`] and never ticks the game. The board is shown
|
|
//! with a blinking cursor (moved with the arrow keys or a right-click) and a
|
|
//! resizable right-hand sidebar that hosts the [menu tree](MenuLevel): a breadcrumb
|
|
//! title and letter-keyed choices that open [dialogs](kiln_ui::dialog) or descend into
|
|
//! sub-menus. `Esc` walks back up the tree, opening the overlay
|
|
//! [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::dialog::{Dialog, DialogResult, ListDialogResponse};
|
|
use kiln_ui::glyph_dialog::GlyphDialog;
|
|
use ratatui::Frame;
|
|
use ratatui::crossterm::event::Event;
|
|
use ratatui::layout::{Constraint, Layout, Rect, Spacing};
|
|
use ratatui::style::{Color, Style};
|
|
use ratatui::symbols::merge::MergeStrategy;
|
|
use ratatui::text::{Line, Span};
|
|
use ratatui::widgets::{Block, Paragraph};
|
|
use std::cell::{Ref, RefMut};
|
|
|
|
/// How long each half of the cursor blink lasts, in seconds.
|
|
const BLINK_SECS: f32 = 0.5;
|
|
/// Default sidebar width in terminal columns (borders included).
|
|
const DEFAULT_SIDEBAR_WIDTH: u16 = 24;
|
|
/// Smallest width either the sidebar or the board may shrink to when resizing.
|
|
const MIN_PANEL_WIDTH: u16 = 8;
|
|
/// The cursor glyph: a solid full block (CP437 219) drawn in light gray.
|
|
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.
|
|
///
|
|
/// Built by reloading the world file, so the running game has no bearing on what
|
|
/// the editor shows. Holds no [`GameState`](kiln_core::game::GameState) — editing
|
|
/// never ticks the game.
|
|
pub(crate) struct EditorState {
|
|
/// The freshly reloaded world being edited.
|
|
pub(crate) world: World,
|
|
/// Key (in [`World::boards`]) of the board currently being edited.
|
|
board_name: String,
|
|
/// The sidebar menu navigation stack; the last element is the current level.
|
|
/// Starts as `[MenuLevel::Main]` and is never emptied below that root.
|
|
pub(crate) menu: Vec<MenuLevel>,
|
|
/// 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>,
|
|
/// The open glyph picker overlay, if any. While `Some` the editor is in
|
|
/// [`InputMode::GlyphDialog`](crate::input::InputMode::GlyphDialog).
|
|
pub(crate) glyph_dialog: Option<GlyphDialog<EditorState>>,
|
|
/// Cursor position in board cells, clamped to the board bounds.
|
|
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).
|
|
sidebar_width: u16,
|
|
/// Whether the blinking cursor is currently in its visible phase.
|
|
blink_on: bool,
|
|
/// Time accumulated toward the next blink toggle.
|
|
blink_timer: f32,
|
|
/// Inner board rect from the last [`draw_editor`] call, used to convert a
|
|
/// right-click's screen position back to a board cell (written during render,
|
|
/// read during input — the same pattern as `ScrollOverlayState::max_scroll`).
|
|
last_board_area: Rect,
|
|
/// Editor-side log lines (the editor has no game log to draw from).
|
|
log: Vec<LogLine>,
|
|
}
|
|
|
|
impl EditorState {
|
|
/// Builds an editor session for `world`, starting on its designated start board.
|
|
pub(crate) fn new(world: World) -> Self {
|
|
let board_name = world.start.clone();
|
|
Self {
|
|
world,
|
|
board_name,
|
|
menu: vec![MenuLevel::Main],
|
|
dialog: None,
|
|
code_editor: None,
|
|
glyph_dialog: None,
|
|
cursor: (0, 0),
|
|
current_archetype: Archetype::Wall,
|
|
current_glyph: Archetype::Wall.default_glyph(),
|
|
draw_mode: false,
|
|
sidebar_width: DEFAULT_SIDEBAR_WIDTH,
|
|
blink_on: true,
|
|
blink_timer: 0.0,
|
|
last_board_area: Rect::default(),
|
|
log: vec![LogLine::raw("editing — press [esc] for the menu")],
|
|
}
|
|
}
|
|
|
|
/// The breadcrumb for the current menu path, e.g. `"World"` or `"World > Scripts"`.
|
|
fn breadcrumb(&self) -> String {
|
|
self.menu
|
|
.iter()
|
|
.map(|l| l.title())
|
|
.collect::<Vec<_>>()
|
|
.join(" > ")
|
|
}
|
|
|
|
/// The menu level currently shown in the sidebar (top of the stack).
|
|
fn current_menu(&self) -> MenuLevel {
|
|
*self.menu.last().expect("menu stack is never empty")
|
|
}
|
|
|
|
/// Handles `Esc` in the menu: pops to the parent level, or — at the root world
|
|
/// menu — opens the existing overlay editor menu (Play / Quit / Resume).
|
|
pub(crate) fn menu_escape(&mut self, ui: &mut Ui) {
|
|
if self.menu.len() > 1 {
|
|
self.menu.pop();
|
|
} else {
|
|
ui.open_menu(editor_menu::editor_menu_items());
|
|
}
|
|
}
|
|
|
|
/// Dispatches a letter key against the current menu level: asks MenuLevel what it should
|
|
/// do with the key, and passes an &mut EditorState to do it.
|
|
pub(crate) fn menu_key(&mut self, c: char) {
|
|
self.current_menu().perform_key(c, self);
|
|
}
|
|
|
|
/// Opens a text dialog to rename the world; OK writes [`World::name`].
|
|
pub(crate) fn open_name_dialog(&mut self) {
|
|
let current = self.world.name.clone();
|
|
self.dialog = Some(Dialog::text(
|
|
"World Name",
|
|
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`].
|
|
pub(crate) fn open_entry_dialog(&mut self) {
|
|
let mut names: Vec<String> = self.world.boards.keys().cloned().collect();
|
|
names.sort();
|
|
self.dialog = Some(Dialog::list(
|
|
"Entry Board",
|
|
names,
|
|
false,
|
|
|resp, ed: &mut EditorState| {
|
|
if let ListDialogResponse::Select(s) = resp {
|
|
ed.world.start = s;
|
|
}
|
|
},
|
|
));
|
|
}
|
|
|
|
/// Opens a list dialog of the world's scripts; selecting one opens it in the code editor.
|
|
pub(crate) 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,
|
|
true,
|
|
|resp, ed: &mut EditorState| match resp {
|
|
ListDialogResponse::Select(name) | ListDialogResponse::Create(name) => {
|
|
let source = ed.world.scripts.get(&name).cloned().unwrap_or_default();
|
|
ed.code_editor = Some(CodeEditor::new(&name, &source));
|
|
}
|
|
ListDialogResponse::Cancel => {}
|
|
},
|
|
));
|
|
}
|
|
|
|
/// 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).
|
|
pub(crate) fn open_boards_dialog(&mut self) {
|
|
let mut names: Vec<String> = self.world.boards.keys().cloned().collect();
|
|
names.sort();
|
|
self.dialog = Some(Dialog::list(
|
|
"Boards",
|
|
names,
|
|
false,
|
|
|_resp, _ed: &mut EditorState| {},
|
|
));
|
|
}
|
|
|
|
/// Opens the glyph picker seeded from the current drawing glyph. On OK the chosen
|
|
/// glyph becomes the new drawing glyph (used by subsequent placements).
|
|
pub(crate) fn open_glyph_picker(&mut self) {
|
|
self.glyph_dialog = Some(GlyphDialog::new(
|
|
"Glyph",
|
|
self.current_glyph,
|
|
|chosen, ed: &mut EditorState| {
|
|
if let Some(g) = chosen {
|
|
ed.current_glyph = g;
|
|
}
|
|
},
|
|
));
|
|
}
|
|
|
|
/// 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.
|
|
pub(crate) fn board(&self) -> Ref<'_, Board> {
|
|
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).
|
|
pub(crate) fn tick(&mut self, dt: f32) {
|
|
self.blink_timer += dt;
|
|
if self.blink_timer >= BLINK_SECS {
|
|
self.blink_timer -= BLINK_SECS;
|
|
self.blink_on = !self.blink_on;
|
|
}
|
|
}
|
|
|
|
/// 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) {
|
|
let (w, h) = {
|
|
let b = self.board();
|
|
(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.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
|
|
/// 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) {
|
|
let cell = screen_to_board(self.last_board_area, &self.board(), sx, sy);
|
|
if let Some((bx, by)) = cell {
|
|
let target = (bx as i32, by as i32);
|
|
let moved = target != self.cursor;
|
|
self.cursor = target;
|
|
if self.draw_mode && moved {
|
|
self.place_current();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Resizes the sidebar to `target` columns, clamped so neither the sidebar nor
|
|
/// the board can shrink below [`MIN_PANEL_WIDTH`] (mirrors [`LogState::resize`]).
|
|
pub(crate) fn resize_sidebar(&mut self, target: u16, total_w: u16) {
|
|
let max = total_w.saturating_sub(MIN_PANEL_WIDTH).max(MIN_PANEL_WIDTH);
|
|
self.sidebar_width = target.clamp(MIN_PANEL_WIDTH, max);
|
|
}
|
|
}
|
|
|
|
/// Handles an input event while a dialog is open ([`InputMode::Dialog`](crate::input::InputMode::Dialog)).
|
|
///
|
|
/// Forwards the event to the dialog; on `Submit`/`Cancel` it takes the dialog out of
|
|
/// [`EditorState::dialog`] (so the callback receives an un-borrowed `&mut EditorState`)
|
|
/// and fires the callback via [`Dialog::finish`].
|
|
pub(crate) fn handle_dialog_input(event: Event, ed: &mut EditorState) {
|
|
let Some(dialog) = ed.dialog.as_mut() else {
|
|
return;
|
|
};
|
|
match dialog.handle_event(&event) {
|
|
DialogResult::Continue => {}
|
|
result => {
|
|
let dialog = ed.dialog.take().expect("dialog present");
|
|
dialog.finish(result, ed);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Handles an input event while the glyph picker is open ([`InputMode::GlyphDialog`](crate::input::InputMode::GlyphDialog)).
|
|
///
|
|
/// Mirrors [`handle_dialog_input`]: forwards to the picker and, on `Submit`/`Cancel`,
|
|
/// takes it out of [`EditorState::glyph_dialog`] and fires its callback via
|
|
/// [`GlyphDialog::finish`].
|
|
pub(crate) fn handle_glyph_dialog_input(event: Event, ed: &mut EditorState) {
|
|
let Some(dialog) = ed.glyph_dialog.as_mut() else {
|
|
return;
|
|
};
|
|
match dialog.handle_event(&event) {
|
|
DialogResult::Continue => {}
|
|
result => {
|
|
let dialog = ed.glyph_dialog.take().expect("glyph dialog present");
|
|
dialog.finish(result, ed);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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 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());
|
|
}
|
|
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()
|
|
.title(format!(" {} ", ed.breadcrumb()))
|
|
.merge_borders(MergeStrategy::Exact);
|
|
let sb_inner = sb_block.inner(sidebar_area);
|
|
frame.render_widget(sb_block, sidebar_area);
|
|
// One line per item (`[k] Label`); an item with a current value (e.g. the
|
|
// world name) gets a second, indented line beneath it in a distinct color.
|
|
// Blank/Separator entries render as spacers to group items.
|
|
let key_style = Style::default().fg(Color::Rgb(150, 200, 255));
|
|
let label_style = Style::default().fg(Color::White);
|
|
let value_style = Style::default().fg(Color::Rgb(200, 180, 120));
|
|
let sep_style = Style::default().fg(Color::DarkGray);
|
|
let mut lines: Vec<Line> = Vec::new();
|
|
for entry in ed.current_menu().entries() {
|
|
match entry {
|
|
MenuEntry::Item { key, label, .. } => {
|
|
lines.push(Line::from(vec![
|
|
Span::styled(format!("[{}] ", key), key_style),
|
|
Span::styled(label, label_style),
|
|
]));
|
|
}
|
|
MenuEntry::Blank => lines.push(Line::from("")),
|
|
// A horizontal rule spanning the sidebar's inner width.
|
|
MenuEntry::Separator => lines.push(Line::from(Span::styled(
|
|
"─".repeat(sb_inner.width as usize),
|
|
sep_style,
|
|
))),
|
|
MenuEntry::CurrentValue(val_f) => {
|
|
let val = val_f(ed);
|
|
lines.push(Line::from(Span::styled(format!(" {val}"), value_style)));
|
|
}
|
|
}
|
|
}
|
|
lines.push(Line::from(""));
|
|
lines.push(Line::from(Span::styled(
|
|
"[esc] menu",
|
|
Style::default().fg(Color::DarkGray),
|
|
)));
|
|
// 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) ─────────────────────────
|
|
if let Some(log_area) = log_area {
|
|
frame.render_stateful_widget(LogWidget::new(&ed.log), log_area, &mut ui.log);
|
|
}
|
|
}
|
|
|
|
/// 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.
|
|
///
|
|
/// When the log is open the screen splits vertically first (so the log spans the
|
|
/// full width), then the top region splits horizontally into board + sidebar.
|
|
/// When closed, only the horizontal board/sidebar split applies. `Overlap(1)`
|
|
/// makes adjacent panels share their dividing border.
|
|
fn editor_layout(area: Rect, ui: &Ui, sidebar_width: u16) -> (Rect, Rect, Option<Rect>) {
|
|
let split_h = |top: Rect| {
|
|
Layout::horizontal([Constraint::Min(3), Constraint::Length(sidebar_width)])
|
|
.spacing(Spacing::Overlap(1))
|
|
.areas::<2>(top)
|
|
};
|
|
if ui.log.open {
|
|
let [top, log_area] =
|
|
Layout::vertical([Constraint::Min(3), Constraint::Length(ui.log.height)])
|
|
.spacing(Spacing::Overlap(1))
|
|
.areas(area);
|
|
let [board_area, sidebar_area] = split_h(top);
|
|
(board_area, sidebar_area, Some(log_area))
|
|
} else {
|
|
let [board_area, sidebar_area] = split_h(area);
|
|
(board_area, sidebar_area, None)
|
|
}
|
|
}
|