mod export;
pub mod extensions_config;
pub mod ffi;
pub mod lifecycle;
pub mod operator;
pub mod paths;
pub mod pie;
mod registries;
pub mod runtime;
pub mod snapshot;
use std::borrow::Cow;
use std::sync::Arc;
use bevy::ecs::{system::IntoObserverSystem, world::EntityWorldMut};
use bevy::prelude::*;
use bevy_enhanced_input::prelude::{Action, Fire};
use jackdaw_panels::{
DockWindowDescriptor, WindowRegistry, WorkspaceDescriptor, WorkspaceRegistry,
};
use operator::{CallOperatorSettings, Operator};
use registries::WindowExtensionRegistry;
use snapshot::{ActiveSnapshotter, SceneSnapshot};
pub use jackdaw_api_macros as macros;
pub use jackdaw_api_macros::operator;
pub use jackdaw_jsn as jsn;
use crate::lifecycle::{ExtensionResourceOf, OperatorAction, ResourceId};
use crate::operator::OperatorCommandsExt as _;
use crate::{
lifecycle::{
ExtensionKind, OperatorEntity, RegisteredMenuEntry, RegisteredWindow,
RegisteredWindowExtension, RegisteredWorkspace,
},
operator::ExecutionContext,
};
pub use jackdaw_panels::area::{DefaultArea, ToAnchorId};
pub use lifecycle::{ActiveModalOperator, Extension, ExtensionCatalog};
pub use operator::{CallOperatorError, OperatorResult, OperatorWorldExt};
pub use pie::PlayState;
pub use snapshot::SceneSnapshotter;
pub mod prelude {
pub use crate::{
ExtensionContext, ExtensionPoint, JackdawExtension, MenuEntryDescriptor, PanelContext,
WindowDescriptor,
lifecycle::{
ActiveModalQuery, Extension, ExtensionAppExt as _, ExtensionCatalog, ExtensionKind,
RegisteredMenuEntry, RegisteredWindow,
},
macros::operator,
operator::{
CallOperatorSettings, ExecutionContext, Operator, OperatorCommandsExt as _,
OperatorParameters, OperatorResult, OperatorSignature, OperatorSystemId,
OperatorWorldExt as _, ParamSpec,
},
pie::PlayState,
runtime::{GameApp, GamePlugin, GameRegistered, GameRegistry, GameSystems},
snapshot::{ActiveSnapshotter, SceneSnapshot, SceneSnapshotter},
};
pub use bevy_enhanced_input::prelude::*;
pub use bevy::ecs::system::SystemId;
}
pub trait JackdawExtension: Send + Sync + 'static {
fn id(&self) -> String;
fn label(&self) -> String {
self.id()
}
fn description(&self) -> String {
"".to_string()
}
fn kind(&self) -> ExtensionKind {
ExtensionKind::Regular
}
#[expect(unused_variables, reason = "The default implementation does nothing")]
fn register_input_context(&self, app: &mut App) {}
fn register(&self, ctx: &mut ExtensionContext);
#[expect(unused_variables, reason = "The default implementation does nothing")]
fn unregister(&self, world: &mut World, extension_entity: Entity) {}
}
pub struct ExtensionContext<'a> {
world: &'a mut World,
extension_entity: Entity,
}
impl<'a> ExtensionContext<'a> {
pub fn new(world: &'a mut World, extension_entity: Entity) -> Self {
Self {
world,
extension_entity,
}
}
pub fn init_resource<T: Resource + Default>(&mut self) -> &mut Self {
let id = self.world.init_resource::<T>();
self.world.spawn(ExtensionResourceOf {
entity: self.id(),
resource_id: ResourceId(id),
});
self
}
pub fn insert_resource<T: Resource>(&mut self, resource: T) -> &mut Self {
self.world.insert_resource(resource);
let id = self
.world
.resource_id::<T>()
.expect("resource_id should be Some since resource was just inserted");
self.world.spawn(ExtensionResourceOf {
entity: self.id(),
resource_id: ResourceId(id),
});
self
}
pub fn add_observer<E: Event, B: Bundle, M>(
&mut self,
system: impl IntoObserverSystem<E, B, M>,
) -> &mut Self {
self.entity_mut().with_child(Observer::new(system));
self
}
pub fn id(&self) -> Entity {
self.extension_entity
}
pub fn register_window(&mut self, descriptor: WindowDescriptor) -> &mut Self {
let ext = self.extension_entity;
let dock_descriptor = DockWindowDescriptor {
id: descriptor.id.clone(),
name: descriptor.name,
icon: descriptor.icon,
default_area: descriptor.default_area.anchor_id().to_string(),
priority: descriptor.priority.unwrap_or(100),
build: descriptor.build,
};
self.world
.resource_mut::<WindowRegistry>()
.register(dock_descriptor);
self.world
.spawn((RegisteredWindow { id: descriptor.id }, ChildOf(ext)));
self
}
pub fn register_workspace(&mut self, descriptor: WorkspaceDescriptor) -> &mut Self {
let ext = self.extension_entity;
let id = descriptor.id.clone();
self.world
.resource_mut::<WorkspaceRegistry>()
.register(descriptor);
self.world.spawn((RegisteredWorkspace { id }, ChildOf(ext)));
self
}
pub fn spawn<'w>(&'w mut self, bundle: impl Bundle) -> EntityWorldMut<'w> {
let ext = self.extension_entity;
let mut ec = self.world.spawn(bundle);
ec.insert(ChildOf(ext));
ec
}
pub fn entity<'w>(&'w self) -> EntityRef<'w> {
self.world.entity(self.extension_entity)
}
pub fn entity_mut<'w>(&'w mut self) -> EntityWorldMut<'w> {
self.world.entity_mut(self.extension_entity)
}
pub fn register_operator<O: Operator>(&mut self) -> &mut Self {
let ext = self.extension_entity;
let (execute, invoke, availability_check, cancel) = {
let mut queue = bevy::ecs::world::CommandQueue::default();
let mut commands = Commands::new(&mut queue, self.world);
let execute = O::register_execute(&mut commands);
let invoke = O::register_invoke(&mut commands);
let availability_check = O::register_availability_check(&mut commands);
let cancel = O::register_cancel(&mut commands);
queue.apply(self.world);
(execute, invoke, availability_check, cancel)
};
self.world.spawn((
OperatorEntity {
id: O::ID,
label: O::LABEL,
description: O::DESCRIPTION,
parameters: O::PARAMETERS,
execute,
invoke,
availability_check,
cancel,
modal: O::MODAL,
allows_undo: O::ALLOWS_UNDO,
},
ChildOf(ext),
children![
Observer::new(move |_: On<Fire<O>>, mut commands: Commands| {
commands
.operator(O::ID)
.settings(CallOperatorSettings {
execution_context: ExecutionContext::Invoke,
creates_history_entry: true,
})
.call();
},),
Observer::new(move |trigger: On<Add, Action<O>>, mut commands: Commands| {
commands
.entity(trigger.event_target())
.insert(OperatorAction(O::ID));
})
],
));
if let Err(err) = self.world.run_system_cached(tag_existing_actions::<O>) {
error!("Failed to tag existing actions: {}", err);
}
self
}
pub fn extend_window(
&mut self,
id: impl Into<Cow<'static, str>>,
build: impl Fn(&mut ChildSpawner) + Send + Sync + 'static,
) -> &mut Self {
let ext = self.extension_entity;
let id = id.into();
let mut registry = self.world.resource_mut::<WindowExtensionRegistry>();
let section_index = registry.get(&id).count();
registry.add(id.clone(), build);
self.world.spawn((
RegisteredWindowExtension {
window_id: id,
section_index,
},
ChildOf(ext),
));
self
}
pub fn register_menu_entry_manual(&mut self, descriptor: MenuEntryDescriptor) -> &mut Self {
let ext = self.extension_entity;
self.world.spawn((
RegisteredMenuEntry {
menu: descriptor.menu,
label: descriptor.label,
operator_id: descriptor.operator_id,
},
ChildOf(ext),
));
self
}
pub fn register_menu_entry<O: Operator>(&mut self, menu: TopLevelMenu) -> &mut Self {
self.register_menu_entry_manual(MenuEntryDescriptor {
menu,
label: O::LABEL.to_string(),
operator_id: O::ID,
})
}
}
fn tag_existing_actions<O: Operator>(
world: &mut World,
actions: &mut QueryState<Entity, With<Action<O>>>,
) {
let existing: Vec<Entity> = actions.iter(world).collect();
for entity in existing {
world.entity_mut(entity).insert(OperatorAction(O::ID));
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum TopLevelMenu {
File,
Edit,
View,
Add,
Tools,
Window,
Custom(String),
}
impl TopLevelMenu {
pub fn id(&self) -> String {
match self {
TopLevelMenu::File => "File".to_string(),
TopLevelMenu::Edit => "Edit".to_string(),
TopLevelMenu::Add => "Add".to_string(),
TopLevelMenu::View => "View".to_string(),
TopLevelMenu::Tools => "Tools".to_string(),
TopLevelMenu::Window => "Window".to_string(),
TopLevelMenu::Custom(id) => id.clone(),
}
}
pub fn order(&self) -> u8 {
match self {
TopLevelMenu::File => 0,
TopLevelMenu::Edit => 1,
TopLevelMenu::Add => 2,
TopLevelMenu::View => 3,
TopLevelMenu::Tools => 4,
TopLevelMenu::Window => 5,
TopLevelMenu::Custom(_) => 6,
}
}
}
pub struct MenuEntryDescriptor {
pub menu: TopLevelMenu,
pub label: String,
pub operator_id: &'static str,
}
pub struct WindowDescriptor {
pub id: String,
pub name: String,
pub icon: Option<String>,
pub default_area: Option<DefaultArea>,
pub priority: Option<i32>,
pub build: Arc<dyn Fn(&mut ChildSpawner) + Send + Sync + 'static>,
}
impl WindowDescriptor {
#[must_use]
pub fn new(id: impl Into<String>) -> Self {
let id = id.into();
Self {
id: id.clone(),
name: id,
..default()
}
}
#[must_use]
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = name.into();
self
}
#[must_use]
pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
self.icon = Some(icon.into());
self
}
#[must_use]
pub fn with_default_area(mut self, area: impl Into<Option<DefaultArea>>) -> Self {
self.default_area = area.into();
self
}
#[must_use]
pub fn with_priority(mut self, priority: i32) -> Self {
self.priority = Some(priority);
self
}
#[must_use]
pub fn with_build(mut self, build: impl Fn(&mut ChildSpawner) + Send + Sync + 'static) -> Self {
self.build = Arc::new(build);
self
}
}
impl Default for WindowDescriptor {
fn default() -> Self {
Self {
id: String::new(),
name: String::new(),
icon: None,
default_area: None,
priority: None,
build: Arc::new(|_| {}),
}
}
}
pub trait ExtensionPoint: 'static {
const ID: &'static str;
}
pub struct InspectorWindow;
impl ExtensionPoint for InspectorWindow {
const ID: &'static str = "jackdaw.inspector.components";
}
pub struct HierarchyWindow;
impl ExtensionPoint for HierarchyWindow {
const ID: &'static str = "jackdaw.hierarchy";
}
pub struct PanelContext {
pub window_id: String,
pub panel_entity: Entity,
}
pub struct ExtensionLoaderPlugin;
impl Plugin for ExtensionLoaderPlugin {
fn build(&self, app: &mut App) {
app.add_plugins((lifecycle::plugin, operator::plugin, registries::plugin));
}
}