use std::sync::OnceLock;
use concinnity_cook::authoring::registry::RegisteredType;
use serde_json::Value;
use super::*;
use crate::editor::behavior::clip;
use crate::editor::behavior::edit::{self, Pick};
use crate::editor::behavior::fault;
use crate::editor::behavior::fields;
use crate::editor::behavior::filter;
use crate::editor::behavior::graph::{self, CardKind, Chart};
use crate::editor::behavior::navigate;
use crate::editor::behavior::outline::{self, Row};
use crate::editor::behavior::pulse;
use crate::editor::behavior::relations;
use crate::editor::behavior_chart;
pub(super) struct BehaviorData {
pub name: String,
pub index: usize,
pub total: usize,
pub rows: Vec<Row>,
pub chart: Chart,
pub overview: Chart,
pub card: Option<usize>,
pub fields: Vec<usize>,
pub picks: Vec<Pick>,
pub matches: Vec<usize>,
pub editable: bool,
pub pulse_cards: Vec<(usize, f32)>,
pub pulse_rows: Vec<(usize, f32)>,
pub break_cards: Vec<usize>,
}
const WHEEL_PAN: f32 = 40.0;
fn component_names() -> &'static [&'static str] {
static NAMES: OnceLock<Vec<&'static str>> = OnceLock::new();
NAMES.get_or_init(|| {
let mut names: Vec<&'static str> =
RegisteredType::all().iter().map(|t| t.as_str()).collect();
names.sort_unstable();
names
})
}
impl EditorHook {
pub(super) fn behavior_entries(&self) -> Vec<usize> {
self.entries
.iter()
.enumerate()
.filter(|(_, e)| entry_type(e) == Some("Behavior"))
.map(|(i, _)| i)
.collect()
}
pub(super) fn behavior_entry(&self) -> Option<usize> {
let all = self.behavior_entries();
all.get(self.behavior_index.min(all.len().saturating_sub(1)))
.copied()
}
pub(super) fn behavior_args(&self) -> Value {
self.behavior_entry()
.and_then(|i| self.entries[i].get("args"))
.filter(|a| a.is_object())
.cloned()
.unwrap_or_else(|| Value::Object(serde_json::Map::new()))
}
pub(super) fn behavior_rows(&self) -> Vec<Row> {
match self.behavior_entry() {
Some(_) => outline::rows(&self.behavior_args()),
None => Vec::new(),
}
}
pub(super) fn variables_args(&self) -> Option<Value> {
self.entries
.iter()
.find(|e| entry_type(e) == Some("Variables"))
.and_then(|e| e.get("args"))
.cloned()
}
pub(super) fn behavior_data(&self) -> BehaviorData {
let all = self.behavior_entries();
let rows = self.behavior_rows();
let selected = self.behavior_row.and_then(|i| rows.get(i));
let chart = match self.behavior_entry() {
Some(_) => graph::chart(&self.behavior_args()),
None => Chart::default(),
};
let card = selected.and_then(|r| fields::owning_card(&chart.cards, &r.path));
let picks = selected.map_or_else(Vec::new, |r| edit::picks(&r.kind, component_names()));
let name = self
.behavior_entry()
.and_then(|i| entry_name(&self.entries[i]))
.unwrap_or("")
.to_string();
let mut pulse_cards = Vec::new();
let mut pulse_rows = Vec::new();
for p in &self.behavior_pulses {
let alpha = pulse::alpha(p.at.elapsed().as_secs_f32());
if alpha <= 0.0 {
continue;
}
if let Some(c) = fields::owning_card(&chart.cards, &p.path) {
pulse_cards.push((c, alpha));
}
if let Some(r) = rows.iter().position(|r| r.path == p.path) {
pulse_rows.push((r, alpha));
}
}
let break_cards = self
.behavior_breakpoints
.iter()
.filter(|(n, _)| *n == name)
.filter_map(|(_, path)| chart.cards.iter().position(|c| &c.path == path))
.collect();
BehaviorData {
name,
index: self.behavior_index.min(all.len().saturating_sub(1)),
total: all.len(),
matches: filter::matching(&picks, &self.behavior_filter),
picks,
editable: selected
.is_some_and(|r| edit::text_value(&self.behavior_args(), r).is_some()),
fields: card
.map(|i| fields::own_rows(&rows, &chart.cards, i))
.unwrap_or_default(),
overview: match self.behavior_mode {
ViewMode::Overview => {
relations::map(&self.behavior_pairs(), &self.declared_assets())
}
_ => Chart::default(),
},
card,
chart,
rows,
pulse_cards,
pulse_rows,
break_cards,
}
}
pub(super) fn behavior_pairs(&self) -> Vec<(String, Value)> {
self.behavior_entries()
.into_iter()
.map(|i| {
let e = &self.entries[i];
(
entry_name(e).unwrap_or("").to_string(),
e.get("args").cloned().unwrap_or(Value::Null),
)
})
.collect()
}
fn declared_assets(&self) -> Vec<(&str, &str)> {
self.entries
.iter()
.filter_map(|e| Some((entry_name(e)?, entry_type(e)?)))
.collect()
}
pub(super) fn make_behavior_view<'a>(
&'a self,
data: &'a BehaviorData,
mouse: [f32; 2],
) -> BehaviorView<'a> {
BehaviorView {
name: &data.name,
index: data.index,
total: data.total,
rows: &data.rows,
scroll: self.behavior_scroll,
selected: self.behavior_row,
chart: &data.chart,
overview: &data.overview,
mode: self.behavior_mode,
pan: self.behavior_pan,
card: data.card,
fields: &data.fields,
picks: &data.picks,
matches: &data.matches,
picking: self.behavior_picking && !data.picks.is_empty(),
pick_scroll: self.behavior_pick_scroll,
pick: self.behavior_pick,
filter_focus: self.behavior_picking
&& !data.picks.is_empty()
&& self.panel_order.last() == Some(&PanelKey::Behavior),
overview_card: self.behavior_overview_card,
editable: data.editable,
focus: self.behavior_focus
&& data.editable
&& self.panel_order.last() == Some(&PanelKey::Behavior),
name_focus: self.behavior_name_focus
&& self.panel_order.last() == Some(&PanelKey::Behavior),
remove_armed: self.behavior_remove_armed,
fault_row: self
.behavior_status
.as_ref()
.and_then(|s| fault::row_of(&data.rows, s.at())),
status: self.behavior_status.as_ref(),
pulse_cards: &data.pulse_cards,
pulse_rows: &data.pulse_rows,
break_cards: &data.break_cards,
mouse,
}
}
pub(super) fn open_behavior(&mut self, world: &mut World) {
let all = self.behavior_entries();
self.behavior_index = self.behavior_index.min(all.len().saturating_sub(1));
self.behavior_row = None;
self.behavior_scroll = 0;
self.behavior_picking = false;
self.clear_behavior_filter(world);
self.behavior_focus = false;
self.behavior_name_focus = false;
self.behavior_remove_armed = false;
self.refresh_behavior_status();
self.seed_behavior_value(world);
self.seed_behavior_name(world);
}
pub(super) fn refresh_behavior_status(&mut self) {
let Some(idx) = self.behavior_entry() else {
self.behavior_status = None;
return;
};
let name = entry_name(&self.entries[idx]).unwrap_or("").to_string();
let args = self.behavior_args();
let vars = self.variables_args();
self.behavior_status = Some(
match concinnity_cook::check::behavior::check_with_variables(
&name,
&args,
vars.as_ref(),
) {
Ok(()) => Status::Ok,
Err(e) => Status::Error {
message: e.message.lines().next().unwrap_or(&e.message).to_string(),
at: fault::to_path(&e.at),
},
},
);
}
pub(super) fn seed_behavior_value(&mut self, world: &mut World) {
let args = self.behavior_args();
let text = self
.behavior_row
.and_then(|i| self.behavior_rows().get(i).cloned())
.and_then(|r| edit::text_value(&args, &r))
.unwrap_or_default();
widget::seed_field(world, behavior_panel::VALUE_INPUT, &text);
}
fn commit_behavior(&mut self, args: Value, world: &mut World) {
let Some(idx) = self.behavior_entry() else {
return;
};
let Some(entry) = self.entries[idx].as_object_mut() else {
return;
};
entry.insert("args".to_string(), args);
self.mark_changed();
let prev_fault = self.behavior_fault_message();
self.refresh_behavior_status();
self.notify_behavior_fault(prev_fault);
self.seed_behavior_value(world);
}
pub(super) fn apply_behavior_action(
&mut self,
action: BehaviorAction,
world: &mut World,
mouse: [f32; 2],
) {
let armed = std::mem::take(&mut self.behavior_remove_armed);
if action != BehaviorAction::FocusName {
self.blur_behavior_name(world);
}
match action {
BehaviorAction::Step(delta) => self.step_behavior(delta, world),
BehaviorAction::New => self.add_behavior(world),
BehaviorAction::Remove => self.remove_behavior(armed, world),
BehaviorAction::FocusName => self.focus_behavior_name(world),
BehaviorAction::Select(i) => self.select_behavior_row(i, world),
BehaviorAction::Palette => {
self.behavior_picking = !self.behavior_picking;
self.clear_behavior_filter(world);
self.behavior_focus = false;
}
BehaviorAction::Choose(i) => self.choose_behavior_pick(i, world),
BehaviorAction::Dismiss => {
self.behavior_picking = false;
self.clear_behavior_filter(world);
}
BehaviorAction::Delete => self.delete_behavior_row(world),
BehaviorAction::Move(delta) => self.move_behavior_row(delta as isize, world),
BehaviorAction::FocusValue => self.behavior_focus = true,
BehaviorAction::ToggleView => self.toggle_behavior_view(),
BehaviorAction::SelectCard(i) => self.select_behavior_card(i, world),
BehaviorAction::OpenCard(i) => self.open_behavior_card(i, world),
BehaviorAction::OpenVariable(i) => self.open_overview_variable(i, world),
BehaviorAction::PanStart => self.start_behavior_pan(mouse),
BehaviorAction::GoToFault => self.select_behavior_fault(world),
BehaviorAction::Copy => self.copy_behavior_row(),
BehaviorAction::Paste => self.paste_behavior_row(world),
BehaviorAction::Duplicate => self.duplicate_behavior_row(world),
BehaviorAction::Consume => self.behavior_focus = false,
}
}
fn step_behavior(&mut self, delta: i32, world: &mut World) {
let total = self.behavior_entries().len();
if total == 0 {
return;
}
let at = self.behavior_index.min(total - 1) as i32;
self.behavior_index = (at + delta).rem_euclid(total as i32) as usize;
self.open_behavior(world);
}
fn add_behavior(&mut self, world: &mut World) {
let name = self.unique_name("behavior");
self.entries.push(serde_json::json!({
"name": name, "type": "Behavior", "args": {"on": "start", "do": []},
}));
self.mark_changed();
self.behavior_index = self.behavior_entries().len().saturating_sub(1);
self.open_behavior(world);
}
pub(super) fn select_behavior_row(&mut self, i: usize, world: &mut World) {
self.behavior_row = Some(i);
self.behavior_picking = false;
let Some(row) = self.behavior_rows().get(i).cloned() else {
return;
};
self.seed_behavior_value(world);
self.behavior_focus = edit::text_value(&self.behavior_args(), &row).is_some();
}
fn clear_behavior_filter(&mut self, world: &mut World) {
self.behavior_filter.clear();
self.behavior_pick = 0;
self.behavior_pick_scroll = 0;
widget::seed_field(world, behavior_panel::FILTER_INPUT, "");
}
fn choose_behavior_pick(&mut self, i: usize, world: &mut World) {
self.behavior_picking = false;
self.clear_behavior_filter(world);
let data = self.behavior_data();
let (Some(row), Some(pick)) = (
self.behavior_row.and_then(|r| data.rows.get(r)),
data.picks.get(i),
) else {
return;
};
let (row, verb) = (row.clone(), pick.verb);
let mut args = self.behavior_args();
if edit::apply_pick(&mut args, &row, verb) {
self.commit_behavior(args, world);
}
}
fn delete_behavior_row(&mut self, world: &mut World) {
let Some(row) = self
.behavior_row
.and_then(|i| self.behavior_rows().get(i).cloned())
else {
return;
};
let mut args = self.behavior_args();
if edit::remove(&mut args, &row) {
self.commit_behavior(args, world);
self.behavior_row = None;
self.behavior_focus = false;
}
}
fn move_behavior_row(&mut self, delta: isize, world: &mut World) {
let Some(row) = self
.behavior_row
.and_then(|i| self.behavior_rows().get(i).cloned())
else {
return;
};
let mut args = self.behavior_args();
let Some(moved) = edit::shift(&mut args, &row, delta) else {
return;
};
self.commit_behavior(args, world);
self.behavior_row = self
.behavior_rows()
.iter()
.position(|r| r.element.as_ref() == Some(&moved));
self.ensure_behavior_visible();
}
pub(super) fn commit_behavior_value(&mut self, world: &mut World) {
if !self.behavior_focus {
return;
}
let Some(row) = self
.behavior_row
.and_then(|i| self.behavior_rows().get(i).cloned())
else {
return;
};
let text = widget::field_text(world, behavior_panel::VALUE_INPUT);
let mut args = self.behavior_args();
match edit::apply_text(&mut args, &row, &text) {
Ok(()) => self.commit_behavior(args, world),
Err(e) => self.behavior_status = Some(Status::message(e)),
}
}
fn toggle_behavior_view(&mut self) {
self.behavior_mode = self.behavior_mode.other();
self.behavior_picking = false;
self.behavior_pan_drag = None;
self.behavior_pan = [0.0, 0.0];
if self.behavior_mode == ViewMode::Overview {
let data = self.behavior_data();
self.behavior_overview_card = data
.overview
.cards
.iter()
.position(|c| c.behavior == Some(data.index));
}
self.ensure_behavior_visible();
}
fn open_behavior_card(&mut self, i: usize, world: &mut World) {
let Some(at) = self
.behavior_data()
.overview
.cards
.get(i)
.and_then(|c| c.behavior)
else {
return;
};
self.behavior_index = at;
self.behavior_mode = ViewMode::Chart;
self.behavior_pan = [0.0, 0.0];
self.open_behavior(world);
}
fn copy_behavior_row(&mut self) {
let data = self.behavior_data();
let Some(row) = self.behavior_row.and_then(|i| data.rows.get(i)) else {
return;
};
if let Some(held) = clip::of(&self.behavior_args(), &data.rows, row) {
self.behavior_clip = Some(held);
}
}
fn paste_behavior_row(&mut self, world: &mut World) {
let Some(held) = self.behavior_clip.clone() else {
return;
};
self.place_behavior_clip(&held, world);
}
fn duplicate_behavior_row(&mut self, world: &mut World) {
let data = self.behavior_data();
let Some(row) = self.behavior_row.and_then(|i| data.rows.get(i)) else {
return;
};
let Some(held) = clip::of(&self.behavior_args(), &data.rows, row) else {
return;
};
self.place_behavior_clip(&held, world);
}
fn place_behavior_clip(&mut self, held: &clip::Clip, world: &mut World) {
let data = self.behavior_data();
let Some(row) = self.behavior_row.and_then(|i| data.rows.get(i)) else {
return;
};
let mut args = self.behavior_args();
let Some(landed) = clip::paste(&mut args, &data.rows, row, held) else {
return;
};
self.commit_behavior(args, world);
self.behavior_row = self
.behavior_rows()
.iter()
.position(|r| r.element.as_ref() == Some(&landed));
self.seed_behavior_value(world);
self.ensure_behavior_visible();
}
pub(super) fn select_behavior_fault(&mut self, world: &mut World) {
let data = self.behavior_data();
let Some(row) = self
.behavior_status
.as_ref()
.and_then(|s| fault::row_of(&data.rows, s.at()))
else {
return;
};
if self.behavior_mode == ViewMode::Overview {
self.behavior_mode = ViewMode::Chart;
self.behavior_pan = [0.0, 0.0];
}
self.select_behavior_row(row, world);
self.ensure_behavior_visible();
}
fn open_overview_variable(&mut self, i: usize, world: &mut World) {
let Some(name) = self
.behavior_data()
.overview
.cards
.get(i)
.map(|c| c.title.clone())
else {
return;
};
self.open_variable_named(&name, world);
}
fn select_behavior_card(&mut self, i: usize, world: &mut World) {
let data = self.behavior_data();
let Some(card) = data.chart.cards.get(i) else {
return;
};
if self.ctrl_held && card.kind == CardKind::Node {
let path = card.path.clone();
self.toggle_behavior_breakpoint(&path);
return;
}
let path = card.path.clone();
let Some(row) = data.rows.iter().position(|r| r.path == path) else {
return;
};
self.select_behavior_row(row, world);
}
fn start_behavior_pan(&mut self, mouse: [f32; 2]) {
self.behavior_focus = false;
self.behavior_pan_drag = Some([
self.behavior_pan[0] + mouse[0],
self.behavior_pan[1] + mouse[1],
]);
}
pub(super) fn drive_behavior_pan(&mut self, input: &FrameInput) {
let Some(anchor) = self.behavior_pan_drag else {
return;
};
if !input.left_button_down {
self.behavior_pan_drag = None;
return;
}
let want = [anchor[0] - input.mouse_x, anchor[1] - input.mouse_y];
let chart = self.behavior_shown_chart();
self.behavior_pan = behavior_chart::clamp_pan(want, &chart, self.behavior_canvas());
}
fn behavior_shown_chart(&self) -> Chart {
let data = self.behavior_data();
match self.behavior_mode {
ViewMode::Overview => data.overview,
_ => data.chart,
}
}
fn behavior_canvas(&self) -> [f32; 2] {
behavior_panel::chart_canvas(self.effective_size(PanelKey::Behavior), self.behavior_mode)
}
fn behavior_rows_shown(&self) -> usize {
behavior_panel::visible_rows(self.effective_size(PanelKey::Behavior)[1])
}
pub(super) fn ensure_behavior_visible(&mut self) {
match self.behavior_mode {
ViewMode::Overview => self.pan_to_overview_card(),
ViewMode::Chart => {
if let Some(row) = self.behavior_row {
self.pan_to_behavior_row(row);
}
}
ViewMode::Outline => {
let Some(row) = self.behavior_row else {
return;
};
let shown = self.behavior_rows_shown();
self.behavior_scroll = navigate::scroll_to(row, self.behavior_scroll, shown);
}
}
}
fn pan_to_overview_card(&mut self) {
let data = self.behavior_data();
let Some(card) = self
.behavior_overview_card
.and_then(|i| data.overview.cards.get(i))
else {
return;
};
self.behavior_pan = behavior_chart::pan_to(
card,
self.behavior_canvas(),
self.behavior_pan,
&data.overview,
);
}
fn pan_to_behavior_row(&mut self, row: usize) {
let data = self.behavior_data();
let Some(card) = data
.rows
.get(row)
.and_then(|r| fields::owning_card(&data.chart.cards, &r.path))
.and_then(|i| data.chart.cards.get(i))
else {
return;
};
self.behavior_pan =
behavior_chart::pan_to(card, self.behavior_canvas(), self.behavior_pan, &data.chart);
}
pub(super) fn scroll_behavior(&mut self, delta: f32) {
if self.behavior_picking {
let total = self.behavior_data().matches.len();
let max = total.saturating_sub(behavior_panel::PICK_POOL);
self.behavior_pick_scroll = scroll_step(self.behavior_pick_scroll, delta, max);
return;
}
if self.behavior_mode.drawn_as_chart() {
let chart = self.behavior_shown_chart();
let canvas = self.behavior_canvas();
let step = if delta > 0.0 { WHEEL_PAN } else { -WHEEL_PAN };
let pan = self.behavior_pan;
let want = if behavior_chart::max_pan(&chart, canvas)[1] > 0.0 {
[pan[0], pan[1] + step]
} else {
[pan[0] + step, pan[1]]
};
self.behavior_pan = behavior_chart::clamp_pan(want, &chart, canvas);
return;
}
let max = self
.behavior_rows()
.len()
.saturating_sub(self.behavior_rows_shown());
self.behavior_scroll = scroll_step(self.behavior_scroll, delta, max);
}
}