use std::{borrow::Cow, collections::BTreeMap};
use bevy::ecs::system::{SystemId, SystemState};
use bevy::prelude::*;
use bevy_enhanced_input::prelude::InputAction;
use jackdaw_commands::{CommandHistory, EditorCommand};
use jackdaw_jsn::PropertyValue;
use crate::lifecycle::ActiveModalQuery;
use crate::{
ActiveSnapshotter, SceneSnapshot,
lifecycle::{ActiveModalOperator, OperatorEntity, OperatorIndex},
};
pub(super) fn plugin(app: &mut App) {
app.add_systems(Update, tick_modal_operator);
}
pub trait Operator: InputAction + 'static {
const ID: &'static str;
const LABEL: &'static str;
const DESCRIPTION: &'static str = "";
const PARAMETERS: &'static [ParamSpec] = &[];
const ALLOWS_UNDO: bool = true;
const MODAL: bool = false;
fn register_execute(commands: &mut Commands) -> OperatorSystemId;
#[expect(unused_variables, reason = "The default implementation noops")]
fn register_availability_check(commands: &mut Commands) -> Option<SystemId<(), bool>> {
None
}
fn register_invoke(commands: &mut Commands) -> OperatorSystemId {
Self::register_execute(commands)
}
#[expect(unused_variables, reason = "The default implementation noops")]
fn register_cancel(commands: &mut Commands) -> Option<SystemId<()>> {
None
}
fn signature() -> OperatorSignature<'static> {
OperatorSignature::new(Self::ID, Self::PARAMETERS)
}
}
#[derive(Debug, Clone, Default, Deref, DerefMut, Reflect)]
pub struct OperatorParameters(pub BTreeMap<String, PropertyValue>);
impl OperatorParameters {
pub fn as_int(&self, key: &str) -> Option<i64> {
match self.get(key)? {
PropertyValue::Int(i) => Some(*i),
_ => None,
}
}
pub fn as_float(&self, key: &str) -> Option<f64> {
match self.get(key)? {
PropertyValue::Float(f) => Some(*f),
_ => None,
}
}
pub fn as_bool(&self, key: &str) -> Option<bool> {
match self.get(key)? {
PropertyValue::Bool(b) => Some(*b),
_ => None,
}
}
pub fn as_str(&self, key: &str) -> Option<&str> {
match self.get(key)? {
PropertyValue::String(s) => Some(s.as_ref()),
_ => None,
}
}
pub fn as_entity(&self, key: &str) -> Option<Entity> {
match self.get(key)? {
PropertyValue::Entity(e) => Some(*e),
_ => None,
}
}
}
#[derive(Clone, Debug)]
pub struct ParamSpec {
pub name: &'static str,
pub ty: &'static str,
pub default: Option<PropertyValue>,
pub doc: &'static str,
}
pub struct OperatorSignature<'a> {
pub id: &'a str,
pub params: &'a [ParamSpec],
}
impl<'a> OperatorSignature<'a> {
pub const fn new(id: &'a str, params: &'a [ParamSpec]) -> Self {
Self { id, params }
}
}
impl std::fmt::Display for OperatorSignature<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.id)?;
f.write_str("(")?;
for (i, spec) in self.params.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
write!(f, "{}: {}", spec.name, spec.ty)?;
if let Some(default) = &spec.default {
write!(f, " = {default}")?;
}
}
f.write_str(")")
}
}
pub type OperatorSystemId = SystemId<In<OperatorParameters>, OperatorResult>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[must_use = "Operators may not be `Finished`, which should usually be handled"]
pub enum OperatorResult {
Finished,
Cancelled,
Running,
}
impl OperatorResult {
pub fn is_finished(&self) -> bool {
matches!(self, OperatorResult::Finished)
}
}
pub trait OperatorWorldExt {
#[must_use = "Operators must be called with `.call()` to execute them"]
fn operator<'a>(
&'a mut self,
id: impl Into<Cow<'static, str>>,
) -> OperatorCallBuilder<'a, World>;
fn cancel_active_modal(&mut self) -> Result;
}
impl OperatorWorldExt for World {
fn operator<'a>(
&'a mut self,
id: impl Into<Cow<'static, str>>,
) -> OperatorCallBuilder<'a, World> {
OperatorCallBuilder {
world_commands: self,
id: id.into(),
params: OperatorParameters::default(),
settings: CallOperatorSettings::default(),
}
}
fn cancel_active_modal(&mut self) -> Result {
self.run_system_cached(cancel_active_modal)
.map_err(From::from)
}
}
pub trait OperatorCommandsExt<'w, 's> {
#[must_use = "Operators must be called with `.call()` to execute them"]
fn operator<'a>(
&'a mut self,
id: impl Into<Cow<'static, str>>,
) -> OperatorCallBuilder<'a, Commands<'w, 's>>;
}
impl<'w, 's> OperatorCommandsExt<'w, 's> for Commands<'w, 's> {
fn operator<'a>(
&'a mut self,
id: impl Into<Cow<'static, str>>,
) -> OperatorCallBuilder<'a, Commands<'w, 's>> {
OperatorCallBuilder {
world_commands: self,
id: id.into(),
params: OperatorParameters::default(),
settings: CallOperatorSettings::default(),
}
}
}
#[derive(Clone, Debug, Copy)]
pub struct CallOperatorSettings {
pub creates_history_entry: bool,
pub execution_context: ExecutionContext,
}
impl Default for CallOperatorSettings {
fn default() -> Self {
Self {
creates_history_entry: false,
execution_context: default(),
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub enum ExecutionContext {
#[default]
Execute,
Invoke,
}
#[derive(Debug)]
pub enum CallOperatorError {
UnknownId(Cow<'static, str>),
ModalAlreadyActive(&'static str),
NotAvailable,
ExecuteFailed,
Other(BevyError),
}
impl std::fmt::Display for CallOperatorError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownId(id) => write!(f, "unknown operator: {id}"),
Self::ModalAlreadyActive(id) => {
write!(f, "modal operator '{id}' is currently active")
}
Self::NotAvailable => f.write_str("operator's availability check failed"),
Self::ExecuteFailed => f.write_str("operator's execute system failed"),
Self::Other(err) => write!(f, "operator execution failed: {err}"),
}
}
}
impl From<BevyError> for CallOperatorError {
fn from(err: BevyError) -> Self {
Self::Other(err)
}
}
impl std::error::Error for CallOperatorError {}
pub struct OperatorCallBuilder<'a, T> {
world_commands: &'a mut T,
id: Cow<'static, str>,
params: OperatorParameters,
settings: CallOperatorSettings,
}
impl<'a, T> OperatorCallBuilder<'a, T> {
#[must_use = "Operators must be called with `.call()` to execute them"]
pub fn param(
mut self,
key: impl Into<Cow<'static, str>>,
value: impl Into<PropertyValue>,
) -> Self {
self.params.insert(key.into().to_string(), value.into());
self
}
#[must_use = "Operators must be called with `.call()` to execute them"]
pub fn settings(mut self, settings: CallOperatorSettings) -> Self {
self.settings = settings;
self
}
}
impl<'a> OperatorCallBuilder<'a, Commands<'_, '_>> {
pub fn call(self) {
self.world_commands.queue(move |world: &mut World| {
world.run_system_cached_with(dispatch_operator, (self.id, self.params, self.settings))
});
}
pub fn cancel(self) {
self.world_commands.queue(|world: &mut World| {
let res: Result = world
.run_system_cached(cancel_active_modal)
.map_err(BevyError::from);
if let Err(err) = res {
error!("Failed to cancel active modal: {err}");
}
});
}
}
impl<'a> OperatorCallBuilder<'a, World> {
pub fn is_modal(self) -> Result<bool, CallOperatorError> {
fn is_modal_inner(
In(id): In<Cow<'static, str>>,
world: &mut World,
) -> Result<bool, CallOperatorError> {
let Some(op_entity) = world
.resource::<OperatorIndex>()
.by_id
.get(id.as_ref())
.copied()
else {
return Err(CallOperatorError::UnknownId(id));
};
let Some(op) = world.get::<OperatorEntity>(op_entity) else {
return Err(CallOperatorError::UnknownId(id));
};
Ok(op.modal)
}
self.world_commands
.run_system_cached_with(is_modal_inner, self.id.clone())
.map_err(BevyError::from)
.map_err(CallOperatorError::from)
.flatten()
}
pub fn is_available(self) -> Result<bool, CallOperatorError> {
fn is_available_inner(
In(id): In<Cow<'static, str>>,
world: &mut World,
active: &mut SystemState<ActiveModalQuery>,
) -> Result<bool, CallOperatorError> {
let Some(op_entity) = world
.resource::<OperatorIndex>()
.by_id
.get(id.as_ref())
.copied()
else {
return Err(CallOperatorError::UnknownId(id));
};
let Some(op) = world.get::<OperatorEntity>(op_entity).cloned() else {
return Err(CallOperatorError::UnknownId(id));
};
if op.modal && active.get(world).is_modal_running() {
return Err(CallOperatorError::ModalAlreadyActive(op.id));
}
let Some(check) = op.availability_check else {
return Ok(true);
};
world
.run_system(check)
.map_err(|_| CallOperatorError::NotAvailable)
}
self.world_commands
.run_system_cached_with(is_available_inner, self.id.clone())
.map_err(BevyError::from)
.map_err(CallOperatorError::from)
.flatten()
}
pub fn call(self) -> Result<OperatorResult, CallOperatorError> {
let result = self
.world_commands
.run_system_cached_with(dispatch_operator, (self.id, self.params, self.settings));
match result {
Ok(result) => result,
Err(_) => Err(CallOperatorError::ExecuteFailed),
}
}
pub fn is_running(self) -> bool {
let result = self
.world_commands
.run_system_cached_with(is_op_running, self.id.clone());
match result {
Ok(result) => result,
Err(_) => {
error!(
"Failed to check if operator is running: {}, treating as `false`",
self.id
);
false
}
}
}
pub fn cancel(self) -> Result {
self.world_commands
.run_system_cached_with(cancel_operator, self.id)
.map_err(From::from)
}
}
fn is_op_running(
In(id): In<Cow<'static, str>>,
world: &mut World,
active: &mut SystemState<ActiveModalQuery>,
) -> bool {
active.get(world).is_operator(id)
}
fn dispatch_operator(
In((id, params, settings)): In<(Cow<'static, str>, OperatorParameters, CallOperatorSettings)>,
world: &mut World,
active: &mut SystemState<ActiveModalQuery>,
) -> Result<OperatorResult, CallOperatorError> {
let Some(op_entity) = world
.resource::<OperatorIndex>()
.by_id
.get(id.as_ref())
.copied()
else {
return Err(CallOperatorError::UnknownId(id));
};
let Some(op) = world.get::<OperatorEntity>(op_entity).cloned() else {
return Err(CallOperatorError::UnknownId(id));
};
if op.modal
&& let Some(active_op) = active.get(world).get_operator()
{
return Err(CallOperatorError::ModalAlreadyActive(active_op.id));
}
if let Some(check) = op.availability_check {
let available = world
.run_system(check)
.map_err(|_| CallOperatorError::NotAvailable)?;
if !available {
debug!("Availability check failed for operator: {id}");
return Ok(OperatorResult::Cancelled);
}
}
let before_snapshot = settings.creates_history_entry.then(|| {
world.resource_scope(|world, snapshotter: Mut<ActiveSnapshotter>| {
snapshotter.0.capture(world)
})
});
let system = match settings.execution_context {
ExecutionContext::Execute => op.execute,
ExecutionContext::Invoke => op.invoke,
};
info!("OPERATOR: {id}");
let result = world.run_system_with(system, params);
let result = result.map_err(|_| CallOperatorError::ExecuteFailed)?;
match result {
OperatorResult::Running if op.modal => {
world
.entity_mut(op_entity)
.insert(ActiveModalOperator { before_snapshot });
}
OperatorResult::Running => {}
OperatorResult::Finished => {
if op.allows_undo
&& let Err(err) =
world.run_system_cached_with(save_history, (op.label, before_snapshot))
{
error!("Failed to finalize modal operator {}: {err:?}", op.label);
}
}
OperatorResult::Cancelled => {
let res: Result = world
.run_system_cached_with(cancel_operator, op.id.into())
.map_err(From::from);
if let Err(err) = res {
error!("Failed to finalize cancel operator: {err:?}");
}
}
}
Ok(result)
}
fn save_history(
In((label, before)): In<(&'static str, Option<Box<dyn SceneSnapshot>>)>,
world: &mut World,
) {
let Some(before) = before else { return };
let after = world
.resource_scope(|world, snapshotter: Mut<ActiveSnapshotter>| snapshotter.0.capture(world));
if before.equals(&*after) {
return;
}
world
.resource_mut::<CommandHistory>()
.push_executed(Box::new(SnapshotDiff {
before,
after,
label: label.to_string(),
}));
}
struct SnapshotDiff {
before: Box<dyn SceneSnapshot>,
after: Box<dyn SceneSnapshot>,
label: String,
}
impl EditorCommand for SnapshotDiff {
fn execute(&mut self, world: &mut World) {
self.after.apply(world);
}
fn undo(&mut self, world: &mut World) {
self.before.apply(world);
}
fn description(&self) -> &str {
&self.label
}
}
pub(crate) fn tick_modal_operator(world: &mut World, active: &mut SystemState<ActiveModalQuery>) {
let Some(op) = active.get(world).get_operator().cloned() else {
return;
};
let result = match world.run_system_with(op.invoke, default()) {
Ok(r) => r,
Err(err) => {
error!("Modal operator's invoke system failed: {err:?}; cancelling");
if let Err(err) = world.run_system_cached_with(finalize_modal, false) {
error!("Failed to finalize modal operator: {err:?}");
}
return;
}
};
match result {
OperatorResult::Running => {}
OperatorResult::Finished => {
if let Err(err) = world.run_system_cached_with(finalize_modal, true) {
error!("Failed to finalize modal operator: {err:?}");
}
}
OperatorResult::Cancelled => {
let res: Result = world
.run_system_cached_with(cancel_operator, op.id.into())
.map_err(BevyError::from);
if let Err(err) = res {
error!("Failed to finalize cancel operator: {err:?}");
}
}
}
}
pub(crate) fn cancel_active_modal(
world: &mut World,
active: &mut SystemState<ActiveModalQuery>,
) -> Result {
let Some(op) = active.get(world).get_operator().cloned() else {
return Ok(());
};
world
.run_system_cached_with(cancel_operator, op.id.into())
.map_err(From::from)
}
pub(crate) fn cancel_operator(
In(id): In<Cow<'static, str>>,
world: &mut World,
ops: &mut QueryState<&OperatorEntity>,
active: &mut SystemState<ActiveModalQuery>,
) -> Result {
let Some(op) = ops.iter(world).find(|o| o.id == id).cloned() else {
warn!("Tried to cancel non-existent operator: {id}");
return Ok(());
};
let mut cancel_err = None;
if let Some(cancel) = op.cancel
&& let Err(err) = world.run_system(cancel)
{
error!("Failed to cancel modal operator {}: {err:?}", op.label);
cancel_err = Some(err);
}
let mut finalize_err = None;
if active.get(world).is_operator(id)
&& let Err(err) = world.run_system_cached_with(finalize_modal, false)
{
error!("Failed to finalize modal operator: {err:?}");
finalize_err = Some(err);
}
match (cancel_err, finalize_err) {
(Some(cancel_err), Some(_finalize_err)) => {
Err(cancel_err.into())
}
(Some(cancel_err), None) => Err(BevyError::from(cancel_err)),
(None, Some(finalize_err)) => Err(BevyError::from(finalize_err)),
(None, None) => Ok(()),
}
}
fn finalize_modal(
In(commit): In<bool>,
world: &mut World,
active: &mut SystemState<Option<Single<(Entity, &OperatorEntity), With<ActiveModalOperator>>>>,
) {
let Some((entity, op)) = active
.get(world)
.map(Single::into_inner)
.map(|(e, o)| (e, o.clone()))
else {
return;
};
let Some(snapshot) = world.entity_mut(entity).take::<ActiveModalOperator>() else {
return;
};
if !commit || !op.allows_undo {
return;
}
if let Err(err) =
world.run_system_cached_with(save_history, (op.label, snapshot.before_snapshot))
{
error!("Failed to finalize modal operator {}: {err:?}", op.label);
}
}