use concinnity_core::components::FrameInput;
use concinnity_core::components::InputKey;
use concinnity_core::ecs::World;
use crate::editor::hook::{EditorHook, entry_type, scroll_step, short_status};
use crate::editor::notify;
use crate::editor::panels::registry::PanelKey;
use crate::editor::panels::story;
use crate::editor::panels::story_panel::{self, StoryAction, StoryView};
use crate::editor::widget;
impl EditorHook {
pub(super) fn story_import_index(&self) -> Option<usize> {
self.entries
.iter()
.position(|e| entry_type(e) == Some("StoryImport"))
}
pub(super) fn story_source(&self) -> Option<String> {
let idx = self.story_import_index()?;
self.entries[idx]
.get("args")
.and_then(|a| a.get("source"))
.and_then(|s| s.as_str())
.filter(|s| !s.is_empty())
.map(String::from)
}
pub(in crate::editor::hook) fn load_story(&mut self, world: &mut World) {
self.story.status = None;
self.story.touched = false;
self.story.line = 0;
self.story.scroll = 0;
self.story.focus = false;
match self.story_source() {
Some(path) => {
match std::fs::read_to_string(&path) {
Ok(content) => self.story.lines = story::lines_of(&content),
Err(e) => {
self.story.lines = story::lines_of("");
self.story.status = Some(short_status(&format!("{path}: {e}")));
}
}
self.story.path = path;
}
None => {
self.story.lines = story::lines_of("");
self.story.path = String::new();
}
}
self.seed_story_line(world);
}
pub(in crate::editor::hook) fn seed_story_line(&mut self, world: &mut World) {
let text = self
.story
.lines
.get(self.story.line)
.cloned()
.unwrap_or_default();
widget::seed_field(world, story_panel::LINE_INPUT, &text);
if let Some(t) = widget::input_mut(world, story_panel::LINE_INPUT) {
t.max_len = 0;
}
}
pub(in crate::editor::hook) fn commit_story_line(&mut self, world: &World) {
if let Some(line) = self.story.lines.get_mut(self.story.line) {
let text = widget::field_text(world, story_panel::LINE_INPUT);
if *line != text {
self.story.touched = true;
*line = text;
}
}
}
pub(super) fn set_story_line(&mut self, world: &mut World, i: usize) {
self.commit_story_line(world);
self.story.line = i.min(self.story.lines.len().saturating_sub(1));
self.seed_story_line(world);
self.story.focus = true;
self.ensure_story_visible();
}
fn story_rows_shown(&self) -> usize {
story_panel::visible_rows(self.effective_size(PanelKey::Story)[1])
}
fn ensure_story_visible(&mut self) {
self.story.ensure_line_visible(self.story_rows_shown());
}
pub(in crate::editor::hook) fn scroll_story(&mut self, delta: f32) {
let max = self
.story
.lines
.len()
.saturating_sub(self.story_rows_shown());
self.story.scroll = scroll_step(self.story.scroll, delta, max);
}
pub(in crate::editor::hook) fn make_story_view(&self, mouse: [f32; 2]) -> StoryView<'_> {
StoryView {
lines: &self.story.lines,
scroll: self.story.scroll,
current: self.story.line,
focus: self.story.focus
&& !self.story.blur
&& self.panel_order.last() == Some(&PanelKey::Story),
path: &self.story.path,
status: self.story.status.as_deref(),
create: self.story_import_index().is_none(),
dirty: self.story.touched,
mouse,
}
}
pub(in crate::editor::hook) fn apply_story_action(
&mut self,
action: StoryAction,
world: &mut World,
) {
match action {
StoryAction::Line(i) => self.set_story_line(world, i),
StoryAction::Create => self.create_story(world),
StoryAction::Apply => self.apply_story(world),
StoryAction::Consume => self.story.focus = false,
}
}
pub(in crate::editor::hook) fn story_keys(&mut self, world: &mut World, input: &FrameInput) {
self.story.blur = false;
if !self.story.focus || self.story_import_index().is_none() {
return;
}
let caret = widget::input(world, story_panel::LINE_INPUT)
.map(|t| t.caret)
.unwrap_or(0);
match input.captured_key {
Some(InputKey::Enter) => {
self.commit_story_line(world);
self.story.line = story::split_line(&mut self.story.lines, self.story.line, caret);
self.seed_story_line(world);
self.set_story_caret(world, 0);
self.ensure_story_visible();
}
Some(InputKey::Up) if self.story.line > 0 => {
self.set_story_line(world, self.story.line - 1);
}
Some(InputKey::Down) if self.story.line + 1 < self.story.lines.len() => {
self.set_story_line(world, self.story.line + 1);
}
Some(InputKey::Backspace) if caret == 0 => {
self.commit_story_line(world);
if let Some((line, caret)) =
story::join_with_previous(&mut self.story.lines, self.story.line)
{
self.story.line = line;
self.seed_story_line(world);
self.set_story_caret(world, caret);
self.ensure_story_visible();
self.story.blur = true;
}
}
_ => {}
}
}
fn set_story_caret(&self, world: &mut World, caret: usize) {
if let Some(t) = widget::input_mut(world, story_panel::LINE_INPUT) {
t.caret = caret;
}
}
pub(in crate::editor::hook) fn apply_story(&mut self, world: &mut World) {
self.commit_story_line(world);
let Some(path) = self.story_source() else {
return;
};
self.story.status = None;
let content = story::join_lines(&self.story.lines);
if let Err(e) = concinnity_cook::build_only::validate_story_source(&content) {
self.story.status = Some(short_status(&e));
self.notifier
.error_with(&format!("Story rejected: {e}"), notify::Action::OpenConsole);
return;
}
if let Err(e) = std::fs::write(&path, content) {
self.story.status = Some(short_status(&format!("{path}: {e}")));
self.notifier.error_with(
&format!("Story write failed: {path}: {e}"),
notify::Action::OpenConsole,
);
return;
}
self.notifier.success(&format!("Applied story {path}"));
self.story.touched = false;
self.require_rebuild();
}
pub(in crate::editor::hook) fn create_story(&mut self, world: &mut World) {
if self.story_import_index().is_some() {
return;
}
let path = free_story_path();
if let Err(e) = std::fs::write(&path, story::STARTER_STORY) {
self.story.status = Some(short_status(&format!("{path}: {e}")));
return;
}
let name = self.unique_name("story");
self.entries.push(serde_json::json!({
"type": "StoryImport", "args": { "$id": name, "source": path },
}));
self.mark_changed();
self.load_story(world);
self.story.focus = true;
}
}
fn free_story_path() -> String {
if !std::path::Path::new("story.md").exists() {
return "story.md".to_string();
}
let mut i = 1;
loop {
let candidate = format!("story_{i}.md");
if !std::path::Path::new(&candidate).exists() {
return candidate;
}
i += 1;
}
}