jackdaw_api_internal 0.4.1

Internal implementation crate for jackdaw_api. Depend on jackdaw_api instead.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
//! Public API for Jackdaw editor extensions.
//!
//! Extensions are entities. An extension entity holds an [`lifecycle::Extension`]
//! component, and every registration (operators, windows, BEI contexts,
//! panel extensions) spawns child entities under it. Unloading an
//! extension is `world.entity_mut(ext).despawn()`; Bevy cascades through
//! the children and a few observers handle the non-ECS cleanup.
//!
//! Minimal extension:
//!
//! ```ignore
//! use bevy::prelude::*;
//! use bevy_enhanced_input::prelude::*;
//! use jackdaw_api::prelude::*;
//!
//! #[operator(id = "sample.place_cube")]
//! fn place_cube(_: In<OperatorParameters>, mut commands: Commands) -> OperatorResult {
//!     commands.spawn((Name::new("Cube"), Transform::default()));
//!     OperatorResult::Finished
//! }
//!
//! #[derive(Component, Default)]
//! struct SamplePluginContext;
//!
//! #[derive(Default)]
//! struct MyCoolExtension;
//!
//! impl JackdawExtension for MyCoolExtension {
//!     fn name() -> String { "The coolest extension".into() }
//!     fn register(&self, ctx: &mut ExtensionContext) {
//!         ctx.register_operator::<PlaceCubeOp>();
//!         ctx.spawn((
//!             SamplePluginContext,
//!             actions!(SamplePluginContext[
//!                 Action::<PlaceCubeOp>::new(),
//!                 bindings![KeyCode::KeyC],
//!             ]),
//!         ));
//!     }
//!     fn register_input_context(&self, app: &mut App) {
//!         app.add_input_context::<SamplePluginContext>();
//!     }
//! }
//! ```

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;

/// Re-exports plugin authors will want in one import.
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},
    };
    // BEI types extension authors need for `actions!` / `bindings!` / observers.
    pub use bevy_enhanced_input::prelude::*;
    // Re-export Bevy's SystemId here so Operator impls don't need to import it.
    pub use bevy::ecs::system::SystemId;
}

/// Trait implemented by every extension. Declares the extension's name
/// and registration logic; the framework handles everything else.
pub trait JackdawExtension: Send + Sync + 'static {
    /// A unique identifier for this extension. This will be used to refer to the extension internally.
    /// The prefix `"jackdaw."` as well as the name `jackdaw` itself are reserved for built-in extensions.
    fn id(&self) -> String;

    /// A human-readable name for this extension. This will be displayed in UIs.
    fn label(&self) -> String {
        self.id()
    }

    /// A human-readable description for this extension. This will be displayed in UIs.
    fn description(&self) -> String {
        "".to_string()
    }

    /// Classify this extension. Defaults to [`ExtensionKind::Regular`].
    ///
    /// The Extensions dialog reads this to split the list into Built-in
    /// and Custom sections. Reserved as a future hook for marketplace
    /// categories.
    fn kind(&self) -> ExtensionKind {
        ExtensionKind::Regular
    }

    /// Hook for one-time BEI input-context registration.
    ///
    /// Called once per catalog entry at app startup, before any
    /// `register()` call. BEI's `add_input_context::<C>()` must run
    /// exactly once per context type per app lifetime, so it cannot live
    /// inside `register` which runs on every enable.
    ///
    /// Defaults to no-op; override only if the extension adds BEI
    /// contexts.
    // FIXME: this leaks memory when the extension is disabled
    #[expect(unused_variables, reason = "The default implementation does nothing")]
    fn register_input_context(&self, app: &mut App) {}

    /// Main registration logic. Called each time the extension is
    /// enabled. Spawn operators, windows, BEI action entities, and any
    /// other owned state here.
    fn register(&self, ctx: &mut ExtensionContext);

    /// Optional hook called before the extension entity despawns.
    ///
    /// Child-entity cleanup handles registered windows, operators, BEI
    /// contexts, and observers automatically. Override only for non-ECS
    /// state (file handles, network sessions, and the like).
    #[expect(unused_variables, reason = "The default implementation does nothing")]
    fn unregister(&self, world: &mut World, extension_entity: Entity) {}
}

