bevy_animation_graph_editor 0.9.0

Animation graph editor for the Bevy game engine
Documentation
use bevy::ecs::{
    component::Component,
    entity::Entity,
    world::{CommandQueue, World},
};
use egui_dock::{DockArea, DockState, NodeIndex};
use egui_notify::Toasts;

use crate::ui::{
    actions::{EditorAction, PushQueue},
    core::{EditorWindowExtension, EguiWindow, GlobalBuffers, LegacyEditorWindowContext},
    editor_windows::ragdoll_editor::RagdollEditorWindow,
    native_windows::{
        EditorWindowContext, NativeEditorWindow, NativeEditorWindowExtension,
        animation_clip_preview::ClipPreviewWindow, debugger::DebuggerWindow,
        event_sender::EventSenderWindow, event_track_editor::EventTrackEditorWindow,
        fsm_editor::FsmEditorWindow, fsm_picker::FsmPickerWindow, graph_editor::GraphEditorWindow,
        graph_picker::GraphPickerWindow, inspector::InspectorWindow,
        preview_hierarchy::PreviewHierarchyWindow, scene_picker::ScenePickerWindow,
        scene_preview::ScenePreviewWindow, scene_preview_errors::ScenePreviewErrorsWindow,
    },
    windows::Windows,
};

#[derive(Component)]
pub struct EditorViewState;

#[derive(Component)]
pub struct EditorView {
    pub(crate) name: String,
}

impl EditorView {
    pub fn init(world: &mut World, name: impl Into<String>) -> Entity {
        world
            .spawn((EditorViewState, EditorView { name: name.into() }))
            .id()
    }
}

fn ragdoll_view(
    _world: &mut World,
    windows: &mut Windows,
    _view_entity: Entity,
) -> DockState<EguiWindow> {
    let preview_window = windows.open(RagdollEditorWindow::default());

    DockState::new(vec![preview_window.into()])
}

fn event_track_view(world: &mut World, view_entity: Entity) -> DockState<EguiWindow> {
    let event_track_window = NativeEditorWindow::create(world, view_entity, EventTrackEditorWindow);
    let clip_preview_window = NativeEditorWindow::create(world, view_entity, ClipPreviewWindow);

    let mut state = DockState::new(vec![event_track_window.into()]);

    let tree = state.main_surface_mut();

    tree.split_above(NodeIndex::root(), 0.5, vec![clip_preview_window.into()]);

    state
}

fn main_view(world: &mut World, view_entity: Entity) -> DockState<EguiWindow> {
    let graph_editor = NativeEditorWindow::create(world, view_entity, GraphEditorWindow);
    let fsm_editor = NativeEditorWindow::create(world, view_entity, FsmEditorWindow);
    let inspector = NativeEditorWindow::create(world, view_entity, InspectorWindow);
    let debugger = NativeEditorWindow::create(world, view_entity, DebuggerWindow);
    let graph_selector = NativeEditorWindow::create(world, view_entity, GraphPickerWindow);
    let scene_selector = NativeEditorWindow::create(world, view_entity, ScenePickerWindow);
    let fsm_selector = NativeEditorWindow::create(world, view_entity, FsmPickerWindow);
    let scene_preview = NativeEditorWindow::create(world, view_entity, ScenePreviewWindow);
    let preview_hierarchy = NativeEditorWindow::create(world, view_entity, PreviewHierarchyWindow);
    let event_sender = NativeEditorWindow::create(world, view_entity, EventSenderWindow);
    let scene_preview_errors =
        NativeEditorWindow::create(world, view_entity, ScenePreviewErrorsWindow);

    let mut state = DockState::new(vec![graph_editor.into(), fsm_editor.into()]);
    let tree = state.main_surface_mut();
    let [graph_editor, inspectors] = tree.split_right(
        NodeIndex::root(),
        0.75,
        vec![inspector.into(), debugger.into()],
    );
    let [_graph_editor, graph_selector] =
        tree.split_left(graph_editor, 0.2, vec![graph_selector.into()]);
    let [_graph_selector, scene_selector] =
        tree.split_below(graph_selector, 0.5, vec![scene_selector.into()]);
    let [_scene_selector, _fsm_selector] =
        tree.split_below(scene_selector, 0.5, vec![fsm_selector.into()]);
    let [_node_inspector, preview] = tree.split_above(
        inspectors,
        0.5,
        vec![scene_preview.into(), preview_hierarchy.into()],
    );
    let [_preview, _preview_errors] = tree.split_below(
        preview,
        0.8,
        vec![event_sender.into(), scene_preview_errors.into()],
    );

    state
}

