use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::Arc;
use crate::extensions_config::init_extension;
use crate::operator::cancel_active_modal;
use crate::{TopLevelMenu, prelude::*};
use bevy::ecs::component::ComponentId;
use bevy::ecs::system::{SystemId, SystemParam};
use bevy::prelude::*;
pub(super) fn plugin(app: &mut App) {
app.init_resource::<ExtensionCatalog>()
.init_resource::<OperatorIndex>()
.add_observer(index_operator_on_add)
.add_observer(deindex_and_cleanup_operator_on_remove)
.add_observer(cleanup_window_on_remove)
.add_observer(cleanup_workspace_on_remove)
.add_observer(cleanup_window_extension_on_remove)
.add_observer(cleanup_resource_on_remove);
app.world_mut().register_component::<ActiveModalOperator>();
}
#[derive(Component, Debug)]
pub struct Extension {
pub id: String,
}
#[derive(Component, Default, Debug, PartialEq, Eq, Deref)]
#[relationship_target(relationship = ExtensionResourceOf, linked_spawn)]
pub(crate) struct ExtensionResources(Vec<Entity>);
#[derive(Component, Debug, PartialEq, Eq)]
#[relationship(relationship_target = ExtensionResources)]
pub(crate) struct ExtensionResourceOf {
#[relationship]
pub(crate) entity: Entity,
pub(crate) resource_id: ResourceId,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct ResourceId(pub(crate) ComponentId);
impl Default for ResourceId {
fn default() -> Self {
Self(ComponentId::new(0))
}
}
#[derive(Component, Debug, Clone)]
pub struct OperatorEntity {
pub(crate) id: &'static str,
pub(crate) label: &'static str,
pub(crate) description: &'static str,
pub(crate) parameters: &'static [crate::operator::ParamSpec],
pub(crate) execute: OperatorSystemId,
pub(crate) invoke: OperatorSystemId,
pub(crate) availability_check: Option<SystemId<(), bool>>,
pub(crate) cancel: Option<SystemId<()>>,
pub(crate) modal: bool,
pub(crate) allows_undo: bool,
}
impl OperatorEntity {
pub fn id(&self) -> &'static str {
self.id
}
pub fn label(&self) -> &'static str {
self.label
}
pub fn description(&self) -> &'static str {
self.description
}
pub fn parameters(&self) -> &'static [crate::operator::ParamSpec] {
self.parameters
}
pub fn is_modal(&self) -> bool {
self.modal
}
pub fn allows_undo(&self) -> bool {
self.allows_undo
}
}
#[derive(Component, Clone, Copy, Debug)]
pub struct OperatorAction(pub &'static str);
#[derive(Component)]
pub struct ActiveModalOperator {
pub(crate) before_snapshot: Option<Box<dyn SceneSnapshot>>,
}
#[derive(SystemParam)]
pub struct ActiveModalQuery<'w, 's> {
maybe_modal: Option<Single<'w, 's, (&'static OperatorEntity, &'static ActiveModalOperator)>>,
commands: Commands<'w, 's>,
}
impl<'w, 's> ActiveModalQuery<'w, 's> {
pub fn is_modal_running(&self) -> bool {
self.maybe_modal.is_some()
}
pub fn is_operator(&self, operator_id: impl AsRef<str>) -> bool {
self.get_operator()
.is_some_and(|op| op.id == operator_id.as_ref())
}
pub fn get_operator(&self) -> Option<&OperatorEntity> {
self.get_operator_and_modal().map(|m| m.0)
}
pub fn get_modal(&self) -> Option<&ActiveModalOperator> {
self.get_operator_and_modal().map(|m| m.1)
}
pub fn get_operator_and_modal(&self) -> Option<(&OperatorEntity, &ActiveModalOperator)> {
self.maybe_modal.as_ref().map(|m| (m.0, m.1))
}
pub fn cancel(&mut self) {
self.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}");
}
});
}
}
#[derive(Component, Clone, Debug)]
pub struct RegisteredWindow {
pub(crate) id: String,
}
#[derive(Component, Clone, Debug)]
pub(crate) struct RegisteredWorkspace {
pub(crate) id: String,
}
#[derive(Component, Clone, Debug)]
pub(crate) struct RegisteredWindowExtension {
pub(crate) window_id: Cow<'static, str>,
pub(crate) section_index: usize,
}
#[derive(Component, Clone, Debug)]
pub struct RegisteredMenuEntry {
pub menu: TopLevelMenu,
pub label: String,
pub operator_id: &'static str,
}
#[derive(Resource, Default, Deref, DerefMut)]
pub(crate) struct OperatorIndex {
pub(crate) by_id: HashMap<&'static str, Entity>,
}
pub(crate) type ExtensionCtor = Arc<dyn Fn() -> Box<dyn crate::JackdawExtension> + Send + Sync>;
#[derive(Resource, Default)]
pub struct ExtensionCatalog {
entries: HashMap<String, CatalogEntry>,
}
struct CatalogEntry {
ctor: ExtensionCtor,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum ExtensionKind {
Builtin = 0,
Regular = 1,
}
impl ExtensionCatalog {
fn register_extension_internal(
&mut self,
ctor: impl Fn() -> Box<dyn crate::JackdawExtension> + Send + Sync + 'static,
) {
let id = ctor().id();
self.entries.insert(
id,
CatalogEntry {
ctor: Arc::new(ctor),
},
);
}
pub fn contains(&self, name: &str) -> bool {
self.entries.contains_key(name)
}
pub fn iter(&self) -> impl Iterator<Item = &str> {
self.entries.keys().map(String::as_str)
}
pub fn iter_with_content(
&self,
) -> impl Iterator<Item = (String, String, String, ExtensionKind)> {
self.entries.iter().map(|(id, entry)| {
let ext = (entry.ctor)();
(
id.to_string(),
ext.label().to_string(),
ext.description().to_string(),
ext.kind(),
)
})
}
pub fn kind(&self, name: &str) -> Option<ExtensionKind> {
self.entries.get(name).map(|e| (e.ctor)().kind())
}
pub fn is_builtin(&self, name: &str) -> bool {
self.kind(name) == Some(ExtensionKind::Builtin)
}
pub fn construct(&self, name: &str) -> Option<Box<dyn crate::JackdawExtension>> {
self.entries.get(name).map(|e| (e.ctor)())
}
}
pub trait ExtensionAppExt {
fn register_extension<T: crate::JackdawExtension + Default>(&mut self) -> &mut Self {
self.register_extension_with(|| Box::new(T::default()))
}
fn register_extension_with(
&mut self,
ctor: impl Fn() -> Box<dyn crate::JackdawExtension> + Send + Sync + 'static,
) -> &mut Self;
}
impl ExtensionAppExt for App {
fn register_extension_with(
&mut self,
ctor: impl Fn() -> Box<dyn crate::JackdawExtension> + Send + Sync + 'static,
) -> &mut Self {
let ext = ctor();
ext.register_input_context(self);
self.world_mut()
.resource_mut::<ExtensionCatalog>()
.register_extension_internal(ctor);
init_extension(ext.id());
self
}
}
pub fn unload_extension(world: &mut World, ext_entity: Entity) {
let ext_name = world
.get::<Extension>(ext_entity)
.map(|e| e.id.clone())
.unwrap_or_default();
info!("Unloading extension: {}", ext_name);
if let Some(stored) = world.entity_mut(ext_entity).take::<StoredExtension>() {
stored.0.unregister(world, ext_entity);
}
if let Ok(ec) = world.get_entity_mut(ext_entity) {
ec.despawn();
}
}
pub fn enable_extension(world: &mut World, id: &str) -> Option<Entity> {
{
let mut query = world.query::<&Extension>();
if query.iter(world).any(|e| e.id == id) {
return None;
}
}
let extension = world.resource::<ExtensionCatalog>().construct(id)?;
Some(load_static_extension(world, extension))
}
pub fn load_static_extension(
world: &mut World,
extension: Box<dyn crate::JackdawExtension>,
) -> Entity {
let id = extension.id();
info!("Loading extension: {id}");
let extension_entity = world.spawn(Extension { id }).id();
let mut ctx = crate::ExtensionContext::new(world, extension_entity);
extension.register(&mut ctx);
world
.entity_mut(extension_entity)
.insert(StoredExtension(extension));
extension_entity
}
pub fn disable_extension(world: &mut World, id: &str) -> bool {
let mut query = world.query::<(Entity, &Extension)>();
let Some(ext_entity) = query.iter(world).find(|(_, e)| e.id == id).map(|(e, _)| e) else {
return false;
};
unload_extension(world, ext_entity);
true
}
#[derive(Component)]
pub(crate) struct StoredExtension(pub(crate) Box<dyn crate::JackdawExtension>);
pub fn register_dylib_extension<F>(world: &mut World, ctor: F)
where
F: Fn() -> Box<dyn crate::JackdawExtension> + Send + Sync + 'static,
{
world
.resource_mut::<ExtensionCatalog>()
.register_extension_internal(ctor);
}
pub(crate) fn index_operator_on_add(
trigger: On<Add, OperatorEntity>,
operators: Query<&OperatorEntity>,
mut index: ResMut<OperatorIndex>,
) {
if let Ok(op) = operators.get(trigger.event_target()) {
index.insert(op.id, trigger.event_target());
}
}
pub(crate) fn deindex_and_cleanup_operator_on_remove(
trigger: On<Remove, OperatorEntity>,
operators: Query<&OperatorEntity>,
mut index: ResMut<OperatorIndex>,
mut commands: Commands,
) {
let Ok(op) = operators.get(trigger.event_target()) else {
return;
};
info!("Unregistering operator: {}", op.id);
index.remove(op.id);
let (exec, inv, check) = (op.execute, op.invoke, op.availability_check);
commands.queue(move |world: &mut World| {
let _ = world.unregister_system(exec);
if exec != inv {
let _ = world.unregister_system(inv);
}
if let Some(c) = check {
let _ = world.unregister_system(c);
}
});
}
pub(crate) fn cleanup_window_on_remove(
trigger: On<Remove, RegisteredWindow>,
windows: Query<&RegisteredWindow>,
mut registry: ResMut<jackdaw_panels::WindowRegistry>,
mut dock_tree: ResMut<jackdaw_panels::tree::DockTree>,
mut workspaces: ResMut<jackdaw_panels::WorkspaceRegistry>,
) {
let Ok(w) = windows.get(trigger.event_target()) else {
return;
};
info!("Unregistering window: {}", w.id);
registry.unregister(&w.id);
dock_tree.remove_window_kind(&w.id);
for workspace in workspaces.workspaces.iter_mut() {
workspace.tree.remove_window_kind(&w.id);
}
}
pub(crate) fn cleanup_workspace_on_remove(
trigger: On<Remove, RegisteredWorkspace>,
workspaces: Query<&RegisteredWorkspace>,
mut registry: ResMut<jackdaw_panels::WorkspaceRegistry>,
) {
if let Ok(w) = workspaces.get(trigger.event_target()) {
registry.unregister(&w.id);
}
}
pub(crate) fn cleanup_window_extension_on_remove(
trigger: On<Remove, RegisteredWindowExtension>,
registrations: Query<&RegisteredWindowExtension>,
mut registry: ResMut<crate::WindowExtensionRegistry>,
) {
if let Ok(r) = registrations.get(trigger.event_target()) {
registry.remove(&r.window_id, r.section_index);
}
}
fn cleanup_resource_on_remove(
trigger: On<Remove, ExtensionResourceOf>,
resource_id: Query<&ExtensionResourceOf>,
mut commands: Commands,
) {
let Ok(relation) = resource_id.get(trigger.entity) else {
return;
};
let component_id = relation.resource_id.0;
commands.queue(move |world: &mut World| {
world.remove_resource_by_id(component_id);
});
}