/// Passed to [`JackdawExtension::register`]. Holds the extension entity
/// and provides helpers that spawn child entities under it.
///
/// Wraps `&mut World` rather than `&mut App` because extensions may be
/// loaded from world-only contexts such as the Extensions dialog's
/// enable/disable observer. One-time setup that genuinely requires App
/// access (BEI input-context registration) runs through
/// [`JackdawExtension::register_input_context`] at catalog-registration
/// time.
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,
        }
    }

    /// Calls [`World::init_resource`] to initialize a resource, ensuring that it is removed on unload.
    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
    }

    /// Calls [`World::insert_resource`] to initialize a resource, ensuring that it is removed on unload.
    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
    }

    /// Calls [`World::add_observer`] to initialize an observer, ensuring that it is removed on unload.
    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
    }

    /// The root [`lifecycle::Extension`] entity.
    ///
    /// See also: [`ExtensionContext::entity`] and [`ExtensionContext::entity_mut`].
    pub fn id(&self) -> Entity {
        self.extension_entity
    }

    /// Register a dock window. Spawns a [`RegisteredWindow`] marker
    /// entity as a child of the extension entity; a cleanup observer
    /// calls `WindowRegistry::unregister` when the marker despawns.
    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
    }

    /// Register a workspace.
    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
    }

    /// Spawn an entity as a child of the extension entity. Typically
    /// used for BEI context entities with action bindings:
    /// `ctx.spawn((MyContext, actions!(MyContext[...])))`.
    ///
    /// The returned [`EntityWorldMut`] lets the caller keep adding
    /// components or children. Anything spawned this way is torn down
    /// when the extension unloads.
    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
    }

    /// Get the extension's root entity. Useful for inserting components that you want to
    /// be torn down on unload.
    pub fn entity<'w>(&'w self) -> EntityRef<'w> {
        self.world.entity(self.extension_entity)
    }

    /// Get the extension's root entity mutably. Useful for inserting components that you want to
    /// be torn down on unload.
    pub fn entity_mut<'w>(&'w mut self) -> EntityWorldMut<'w> {
        self.world.entity_mut(self.extension_entity)
    }

    /// Register an operator. Spawns an `OperatorEntity` as a child
    /// of the extension entity and a `Fire<O>` observer that dispatches the
    /// operator through [`crate::OperatorWorldExt::operator`]. BEI binding
    /// modifiers on the actions shape timing (press / release / hold).
    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();
                },),
                // Auto-tag any BEI action entity for this operator with
                // `OperatorAction(Op::ID)` so id-keyed lookups (tooltip
                // keybind discovery, future command palette) can find the
                // bindings without naming the typed `Action<Op>`. The
                // observer covers future spawns; the immediate query pass
                // below covers entities already spawned before this call
                // (some `add_to_extension` modules spawn actions first and
                // register the operator afterwards).
                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
    }

    /// Inject a section into an existing window (e.g. add a sub-section to
    /// the Inspector window). Section runs with `In<PanelContext>` each time
    /// the window re-renders.
    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
    }

    /// Contribute an entry to one of the editor's top-level menus
    /// (`"Add"`, `"Tools"`, etc.). Clicking the entry dispatches the
    /// referenced operator.
    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
    }

    /// Convenience that registers a menu entry using `O::LABEL` and
    /// `O::ID` from the operator type, so callers only need to supply the
    /// menu name. Equivalent to calling
    /// [`Self::register_menu_entry_manual`] with a full [`MenuEntryDescriptor`].
    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,
        })
    }
}

/// Tag any `Action<O>` entities that already exist with `OperatorAction(O::ID)`.
/// Run from `register_operator` to cover the case where action entities were
/// spawned before the operator was registered. Bevy caches the `QueryState`
/// across calls when this is invoked via `run_system_cached`.
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));
    }
}

/// Top level menus available for menu bar entries.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum TopLevelMenu {
    File,
    Edit,
    View,
    Add,
    Tools,
    Window,
    Custom(String),
}

impl TopLevelMenu {
    /// Returns the unique ID of the menu, used internally by the UI.
    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(),
        }
    }

    /// Returns the order of the menu, used to sort menu items in the UI.
    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,
        }
    }
}

/// Extension-facing descriptor for a menu bar entry. See
/// [`ExtensionContext::register_menu_entry_manual`].
pub struct MenuEntryDescriptor {
    /// Top-level menu.
    pub menu: TopLevelMenu,
    /// Text shown on the menu item.
    pub label: String,
    /// ID of an operator registered on the same extension, or any other
    /// loaded extension. Operator IDs are global. Clicking the menu
    /// entry dispatches this operator.
    pub operator_id: &'static str,
}

/// Extension-facing descriptor for a dock window. Mirrors
/// [`jackdaw_panels::DockWindowDescriptor`] but with `default_area`
/// optional: third-party extensions leave it `None` so their windows are
/// not auto-placed, while built-in Jackdaw extensions set it to preserve
/// the default layout.
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 {
    /// Creates a new `WindowDescriptor` with the given unique `id`.
    #[must_use]
    pub fn new(id: impl Into<String>) -> Self {
        let id = id.into();
        Self {
            id: id.clone(),
            name: id,
            ..default()
        }
    }

    /// Sets the name of the window shown in the UI.
    #[must_use]
    pub fn with_name(mut self, name: impl Into<String>) -> Self {
        self.name = name.into();
        self
    }

    /// Sets the icon of the window.
    #[must_use]
    pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
        self.icon = Some(icon.into());
        self
    }

    /// Sets the default area of the window used when adding the window.
    #[must_use]
    pub fn with_default_area(mut self, area: impl Into<Option<DefaultArea>>) -> Self {
        self.default_area = area.into();
        self
    }

    /// Sets the priority of the window.
    #[must_use]
    pub fn with_priority(mut self, priority: i32) -> Self {
        self.priority = Some(priority);
        self
    }

    /// Sets the build function for the window, which is used for building the window's UI.
    #[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(|_| {}),
        }
    }
}

/// Marker trait for panels that accept extension sections.
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";
}

/// Context passed to a panel-extension section when it's rendered.
pub struct PanelContext {
    pub window_id: String,
    pub panel_entity: Entity,
}

/// Plugin that wires up the extension framework into the editor.
///
/// Adds BEI, sets up the required resources (`OperatorIndex`,
/// `PanelExtensionRegistry`, `ExtensionCatalog`, `ActiveModalOperator`),
/// and registers the cleanup observers that keep non-ECS state in sync
/// when extension entities are despawned.
///
/// Also runs `tick_modal_operator` each frame in Update so modal
/// operators (Blender-style grab/rotate/scale) re-run their invoke
/// system until they return `Finished` or `Cancelled`.
pub struct ExtensionLoaderPlugin;

impl Plugin for ExtensionLoaderPlugin {
    fn build(&self, app: &mut App) {
        app.add_plugins((lifecycle::plugin, operator::plugin, registries::plugin));
    }
}