use concinnity_cook::authoring::registry::RegisteredType;
use concinnity_cook::authoring::world::{
args_without_id, entry_handle, entry_handles, find_entry, replace_args,
};
use concinnity_core::components::FrameInput;
use concinnity_core::ecs::World;
use serde_json::Value;
use std::sync::OnceLock;
use crate::editor::behavior;
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::panel::{BehaviorAction, BehaviorView, Status, ViewMode};
use crate::editor::behavior::pulse;
use crate::editor::behavior::relations;
use crate::editor::hook::{EditorHook, entry_type, scroll_step};
use crate::editor::panels::registry::PanelKey;
use crate::editor::widget;
pub(in crate::editor::hook) 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(in crate::editor::hook) fn behavior_entries(&self) -> Vec<usize> {
self.entries
.iter()
.enumerate()
.filter(|(_, e)| entry_type(e) == Some("Behavior"))
.map(|(i, _)| i)
.collect()
}
pub(in crate::editor::hook) fn behavior_entry(&self) -> Option<usize> {
let all = self.behavior_entries();
all.get(self.behavior.index.min(all.len().saturating_sub(1)))
.copied()
}
pub(in crate::editor::hook) fn behavior_args(&self) -> Value {
self.behavior_entry()
.and_then(|i| self.entries[i].get("args"))
.filter(|a| a.is_object())
.cloned()
.map(args_without_id)
.unwrap_or_else(|| Value::Object(serde_json::Map::new()))
}
pub(in crate::editor::hook) 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()
.map(args_without_id)
}
pub(in crate::editor::hook) 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_handle(&self.entries, i))
.unwrap_or_default();
let mut pulse_cards = Vec::new();
let mut pulse_rows = Vec::new();
for p in &self.behavior.pulses {
let alpha = pulse::alpha(p.age);
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| {
(
entry_handle(&self.entries, i).unwrap_or_default(),
self.entries[i].get("args").cloned().unwrap_or(Value::Null),
)
})
.collect()
}
fn declared_assets(&self) -> Vec<relations::Declared> {
let handles = entry_handles(&self.entries);
self.entries
.iter()
.enumerate()
.filter_map(|(i, entry)| {
Some(relations::Declared {
name: handles.get(i)?.clone()?,
asset_type: RegisteredType::parse(entry_type(entry)?)?,
key: self.entries.key_at(i)?,
})
})
.collect()
}
pub(in crate::editor::hook) 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(in crate::editor::hook) 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(in crate::editor::hook) fn refresh_behavior_status(&mut self) {
let Some(idx) = self.behavior_entry() else {
self.behavior.status = None;
return;
};
let name = entry_handle(&self.entries, idx).unwrap_or_default();
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;
};
replace_args(&mut self.entries[idx], 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(in crate::editor::hook) 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::SelectAsset(i) => self.select_overview_asset(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!({
"type": "Behavior", "args": {"$id": name, "on": "start", "do": []},
}));
self.mark_changed();
self.behavior.index = self.behavior_entries().len().saturating_sub(1);
self.open_behavior(world);
}
pub(in crate::editor::hook) 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(in crate::editor::hook) 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.toggle_view();
if self.behavior.mode == ViewMode::Overview {
let at = self.behavior_data().index;
self.point_overview_at(at);
}
self.ensure_behavior_visible();
}
fn point_overview_at(&mut self, at: usize) {
self.behavior.overview_card = self
.behavior_data()
.overview
.cards
.iter()
.position(|c| c.behavior == Some(at));
}
fn open_behavior_card(&mut self, i: usize, world: &mut World) {
let card = self.behavior_data().overview.cards.get(i).cloned();
let Some(at) = card.as_ref().and_then(|c| c.behavior) else {
return;
};
self.behavior.overview_card = Some(i);
if let Some(handle) = card.and_then(|c| c.handle) {
self.select_handle(&handle, world);
}
self.behavior.index = at;
self.behavior.mode = ViewMode::Chart;
self.behavior.pan = [0.0, 0.0];
self.open_behavior(world);
}
fn select_overview_asset(&mut self, i: usize, world: &mut World) {
let Some(handle) = self
.behavior_data()
.overview
.cards
.get(i)
.and_then(|c| c.handle.clone())
else {
return;
};
self.behavior.overview_card = Some(i);
self.select_handle(&handle, world);
}
pub(in crate::editor::hook) fn open_behavior_named(&mut self, name: &str, world: &mut World) {
let target = find_entry(&self.entries, name);
let ordinal = self
.behavior_entries()
.iter()
.position(|&i| Some(i) == target);
let Some(ordinal) = ordinal else {
self.select_named(name);
self.focus_ui_on(name, world);
return;
};
self.behavior.index = ordinal;
self.open[PanelKey::Behavior] = true;
self.open_behavior(world);
self.focus_panel(PanelKey::Behavior);
if self.behavior.mode == ViewMode::Overview {
self.point_overview_at(ordinal);
}
self.ensure_behavior_visible();
}
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(in crate::editor::hook) 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(in crate::editor::hook) 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(in crate::editor::hook) 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(in crate::editor::hook) 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);
}
}