use concinnity_cook::authoring::world::{entry_handle, find_entry, set_entry_id};
use concinnity_core::components::FrameInput;
use concinnity_core::components::InputKey;
use concinnity_core::ecs::World;
use std::sync::atomic::Ordering;
use crate::editor::entry_list::build_text;
use crate::editor::hook::{EditorHook, declared_id, entry_type, scroll_step};
use crate::editor::notify;
use crate::editor::panels::console;
use crate::editor::panels::console_panel::{self, ConsoleAction, ConsoleView};
use crate::editor::panels::registry::PanelKey;
use crate::editor::viewport::snap;
use crate::editor::widget;
impl EditorHook {
pub(in crate::editor::hook) fn toggle_console(&mut self, world: &mut World) {
self.open[PanelKey::Console] = !self.open[PanelKey::Console];
if self.open[PanelKey::Console] {
self.console.focus = true;
self.console.pinned = true;
widget::seed_field(world, console_panel::INPUT, "");
if let Some(t) = widget::input_mut(world, console_panel::INPUT) {
t.max_len = 0;
}
self.focus_panel(PanelKey::Console);
} else {
self.console.focus = false;
}
}
pub(in crate::editor::hook) fn drive_console_toggle(
&mut self,
input: &FrameInput,
world: &mut World,
) {
if !input.pressed_fresh(InputKey::Backtick) || input.ctrl {
return;
}
if self.sim.playing() || self.non_console_text_focus() {
return;
}
self.toggle_console(world);
if self.open[PanelKey::Console] {
self.console.blur = true;
}
}
pub(in crate::editor::hook) fn console_window(
&self,
) -> (Vec<console::ConsoleLine>, usize, usize) {
let shown = console_panel::visible_lines(self.effective_size(PanelKey::Console)[1]);
let total = self.console_sink.len();
let max_first = total.saturating_sub(shown);
let first = if self.console.pinned {
max_first
} else {
self.console.scroll.min(max_first)
};
(self.console_sink.window(first, shown), total, first)
}
pub(in crate::editor::hook) fn make_console_view<'a>(
&self,
lines: &'a [console::ConsoleLine],
total: usize,
first: usize,
ghost: &'a str,
mouse: [f32; 2],
) -> ConsoleView<'a> {
ConsoleView {
lines,
total,
first,
focus: self.console.focus
&& !self.console.blur
&& self.panel_order.last() == Some(&PanelKey::Console),
ghost,
mouse,
}
}
pub(in crate::editor::hook) fn console_ghost(&self, world: &World) -> String {
let text = widget::field_text(world, console_panel::INPUT);
console::del_ghost(&text, self.entries.iter().filter_map(declared_id)).unwrap_or_default()
}
pub(in crate::editor::hook) fn scroll_console(&mut self, delta: f32) {
let shown = console_panel::visible_lines(self.effective_size(PanelKey::Console)[1]);
let max = self.console_sink.len().saturating_sub(shown);
let cur = if self.console.pinned {
max
} else {
self.console.scroll.min(max)
};
let next = scroll_step(cur, delta, max);
self.console.scroll = next;
self.console.pinned = next >= max;
}
pub(in crate::editor::hook) fn apply_console_action(
&mut self,
action: ConsoleAction,
_world: &mut World,
) {
match action {
ConsoleAction::FocusInput => self.console.focus = true,
ConsoleAction::Consume => self.console.focus = false,
}
}
pub(in crate::editor::hook) fn console_keys(&mut self, world: &mut World, input: &FrameInput) {
if !self.console.focus {
return;
}
for press in input.key_presses() {
match press.key {
InputKey::Enter if !press.repeat => {
let line = widget::field_text(world, console_panel::INPUT);
widget::seed_field(world, console_panel::INPUT, "");
let line = line.trim().to_string();
if !line.is_empty() {
self.run_console_line(world, &line);
}
}
InputKey::Tab => self.accept_console_ghost(world),
InputKey::Right => {
let at_end = widget::input(world, console_panel::INPUT)
.map(|t| t.caret >= t.content.chars().count())
.unwrap_or(false);
if at_end {
self.accept_console_ghost(world);
}
}
_ => {}
}
}
}
fn accept_console_ghost(&mut self, world: &mut World) {
let text = widget::field_text(world, console_panel::INPUT);
if let Some(ghost) = console::del_ghost(&text, self.entries.iter().filter_map(declared_id))
{
widget::focus_field_with(world, console_panel::INPUT, &format!("{text}{ghost}"));
}
}
pub(in crate::editor::hook) fn run_console_line(&mut self, world: &mut World, line: &str) {
self.console_sink
.push(console::Severity::Command, &format!("> {line}"));
self.console.pinned = true;
match console::parse_command(line) {
Ok(console::Command::Echo(_)) => {}
Ok(console::Command::Add { target, name }) => {
self.console_add(&target, name.as_deref());
}
Ok(console::Command::Del { name }) => self.console_del(&name),
Ok(console::Command::Cook) => self.console_build(),
Ok(console::Command::Snap(cmd)) => self.console_snap(cmd),
Ok(console::Command::Dup) => {
let made = self.duplicate_selection();
self.console_sink.info(&format!("duplicated {made}"));
}
Ok(console::Command::Floor) => {
let moved = self.drop_selection_to_floor(world);
self.console_sink.info(&format!("dropped {moved}"));
}
Ok(console::Command::Select(cmd)) => self.console_select(cmd, world),
Ok(console::Command::Export { name, bake }) => {
self.console_export(name.as_deref(), bake);
}
Ok(console::Command::Help) => {
for l in console::help_lines() {
self.console_sink.info(&l);
}
}
Err(e) => self.console_sink.error(&e),
}
}
fn console_add(&mut self, target: &str, name: Option<&str>) {
if crate::authoring::is_path_like(target) && !std::path::Path::new(target).is_file() {
self.console_sink.error(&format!("{target}: no such file"));
return;
}
let mut new_entries = match crate::authoring::resolve_add_target(target) {
Ok(entries) => entries,
Err(e) => {
self.console_sink.error(&e.to_string());
return;
}
};
if let Some(n) = name {
crate::authoring::apply_id_override(&mut new_entries, n);
}
if let Some((name, source)) =
crate::authoring::try_retarget_environment_map(&mut self.entries, &new_entries)
{
self.console_sink.info(&format!("{name} now uses {source}"));
self.mark_changed();
return;
}
for entry in &mut new_entries {
if let Some(base) = declared_id(entry).map(str::to_string) {
set_entry_id(entry, &self.unique_from(&base));
}
let ty = entry_type(entry).unwrap_or("?").to_string();
self.entries.push(entry.clone());
let handle = entry_handle(&self.entries, self.entries.len() - 1).unwrap_or_default();
self.console_sink.info(&format!("added '{handle}' ({ty})"));
}
self.mark_changed();
}
fn console_del(&mut self, name: &str) {
let Some(idx) = find_entry(&self.entries, name) else {
self.console_sink
.error(&format!("no authored asset named '{name}'"));
return;
};
let ty = entry_type(&self.entries[idx]).unwrap_or("?").to_string();
self.remove_entry_at(idx);
self.assets.row_menu = None;
self.console_sink.info(&format!("removed '{name}' ({ty})"));
}
fn console_snap(&mut self, cmd: console::SnapCmd) {
use console::{SnapCmd, SnapSet};
fn set(snap: &mut snap::Snap, s: SnapSet) {
match s {
SnapSet::Enable(on) => snap.enabled = on,
SnapSet::Step(step) => {
snap.step = step;
snap.enabled = true;
}
}
}
match cmd {
SnapCmd::Status => {}
SnapCmd::All(on) => {
self.snap.translate.enabled = on;
self.snap.rotate.enabled = on;
}
SnapCmd::Move(s) => set(&mut self.snap.translate, s),
SnapCmd::Rotate(s) => set(&mut self.snap.rotate, s),
}
self.console_sink.info(&self.snap.describe());
}
fn console_build(&mut self) {
if self.console_build_running.swap(true, Ordering::SeqCst) {
self.console_sink.warn("cook already running");
return;
}
let content = match build_text(&self.entries) {
Ok(c) => c,
Err(e) => {
self.console_build_running.store(false, Ordering::SeqCst);
self.console_sink.error(&format!("cook failed: {e}"));
return;
}
};
let sink = self.console_sink.clone();
let toasts = self.notifier.clone();
sink.info("cook started");
self.spawn_cook_worker("Cooking", content, move |outcome, secs| match outcome {
Ok(()) => {
sink.info(&format!("cook finished in {secs:.1}s"));
toasts.success(&format!("Cook finished in {secs:.1}s"));
}
Err(e) => {
sink.error(&format!("cook failed: {e}"));
toasts.error_with(&format!("Cook failed: {e}"), notify::Action::OpenConsole);
}
});
}
}