pub struct EditorViewContext<'a> {
    pub view_entity: Entity,
    pub notifications: &'a mut Toasts,
    pub command_queue: &'a mut CommandQueue,
    pub buffers: &'a mut GlobalBuffers,

    // For legacy windows
    pub windows: &'a mut Windows,
    pub editor_actions: &'a mut PushQueue<EditorAction>,
}

pub struct EditorViewUiState {
    pub entity: Entity,
    pub dock_state: DockState<EguiWindow>,
}

impl EditorViewUiState {
    pub fn init(entity: Entity, dock_state: DockState<EguiWindow>) -> Self {
        Self { entity, dock_state }
    }

    pub fn ui(&mut self, ctx: &mut egui::Context, world: &mut World, context: EditorViewContext) {
        let mut tab_viewer = TabViewer { world, context };

        DockArea::new(&mut self.dock_state)
            .style(egui_dock::Style::from_egui(ctx.style().as_ref()))
            .id(egui::Id::new(self.entity))
            .show(ctx, &mut tab_viewer);
    }

    pub fn empty(world: &mut World, name: impl Into<String>) -> Self {
        let entity = EditorView::init(world, name);
        let dock_state = DockState::new(vec![]);
        Self::init(entity, dock_state)
    }

    pub fn ragdoll(world: &mut World, windows: &mut Windows, name: impl Into<String>) -> Self {
        let entity = EditorView::init(world, name);
        let dock_state = ragdoll_view(world, windows, entity);
        Self::init(entity, dock_state)
    }

    pub fn event_tracks(world: &mut World, name: impl Into<String>) -> Self {
        let entity = EditorView::init(world, name);
        let dock_state = event_track_view(world, entity);
        Self::init(entity, dock_state)
    }

    pub fn animation_graphs(world: &mut World, name: impl Into<String>) -> Self {
        let entity = EditorView::init(world, name);
        let dock_state = main_view(world, entity);
        Self::init(entity, dock_state)
    }
}

pub struct TabViewer<'a> {
    world: &'a mut World,
    context: EditorViewContext<'a>,
}

impl egui_dock::TabViewer for TabViewer<'_> {
    type Tab = EguiWindow;

    fn ui(&mut self, ui: &mut egui::Ui, window: &mut Self::Tab) {
        match window {
            EguiWindow::DynWindow(editor_window) => {
                self.context
                    .windows
                    .with_window_mut(*editor_window, |window, _windows| {
                        let mut ctx = LegacyEditorWindowContext {
                            window_id: *editor_window,
                            notifications: self.context.notifications,
                            editor_actions: self.context.editor_actions,
                        };

                        ui.push_id(ui.id().with(*editor_window), |ui| {
                            window.ui(ui, self.world, &mut ctx);
                        })
                    });
            }
            EguiWindow::EntityWindow(window) => {
                let mut ctx = EditorWindowContext {
                    window_entity: window.entity,
                    view_entity: self.context.view_entity,
                    notifications: self.context.notifications,
                    command_queue: self.context.command_queue,
                    buffers: self.context.buffers.for_window(window.entity),
                    editor_actions: self.context.editor_actions,
                };

                window.ui(ui, self.world, &mut ctx);
            }
        }
    }

    fn title(&mut self, window: &mut Self::Tab) -> egui::WidgetText {
        window.display_name(self.context.windows).into()
    }

    fn force_close(&mut self, window: &mut Self::Tab) -> bool {
        match window {
            EguiWindow::DynWindow(window_id) => !self.context.windows.window_exists(*window_id),
            EguiWindow::EntityWindow(window) => self.world.get_entity(window.entity).is_err(),
        }
    }

    fn closeable(&mut self, tab: &mut Self::Tab) -> bool {
        match tab {
            EguiWindow::DynWindow(window_id) => self
                .context
                .windows
                .get_window(*window_id)
                .is_some_and(|w| w.closeable()),
            EguiWindow::EntityWindow(window) => window.closeable(),
        }
    }
}