mirror of
https://github.com/FyroxEngine/Fyrox.git
synced 2026-10-12 02:02:20 +00:00
3345 lines
129 KiB
Rust
3345 lines
129 KiB
Rust
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in all
|
|
// copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|
|
|
|
#![allow(deprecated)] // TODO
|
|
#![allow(irrefutable_let_patterns)]
|
|
#![allow(clippy::too_many_arguments)]
|
|
#![allow(clippy::large_enum_variant)]
|
|
#![allow(clippy::mixed_read_write_in_expression)]
|
|
// These are useless.
|
|
#![allow(clippy::upper_case_acronyms)]
|
|
#![allow(clippy::inconsistent_struct_constructor)]
|
|
#![allow(clippy::mutable_key_type)]
|
|
#![allow(mismatched_lifetime_syntaxes)]
|
|
|
|
pub mod asset;
|
|
pub mod audio;
|
|
pub mod camera;
|
|
pub mod command;
|
|
pub mod configurator;
|
|
pub mod export;
|
|
pub mod highlight;
|
|
pub mod interaction;
|
|
pub mod light;
|
|
pub mod menu;
|
|
pub mod mesh;
|
|
pub mod message;
|
|
pub mod overlay;
|
|
pub mod particle;
|
|
pub mod plugin;
|
|
pub mod plugins;
|
|
pub mod preview;
|
|
pub mod scene;
|
|
pub mod scene_viewer;
|
|
pub mod settings;
|
|
pub mod stats;
|
|
pub mod test;
|
|
pub mod ui_scene;
|
|
pub mod utils;
|
|
pub mod world;
|
|
|
|
pub use fyrox;
|
|
|
|
use crate::world::create::EntityCreator;
|
|
use crate::{
|
|
asset::{item::AssetItem, AssetBrowser},
|
|
audio::{preview::AudioPreviewPanel, AudioPanel},
|
|
camera::panel::CameraPreviewControlPanel,
|
|
command::{panel::CommandStackViewer, Command, CommandTrait},
|
|
configurator::Configurator,
|
|
export::ExportWindow,
|
|
fyrox::{
|
|
asset::{io::FsResourceIo, manager::ResourceManager, untyped::ResourceKind},
|
|
core::info,
|
|
core::{
|
|
algebra::{Matrix3, Vector2},
|
|
color::Color,
|
|
log::{Log, MessageKind},
|
|
make_relative_path,
|
|
parking_lot::Mutex,
|
|
pool::Handle,
|
|
reflect::prelude::*,
|
|
task::TaskPool,
|
|
uuid::Uuid,
|
|
watcher::FileSystemWatcher,
|
|
SafeLock,
|
|
},
|
|
dpi::{PhysicalPosition, PhysicalSize},
|
|
engine::GraphicsContext,
|
|
engine::{
|
|
ApplicationLoopController, Engine, EngineInitParams, GraphicsContextParams,
|
|
SerializationContext,
|
|
},
|
|
event::{Event, WindowEvent},
|
|
event_loop::ActiveEventLoop,
|
|
event_loop::EventLoop,
|
|
fxhash::{FxHashMap, FxHashSet},
|
|
gui::{
|
|
border::BorderBuilder,
|
|
brush::Brush,
|
|
button::ButtonBuilder,
|
|
constructor::new_widget_constructor_container,
|
|
dock::{
|
|
config::DockingManagerLayoutDescriptor, DockingManager, DockingManagerBuilder,
|
|
DockingManagerMessage, TileBuilder, TileContent,
|
|
},
|
|
dropdown_list::DropdownListBuilder,
|
|
file_browser::{
|
|
FileSelector, FileSelectorBuilder, FileSelectorMode, FileType, PathFilter,
|
|
},
|
|
formatted_text::WrapMode,
|
|
grid::{Column, Grid, GridBuilder, Row},
|
|
inspector::editors::PropertyEditorDefinitionContainer,
|
|
key::HotKey,
|
|
log::LogPanel,
|
|
message::{MessageDirection, UiMessage},
|
|
messagebox::{
|
|
MessageBox, MessageBoxBuilder, MessageBoxButtons, MessageBoxMessage,
|
|
MessageBoxResult,
|
|
},
|
|
screen::ScreenBuilder,
|
|
stack_panel::StackPanelBuilder,
|
|
style::{resource::StyleResource, Style},
|
|
text::{Text, TextBuilder, TextMessage},
|
|
widget::{WidgetBuilder, WidgetMessage},
|
|
window::{Window, WindowAlignment, WindowBuilder, WindowMessage, WindowTitle},
|
|
BuildContext, Thickness, UiNode, UserInterface, VerticalAlignment,
|
|
},
|
|
material::{
|
|
shader::{ShaderResource, ShaderResourceExtension},
|
|
Material, MaterialResource,
|
|
},
|
|
plugin::{dylib::DyLibDynamicPlugin, DynamicPlugin, Plugin, PluginContainer},
|
|
resource::texture::{
|
|
CompressionOptions, TextureImportOptions, TextureMinificationFilter, TextureResource,
|
|
TextureResourceExtension,
|
|
},
|
|
scene::{graph::GraphUpdateSwitches, mesh::Mesh, Scene, SceneLoader},
|
|
utils::{translate_cursor_icon, translate_event},
|
|
window::WindowAttributes,
|
|
},
|
|
highlight::HighlightRenderPass,
|
|
interaction::{
|
|
move_mode::MoveInteractionMode,
|
|
navmesh::{EditNavmeshMode, NavmeshPanel},
|
|
rotate_mode::RotateInteractionMode,
|
|
scale_mode::ScaleInteractionMode,
|
|
select_mode::SelectInteractionMode,
|
|
terrain::TerrainInteractionMode,
|
|
},
|
|
light::LightPanel,
|
|
menu::{Menu, MenuContext, Panels},
|
|
mesh::{MeshControlPanel, SurfaceDataViewer},
|
|
message::MessageSender,
|
|
overlay::OverlayRenderPass,
|
|
particle::ParticleSystemPreviewControlPanel,
|
|
plugin::{EditorPlugin, EditorPluginsContainer},
|
|
plugins::{
|
|
absm::{AbsmEditor, AbsmEditorPlugin},
|
|
animation::AnimationEditorPlugin,
|
|
collider::ColliderPlugin,
|
|
curve_editor::CurveEditorPlugin,
|
|
inspector::editors::make_property_editors_container,
|
|
material::MaterialPlugin,
|
|
probe::ReflectionProbePlugin,
|
|
ragdoll::RagdollPlugin,
|
|
settings::SettingsPlugin,
|
|
stats::EditorStatisticsPlugin,
|
|
tilemap::TileMapEditorPlugin,
|
|
},
|
|
scene::{
|
|
commands::{
|
|
make_delete_selection_command, ChangeSelectionCommand, GameSceneContext, PasteCommand,
|
|
},
|
|
container::{EditorSceneEntry, SceneContainer},
|
|
dialog::NodeRemovalDialog,
|
|
settings::SceneSettingsWindow,
|
|
GameScene, Selection,
|
|
},
|
|
scene_viewer::SceneViewer,
|
|
settings::{general::EditorStyle, Settings},
|
|
stats::{StatisticsWindow, StatisticsWindowAction},
|
|
ui_scene::{
|
|
bbcode::BBCodePanel, commands::graph::PasteWidgetCommand, menu::WidgetContextMenu,
|
|
utils::UiSceneWorldViewerDataProvider, UiScene,
|
|
},
|
|
utils::doc::DocWindow,
|
|
world::{graph::EditorSceneWrapper, menu::SceneNodeContextMenu, WorldViewer},
|
|
};
|
|
use fyrox::core::uuid::uuid;
|
|
use fyrox_build_tools::{build::BuildWindow, CommandDescriptor};
|
|
pub use message::Message;
|
|
use plugins::inspector::InspectorPlugin;
|
|
use std::cell::Cell;
|
|
use std::{
|
|
cell::RefCell,
|
|
collections::VecDeque,
|
|
io::{BufRead, BufReader, Cursor, Read},
|
|
path::{Path, PathBuf},
|
|
process::Stdio,
|
|
rc::Rc,
|
|
sync::{
|
|
atomic::{AtomicBool, Ordering},
|
|
mpsc::{self, channel, Receiver},
|
|
Arc, LazyLock,
|
|
},
|
|
time::{Duration, Instant},
|
|
};
|
|
use toml_edit::DocumentMut;
|
|
|
|
pub const FIXED_TIMESTEP: f32 = 1.0 / 60.0;
|
|
|
|
static EDITOR_VERSION: LazyLock<String> = LazyLock::new(|| {
|
|
let manifest = include_bytes!("../Cargo.toml");
|
|
let mut file = Cursor::new(&manifest);
|
|
let mut toml = String::new();
|
|
if file.read_to_string(&mut toml).is_ok() {
|
|
if let Ok(document) = toml.parse::<DocumentMut>() {
|
|
if let Some(package) = document.get("package").and_then(|i| i.as_table()) {
|
|
if let Some(version) = package.get("version") {
|
|
return version.to_string().replace('\"', "");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
"<unknown>".to_string()
|
|
});
|
|
|
|
static EDITOR_TEXTURE_CACHE: LazyLock<Mutex<FxHashMap<usize, TextureResource>>> =
|
|
LazyLock::new(Mutex::default);
|
|
|
|
pub fn load_texture_internal(data: &[u8]) -> Option<TextureResource> {
|
|
let mut cache = EDITOR_TEXTURE_CACHE.safe_lock();
|
|
|
|
// Editor use data that is embedded in the binary, so each such piece of data will have fixed
|
|
// location in memory. This fact allows us to cache the resources and skip redundant loading if
|
|
// they're already loaded.
|
|
let id = data.as_ptr() as usize;
|
|
|
|
if let Some(existing) = cache.get(&id) {
|
|
Some(existing.clone())
|
|
} else {
|
|
let texture = TextureResource::load_from_memory(
|
|
Uuid::new_v4(),
|
|
ResourceKind::Embedded,
|
|
data,
|
|
TextureImportOptions::default()
|
|
.with_compression(CompressionOptions::NoCompression)
|
|
.with_minification_filter(TextureMinificationFilter::LinearMipMapLinear)
|
|
.with_lod_bias(-1.0),
|
|
)
|
|
.ok()?;
|
|
|
|
cache.insert(id, texture.clone());
|
|
|
|
Some(texture)
|
|
}
|
|
}
|
|
|
|
pub fn load_image_internal(data: &[u8]) -> Option<TextureResource> {
|
|
load_texture_internal(data)
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! load_texture {
|
|
($file:expr $(,)?) => {
|
|
$crate::load_texture_internal(include_bytes!($file))
|
|
};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! load_image {
|
|
($file:expr $(,)?) => {
|
|
$crate::load_image_internal(include_bytes!($file))
|
|
};
|
|
}
|
|
|
|
#[cfg(feature = "backend_opengl")]
|
|
static GIZMO_SHADER: LazyLock<ShaderResource> = LazyLock::new(|| {
|
|
ShaderResource::from_str(
|
|
Uuid::new_v4(),
|
|
include_str!("../resources/shaders/opengl/gizmo.shader"),
|
|
Default::default(),
|
|
)
|
|
.unwrap()
|
|
});
|
|
|
|
#[cfg(not(feature = "backend_opengl"))]
|
|
static GIZMO_SHADER: LazyLock<ShaderResource> = LazyLock::new(|| {
|
|
ShaderResource::from_str(
|
|
Uuid::new_v4(),
|
|
include_str!("../resources/shaders/wgpu/gizmo.shader"),
|
|
Default::default(),
|
|
)
|
|
.unwrap()
|
|
});
|
|
|
|
pub fn make_color_material(color: Color) -> MaterialResource {
|
|
let mut material = Material::from_shader(GIZMO_SHADER.clone());
|
|
material.set_property("diffuseColor", color);
|
|
MaterialResource::new_embedded(material)
|
|
}
|
|
|
|
pub fn set_mesh_diffuse_color(mesh: &mut Mesh, color: Color) {
|
|
for surface in mesh.surfaces() {
|
|
surface
|
|
.material()
|
|
.data_ref()
|
|
.set_property("diffuseColor", color);
|
|
}
|
|
}
|
|
|
|
pub fn create_terrain_layer_material() -> MaterialResource {
|
|
let mut material = Material::standard_terrain();
|
|
material.set_property("texCoordScale", Vector2::new(10.0, 10.0));
|
|
MaterialResource::new_embedded(material)
|
|
}
|
|
|
|
pub fn make_save_file_selector(
|
|
ctx: &mut BuildContext,
|
|
default_file_name: PathBuf,
|
|
file_type: FileType,
|
|
id: Uuid,
|
|
) -> Handle<FileSelector> {
|
|
FileSelectorBuilder::new(
|
|
WindowBuilder::new(
|
|
WidgetBuilder::new()
|
|
.with_id(id)
|
|
.with_width(300.0)
|
|
.with_height(400.0),
|
|
)
|
|
.with_title(WindowTitle::text("Save Scene As"))
|
|
.open(false)
|
|
.with_remove_on_close(true),
|
|
)
|
|
.with_mode(FileSelectorMode::Save { default_file_name })
|
|
.with_filter(PathFilter::new().with_file_type(file_type))
|
|
.with_path("./")
|
|
.build(ctx)
|
|
}
|
|
|
|
pub enum Mode {
|
|
Edit,
|
|
Build {
|
|
queue: VecDeque<CommandDescriptor>,
|
|
process: Option<std::process::Child>,
|
|
play_after_build: bool,
|
|
},
|
|
Play {
|
|
process: std::process::Child,
|
|
active: Arc<AtomicBool>,
|
|
},
|
|
}
|
|
|
|
impl Mode {
|
|
pub fn is_edit(&self) -> bool {
|
|
matches!(self, Mode::Edit)
|
|
}
|
|
}
|
|
|
|
pub struct GameLoopData {
|
|
clock: Instant,
|
|
lag: f32,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct StartupData {
|
|
/// Working directory that should be set when starting the editor. If it is empty, then
|
|
/// current working directory won't be changed.
|
|
pub working_directory: PathBuf,
|
|
|
|
/// A scene to load at the editor start. If it is empty, no scene will be loaded.
|
|
pub scenes: Vec<PathBuf>,
|
|
|
|
pub named_objects: bool,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum SaveSceneConfirmationDialogAction {
|
|
/// Do nothing.
|
|
None,
|
|
/// Opens `Load Scene` dialog.
|
|
OpenLoadSceneDialog,
|
|
/// Load specified scene.
|
|
LoadScene(PathBuf),
|
|
/// Immediately creates new scene.
|
|
MakeNewScene,
|
|
/// Closes the specified scene.
|
|
CloseScene(Uuid),
|
|
}
|
|
|
|
pub struct SaveSceneConfirmationDialog {
|
|
save_message_box: Handle<MessageBox>,
|
|
action: SaveSceneConfirmationDialogAction,
|
|
id: Uuid,
|
|
}
|
|
|
|
impl SaveSceneConfirmationDialog {
|
|
pub const DIALOG_ID: Uuid = uuid!("9828ff5f-c2e2-4e72-9b17-0d5192155a77");
|
|
|
|
pub fn new(ctx: &mut BuildContext) -> Self {
|
|
let save_message_box = MessageBoxBuilder::new(
|
|
WindowBuilder::new(
|
|
WidgetBuilder::new()
|
|
.with_id(Self::DIALOG_ID)
|
|
.with_width(300.0)
|
|
.with_height(100.0),
|
|
)
|
|
.can_close(false)
|
|
.can_minimize(false)
|
|
.open(false)
|
|
.with_title(WindowTitle::text("Unsaved changes")),
|
|
)
|
|
.with_buttons(MessageBoxButtons::YesNoCancel)
|
|
.build(ctx);
|
|
|
|
Self {
|
|
save_message_box,
|
|
action: SaveSceneConfirmationDialogAction::None,
|
|
id: Default::default(),
|
|
}
|
|
}
|
|
|
|
pub fn open(
|
|
&mut self,
|
|
ui: &UserInterface,
|
|
id: Uuid,
|
|
scenes: &SceneContainer,
|
|
action: SaveSceneConfirmationDialogAction,
|
|
) {
|
|
self.id = id;
|
|
self.action = action;
|
|
|
|
if let Some(entry) = scenes.entry_by_scene_id(self.id) {
|
|
ui.send(
|
|
self.save_message_box,
|
|
MessageBoxMessage::Open {
|
|
title: None,
|
|
text: Some(format!(
|
|
"There are unsaved changes in the {} scene. \
|
|
Do you wish to save them before continue?",
|
|
entry.name(),
|
|
)),
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
pub fn handle_ui_message(
|
|
&mut self,
|
|
message: &UiMessage,
|
|
sender: &MessageSender,
|
|
scenes: &SceneContainer,
|
|
) {
|
|
if let Some(MessageBoxMessage::Close(result)) = message.data_from(self.save_message_box) {
|
|
match result {
|
|
MessageBoxResult::No => match self.action {
|
|
SaveSceneConfirmationDialogAction::None => {}
|
|
SaveSceneConfirmationDialogAction::OpenLoadSceneDialog => {
|
|
sender.send(Message::OpenLoadSceneDialog)
|
|
}
|
|
SaveSceneConfirmationDialogAction::MakeNewScene => {
|
|
sender.send(Message::NewScene)
|
|
}
|
|
SaveSceneConfirmationDialogAction::CloseScene(scene) => {
|
|
sender.send(Message::CloseScene(scene))
|
|
}
|
|
SaveSceneConfirmationDialogAction::LoadScene(ref path) => {
|
|
sender.send(Message::LoadScene(path.clone()))
|
|
}
|
|
},
|
|
MessageBoxResult::Yes => {
|
|
if let Some(entry) = scenes.entry_by_scene_id(self.id) {
|
|
if let Some(path) = entry.path.clone() {
|
|
// If the scene was already saved into some file - save it
|
|
// immediately and perform the requested action.
|
|
sender.send(Message::SaveScene { id: self.id, path });
|
|
|
|
match self.action {
|
|
SaveSceneConfirmationDialogAction::None => {}
|
|
SaveSceneConfirmationDialogAction::OpenLoadSceneDialog => {
|
|
sender.send(Message::OpenLoadSceneDialog)
|
|
}
|
|
SaveSceneConfirmationDialogAction::MakeNewScene => {
|
|
sender.send(Message::NewScene)
|
|
}
|
|
SaveSceneConfirmationDialogAction::CloseScene(scene) => {
|
|
sender.send(Message::CloseScene(scene))
|
|
}
|
|
SaveSceneConfirmationDialogAction::LoadScene(ref path) => {
|
|
sender.send(Message::LoadScene(path.clone()))
|
|
}
|
|
}
|
|
|
|
self.action = SaveSceneConfirmationDialogAction::None;
|
|
} else {
|
|
// Otherwise, open save scene dialog and do the action after the
|
|
// scene was saved.
|
|
sender.send(Message::OpenSaveSceneDialog {
|
|
default_file_info: entry.default_file_info(),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_message(&mut self, message: &Message, sender: &MessageSender) {
|
|
if let Message::SaveScene { id: scene, .. } = message {
|
|
if *scene == self.id {
|
|
self.id = Default::default();
|
|
|
|
match std::mem::replace(&mut self.action, SaveSceneConfirmationDialogAction::None) {
|
|
SaveSceneConfirmationDialogAction::None => {}
|
|
SaveSceneConfirmationDialogAction::OpenLoadSceneDialog => {
|
|
sender.send(Message::OpenLoadSceneDialog);
|
|
}
|
|
SaveSceneConfirmationDialogAction::MakeNewScene => {
|
|
sender.send(Message::NewScene)
|
|
}
|
|
SaveSceneConfirmationDialogAction::CloseScene(scene) => {
|
|
sender.send(Message::CloseScene(scene));
|
|
}
|
|
SaveSceneConfirmationDialogAction::LoadScene(path) => {
|
|
sender.send(Message::LoadScene(path))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct UpdateLoopState(u32);
|
|
|
|
impl Default for UpdateLoopState {
|
|
fn default() -> Self {
|
|
// Run at least a second from the start to ensure that all OS-specific stuff was done.
|
|
Self(60)
|
|
}
|
|
}
|
|
|
|
impl UpdateLoopState {
|
|
fn request_update_in_next_frame(&mut self) {
|
|
self.0 = self.0.max(3);
|
|
}
|
|
|
|
fn request_update_in_current_frame(&mut self) {
|
|
self.0 = self.0.max(1);
|
|
}
|
|
|
|
fn decrease_counter(&mut self) {
|
|
self.0 = self.0.saturating_sub(1);
|
|
}
|
|
|
|
fn is_suspended(&self) -> bool {
|
|
self.0 == 0
|
|
}
|
|
}
|
|
|
|
pub struct SceneLoadingWindow {
|
|
window: Handle<Window>,
|
|
scene_list_text: Handle<Text>,
|
|
}
|
|
|
|
impl SceneLoadingWindow {
|
|
pub fn new(ctx: &mut BuildContext) -> Self {
|
|
let scene_list_text = TextBuilder::new(WidgetBuilder::new()).build(ctx);
|
|
let window = WindowBuilder::new(WidgetBuilder::new().with_width(300.0).with_height(100.0))
|
|
.with_title(WindowTitle::text("Please wait..."))
|
|
.can_close(false)
|
|
.can_minimize(false)
|
|
.open(false)
|
|
.can_maximize(false)
|
|
.with_remove_on_close(true)
|
|
.with_content(
|
|
StackPanelBuilder::new(
|
|
WidgetBuilder::new()
|
|
.with_margin(Thickness::uniform(2.0))
|
|
.with_child(
|
|
TextBuilder::new(
|
|
WidgetBuilder::new().with_margin(Thickness::uniform(2.0)),
|
|
)
|
|
.with_wrap(WrapMode::Word)
|
|
.with_text(
|
|
"Please wait until the following scene(s) are \
|
|
fully loaded.",
|
|
)
|
|
.build(ctx),
|
|
)
|
|
.with_child(scene_list_text),
|
|
)
|
|
.build(ctx),
|
|
)
|
|
.build(ctx);
|
|
|
|
ctx.inner().send(
|
|
window,
|
|
WindowMessage::Open {
|
|
alignment: WindowAlignment::Center,
|
|
modal: true,
|
|
focus_content: true,
|
|
},
|
|
);
|
|
|
|
Self {
|
|
window,
|
|
scene_list_text,
|
|
}
|
|
}
|
|
|
|
pub fn update(self, set: &FxHashSet<PathBuf>, ui: &UserInterface) -> Option<Self> {
|
|
if set.is_empty() {
|
|
ui.send(self.window, WindowMessage::Close);
|
|
return None;
|
|
}
|
|
|
|
let list = set.iter().fold(String::new(), |mut str, path| {
|
|
str.push_str(&path.to_string_lossy());
|
|
str.push('\n');
|
|
str
|
|
});
|
|
ui.send(self.scene_list_text, TextMessage::Text(list));
|
|
|
|
Some(self)
|
|
}
|
|
}
|
|
|
|
pub struct Editor {
|
|
pub game_loop_data: GameLoopData,
|
|
pub scenes: SceneContainer,
|
|
pub message_sender: MessageSender,
|
|
pub message_receiver: Receiver<Message>,
|
|
pub world_viewer: WorldViewer,
|
|
pub root_grid: Handle<Grid>,
|
|
pub scene_viewer: SceneViewer,
|
|
pub asset_browser: AssetBrowser,
|
|
pub exit_message_box: Handle<MessageBox>,
|
|
pub save_scene_dialog: SaveSceneConfirmationDialog,
|
|
pub light_panel: LightPanel,
|
|
pub menu: Menu,
|
|
pub configurator: Configurator,
|
|
pub log: LogPanel,
|
|
pub command_stack_viewer: CommandStackViewer,
|
|
pub validation_message_box: Handle<MessageBox>,
|
|
pub navmesh_panel: NavmeshPanel,
|
|
pub settings: Settings,
|
|
pub audio_panel: AudioPanel,
|
|
pub mode: Mode,
|
|
pub build_window: Option<BuildWindow>,
|
|
pub scene_settings: SceneSettingsWindow,
|
|
pub bbcode_panel: BBCodePanel,
|
|
pub particle_system_control_panel: ParticleSystemPreviewControlPanel,
|
|
pub camera_control_panel: CameraPreviewControlPanel,
|
|
pub mesh_control_panel: MeshControlPanel,
|
|
pub audio_preview_panel: AudioPreviewPanel,
|
|
pub doc_window: DocWindow,
|
|
pub docking_manager: Handle<DockingManager>,
|
|
pub node_removal_dialog: NodeRemovalDialog,
|
|
pub engine: Engine,
|
|
pub plugins: EditorPluginsContainer,
|
|
pub focused: bool,
|
|
pub entity_creator: EntityCreator,
|
|
pub update_loop_state: UpdateLoopState,
|
|
pub is_suspended: bool,
|
|
pub scene_node_context_menu: Rc<RefCell<SceneNodeContextMenu>>,
|
|
pub widget_context_menu: Rc<RefCell<WidgetContextMenu>>,
|
|
pub overlay_pass: Option<Rc<RefCell<OverlayRenderPass>>>,
|
|
pub highlighter: Option<Rc<RefCell<HighlightRenderPass>>>,
|
|
pub export_window: Option<ExportWindow>,
|
|
pub statistics_window: Option<StatisticsWindow>,
|
|
pub surface_data_viewer: Option<SurfaceDataViewer>,
|
|
pub processed_ui_messages: usize,
|
|
pub styles: FxHashMap<EditorStyle, StyleResource>,
|
|
pub running_game_process: Option<(std::process::Child, Arc<AtomicBool>)>,
|
|
pub user_project_icon: Option<Vec<u8>>,
|
|
pub user_project_name: String,
|
|
pub user_project_version: String,
|
|
pub loading_scenes: Arc<Mutex<FxHashSet<PathBuf>>>,
|
|
pub scene_loading_window: Option<SceneLoadingWindow>,
|
|
pub default_layout: DockingManagerLayoutDescriptor,
|
|
pub startup_data: Option<StartupData>,
|
|
/// Allows you to register your property editors for custom types.
|
|
pub property_editors: Arc<PropertyEditorDefinitionContainer>,
|
|
}
|
|
|
|
fn make_dark_style() -> StyleResource {
|
|
let mut dark_style = Style::dark_style();
|
|
dark_style
|
|
.set(WorldViewer::INSTANCE_BRUSH, Brush::solid(160, 160, 200))
|
|
.set(AssetItem::SELECTED_PREVIEW, Brush::solid(217, 230, 255))
|
|
.set(
|
|
AssetItem::SELECTED_TEXT_BORDER_BACKGROUND,
|
|
Brush::solid(44, 93, 135),
|
|
)
|
|
.set(AssetItem::TEXT_BORDER_DROP_BRUSH, Brush::solid(51, 81, 226))
|
|
.set(AssetItem::DESELECTED_PREVIEW, Brush::Solid(Color::WHITE))
|
|
.set(
|
|
AssetItem::DESELECTED_TEXT_BORDER_BACKGROUND,
|
|
Brush::transparent(),
|
|
)
|
|
.set(AssetItem::NORMAL_TEXT_BORDER_BRUSH, Brush::transparent())
|
|
.set(ExportWindow::TITLE_BRUSH, Brush::Solid(Color::CORN_SILK))
|
|
.set(AbsmEditor::NORMAL_ROOT_COLOR, Brush::solid(40, 80, 0))
|
|
.set(AbsmEditor::SELECTED_ROOT_COLOR, Brush::solid(60, 100, 0));
|
|
|
|
StyleResource::new_embedded(dark_style)
|
|
}
|
|
|
|
fn make_light_style() -> StyleResource {
|
|
let mut light_style = Style::light_style();
|
|
light_style
|
|
.set(WorldViewer::INSTANCE_BRUSH, Brush::solid(70, 70, 120))
|
|
.set(AssetItem::SELECTED_PREVIEW, Brush::solid(217, 230, 255))
|
|
.set(
|
|
AssetItem::SELECTED_TEXT_BORDER_BACKGROUND,
|
|
Brush::solid(44, 93, 135),
|
|
)
|
|
.set(AssetItem::TEXT_BORDER_DROP_BRUSH, Brush::solid(51, 81, 226))
|
|
.set(AssetItem::DESELECTED_PREVIEW, Brush::Solid(Color::WHITE))
|
|
.set(
|
|
AssetItem::DESELECTED_TEXT_BORDER_BACKGROUND,
|
|
Brush::transparent(),
|
|
)
|
|
.set(AssetItem::NORMAL_TEXT_BORDER_BRUSH, Brush::transparent())
|
|
.set(ExportWindow::TITLE_BRUSH, Brush::Solid(Color::CORN_SILK))
|
|
.set(AbsmEditor::NORMAL_ROOT_COLOR, Brush::solid(40, 80, 0))
|
|
.set(AbsmEditor::SELECTED_ROOT_COLOR, Brush::solid(60, 100, 0));
|
|
|
|
StyleResource::new_embedded(light_style)
|
|
}
|
|
|
|
impl Editor {
|
|
pub const UI_FONT_SIZE: &'static str = "Editor.UI.Font.Size";
|
|
|
|
pub fn new(startup_data: Option<StartupData>) -> Self {
|
|
Self::new_with_settings(startup_data, Default::default())
|
|
}
|
|
|
|
pub fn new_with_settings(startup_data: Option<StartupData>, settings: Settings) -> Self {
|
|
// Useful for debugging purposes when users don't bother to mention editor version
|
|
// they're using.
|
|
Log::info(format!("Editor version: {}", *EDITOR_VERSION));
|
|
|
|
let (log_message_sender, log_message_receiver) = channel();
|
|
|
|
Log::add_listener(log_message_sender);
|
|
|
|
let styles = [
|
|
(EditorStyle::Dark, make_dark_style()),
|
|
(EditorStyle::Light, make_light_style()),
|
|
]
|
|
.into_iter()
|
|
.collect::<FxHashMap<_, _>>();
|
|
|
|
let mut settings = settings;
|
|
|
|
match Settings::load() {
|
|
Ok(s) => {
|
|
settings = s;
|
|
|
|
Log::info("Editor settings were loaded successfully!");
|
|
}
|
|
Err(e) => Log::warn(format!(
|
|
"Failed to load settings, fallback to default. Reason: {e:?}"
|
|
)),
|
|
}
|
|
|
|
let inner_size = PhysicalSize::new(
|
|
settings.windows.window_size.x,
|
|
settings.windows.window_size.y,
|
|
);
|
|
|
|
let mut window_attributes = WindowAttributes::default();
|
|
window_attributes.maximized = settings.windows.window_maximized;
|
|
window_attributes.inner_size = Some(inner_size.into());
|
|
window_attributes.position = Some(
|
|
PhysicalPosition::new(
|
|
settings.windows.window_position.x,
|
|
settings.windows.window_position.y,
|
|
)
|
|
.into(),
|
|
);
|
|
window_attributes.resizable = true;
|
|
window_attributes.title = "FyroxEd".to_string();
|
|
let graphics_context_params = GraphicsContextParams {
|
|
window_attributes,
|
|
vsync: true,
|
|
msaa_sample_count: Some(4),
|
|
graphics_server_constructor: Default::default(),
|
|
named_objects: startup_data
|
|
.as_ref()
|
|
.map(|d| d.named_objects)
|
|
.unwrap_or_default(),
|
|
};
|
|
|
|
let serialization_context = Arc::new(SerializationContext::new());
|
|
let task_pool = Arc::new(TaskPool::new());
|
|
let mut engine = Engine::new(EngineInitParams {
|
|
graphics_context_params,
|
|
resource_manager: ResourceManager::new(Arc::new(FsResourceIo), task_pool.clone()),
|
|
serialization_context,
|
|
task_pool,
|
|
widget_constructors: Arc::new(new_widget_constructor_container()),
|
|
dyn_type_constructors: Arc::new(Default::default()),
|
|
})
|
|
.unwrap();
|
|
|
|
let (message_sender, message_receiver) = mpsc::channel();
|
|
let message_sender = MessageSender(message_sender);
|
|
|
|
engine
|
|
.user_interfaces
|
|
.add(UserInterface::new(Vector2::new(100.0, 100.0)));
|
|
|
|
let ui = engine.user_interfaces.first_mut();
|
|
if let Some(style) = styles.get(&settings.general.style) {
|
|
ui.set_style(style.clone());
|
|
}
|
|
|
|
let property_editors = Arc::new(make_property_editors_container(
|
|
message_sender.clone(),
|
|
engine.resource_manager.clone(),
|
|
));
|
|
let configurator = Configurator::new(message_sender.clone(), &mut ui.build_ctx());
|
|
|
|
let scene_viewer = SceneViewer::new(&mut engine, message_sender.clone(), &mut settings);
|
|
let asset_browser = AssetBrowser::new(&mut engine);
|
|
let menu = Menu::new(&mut engine, message_sender.clone(), &settings);
|
|
let light_panel = LightPanel::new(&mut engine, property_editors.clone());
|
|
let audio_panel = AudioPanel::new(
|
|
&mut engine,
|
|
message_sender.clone(),
|
|
asset_browser.preview_sender.clone(),
|
|
);
|
|
let ctx = &mut engine.user_interfaces.first_mut().build_ctx();
|
|
ScreenBuilder::new(
|
|
WidgetBuilder::new().with_child(BorderBuilder::new(WidgetBuilder::new()).build(ctx)),
|
|
)
|
|
.build(ctx);
|
|
let navmesh_panel = NavmeshPanel::new(scene_viewer.frame(), ctx, message_sender.clone());
|
|
let scene_node_context_menu = Rc::new(RefCell::new(SceneNodeContextMenu::new(ctx)));
|
|
let widget_context_menu = Rc::new(RefCell::new(WidgetContextMenu::new(ctx)));
|
|
let world_outliner = WorldViewer::new(ctx, message_sender.clone(), &settings);
|
|
let command_stack_viewer = CommandStackViewer::new(ctx, message_sender.clone());
|
|
let log = LogPanel::new(
|
|
ctx,
|
|
log_message_receiver,
|
|
load_image!("../resources/clear.png"),
|
|
true,
|
|
);
|
|
let inspector_plugin = InspectorPlugin::new(ctx);
|
|
let bbcode_panel = BBCodePanel::new(inspector_plugin.head, ctx);
|
|
let particle_system_control_panel =
|
|
ParticleSystemPreviewControlPanel::new(inspector_plugin.head, ctx);
|
|
let camera_control_panel = CameraPreviewControlPanel::new(scene_viewer.frame(), ctx);
|
|
let mesh_control_panel = MeshControlPanel::new(inspector_plugin.head, ctx);
|
|
let audio_preview_panel = AudioPreviewPanel::new(inspector_plugin.head, ctx);
|
|
let doc_window = DocWindow::new(ctx);
|
|
let node_removal_dialog = NodeRemovalDialog::new(ctx);
|
|
let scene_settings = SceneSettingsWindow::new(ctx, property_editors.clone());
|
|
|
|
let docking_manager;
|
|
let root_grid = GridBuilder::new(
|
|
WidgetBuilder::new()
|
|
.with_width(inner_size.width)
|
|
.with_height(inner_size.height)
|
|
.with_child(menu.menu)
|
|
.with_child({
|
|
docking_manager =
|
|
DockingManagerBuilder::new(WidgetBuilder::new().on_row(1).with_child({
|
|
TileBuilder::new(WidgetBuilder::new())
|
|
.with_content(TileContent::VerticalTiles {
|
|
splitter: 0.75,
|
|
tiles: [
|
|
TileBuilder::new(WidgetBuilder::new())
|
|
.with_content(TileContent::HorizontalTiles {
|
|
splitter: 0.25,
|
|
tiles: [
|
|
TileBuilder::new(WidgetBuilder::new())
|
|
.with_content(TileContent::Window(
|
|
world_outliner.window,
|
|
))
|
|
.build(ctx),
|
|
TileBuilder::new(WidgetBuilder::new())
|
|
.with_content(
|
|
TileContent::HorizontalTiles {
|
|
splitter: 0.66,
|
|
tiles: [
|
|
TileBuilder::new(
|
|
WidgetBuilder::new(),
|
|
)
|
|
.with_content(
|
|
TileContent::Window(
|
|
scene_viewer.window(),
|
|
),
|
|
)
|
|
.build(ctx),
|
|
TileBuilder::new(
|
|
WidgetBuilder::new(),
|
|
)
|
|
.with_content(
|
|
TileContent::Window(
|
|
inspector_plugin.window,
|
|
),
|
|
)
|
|
.build(ctx),
|
|
],
|
|
},
|
|
)
|
|
.build(ctx),
|
|
],
|
|
})
|
|
.build(ctx),
|
|
TileBuilder::new(WidgetBuilder::new())
|
|
.with_content(TileContent::HorizontalTiles {
|
|
splitter: 0.66,
|
|
tiles: [
|
|
TileBuilder::new(WidgetBuilder::new())
|
|
.with_content(
|
|
TileContent::HorizontalTiles {
|
|
splitter: 0.80,
|
|
tiles: [
|
|
TileBuilder::new(
|
|
WidgetBuilder::new(),
|
|
)
|
|
.with_content(
|
|
TileContent::Window(
|
|
asset_browser.window,
|
|
),
|
|
)
|
|
.build(ctx),
|
|
TileBuilder::new(
|
|
WidgetBuilder::new(),
|
|
)
|
|
.with_content(
|
|
TileContent::Window(
|
|
command_stack_viewer
|
|
.window,
|
|
),
|
|
)
|
|
.build(ctx),
|
|
],
|
|
},
|
|
)
|
|
.build(ctx),
|
|
TileBuilder::new(WidgetBuilder::new())
|
|
.with_content(
|
|
TileContent::HorizontalTiles {
|
|
splitter: 0.5,
|
|
tiles: [
|
|
TileBuilder::new(
|
|
WidgetBuilder::new(),
|
|
)
|
|
.with_content(
|
|
TileContent::Window(
|
|
log.window,
|
|
),
|
|
)
|
|
.build(ctx),
|
|
TileBuilder::new(
|
|
WidgetBuilder::new(),
|
|
)
|
|
.with_content(
|
|
TileContent::Window(
|
|
audio_panel.window,
|
|
),
|
|
)
|
|
.build(ctx),
|
|
],
|
|
},
|
|
)
|
|
.build(ctx),
|
|
],
|
|
})
|
|
.build(ctx),
|
|
],
|
|
})
|
|
.build(ctx)
|
|
}))
|
|
.with_floating_windows(vec![
|
|
camera_control_panel.window,
|
|
navmesh_panel.window,
|
|
doc_window.window,
|
|
light_panel.window,
|
|
scene_settings.window,
|
|
])
|
|
.build(ctx);
|
|
docking_manager
|
|
}),
|
|
)
|
|
.add_row(Row::strict(25.0))
|
|
.add_row(Row::stretch())
|
|
.add_column(Column::stretch())
|
|
.build(ctx);
|
|
|
|
let exit_message_box = MessageBoxBuilder::new(
|
|
WindowBuilder::new(WidgetBuilder::new().with_width(300.0).with_height(100.0))
|
|
.can_close(false)
|
|
.can_minimize(false)
|
|
.open(false)
|
|
.with_title(WindowTitle::text("Unsaved changes")),
|
|
)
|
|
.with_buttons(MessageBoxButtons::YesNoCancel)
|
|
.build(ctx);
|
|
|
|
let validation_message_box = MessageBoxBuilder::new(
|
|
WindowBuilder::new(WidgetBuilder::new().with_width(400.0).with_height(500.0))
|
|
.can_close(false)
|
|
.can_minimize(false)
|
|
.open(false)
|
|
.with_title(WindowTitle::text("Validation failed!")),
|
|
)
|
|
.with_buttons(MessageBoxButtons::Ok)
|
|
.build(ctx);
|
|
let entity_creator = EntityCreator::new(ctx);
|
|
|
|
let save_scene_dialog = SaveSceneConfirmationDialog::new(ctx);
|
|
|
|
let ui = engine.user_interfaces.first_mut();
|
|
ui.update(
|
|
Vector2::new(inner_size.width, inner_size.height),
|
|
1.0 / 60.0,
|
|
&Default::default(),
|
|
);
|
|
let default_layout = ui[docking_manager].layout(ui);
|
|
if let Some(layout) = settings.windows.layout.as_ref() {
|
|
ui.send(
|
|
docking_manager,
|
|
DockingManagerMessage::Layout(layout.clone()),
|
|
);
|
|
}
|
|
|
|
let scenes = SceneContainer::new(&mut engine, &mut settings, message_sender.clone());
|
|
let editor = Self {
|
|
docking_manager,
|
|
engine,
|
|
navmesh_panel,
|
|
scene_viewer,
|
|
scenes,
|
|
message_sender,
|
|
message_receiver,
|
|
world_viewer: world_outliner,
|
|
root_grid,
|
|
menu,
|
|
asset_browser,
|
|
exit_message_box,
|
|
configurator,
|
|
log,
|
|
light_panel,
|
|
command_stack_viewer,
|
|
validation_message_box,
|
|
settings,
|
|
audio_panel,
|
|
save_scene_dialog,
|
|
mode: Mode::Edit,
|
|
game_loop_data: GameLoopData {
|
|
clock: Instant::now(),
|
|
lag: 0.0,
|
|
},
|
|
build_window: None,
|
|
scene_settings,
|
|
bbcode_panel,
|
|
particle_system_control_panel,
|
|
camera_control_panel,
|
|
mesh_control_panel,
|
|
audio_preview_panel,
|
|
node_removal_dialog,
|
|
doc_window,
|
|
plugins: EditorPluginsContainer::new()
|
|
.with(ColliderPlugin::default())
|
|
.with(TileMapEditorPlugin::default())
|
|
.with(MaterialPlugin::default())
|
|
.with(RagdollPlugin::default())
|
|
.with(SettingsPlugin::default())
|
|
.with(AnimationEditorPlugin::default())
|
|
.with(AbsmEditorPlugin::default())
|
|
.with(EditorStatisticsPlugin::default())
|
|
.with(CurveEditorPlugin::default())
|
|
.with(ReflectionProbePlugin::default())
|
|
.with(inspector_plugin),
|
|
// Apparently, some window managers (like Wayland), does not send `Focused` event after the window
|
|
// was created. So we must assume that the editor is focused by default, otherwise editor's thread
|
|
// will sleep forever and the window won't come up.
|
|
focused: true,
|
|
entity_creator,
|
|
update_loop_state: UpdateLoopState::default(),
|
|
is_suspended: false,
|
|
scene_node_context_menu,
|
|
widget_context_menu,
|
|
overlay_pass: None,
|
|
highlighter: None,
|
|
export_window: None,
|
|
statistics_window: None,
|
|
surface_data_viewer: None,
|
|
processed_ui_messages: 0,
|
|
styles,
|
|
running_game_process: None,
|
|
user_project_icon: None,
|
|
user_project_name: Default::default(),
|
|
user_project_version: Default::default(),
|
|
loading_scenes: Default::default(),
|
|
scene_loading_window: None,
|
|
default_layout,
|
|
startup_data: startup_data.clone(),
|
|
property_editors,
|
|
};
|
|
|
|
let mut startup_scenes = editor
|
|
.settings
|
|
.general
|
|
.startup_scenes
|
|
.iter()
|
|
.cloned()
|
|
.collect::<FxHashSet<_>>();
|
|
|
|
if let Some(data) = startup_data {
|
|
editor.message_sender.send(Message::Configure {
|
|
working_directory: if data.working_directory == PathBuf::default() {
|
|
std::env::current_dir().unwrap()
|
|
} else {
|
|
data.working_directory
|
|
},
|
|
});
|
|
|
|
for path in data.scenes {
|
|
startup_scenes.insert(path);
|
|
}
|
|
} else if startup_scenes.is_empty() {
|
|
// Open configurator as usual.
|
|
editor.engine.user_interfaces.first().send(
|
|
editor.configurator.window,
|
|
WindowMessage::Open {
|
|
alignment: WindowAlignment::Center,
|
|
modal: true,
|
|
focus_content: true,
|
|
},
|
|
);
|
|
} else {
|
|
editor.message_sender.send(Message::Configure {
|
|
working_directory: std::env::current_dir().unwrap(),
|
|
});
|
|
}
|
|
|
|
for path in startup_scenes.iter() {
|
|
if path.exists() {
|
|
editor.message_sender.send(Message::LoadScene(path.clone()));
|
|
}
|
|
}
|
|
|
|
editor
|
|
}
|
|
|
|
fn reload_settings(&mut self) {
|
|
let old_subscribers = std::mem::take(&mut self.settings.subscribers);
|
|
|
|
match Settings::load() {
|
|
Ok(settings) => {
|
|
self.settings = settings;
|
|
self.settings.subscribers = old_subscribers;
|
|
|
|
Log::info("Editor settings were reloaded successfully!");
|
|
}
|
|
Err(e) => {
|
|
self.settings = Default::default();
|
|
|
|
Log::warn(format!(
|
|
"Failed to load settings, fallback to default. Reason: {e:?}"
|
|
))
|
|
}
|
|
}
|
|
|
|
self.menu
|
|
.file_menu
|
|
.update_recent_files_list(self.engine.user_interfaces.first_mut(), &self.settings);
|
|
|
|
if let GraphicsContext::Initialized(ref mut graphics_context) = self.engine.graphics_context
|
|
{
|
|
match graphics_context
|
|
.renderer
|
|
.set_quality_settings(&self.settings.graphics.quality)
|
|
{
|
|
Ok(_) => {
|
|
Log::info("Graphics settings were applied successfully!");
|
|
}
|
|
Err(e) => Log::info(format!("Failed to apply graphics settings! Reason: {e:?}")),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn add_scene(&mut self, entry: EditorSceneEntry) {
|
|
self.try_leave_preview_mode();
|
|
|
|
self.sync_to_model();
|
|
self.poll_ui_messages();
|
|
|
|
if let Some(path) = entry.path.as_ref() {
|
|
if !self.settings.recent.scenes.contains(path) {
|
|
self.settings.recent.scenes.push(path.clone());
|
|
self.menu.file_menu.update_recent_files_list(
|
|
self.engine.user_interfaces.first_mut(),
|
|
&self.settings,
|
|
);
|
|
}
|
|
}
|
|
|
|
let old_selection = self.scenes.current_scene_entry_ref().selection.clone();
|
|
|
|
self.scenes.add_and_select(entry);
|
|
|
|
self.scene_viewer
|
|
.reset_camera_projection(self.engine.user_interfaces.first());
|
|
|
|
self.on_scene_changed(old_selection);
|
|
}
|
|
|
|
pub fn handle_hotkeys(&mut self, message: &UiMessage) {
|
|
// A message could be handled already somewhere else (for example in a TextBox or any other
|
|
// widget, that handles keyboard input), we must not respond to such messages.
|
|
if message.handled() {
|
|
return;
|
|
}
|
|
|
|
let modifiers = self.engine.user_interfaces.first_mut().keyboard_modifiers();
|
|
let sender = self.message_sender.clone();
|
|
let engine = &mut self.engine;
|
|
|
|
if let Some(WidgetMessage::KeyDown(key)) = message.data() {
|
|
let hot_key = HotKey::Some {
|
|
code: *key,
|
|
modifiers,
|
|
};
|
|
|
|
let mut processed = false;
|
|
let scene = self.scenes.current_scene_entry_mut();
|
|
if let Some(current_interaction_mode) = scene
|
|
.current_interaction_mode
|
|
.and_then(|current_mode| scene.interaction_modes.get_mut(¤t_mode))
|
|
{
|
|
processed |= current_interaction_mode.on_hot_key_pressed(
|
|
&hot_key,
|
|
&mut *scene.controller,
|
|
engine,
|
|
&self.settings,
|
|
);
|
|
}
|
|
|
|
if !processed {
|
|
let key_bindings = &self.settings.key_bindings;
|
|
|
|
if hot_key == key_bindings.redo {
|
|
sender.send(Message::RedoCurrentSceneCommand);
|
|
} else if hot_key == key_bindings.undo {
|
|
sender.send(Message::UndoCurrentSceneCommand);
|
|
} else if hot_key == key_bindings.enable_select_mode {
|
|
sender.send(Message::SetInteractionMode(
|
|
SelectInteractionMode::type_info().type_uuid,
|
|
));
|
|
} else if hot_key == key_bindings.enable_move_mode {
|
|
sender.send(Message::SetInteractionMode(
|
|
MoveInteractionMode::type_info().type_uuid,
|
|
));
|
|
} else if hot_key == key_bindings.enable_rotate_mode {
|
|
sender.send(Message::SetInteractionMode(
|
|
RotateInteractionMode::type_info().type_uuid,
|
|
));
|
|
} else if hot_key == key_bindings.enable_scale_mode {
|
|
sender.send(Message::SetInteractionMode(
|
|
ScaleInteractionMode::type_info().type_uuid,
|
|
));
|
|
} else if hot_key == key_bindings.enable_navmesh_mode {
|
|
sender.send(Message::SetInteractionMode(
|
|
EditNavmeshMode::type_info().type_uuid,
|
|
));
|
|
} else if hot_key == key_bindings.enable_terrain_mode {
|
|
sender.send(Message::SetInteractionMode(
|
|
TerrainInteractionMode::type_info().type_uuid,
|
|
));
|
|
} else if hot_key == key_bindings.load_scene {
|
|
sender.send(Message::OpenLoadSceneDialog);
|
|
} else if hot_key == key_bindings.run_game {
|
|
sender.send(Message::SwitchToBuildMode {
|
|
play_after_build: true,
|
|
});
|
|
} else if hot_key == key_bindings.save_scene {
|
|
let entry = self.scenes.current_scene_entry_ref();
|
|
if let Some(path) = entry.path.as_ref() {
|
|
self.message_sender.send(Message::SaveScene {
|
|
id: entry.id,
|
|
path: path.clone(),
|
|
});
|
|
} else {
|
|
self.message_sender.send(Message::OpenSaveSceneDialog {
|
|
default_file_info: entry.default_file_info(),
|
|
});
|
|
}
|
|
} else if hot_key == key_bindings.save_scene_as {
|
|
let entry = self.scenes.current_scene_entry_ref();
|
|
self.menu.file_menu.open_save_file_selector(
|
|
engine.user_interfaces.first_mut(),
|
|
&engine.resource_manager,
|
|
entry.default_file_info(),
|
|
);
|
|
} else if hot_key == key_bindings.save_all_scenes {
|
|
self.message_sender.send(Message::SaveAllScenes);
|
|
} else if hot_key == key_bindings.copy_selection {
|
|
let entry = self.scenes.current_scene_entry_mut();
|
|
if let Some(graph_selection) = entry.selection.as_graph() {
|
|
if let Some(game_scene) = entry.controller.downcast_mut::<GameScene>() {
|
|
game_scene.clipboard.fill_from_selection(
|
|
graph_selection,
|
|
game_scene.scene,
|
|
engine,
|
|
);
|
|
} else if let Some(ui_scene) = entry.controller.downcast_mut::<UiScene>() {
|
|
if let Some(selection) = entry.selection.as_ui() {
|
|
ui_scene
|
|
.clipboard
|
|
.fill_from_selection(selection, &ui_scene.ui);
|
|
}
|
|
}
|
|
}
|
|
} else if hot_key == key_bindings.paste {
|
|
let controller = self.scenes.current_scene_controller_mut();
|
|
if let Some(game_scene) = controller.downcast_mut::<GameScene>() {
|
|
if !game_scene.clipboard.is_empty() {
|
|
sender.do_command(PasteCommand::new(game_scene.scene_content_root));
|
|
}
|
|
} else if let Some(ui_scene) = controller.downcast_mut::<UiScene>() {
|
|
if !ui_scene.clipboard.is_empty() {
|
|
sender.do_command(PasteWidgetCommand::new(ui_scene.ui.root()));
|
|
}
|
|
}
|
|
} else if hot_key == key_bindings.new_scene {
|
|
sender.send(Message::NewScene);
|
|
} else if hot_key == key_bindings.close_scene {
|
|
let entry = self.scenes.current_scene_entry_ref();
|
|
sender.send(Message::CloseScene(entry.id));
|
|
} else if hot_key == key_bindings.remove_selection {
|
|
let entry = self.scenes.current_scene_entry_mut();
|
|
if !entry.selection.is_empty() {
|
|
if entry.selection.is_graph() {
|
|
if let Some(game_scene) = entry.controller.downcast_mut::<GameScene>() {
|
|
if self.settings.general.show_node_removal_dialog
|
|
&& game_scene.is_current_selection_has_external_refs(
|
|
&entry.selection,
|
|
&engine.scenes[game_scene.scene].graph,
|
|
)
|
|
{
|
|
sender.send(Message::OpenNodeRemovalDialog);
|
|
} else {
|
|
sender.send(Message::DoCommand(make_delete_selection_command(
|
|
&entry.selection,
|
|
game_scene,
|
|
engine,
|
|
)));
|
|
}
|
|
}
|
|
} else if let Some(selection) = entry.selection.as_ui() {
|
|
if let Some(ui_scene) = entry.controller.downcast_mut::<UiScene>() {
|
|
sender.send(Message::DoCommand(
|
|
selection.make_deletion_command(&ui_scene.ui),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
} else if hot_key == key_bindings.focus {
|
|
let entry = self.scenes.current_scene_entry_mut();
|
|
if let Some(selection) = entry.selection.as_graph() {
|
|
if let Some(first) = selection.nodes.first() {
|
|
sender.send(Message::FocusObject(*first));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if let Some(WidgetMessage::KeyUp(key)) = message.data() {
|
|
let hot_key = HotKey::Some {
|
|
code: *key,
|
|
modifiers,
|
|
};
|
|
|
|
let scene = self.scenes.current_scene_entry_mut();
|
|
if let Some(current_interaction_mode) = scene
|
|
.current_interaction_mode
|
|
.and_then(|current_mode| scene.interaction_modes.get_mut(¤t_mode))
|
|
{
|
|
current_interaction_mode.on_hot_key_released(
|
|
&hot_key,
|
|
&mut *scene.controller,
|
|
engine,
|
|
&self.settings,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn handle_ui_message(&mut self, message: &mut UiMessage) {
|
|
for_each_plugin!(self.plugins => on_ui_message(message, self));
|
|
|
|
let engine = &mut self.engine;
|
|
|
|
self.save_scene_dialog
|
|
.handle_ui_message(message, &self.message_sender, &self.scenes);
|
|
|
|
let current_scene_entry = self.scenes.current_scene_entry_mut();
|
|
|
|
self.configurator.handle_ui_message(message, engine);
|
|
let inspector = self.plugins.get::<InspectorPlugin>();
|
|
self.menu.handle_ui_message(
|
|
message,
|
|
MenuContext {
|
|
engine,
|
|
game_scene: current_scene_entry,
|
|
panels: Panels {
|
|
scene_frame: self.scene_viewer.frame(),
|
|
inspector_window: inspector.window,
|
|
world_outliner_window: self.world_viewer.window,
|
|
asset_window: self.asset_browser.window,
|
|
light_panel: self.light_panel.window,
|
|
log_panel: self.log.window,
|
|
navmesh_panel: self.navmesh_panel.window,
|
|
audio_panel: self.audio_panel.window,
|
|
configurator_window: self.configurator.window,
|
|
command_stack_panel: self.command_stack_viewer.window,
|
|
scene_settings: &self.scene_settings,
|
|
export_window: &mut self.export_window,
|
|
statistics_window: &mut self.statistics_window,
|
|
},
|
|
settings: &mut self.settings,
|
|
icon_request_sender: self.asset_browser.preview_sender.clone(),
|
|
},
|
|
);
|
|
|
|
if let Some(surface_data_viewer) = self.surface_data_viewer.take() {
|
|
self.surface_data_viewer = surface_data_viewer.handle_ui_message(message, engine);
|
|
}
|
|
|
|
let ui = engine.user_interfaces.first_mut();
|
|
if let Some(build_window) = self.build_window.take() {
|
|
self.build_window = build_window.handle_ui_message(message, ui, || {
|
|
self.message_sender.send(Message::SwitchToEditMode)
|
|
});
|
|
if self.build_window.is_none() {
|
|
if let Some((process, active)) = self.running_game_process.take() {
|
|
self.mode = Mode::Play { process, active };
|
|
}
|
|
}
|
|
}
|
|
if let Some(export_window) = self.export_window.as_mut() {
|
|
export_window.handle_ui_message(
|
|
message,
|
|
ui,
|
|
&self.message_sender,
|
|
engine.resource_manager.clone(),
|
|
&self.settings.build,
|
|
);
|
|
}
|
|
if let Some(stats) = self.statistics_window.as_ref() {
|
|
if let StatisticsWindowAction::Remove = stats.handle_ui_message(message, ui) {
|
|
self.statistics_window.take();
|
|
}
|
|
}
|
|
self.log.handle_ui_message(message, ui);
|
|
|
|
self.command_stack_viewer.handle_ui_message(message);
|
|
self.scene_viewer.handle_ui_message(
|
|
message,
|
|
engine,
|
|
&mut self.scenes,
|
|
&mut self.settings,
|
|
&self.mode,
|
|
);
|
|
|
|
let current_scene_entry = self.scenes.current_scene_entry_mut();
|
|
|
|
self.asset_browser.handle_ui_message(
|
|
message,
|
|
engine,
|
|
current_scene_entry,
|
|
self.message_sender.clone(),
|
|
);
|
|
|
|
self.scene_settings.handle_ui_message(
|
|
&*current_scene_entry.controller,
|
|
message,
|
|
&self.message_sender,
|
|
);
|
|
|
|
if let Some(game_scene) = current_scene_entry.controller.downcast_mut::<GameScene>() {
|
|
self.entity_creator.handle_ui_message_with_game_scene(
|
|
game_scene,
|
|
engine,
|
|
message,
|
|
¤t_scene_entry.selection,
|
|
&self.message_sender,
|
|
&mut self.settings,
|
|
current_scene_entry.path.as_ref(),
|
|
);
|
|
self.particle_system_control_panel.handle_ui_message(
|
|
message,
|
|
¤t_scene_entry.selection,
|
|
game_scene,
|
|
engine,
|
|
);
|
|
self.mesh_control_panel.handle_ui_message(
|
|
message,
|
|
¤t_scene_entry.selection,
|
|
game_scene,
|
|
engine,
|
|
&self.message_sender,
|
|
);
|
|
self.audio_preview_panel.handle_ui_message(
|
|
message,
|
|
¤t_scene_entry.selection,
|
|
game_scene,
|
|
engine,
|
|
);
|
|
|
|
self.audio_panel.handle_ui_message(
|
|
message,
|
|
¤t_scene_entry.selection,
|
|
&self.message_sender,
|
|
engine,
|
|
);
|
|
self.node_removal_dialog.handle_ui_message(
|
|
¤t_scene_entry.selection,
|
|
game_scene,
|
|
message,
|
|
engine,
|
|
&self.message_sender,
|
|
);
|
|
|
|
self.navmesh_panel
|
|
.handle_message(message, ¤t_scene_entry.selection);
|
|
|
|
if let Some(interaction_mode) =
|
|
current_scene_entry
|
|
.current_interaction_mode
|
|
.and_then(|current_mode| {
|
|
current_scene_entry.interaction_modes.get_mut(¤t_mode)
|
|
})
|
|
{
|
|
interaction_mode.handle_ui_message(
|
|
message,
|
|
¤t_scene_entry.selection,
|
|
game_scene,
|
|
engine,
|
|
);
|
|
}
|
|
|
|
self.scene_node_context_menu.borrow_mut().handle_ui_message(
|
|
message,
|
|
¤t_scene_entry.selection,
|
|
game_scene,
|
|
engine,
|
|
&self.message_sender,
|
|
&self.settings,
|
|
);
|
|
self.world_viewer.handle_ui_message(
|
|
message,
|
|
&mut EditorSceneWrapper {
|
|
selection: ¤t_scene_entry.selection,
|
|
game_scene,
|
|
scene: &mut engine.scenes[game_scene.scene],
|
|
sender: &self.message_sender,
|
|
path: current_scene_entry.path.as_deref(),
|
|
resource_manager: &engine.resource_manager,
|
|
instantiation_scale: self.settings.model.instantiation_scale,
|
|
},
|
|
engine.user_interfaces.first(),
|
|
&mut self.settings,
|
|
);
|
|
|
|
self.light_panel
|
|
.handle_ui_message(message, game_scene, engine);
|
|
} else if let Some(ui_scene) = current_scene_entry.controller.downcast_mut::<UiScene>() {
|
|
self.entity_creator.handle_ui_message_with_ui_scene(
|
|
ui_scene,
|
|
engine,
|
|
message,
|
|
¤t_scene_entry.selection,
|
|
&self.message_sender,
|
|
&mut self.settings,
|
|
current_scene_entry.path.as_ref(),
|
|
);
|
|
|
|
self.bbcode_panel.handle_ui_message(
|
|
message,
|
|
¤t_scene_entry.selection,
|
|
ui_scene,
|
|
engine,
|
|
&self.message_sender,
|
|
);
|
|
self.world_viewer.handle_ui_message(
|
|
message,
|
|
&mut UiSceneWorldViewerDataProvider {
|
|
ui: &mut ui_scene.ui,
|
|
path: current_scene_entry.path.as_deref(),
|
|
selection: ¤t_scene_entry.selection,
|
|
sender: &self.message_sender,
|
|
resource_manager: &engine.resource_manager,
|
|
},
|
|
engine.user_interfaces.first(),
|
|
&mut self.settings,
|
|
);
|
|
|
|
self.widget_context_menu.borrow_mut().handle_ui_message(
|
|
message,
|
|
¤t_scene_entry.selection,
|
|
ui_scene,
|
|
engine,
|
|
&self.message_sender,
|
|
);
|
|
}
|
|
|
|
if let Some(MessageBoxMessage::Close(result)) = message.data_from(self.exit_message_box) {
|
|
match result {
|
|
MessageBoxResult::No => {
|
|
self.message_sender.send(Message::Exit { force: true });
|
|
}
|
|
MessageBoxResult::Yes => {
|
|
if let Some(first_unsaved) = self.scenes.first_unsaved_scene() {
|
|
if first_unsaved.need_save() {
|
|
if let Some(path) = first_unsaved.path.as_ref() {
|
|
self.message_sender.send(Message::SaveScene {
|
|
id: first_unsaved.id,
|
|
path: path.clone(),
|
|
});
|
|
|
|
self.message_sender
|
|
.send(Message::CloseScene(first_unsaved.id));
|
|
|
|
self.message_sender.send(Message::Exit {
|
|
force: self.scenes.unsaved_scene_count() == 1,
|
|
});
|
|
} else {
|
|
self.message_sender.send(Message::OpenSaveSceneDialog {
|
|
default_file_info: first_unsaved.default_file_info(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
self.handle_hotkeys(message);
|
|
}
|
|
|
|
fn set_play_mode(&mut self) {
|
|
if let Some(build_window) = self.build_window.take() {
|
|
build_window.destroy(self.engine.user_interfaces.first());
|
|
}
|
|
|
|
if self.scenes.is_empty() {
|
|
Log::err("Cannot enter build mode when there is no scene!");
|
|
return;
|
|
};
|
|
|
|
let entry = self.scenes.current_scene_entry_ref();
|
|
|
|
let Some(path) = entry.path.as_ref().cloned() else {
|
|
Log::err("Save your scene first!");
|
|
return;
|
|
};
|
|
|
|
self.save_scene(entry.id, path.clone());
|
|
|
|
let Some(build_profile) = self
|
|
.settings
|
|
.build
|
|
.profiles
|
|
.get(self.settings.build.selected_profile)
|
|
else {
|
|
Log::err("Selected build profile index is invalid.");
|
|
return;
|
|
};
|
|
|
|
Log::info(format!(
|
|
"Trying to run the game using command: {}",
|
|
build_profile.run_command
|
|
));
|
|
|
|
let mut command = build_profile.run_command.make_command();
|
|
|
|
let command = command.stdout(Stdio::piped()).stderr(Stdio::piped());
|
|
if !build_profile.run_command.skip_passthrough_marker {
|
|
command.arg("--");
|
|
}
|
|
command.arg("--override-scene").arg(path);
|
|
|
|
match command.spawn() {
|
|
Ok(mut process) => {
|
|
let active = Arc::new(AtomicBool::new(true));
|
|
|
|
// Capture output from child process.
|
|
let mut stdout = process.stdout.take().unwrap();
|
|
let mut stderr = process.stderr.take().unwrap();
|
|
let reader_active = active.clone();
|
|
std::thread::spawn(move || {
|
|
while reader_active.load(Ordering::SeqCst) {
|
|
for line in BufReader::new(&mut stdout).lines().take(10).flatten() {
|
|
Log::info(line);
|
|
}
|
|
}
|
|
});
|
|
let reader_active = active.clone();
|
|
std::thread::spawn(move || {
|
|
while reader_active.load(Ordering::SeqCst) {
|
|
for line in BufReader::new(&mut stderr).lines().take(10).flatten() {
|
|
Log::err(line);
|
|
}
|
|
}
|
|
});
|
|
|
|
self.mode = Mode::Play { active, process };
|
|
|
|
self.on_mode_changed();
|
|
}
|
|
Err(e) => Log::err(format!("Failed to enter play mode: {e:?}")),
|
|
}
|
|
}
|
|
|
|
fn set_build_mode(&mut self, play_after_build: bool) {
|
|
if matches!(self.mode, Mode::Build { .. }) {
|
|
Log::err("Cannot enter build mode when another build mode is active!");
|
|
return;
|
|
}
|
|
|
|
if self.scenes.is_empty() {
|
|
Log::err("Cannot enter build mode when there is no scene!");
|
|
return;
|
|
};
|
|
|
|
if self.scenes.current_scene_entry_ref().path.is_none() {
|
|
Log::err("Save your scene first!");
|
|
return;
|
|
}
|
|
|
|
let Some(build_profile) = self
|
|
.settings
|
|
.build
|
|
.profiles
|
|
.get(self.settings.build.selected_profile)
|
|
else {
|
|
Log::err("Selected build profile index is invalid.");
|
|
return;
|
|
};
|
|
|
|
let queue = build_profile
|
|
.build_commands
|
|
.iter()
|
|
.cloned()
|
|
.collect::<VecDeque<_>>();
|
|
|
|
let old_mode = std::mem::replace(
|
|
&mut self.mode,
|
|
Mode::Build {
|
|
queue,
|
|
process: None,
|
|
play_after_build,
|
|
},
|
|
);
|
|
|
|
match old_mode {
|
|
Mode::Edit => {}
|
|
Mode::Build { .. } => {
|
|
unreachable!();
|
|
}
|
|
Mode::Play { process, active } => {
|
|
self.running_game_process = Some((process, active));
|
|
}
|
|
}
|
|
|
|
let ui = self.engine.user_interfaces.first_mut();
|
|
self.build_window = Some(BuildWindow::new("your game", &mut ui.build_ctx()));
|
|
|
|
self.on_mode_changed();
|
|
}
|
|
|
|
fn set_editor_mode(&mut self) {
|
|
match std::mem::replace(&mut self.mode, Mode::Edit) {
|
|
Mode::Play {
|
|
mut process,
|
|
active,
|
|
} => {
|
|
active.store(false, Ordering::SeqCst);
|
|
Log::verify(process.kill());
|
|
self.on_mode_changed();
|
|
}
|
|
Mode::Build { process, .. } => {
|
|
if let Some(mut process) = process {
|
|
Log::verify(process.kill());
|
|
}
|
|
self.on_mode_changed();
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn on_mode_changed(&mut self) {
|
|
for_each_plugin!(self.plugins => on_mode_changed(self));
|
|
|
|
let engine = &mut self.engine;
|
|
let ui = engine.user_interfaces.first();
|
|
self.scene_viewer.on_mode_changed(ui, &self.mode);
|
|
self.world_viewer.on_mode_changed(ui, &self.mode);
|
|
self.asset_browser.on_mode_changed(ui, &self.mode);
|
|
self.command_stack_viewer.on_mode_changed(ui, &self.mode);
|
|
self.audio_panel.on_mode_changed(ui, &self.mode);
|
|
self.navmesh_panel.on_mode_changed(ui, &self.mode);
|
|
self.menu.on_mode_changed(ui, &self.mode);
|
|
}
|
|
|
|
fn sync_to_model(&mut self) {
|
|
for_each_plugin!(self.plugins => on_sync_to_model(self));
|
|
|
|
let engine = &mut self.engine;
|
|
|
|
self.menu.sync_to_model(
|
|
self.scenes.has_active_scene(),
|
|
engine.user_interfaces.first_mut(),
|
|
);
|
|
|
|
self.scene_viewer
|
|
.sync_to_model(&self.scenes, &self.settings, engine);
|
|
if let Some(exporter) = self.export_window.as_ref() {
|
|
exporter.sync_to_model(engine.user_interfaces.first_mut());
|
|
}
|
|
|
|
let current_scene_entry = self.scenes.current_scene_entry_mut();
|
|
self.command_stack_viewer.sync_to_model(
|
|
current_scene_entry.command_stack.top,
|
|
current_scene_entry.controller.command_names(
|
|
&mut current_scene_entry.command_stack,
|
|
&mut current_scene_entry.selection,
|
|
engine,
|
|
),
|
|
engine.user_interfaces.first_mut(),
|
|
);
|
|
|
|
self.scene_settings.sync_to_model(
|
|
false,
|
|
&*current_scene_entry.controller,
|
|
engine,
|
|
self.message_sender.clone(),
|
|
self.asset_browser.preview_sender.clone(),
|
|
);
|
|
|
|
if let Some(game_scene) = current_scene_entry.controller.downcast_mut::<GameScene>() {
|
|
let sender = &self.message_sender;
|
|
self.world_viewer.sync_to_model(
|
|
&EditorSceneWrapper {
|
|
selection: ¤t_scene_entry.selection,
|
|
game_scene,
|
|
scene: &mut engine.scenes[game_scene.scene],
|
|
sender,
|
|
path: current_scene_entry.path.as_deref(),
|
|
resource_manager: &engine.resource_manager,
|
|
instantiation_scale: self.settings.model.instantiation_scale,
|
|
},
|
|
engine.user_interfaces.first_mut(),
|
|
&self.settings,
|
|
);
|
|
|
|
self.audio_panel
|
|
.sync_to_model(¤t_scene_entry.selection, game_scene, engine);
|
|
self.navmesh_panel
|
|
.sync_to_model(engine, ¤t_scene_entry.selection, game_scene);
|
|
} else if let Some(ui_scene) = current_scene_entry.controller.downcast_mut::<UiScene>() {
|
|
self.bbcode_panel.sync_to_model(
|
|
¤t_scene_entry.selection,
|
|
ui_scene,
|
|
engine.user_interfaces.first_mut(),
|
|
);
|
|
self.world_viewer.sync_to_model(
|
|
&UiSceneWorldViewerDataProvider {
|
|
ui: &mut ui_scene.ui,
|
|
path: current_scene_entry.path.as_deref(),
|
|
selection: ¤t_scene_entry.selection,
|
|
sender: &self.message_sender,
|
|
resource_manager: &engine.resource_manager,
|
|
},
|
|
engine.user_interfaces.first_mut(),
|
|
&self.settings,
|
|
);
|
|
} else {
|
|
self.world_viewer.clear(engine.user_interfaces.first());
|
|
}
|
|
}
|
|
|
|
fn post_update(&mut self, loop_controller: ApplicationLoopController) {
|
|
let entry = self.scenes.current_scene_entry_mut();
|
|
if let Some(game_scene) = entry.controller.downcast_ref::<GameScene>() {
|
|
self.world_viewer.post_update(
|
|
&EditorSceneWrapper {
|
|
selection: &entry.selection,
|
|
game_scene,
|
|
scene: &mut self.engine.scenes[game_scene.scene],
|
|
sender: &self.message_sender,
|
|
path: entry.path.as_deref(),
|
|
resource_manager: &self.engine.resource_manager,
|
|
instantiation_scale: self.settings.model.instantiation_scale,
|
|
},
|
|
self.engine.user_interfaces.first_mut(),
|
|
&self.settings,
|
|
);
|
|
} else if let Some(ui_scene) = entry.controller.downcast_mut::<UiScene>() {
|
|
self.world_viewer.post_update(
|
|
&UiSceneWorldViewerDataProvider {
|
|
ui: &mut ui_scene.ui,
|
|
path: entry.path.as_deref(),
|
|
selection: &entry.selection,
|
|
sender: &self.message_sender,
|
|
resource_manager: &self.engine.resource_manager,
|
|
},
|
|
self.engine.user_interfaces.first_mut(),
|
|
&self.settings,
|
|
);
|
|
}
|
|
|
|
for_each_plugin!(self.plugins => on_post_update(self, loop_controller));
|
|
}
|
|
|
|
fn do_current_scene_command(&mut self, command: Command) -> bool {
|
|
let engine = &mut self.engine;
|
|
let current_scene_entry = self.scenes.current_scene_entry_mut();
|
|
current_scene_entry.has_unsaved_changes |= command.is_significant();
|
|
|
|
current_scene_entry.controller.do_command(
|
|
&mut current_scene_entry.command_stack,
|
|
command,
|
|
&mut current_scene_entry.selection,
|
|
engine,
|
|
);
|
|
|
|
true
|
|
}
|
|
|
|
fn undo_current_scene_command(&mut self) -> bool {
|
|
let engine = &mut self.engine;
|
|
let current_scene_entry = self.scenes.current_scene_entry_mut();
|
|
if let Some(command) = current_scene_entry.command_stack.top_command() {
|
|
current_scene_entry.has_unsaved_changes |= command.is_significant();
|
|
}
|
|
|
|
current_scene_entry.controller.undo(
|
|
&mut current_scene_entry.command_stack,
|
|
&mut current_scene_entry.selection,
|
|
engine,
|
|
);
|
|
|
|
true
|
|
}
|
|
|
|
fn redo_current_scene_command(&mut self) -> bool {
|
|
let engine = &mut self.engine;
|
|
let current_scene_entry = self.scenes.current_scene_entry_mut();
|
|
current_scene_entry.controller.redo(
|
|
&mut current_scene_entry.command_stack,
|
|
&mut current_scene_entry.selection,
|
|
engine,
|
|
);
|
|
|
|
if let Some(command) = current_scene_entry.command_stack.top_command() {
|
|
current_scene_entry.has_unsaved_changes |= command.is_significant();
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
fn clear_current_scene_command_stack(&mut self) -> bool {
|
|
let engine = &mut self.engine;
|
|
let current_scene_entry = self.scenes.current_scene_entry_mut();
|
|
current_scene_entry.controller.clear_command_stack(
|
|
&mut current_scene_entry.command_stack,
|
|
&mut current_scene_entry.selection,
|
|
&mut engine.scenes,
|
|
);
|
|
true
|
|
}
|
|
|
|
fn try_leave_preview_mode(&mut self) {
|
|
let entry = self.scenes.current_scene_entry_mut();
|
|
if let Some(game_scene) = entry.controller.downcast_mut::<GameScene>() {
|
|
let engine = &mut self.engine;
|
|
self.particle_system_control_panel
|
|
.leave_preview_mode(game_scene, engine);
|
|
self.camera_control_panel
|
|
.leave_preview_mode(game_scene, engine);
|
|
self.audio_preview_panel
|
|
.leave_preview_mode(game_scene, engine);
|
|
}
|
|
|
|
for_each_plugin!(self.plugins => on_leave_preview_mode(self));
|
|
}
|
|
|
|
pub fn is_in_preview_mode(&mut self) -> bool {
|
|
let mut is_any_plugin_in_preview_mode = false;
|
|
let mut i = 0;
|
|
while i < self.plugins.0.len() {
|
|
if let Some(plugin) = self.plugins.0.get_mut(i).and_then(|p| p.take()) {
|
|
is_any_plugin_in_preview_mode |= plugin.is_in_preview_mode(self);
|
|
|
|
if let Some(entry) = self.plugins.0.get_mut(i) {
|
|
*entry = Some(plugin);
|
|
}
|
|
}
|
|
|
|
i += 1;
|
|
}
|
|
|
|
let stays_active = match self.mode {
|
|
Mode::Edit => false,
|
|
Mode::Build { .. } => true,
|
|
Mode::Play { .. } => false,
|
|
};
|
|
|
|
self.particle_system_control_panel.is_in_preview_mode()
|
|
|| self.audio_preview_panel.is_in_preview_mode()
|
|
|| self.light_panel.is_in_preview_mode()
|
|
|| self.export_window.is_some()
|
|
|| is_any_plugin_in_preview_mode
|
|
|| self.scenes.current_scene_controller_ref().is_interacting()
|
|
|| stays_active
|
|
}
|
|
|
|
fn save_scene(&mut self, id: Uuid, path: PathBuf) {
|
|
let path = match make_relative_path(path.clone()) {
|
|
Ok(path) => path,
|
|
Err(err) => {
|
|
Log::err(format!(
|
|
"Failed to create relative path for {}. Reason: {err}",
|
|
path.display()
|
|
));
|
|
return;
|
|
}
|
|
};
|
|
|
|
// If there is some other open scene with the same name, then close it.
|
|
for entry in self.scenes.entries.iter() {
|
|
if entry.id != id && entry.path.as_ref() == Some(&path) {
|
|
self.close_scene(entry.id);
|
|
break;
|
|
}
|
|
}
|
|
|
|
self.try_leave_preview_mode();
|
|
|
|
let engine = &mut self.engine;
|
|
if let Some(entry) = self.scenes.entry_by_scene_id_mut(id) {
|
|
if !self.settings.recent.scenes.contains(&path) {
|
|
self.settings.recent.scenes.push(path.clone());
|
|
self.menu
|
|
.file_menu
|
|
.update_recent_files_list(engine.user_interfaces.first_mut(), &self.settings);
|
|
}
|
|
|
|
match entry.save(path.clone(), &self.settings, engine) {
|
|
Ok(message) => {
|
|
self.scene_viewer.set_title(
|
|
engine.user_interfaces.first(),
|
|
format!("Scene Preview - {}", path.display()),
|
|
);
|
|
Log::info(message);
|
|
|
|
entry.has_unsaved_changes = false;
|
|
}
|
|
Err(message) => {
|
|
Log::err(message.clone());
|
|
engine.user_interfaces.first_mut().send(
|
|
self.validation_message_box,
|
|
MessageBoxMessage::Open {
|
|
title: None,
|
|
text: Some(message),
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// We must process file system events when a scene was saved, because such scene can be used
|
|
// as a prefab. All prefab instances must be updated in the loaded scenes.
|
|
self.try_process_file_system_events();
|
|
|
|
self.sync_to_model();
|
|
}
|
|
|
|
fn load_scene(&mut self, scene_path: PathBuf) {
|
|
let scene_path = match make_relative_path(scene_path) {
|
|
Ok(path) => path,
|
|
Err(err) => {
|
|
Log::err(err.to_string());
|
|
return;
|
|
}
|
|
};
|
|
|
|
for entry in self.scenes.entries.iter() {
|
|
if entry.path.as_ref() == Some(&scene_path) {
|
|
self.set_current_scene(entry.id);
|
|
return;
|
|
}
|
|
}
|
|
|
|
let mut loading_scenes = self.loading_scenes.safe_lock();
|
|
if loading_scenes.contains(&scene_path) {
|
|
return;
|
|
}
|
|
loading_scenes.insert(scene_path.clone());
|
|
drop(loading_scenes);
|
|
|
|
let ui = self.engine.user_interfaces.first_mut();
|
|
self.scene_loading_window
|
|
.get_or_insert_with(|| SceneLoadingWindow::new(&mut ui.build_ctx()));
|
|
self.update_loop_state.request_update_in_next_frame();
|
|
|
|
if let Some(ext) = scene_path.extension() {
|
|
let resource_manager = self.engine.resource_manager.clone();
|
|
let sender = self.message_sender.clone();
|
|
let loading_scenes = self.loading_scenes.clone();
|
|
if ext == "rgs" {
|
|
let serialization_context = self.engine.serialization_context.clone();
|
|
let dyn_type_constructors = self.engine.dyn_type_constructors.clone();
|
|
self.engine.task_pool.inner().spawn_task(async move {
|
|
let result = SceneLoader::from_file(
|
|
&scene_path,
|
|
&FsResourceIo,
|
|
serialization_context,
|
|
dyn_type_constructors,
|
|
resource_manager,
|
|
)
|
|
.await;
|
|
loading_scenes.safe_lock().remove(&scene_path);
|
|
match result {
|
|
Ok(loader) => {
|
|
let scene = loader.0.finish().await;
|
|
sender.send(Message::AddScene {
|
|
scene,
|
|
path: scene_path,
|
|
});
|
|
}
|
|
Err(e) => {
|
|
Log::err(e.to_string());
|
|
}
|
|
}
|
|
});
|
|
} else if ext == "ui" {
|
|
let widget_constructors = self.engine.widget_constructors.clone();
|
|
let dyn_type_constructors = self.engine.dyn_type_constructors.clone();
|
|
self.engine.task_pool.inner().spawn_task(async move {
|
|
let result = UserInterface::load_from_file_ex(
|
|
&scene_path,
|
|
widget_constructors,
|
|
dyn_type_constructors,
|
|
resource_manager,
|
|
&FsResourceIo,
|
|
)
|
|
.await;
|
|
loading_scenes.safe_lock().remove(&scene_path);
|
|
match result {
|
|
Ok((ui, _)) => {
|
|
sender.send(Message::AddUiScene {
|
|
ui,
|
|
path: scene_path,
|
|
});
|
|
}
|
|
Err(e) => {
|
|
Log::err(e.to_string());
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
Log::err(format!(
|
|
"{} is not a game scene or UI scene!",
|
|
scene_path.display()
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
fn exit(&mut self, force: bool, loop_controller: ApplicationLoopController) {
|
|
let engine = &mut self.engine;
|
|
if force {
|
|
loop_controller.exit();
|
|
} else if let Some(first_unsaved) = self.scenes.first_unsaved_scene() {
|
|
engine.user_interfaces.first_mut().send(
|
|
self.exit_message_box,
|
|
MessageBoxMessage::Open {
|
|
title: None,
|
|
text: Some(format!(
|
|
"There are unsaved changes in the {} scene. \
|
|
Do you wish to save them before exit?",
|
|
first_unsaved.name()
|
|
)),
|
|
},
|
|
);
|
|
} else {
|
|
loop_controller.exit();
|
|
}
|
|
}
|
|
|
|
fn on_exit(&mut self) {
|
|
let ids = self.scenes.entries.iter().map(|e| e.id).collect::<Vec<_>>();
|
|
for id in ids {
|
|
self.close_scene(id);
|
|
}
|
|
|
|
self.settings.force_save();
|
|
|
|
for_each_plugin!(self.plugins => on_exit(self));
|
|
|
|
info!("The editor was closed.");
|
|
}
|
|
|
|
fn close_scene(&mut self, id: Uuid) -> bool {
|
|
let closing_current_scene = self.scenes.current_scene_entry_ref().id == id;
|
|
|
|
if closing_current_scene {
|
|
self.try_leave_preview_mode();
|
|
}
|
|
|
|
let engine = &mut self.engine;
|
|
if let Some(mut entry) = self.scenes.take_scene(id) {
|
|
entry
|
|
.controller
|
|
.on_destroy(&mut entry.command_stack, engine, &mut entry.selection);
|
|
|
|
if closing_current_scene {
|
|
// Preview frame has scene frame texture assigned, it must be cleared explicitly,
|
|
// otherwise it will show last rendered frame in preview which is not what we want.
|
|
self.scene_viewer
|
|
.set_render_target(engine.user_interfaces.first(), None);
|
|
// Set default title scene
|
|
self.scene_viewer
|
|
.set_title(engine.user_interfaces.first(), "Scene Preview".to_string());
|
|
}
|
|
|
|
entry.before_drop(engine);
|
|
|
|
if closing_current_scene {
|
|
self.on_scene_changed(entry.selection);
|
|
}
|
|
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
fn set_current_scene(&mut self, id: Uuid) {
|
|
self.try_leave_preview_mode();
|
|
let old_selection = self.scenes.current_scene_entry_ref().selection.clone();
|
|
if self.scenes.set_current_scene(id) {
|
|
self.on_scene_changed(old_selection);
|
|
}
|
|
}
|
|
|
|
fn on_scene_changed(&mut self, old_selection: Selection) {
|
|
self.refresh_entity_creator();
|
|
|
|
let ui = self.engine.user_interfaces.first_mut();
|
|
let entry = self.scenes.current_scene_entry_ref();
|
|
if let Some(game_scene) = entry.controller.downcast_ref::<GameScene>() {
|
|
self.world_viewer.item_context_menu = Some(self.scene_node_context_menu.clone());
|
|
if let Some(overlay_pass) = self.overlay_pass.as_ref() {
|
|
overlay_pass.borrow_mut().scene_handle = game_scene.scene;
|
|
}
|
|
} else if entry.controller.downcast_ref::<UiScene>().is_some() {
|
|
self.world_viewer.item_context_menu = Some(self.widget_context_menu.clone());
|
|
} else {
|
|
self.world_viewer.item_context_menu = None;
|
|
}
|
|
|
|
self.message_sender
|
|
.send(Message::SelectionChanged { old_selection });
|
|
|
|
self.world_viewer.clear(ui);
|
|
|
|
self.poll_ui_messages();
|
|
|
|
self.world_viewer.sync_selection = true;
|
|
|
|
let scene = self.scenes.current_scene_entry_mut();
|
|
self.scene_viewer
|
|
.on_current_scene_changed(scene, &mut self.engine);
|
|
|
|
for_each_plugin!(self.plugins => on_scene_changed(self));
|
|
|
|
self.sync_to_model();
|
|
self.poll_ui_messages();
|
|
}
|
|
|
|
fn create_new_scene(&mut self) {
|
|
let entry = EditorSceneEntry::new_game_scene(
|
|
Scene::new(),
|
|
None,
|
|
&mut self.engine,
|
|
&mut self.settings,
|
|
self.message_sender.clone(),
|
|
&self.scene_viewer,
|
|
self.highlighter.clone(),
|
|
);
|
|
self.add_scene(entry);
|
|
}
|
|
|
|
fn create_new_ui_scene(&mut self) {
|
|
let mut ui = UserInterface::new(Vector2::new(200.0, 200.0));
|
|
|
|
// Create test content.
|
|
ButtonBuilder::new(
|
|
WidgetBuilder::new()
|
|
.with_width(160.0)
|
|
.with_height(32.0)
|
|
.with_desired_position(Vector2::new(20.0, 20.0)),
|
|
)
|
|
.with_text("Click Me!")
|
|
.build(&mut ui.build_ctx());
|
|
|
|
TextBuilder::new(WidgetBuilder::new().with_desired_position(Vector2::new(300.0, 300.0)))
|
|
.with_text("This is some text.")
|
|
.build(&mut ui.build_ctx());
|
|
|
|
let entry = EditorSceneEntry::new_ui_scene(
|
|
ui,
|
|
None,
|
|
self.message_sender.clone(),
|
|
&self.scene_viewer,
|
|
&mut self.engine,
|
|
&self.settings,
|
|
);
|
|
self.add_scene(entry);
|
|
}
|
|
|
|
fn configure(&mut self, working_directory: PathBuf) {
|
|
assert!(self.scenes.is_empty());
|
|
|
|
let current_working_directory = std::env::current_dir().unwrap();
|
|
if current_working_directory != working_directory {
|
|
Log::info(format!(
|
|
"Changing working directory to {working_directory:?}"
|
|
));
|
|
std::env::set_current_dir(working_directory.clone()).unwrap();
|
|
self.engine.resource_manager.update_or_load_registry();
|
|
self.reload_settings();
|
|
self.load_layout();
|
|
}
|
|
|
|
let engine = &mut self.engine;
|
|
|
|
if let GraphicsContext::Initialized(ref graphics_context) = engine.graphics_context {
|
|
graphics_context.window.set_title(&format!(
|
|
"FyroxEd{} {}{}: {}",
|
|
self.user_project_name,
|
|
*EDITOR_VERSION,
|
|
self.user_project_version,
|
|
working_directory.to_string_lossy()
|
|
));
|
|
}
|
|
|
|
let registry_folder = engine.resource_manager.registry_folder();
|
|
match FileSystemWatcher::new(®istry_folder, Duration::from_secs(1)) {
|
|
Ok(watcher) => {
|
|
engine.resource_manager.state().set_watcher(Some(watcher));
|
|
}
|
|
Err(e) => {
|
|
Log::err(format!("Unable to create resource watcher. Reason {e:?}"));
|
|
}
|
|
}
|
|
|
|
engine.resource_manager.state().destroy_unused_resources();
|
|
|
|
self.asset_browser.set_working_directory(engine);
|
|
|
|
self.world_viewer
|
|
.on_configure(engine.user_interfaces.first(), &self.settings);
|
|
|
|
Log::info(format!(
|
|
"New working directory was successfully set: {working_directory:?}"
|
|
));
|
|
}
|
|
|
|
fn poll_ui_messages(&mut self) -> usize {
|
|
let mut processed = 0;
|
|
|
|
loop {
|
|
let poll_result = self.engine.user_interfaces.first_mut().poll_message_queue();
|
|
if let Some(mut ui_message) = poll_result.message {
|
|
self.handle_ui_message(&mut ui_message);
|
|
} else {
|
|
break;
|
|
}
|
|
processed += poll_result.processed_messages;
|
|
}
|
|
|
|
if processed > 0 {
|
|
// We need to ensure, that all the changes will be correctly rendered on screen. So
|
|
// request update and render on next frame.
|
|
self.update_loop_state.request_update_in_next_frame();
|
|
}
|
|
|
|
processed
|
|
}
|
|
|
|
fn handle_modes(&mut self, dt: f32) {
|
|
match self.mode {
|
|
Mode::Play {
|
|
ref mut process,
|
|
ref active,
|
|
} => {
|
|
match process.try_wait() {
|
|
Ok(status) => {
|
|
if let Some(status) = status {
|
|
// Stop reader thread.
|
|
active.store(false, Ordering::SeqCst);
|
|
|
|
self.mode = Mode::Edit;
|
|
self.on_mode_changed();
|
|
|
|
Log::warn(format!("Game was closed: {status:?}"))
|
|
}
|
|
}
|
|
Err(err) => Log::err(format!("Failed to wait for game process: {err:?}")),
|
|
}
|
|
}
|
|
Mode::Build {
|
|
ref mut process,
|
|
ref mut queue,
|
|
play_after_build,
|
|
} => {
|
|
if process.is_none() {
|
|
if let Some(build_command) = queue.pop_front() {
|
|
Log::info(format!("Trying to run build command: {build_command}"));
|
|
|
|
match build_command
|
|
.make_command()
|
|
.stderr(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.spawn()
|
|
{
|
|
Ok(mut new_process) => {
|
|
if let Some(build_window) = self.build_window.as_mut() {
|
|
build_window.listen(
|
|
(
|
|
new_process.stderr.take().unwrap(),
|
|
new_process.stdout.take().unwrap(),
|
|
),
|
|
self.engine.user_interfaces.first(),
|
|
);
|
|
}
|
|
|
|
*process = Some(new_process);
|
|
}
|
|
Err(e) => Log::err(format!("Failed to enter build mode: {e:?}")),
|
|
}
|
|
} else {
|
|
Log::warn("Empty build command queue!");
|
|
self.mode = Mode::Edit;
|
|
self.on_mode_changed();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if let Some(process_ref) = process {
|
|
if let Some(build_window) = self.build_window.as_mut() {
|
|
build_window.update(self.engine.user_interfaces.first(), dt);
|
|
}
|
|
|
|
match process_ref.try_wait() {
|
|
Ok(status) => {
|
|
if let Some(status) = status {
|
|
let success_code = 0;
|
|
let wtf_code = 12345;
|
|
let code = status.code().unwrap_or(wtf_code);
|
|
if code != success_code {
|
|
Log::err("Failed to build the game!");
|
|
self.mode = Mode::Edit;
|
|
self.on_mode_changed();
|
|
} else if queue.is_empty() && play_after_build {
|
|
self.set_play_mode();
|
|
} else {
|
|
let ui = self.engine.user_interfaces.first();
|
|
if queue.is_empty() {
|
|
if let Some(build_window) = self.build_window.take() {
|
|
build_window.destroy(ui);
|
|
}
|
|
if let Some((process, active)) =
|
|
self.running_game_process.take()
|
|
{
|
|
self.mode = Mode::Play { process, active };
|
|
self.on_mode_changed();
|
|
} else {
|
|
self.mode = Mode::Edit;
|
|
self.on_mode_changed();
|
|
}
|
|
} else {
|
|
if let Some(build_window) = self.build_window.as_mut() {
|
|
build_window.reset(ui);
|
|
}
|
|
// Continue on next command.
|
|
*process = None;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Err(err) => Log::err(format!("Failed to wait for game process: {err:?}")),
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn update(&mut self, dt: f32, loop_controller: ApplicationLoopController) {
|
|
for_each_plugin!(self.plugins => on_update(self, loop_controller));
|
|
|
|
let settings_data = self.settings.data_mut();
|
|
settings_data.log.log_info = Log::is_logging_info();
|
|
settings_data.log.log_warning = Log::is_logging_warning();
|
|
settings_data.log.log_error = Log::is_logging_error();
|
|
|
|
self.handle_modes(dt);
|
|
|
|
let ui = self.engine.user_interfaces.first_mut();
|
|
|
|
if let Some(loading_window) = self.scene_loading_window.take() {
|
|
let set = self.loading_scenes.safe_lock();
|
|
self.scene_loading_window = loading_window.update(&set, ui);
|
|
}
|
|
|
|
if let Some(active_tooltip) = ui.active_tooltip() {
|
|
if !active_tooltip.shown {
|
|
// Keep the editor running until the current tooltip is not shown.
|
|
self.update_loop_state.request_update_in_next_frame();
|
|
}
|
|
}
|
|
|
|
if !self.loading_scenes.safe_lock().is_empty() {
|
|
self.update_loop_state.request_update_in_next_frame();
|
|
}
|
|
|
|
self.log.update(self.settings.general.max_log_entries, ui);
|
|
if let Some(export_window) = self.export_window.as_mut() {
|
|
export_window.update(ui);
|
|
}
|
|
|
|
self.asset_browser
|
|
.update(&mut self.engine, &self.message_sender);
|
|
if let Some(surface_data_viewer) = self.surface_data_viewer.as_mut() {
|
|
surface_data_viewer.update(&mut self.engine);
|
|
}
|
|
|
|
self.scene_viewer
|
|
.pre_update(&self.settings, &mut self.engine);
|
|
let entry = self.scenes.current_scene_entry_ref();
|
|
if let Some(game_scene) = entry.controller.downcast_ref::<GameScene>() {
|
|
if let Some(stats) = self.statistics_window.as_ref() {
|
|
stats.update(game_scene.scene, &self.engine);
|
|
}
|
|
|
|
self.light_panel.update(game_scene, &mut self.engine);
|
|
self.audio_preview_panel
|
|
.update(&entry.selection, game_scene, &self.engine);
|
|
self.scene_viewer.update(game_scene, &mut self.engine);
|
|
}
|
|
|
|
if let Some(overlay_pass) = self.overlay_pass.as_ref() {
|
|
overlay_pass.borrow_mut().pictogram_size = self.settings.debugging.pictogram_size;
|
|
}
|
|
|
|
self.processed_ui_messages = 0;
|
|
let mut iterations = 1;
|
|
while iterations > 0 {
|
|
iterations -= 1;
|
|
|
|
let ui_messages_processed_count = self.poll_ui_messages();
|
|
self.processed_ui_messages += ui_messages_processed_count;
|
|
|
|
let mut needs_sync = false;
|
|
|
|
let mut editor_messages_processed_count = 0;
|
|
while let Ok(message) = self.message_receiver.try_recv() {
|
|
for_each_plugin!(self.plugins => on_message(&message, self));
|
|
|
|
editor_messages_processed_count += 1;
|
|
|
|
self.save_scene_dialog
|
|
.handle_message(&message, &self.message_sender);
|
|
|
|
let entry = self.scenes.current_scene_entry_mut();
|
|
self.asset_browser
|
|
.on_message(&mut self.engine, entry, &message, &self.plugins);
|
|
self.particle_system_control_panel.handle_message(
|
|
&message,
|
|
&entry.selection,
|
|
entry.controller.downcast_mut::<GameScene>(),
|
|
&mut self.engine,
|
|
);
|
|
self.camera_control_panel.handle_message(
|
|
&message,
|
|
&entry.selection,
|
|
entry.controller.downcast_mut::<GameScene>(),
|
|
&mut self.engine,
|
|
);
|
|
self.mesh_control_panel.handle_message(
|
|
&message,
|
|
&entry.selection,
|
|
entry.controller.downcast_mut::<GameScene>(),
|
|
&mut self.engine,
|
|
);
|
|
self.audio_preview_panel.handle_message(
|
|
&message,
|
|
&entry.selection,
|
|
entry.controller.downcast_mut::<GameScene>(),
|
|
&mut self.engine,
|
|
);
|
|
self.bbcode_panel.handle_message(
|
|
&message,
|
|
&entry.selection,
|
|
entry.controller.downcast_ref::<UiScene>(),
|
|
&mut self.engine,
|
|
);
|
|
needs_sync |=
|
|
entry
|
|
.controller
|
|
.on_message(&message, &entry.selection, &mut self.engine);
|
|
needs_sync |=
|
|
entry
|
|
.controller
|
|
.on_message(&message, &entry.selection, &mut self.engine);
|
|
self.scene_viewer.handle_message(&message, &mut self.engine);
|
|
|
|
match message {
|
|
Message::DoCommand(command) => {
|
|
needs_sync |= self.do_current_scene_command(command);
|
|
}
|
|
Message::UndoCurrentSceneCommand => {
|
|
needs_sync |= self.undo_current_scene_command();
|
|
}
|
|
Message::RedoCurrentSceneCommand => {
|
|
needs_sync |= self.redo_current_scene_command();
|
|
}
|
|
Message::ClearCurrentSceneCommandStack => {
|
|
needs_sync |= self.clear_current_scene_command_stack();
|
|
}
|
|
Message::SelectionChanged { .. } => {
|
|
self.world_viewer.sync_selection = true;
|
|
}
|
|
Message::SaveScene { id: scene, path } => self.save_scene(scene, path),
|
|
Message::AddScene { scene, path } => {
|
|
let entry = EditorSceneEntry::new_game_scene(
|
|
scene,
|
|
Some(path),
|
|
&mut self.engine,
|
|
&mut self.settings,
|
|
self.message_sender.clone(),
|
|
&self.scene_viewer,
|
|
self.highlighter.clone(),
|
|
);
|
|
self.add_scene(entry);
|
|
needs_sync = true;
|
|
}
|
|
Message::AddUiScene { ui, path } => {
|
|
let entry = EditorSceneEntry::new_ui_scene(
|
|
ui,
|
|
Some(path),
|
|
self.message_sender.clone(),
|
|
&self.scene_viewer,
|
|
&mut self.engine,
|
|
&self.settings,
|
|
);
|
|
self.add_scene(entry);
|
|
}
|
|
Message::LoadScene(scene_path) => {
|
|
self.load_scene(scene_path);
|
|
needs_sync = true;
|
|
}
|
|
Message::SetInteractionMode(mode_kind) => {
|
|
self.scenes
|
|
.current_scene_entry_mut()
|
|
.set_interaction_mode(&mut self.engine, Some(mode_kind));
|
|
}
|
|
Message::Exit { force } => self.exit(force, loop_controller),
|
|
Message::CloseScene(scene) => {
|
|
needs_sync |= self.close_scene(scene);
|
|
}
|
|
Message::NewScene => {
|
|
self.create_new_scene();
|
|
needs_sync = true;
|
|
}
|
|
Message::NewUiScene => {
|
|
self.create_new_ui_scene();
|
|
needs_sync = true;
|
|
}
|
|
Message::SetCurrentScene(scene) => {
|
|
self.set_current_scene(scene);
|
|
needs_sync = true;
|
|
}
|
|
Message::Configure { working_directory } => {
|
|
self.configure(working_directory);
|
|
needs_sync = true;
|
|
}
|
|
Message::OpenNodeRemovalDialog => {
|
|
let entry = self.scenes.current_scene_entry_ref();
|
|
// TODO
|
|
if let Some(game_scene) = entry.controller.downcast_ref::<GameScene>() {
|
|
self.node_removal_dialog.open(
|
|
&entry.selection,
|
|
game_scene,
|
|
&self.engine,
|
|
)
|
|
}
|
|
}
|
|
Message::OpenEntityCreator(mode) => {
|
|
self.entity_creator
|
|
.open(mode, self.engine.user_interfaces.first());
|
|
}
|
|
Message::SetAssetBrowserCurrentDir(path) => {
|
|
self.asset_browser
|
|
.request_current_path(path, self.engine.user_interfaces.first());
|
|
}
|
|
Message::ShowInAssetBrowser(path) => {
|
|
self.asset_browser
|
|
.locate_path(self.engine.user_interfaces.first(), path);
|
|
}
|
|
Message::LocateObject { handle } => self
|
|
.world_viewer
|
|
.try_locate_object(handle, self.engine.user_interfaces.first()),
|
|
Message::SwitchMode => match self.mode {
|
|
Mode::Edit => self.set_build_mode(true),
|
|
_ => self.set_editor_mode(),
|
|
},
|
|
Message::SwitchToBuildMode { play_after_build } => {
|
|
self.set_build_mode(play_after_build)
|
|
}
|
|
Message::SwitchToEditMode => self.set_editor_mode(),
|
|
Message::OpenLoadSceneDialog => {
|
|
self.menu.open_load_file_selector(
|
|
&self.engine.resource_manager,
|
|
self.engine.user_interfaces.first_mut(),
|
|
);
|
|
}
|
|
Message::OpenSaveSceneDialog { default_file_info } => {
|
|
self.menu.open_save_file_selector(
|
|
self.engine.user_interfaces.first_mut(),
|
|
&self.engine.resource_manager,
|
|
default_file_info,
|
|
);
|
|
}
|
|
Message::OpenSaveSceneConfirmationDialog { id, action } => {
|
|
self.save_scene_dialog.open(
|
|
self.engine.user_interfaces.first(),
|
|
id,
|
|
&self.scenes,
|
|
action,
|
|
);
|
|
}
|
|
Message::ForceSync => {
|
|
needs_sync = true;
|
|
}
|
|
Message::SaveAllScenes => {
|
|
for scene in self.scenes.iter() {
|
|
if let Some(path) = scene.path.clone() {
|
|
self.message_sender
|
|
.send(Message::SaveScene { id: scene.id, path })
|
|
}
|
|
}
|
|
}
|
|
Message::ShowDocumentation(doc) => {
|
|
self.doc_window
|
|
.open(doc, self.engine.user_interfaces.first_mut());
|
|
}
|
|
Message::SaveLayout => {
|
|
self.save_layout();
|
|
}
|
|
Message::LoadLayout => {
|
|
self.load_layout();
|
|
}
|
|
Message::ResetLayout => {
|
|
self.reset_layout();
|
|
}
|
|
Message::ViewSurfaceData(data) => {
|
|
let mut viewer = SurfaceDataViewer::new(&mut self.engine);
|
|
viewer.open(data, &mut self.engine);
|
|
self.surface_data_viewer = Some(viewer);
|
|
}
|
|
Message::SyncInteractionModes => {
|
|
self.scene_viewer.sync_interaction_modes(
|
|
self.scenes.current_scene_entry_mut(),
|
|
self.engine.user_interfaces.first_mut(),
|
|
);
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
if needs_sync {
|
|
self.sync_to_model();
|
|
}
|
|
|
|
if editor_messages_processed_count > 0 {
|
|
self.update_loop_state.request_update_in_next_frame();
|
|
}
|
|
|
|
// Any processed UI message can produce editor messages and vice versa, in this case we
|
|
// must do another pass.
|
|
if ui_messages_processed_count > 0 || editor_messages_processed_count > 0 {
|
|
iterations += 1;
|
|
}
|
|
}
|
|
|
|
let entry = self.scenes.current_scene_entry_mut();
|
|
let controller = &mut entry.controller;
|
|
|
|
let screen_bounds = self
|
|
.scene_viewer
|
|
.frame_bounds(self.engine.user_interfaces.first());
|
|
if let Some(new_render_target) = controller.update(
|
|
&entry.selection,
|
|
&mut self.engine,
|
|
dt,
|
|
entry.path.as_deref(),
|
|
&mut self.settings,
|
|
screen_bounds,
|
|
) {
|
|
self.scene_viewer
|
|
.set_render_target(self.engine.user_interfaces.first(), Some(new_render_target));
|
|
}
|
|
|
|
if let Some(interaction_mode) = entry
|
|
.current_interaction_mode
|
|
.and_then(|current_mode| entry.interaction_modes.get_mut(¤t_mode))
|
|
{
|
|
interaction_mode.update(
|
|
&entry.selection,
|
|
&mut **controller,
|
|
&mut self.engine,
|
|
&self.settings,
|
|
);
|
|
}
|
|
|
|
if self.settings.try_save() {
|
|
let ui = self.engine.user_interfaces.first_mut();
|
|
if let Some(style) = self.styles.get(&self.settings.general.style) {
|
|
if style != ui.style() {
|
|
ui.set_style(style.clone());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn save_layout(&mut self) {
|
|
let ui = self.engine.user_interfaces.first();
|
|
let layout = ui[self.docking_manager].layout(ui);
|
|
self.settings.windows.layout = Some(layout);
|
|
}
|
|
|
|
fn load_layout(&mut self) {
|
|
if let Some(layout) = self.settings.windows.layout.as_ref() {
|
|
self.engine.user_interfaces.first().send(
|
|
self.docking_manager,
|
|
DockingManagerMessage::Layout(layout.clone()),
|
|
);
|
|
}
|
|
}
|
|
|
|
fn reset_layout(&self) {
|
|
self.engine.user_interfaces.first().send(
|
|
self.docking_manager,
|
|
DockingManagerMessage::Layout(self.default_layout.clone()),
|
|
);
|
|
}
|
|
|
|
fn refresh_entity_creator(&mut self) {
|
|
let ui = self.engine.user_interfaces.first_mut();
|
|
let entry = self.scenes.current_scene_entry_ref();
|
|
if entry.controller.downcast_ref::<GameScene>().is_some() {
|
|
self.entity_creator
|
|
.on_constructors_changed(ui, &self.engine.serialization_context.node_constructors);
|
|
} else if entry.controller.downcast_ref::<UiScene>().is_some() {
|
|
self.entity_creator
|
|
.on_constructors_changed(ui, &self.engine.widget_constructors);
|
|
}
|
|
if let Some(scene_settings) = entry
|
|
.path
|
|
.as_ref()
|
|
.and_then(|p| self.settings.scene_settings.get(p))
|
|
{
|
|
self.entity_creator.on_scene_changed(ui, scene_settings);
|
|
}
|
|
}
|
|
|
|
fn on_game_plugin_added(&mut self) {
|
|
self.refresh_entity_creator();
|
|
}
|
|
|
|
pub fn add_game_plugin<P>(&mut self, plugin: P)
|
|
where
|
|
P: Plugin + 'static,
|
|
{
|
|
plugin.register_property_editors(self.property_editors.clone());
|
|
self.engine.add_plugin(plugin);
|
|
self.on_game_plugin_added();
|
|
}
|
|
|
|
/// Tries to add a new dynamic plugin. This method attempts to load a dynamic library by the
|
|
/// given path and searches for `fyrox_plugin` function. This function is called to create a
|
|
/// plugin instance. This method will fail if there's no dynamic library at the given path or
|
|
/// the `fyrox_plugin` function is not found.
|
|
///
|
|
/// # Hot reloading
|
|
///
|
|
/// This method can enable hot reloading for the plugin, by setting `reload_when_changed` parameter
|
|
/// to `true`. When enabled, the engine will clone the library to implementation-defined path
|
|
/// and load it. It will setup file system watcher to receive changes from the OS and reload
|
|
/// the plugin.
|
|
pub fn add_dynamic_plugin<P>(
|
|
&mut self,
|
|
path: P,
|
|
reload_when_changed: bool,
|
|
use_relative_paths: bool,
|
|
) -> Result<(), String>
|
|
where
|
|
P: AsRef<Path> + 'static,
|
|
{
|
|
self.add_dynamic_plugin_custom(DyLibDynamicPlugin::new(
|
|
path,
|
|
reload_when_changed,
|
|
use_relative_paths,
|
|
)?)
|
|
}
|
|
|
|
pub fn add_dynamic_plugin_custom<P>(&mut self, plugin: P) -> Result<(), String>
|
|
where
|
|
P: DynamicPlugin + 'static,
|
|
{
|
|
let plugin = self.engine.add_dynamic_plugin_custom(plugin);
|
|
*self.property_editors.context_type_id.safe_lock() = plugin.type_id();
|
|
plugin.register_property_editors(self.property_editors.clone());
|
|
self.on_game_plugin_added();
|
|
Ok(())
|
|
}
|
|
|
|
pub fn add_editor_plugin<P>(&mut self, plugin: P)
|
|
where
|
|
P: EditorPlugin + 'static,
|
|
{
|
|
self.plugins.add(plugin);
|
|
}
|
|
|
|
pub fn is_active(&self) -> bool {
|
|
self.settings.general.keep_editor_active
|
|
|| (!self.update_loop_state.is_suspended()
|
|
&& (self.focused || !self.settings.general.suspend_unfocused_editor)
|
|
// Keep the editor active if user holds any mouse button.
|
|
|| self.engine.user_interfaces.first().captured_node().is_some()
|
|
// Keep the editor active until it fully loads all the queued scenes.
|
|
||!self.loading_scenes.safe_lock().is_empty())
|
|
}
|
|
|
|
fn on_resumed(&mut self, event_loop: &ActiveEventLoop) {
|
|
let engine = &mut self.engine;
|
|
|
|
Log::verify(engine.initialize_graphics_context(event_loop));
|
|
|
|
if let GraphicsContext::Initialized(ref mut graphics_context) = engine.graphics_context {
|
|
graphics_context.set_window_icon_from_memory(
|
|
self.user_project_icon
|
|
.as_deref()
|
|
.unwrap_or(include_bytes!("../resources/icon.png")),
|
|
);
|
|
|
|
// High-DPI screen support
|
|
Log::info(format!(
|
|
"UI scaling of your OS is: {}",
|
|
graphics_context.window.scale_factor()
|
|
));
|
|
|
|
set_ui_scaling(
|
|
engine.user_interfaces.first(),
|
|
graphics_context.window.scale_factor() as f32,
|
|
);
|
|
|
|
let overlay_pass = OverlayRenderPass::new(graphics_context.renderer.graphics_server());
|
|
graphics_context
|
|
.renderer
|
|
.add_render_pass(overlay_pass.clone());
|
|
self.overlay_pass = Some(overlay_pass);
|
|
|
|
let highlighter = HighlightRenderPass::new(
|
|
&*graphics_context.renderer.server,
|
|
self.settings.windows.window_size.x as usize,
|
|
self.settings.windows.window_size.y as usize,
|
|
);
|
|
graphics_context
|
|
.renderer
|
|
.add_render_pass(highlighter.clone());
|
|
self.highlighter = Some(highlighter);
|
|
|
|
match graphics_context
|
|
.renderer
|
|
.set_quality_settings(&self.settings.graphics.quality)
|
|
{
|
|
Ok(_) => {
|
|
Log::info("Graphics settings were applied successfully!");
|
|
}
|
|
Err(e) => Log::err(format!("Failed to apply graphics settings! Reason: {e:?}")),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn on_suspended(&mut self) {
|
|
self.overlay_pass.take();
|
|
self.highlighter.take();
|
|
|
|
self.engine.destroy_graphics_context().unwrap();
|
|
}
|
|
|
|
fn try_process_file_system_events(&mut self) {
|
|
if self.focused {
|
|
self.engine
|
|
.resource_manager
|
|
.state()
|
|
.process_filesystem_events();
|
|
}
|
|
}
|
|
|
|
fn on_start(&mut self) {
|
|
Log::info("Initializing resource registry.");
|
|
self.engine.resource_manager.update_or_load_registry();
|
|
|
|
for_each_plugin!(self.plugins => on_start(self));
|
|
}
|
|
|
|
pub fn run(mut self, event_loop: EventLoop<()>) {
|
|
self.on_start();
|
|
|
|
event_loop
|
|
.run(move |event, event_loop| {
|
|
match event {
|
|
Event::AboutToWait => {
|
|
if self.is_active() {
|
|
update(
|
|
&mut self,
|
|
ApplicationLoopController::ActiveEventLoop(event_loop),
|
|
);
|
|
}
|
|
}
|
|
Event::Resumed => {
|
|
self.on_resumed(event_loop);
|
|
}
|
|
Event::Suspended => {
|
|
self.on_suspended();
|
|
}
|
|
Event::WindowEvent { ref event, .. } => {
|
|
match event {
|
|
WindowEvent::CloseRequested => {
|
|
self.message_sender.send(Message::Exit { force: false });
|
|
}
|
|
WindowEvent::Resized(size) => {
|
|
if let Err(e) = self.engine.set_frame_size((*size).into()) {
|
|
fyrox::core::log::Log::writeln(
|
|
MessageKind::Error,
|
|
format!("Failed to set renderer size! Reason: {e:?}"),
|
|
);
|
|
}
|
|
|
|
if let GraphicsContext::Initialized(ref graphics_context) =
|
|
self.engine.graphics_context
|
|
{
|
|
let window = &graphics_context.window;
|
|
|
|
let logical_size = size.to_logical(window.scale_factor());
|
|
self.engine.user_interfaces.first().send_many(
|
|
self.root_grid,
|
|
[
|
|
WidgetMessage::Width(logical_size.width),
|
|
WidgetMessage::Height(logical_size.height),
|
|
],
|
|
);
|
|
|
|
if size.width > 0 && size.height > 0 {
|
|
self.settings.windows.window_size.x = size.width as f32;
|
|
self.settings.windows.window_size.y = size.height as f32;
|
|
}
|
|
|
|
self.settings.windows.window_maximized = window.is_maximized();
|
|
}
|
|
}
|
|
WindowEvent::Focused(focused) => {
|
|
self.focused = *focused;
|
|
self.try_process_file_system_events();
|
|
}
|
|
WindowEvent::Moved(new_position)
|
|
// Allow the window to go outside the screen bounds by a little. This
|
|
// happens when the window is maximized.
|
|
if new_position.x > -50 && new_position.y > -50 => {
|
|
self.settings.windows.window_position.x = new_position.x as f32;
|
|
self.settings.windows.window_position.y = new_position.y as f32;
|
|
}
|
|
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
|
|
set_ui_scaling(
|
|
self.engine.user_interfaces.first(),
|
|
*scale_factor as f32,
|
|
);
|
|
}
|
|
WindowEvent::RedrawRequested
|
|
if self.is_active() => {
|
|
let entry = self.scenes.current_scene_entry_mut();
|
|
entry
|
|
.controller
|
|
.on_before_render(&entry.selection, &mut self.engine);
|
|
|
|
self.engine.render().unwrap();
|
|
|
|
self.scenes
|
|
.current_scene_controller_mut()
|
|
.on_after_render(&mut self.engine);
|
|
}
|
|
_ => (),
|
|
}
|
|
|
|
// Any action in the window, other than a redraw request forces the editor to
|
|
// do another update pass which then pushes a redraw request to the event
|
|
// queue. This check prevents infinite loop of this kind.
|
|
if !matches!(event, WindowEvent::RedrawRequested) {
|
|
self.update_loop_state.request_update_in_current_frame();
|
|
}
|
|
|
|
if let Some(os_event) = translate_event(event) {
|
|
self.engine
|
|
.user_interfaces
|
|
.first_mut()
|
|
.process_os_event(&os_event);
|
|
}
|
|
}
|
|
Event::LoopExiting => {
|
|
self.on_exit();
|
|
}
|
|
_ => {
|
|
if self.is_active() {
|
|
if self.is_suspended {
|
|
for_each_plugin!(self.plugins => on_resumed(&mut self));
|
|
self.is_suspended = false;
|
|
}
|
|
} else if !self.is_suspended {
|
|
for_each_plugin!(self.plugins => on_suspended(&mut self));
|
|
self.is_suspended = true;
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.unwrap();
|
|
}
|
|
|
|
pub fn run_headless(mut self) {
|
|
self.on_start();
|
|
|
|
let is_running = Cell::new(true);
|
|
|
|
while is_running.get() {
|
|
let elapsed = update(
|
|
&mut self,
|
|
ApplicationLoopController::Headless {
|
|
running: &is_running,
|
|
},
|
|
);
|
|
|
|
// Only sleep for two-third of the remaining time step because thread::sleep tends to overshoot.
|
|
let sleep_time = (FIXED_TIMESTEP - elapsed).max(0.0) * 0.66666;
|
|
|
|
if sleep_time > 0.0 {
|
|
std::thread::sleep(Duration::from_secs_f32(sleep_time));
|
|
}
|
|
}
|
|
|
|
self.on_exit();
|
|
}
|
|
}
|
|
|
|
fn set_ui_scaling(ui: &UserInterface, scale: f32) {
|
|
// High-DPI screen support
|
|
ui.send(
|
|
ui.root(),
|
|
WidgetMessage::RenderTransform(Matrix3::new_scaling(scale)),
|
|
);
|
|
}
|
|
|
|
fn update(editor: &mut Editor, loop_controller: ApplicationLoopController) -> f32 {
|
|
let elapsed = editor.game_loop_data.clock.elapsed().as_secs_f32();
|
|
editor.game_loop_data.clock = Instant::now();
|
|
editor.game_loop_data.lag += elapsed;
|
|
|
|
while editor.game_loop_data.lag >= FIXED_TIMESTEP {
|
|
editor.game_loop_data.lag -= FIXED_TIMESTEP;
|
|
|
|
let mut switches = FxHashMap::default();
|
|
|
|
for other_game_scene_entry in editor.scenes.entries.iter() {
|
|
if let Some(other_game_scene) = other_game_scene_entry
|
|
.controller
|
|
.downcast_ref::<GameScene>()
|
|
{
|
|
if let Some(current_game_scene) = editor
|
|
.scenes
|
|
.current_scene_controller_ref()
|
|
.downcast_ref::<GameScene>()
|
|
{
|
|
switches.insert(
|
|
current_game_scene.scene,
|
|
current_game_scene.graph_switches.clone(),
|
|
);
|
|
|
|
if current_game_scene.scene == other_game_scene.scene {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Other scenes will be paused.
|
|
switches.insert(
|
|
other_game_scene.scene,
|
|
GraphUpdateSwitches {
|
|
paused: true,
|
|
..Default::default()
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
editor.engine.pre_update(
|
|
FIXED_TIMESTEP,
|
|
loop_controller,
|
|
&mut editor.game_loop_data.lag,
|
|
switches,
|
|
);
|
|
|
|
let mut need_reload_plugins = false;
|
|
for plugin_index in 0..editor.engine.plugins().len() {
|
|
let plugin = &editor.engine.plugins()[plugin_index];
|
|
|
|
if let PluginContainer::Dynamic(plugin) = plugin {
|
|
let plugin_type_id = plugin.as_loaded_ref().type_id();
|
|
|
|
if plugin.is_reload_needed_now() {
|
|
// Clear command stacks for scenes. This is mandatory step, because command stack
|
|
// could contain objects from plugins and any attempt to use them after the plugin is
|
|
// unloaded will cause crash.
|
|
for i in 0..editor.scenes.entries.len() {
|
|
let entry = &mut editor.scenes.entries[i];
|
|
entry.controller.clear_command_stack(
|
|
&mut entry.command_stack,
|
|
&mut entry.selection,
|
|
&mut editor.engine.scenes,
|
|
);
|
|
entry.selection = Default::default();
|
|
|
|
Log::warn(format!("Command stack flushed for scene {i}"));
|
|
}
|
|
|
|
editor.message_sender.send(Message::SelectionChanged {
|
|
old_selection: Default::default(),
|
|
});
|
|
editor.message_sender.send(Message::ForceSync);
|
|
|
|
// Remove property editors that were created from the plugin.
|
|
let mut definitions = editor.property_editors.definitions_mut();
|
|
|
|
let mut to_be_removed = Vec::new();
|
|
for (type_id, entry) in &mut *definitions {
|
|
if entry.source_type_id == plugin_type_id {
|
|
to_be_removed.push(*type_id);
|
|
}
|
|
}
|
|
|
|
for type_id in to_be_removed {
|
|
definitions.remove(&type_id);
|
|
}
|
|
|
|
need_reload_plugins = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
editor.update(FIXED_TIMESTEP, loop_controller);
|
|
|
|
editor.engine.post_update(
|
|
FIXED_TIMESTEP,
|
|
&Default::default(),
|
|
&mut editor.game_loop_data.lag,
|
|
loop_controller,
|
|
);
|
|
|
|
if need_reload_plugins {
|
|
let on_plugin_reloaded = |plugin: &dyn Plugin| {
|
|
*editor.property_editors.context_type_id.safe_lock() = plugin.type_id();
|
|
plugin.register_property_editors(editor.property_editors.clone());
|
|
};
|
|
|
|
editor.engine.handle_plugins_hot_reloading(
|
|
FIXED_TIMESTEP,
|
|
loop_controller,
|
|
&mut editor.game_loop_data.lag,
|
|
on_plugin_reloaded,
|
|
);
|
|
}
|
|
|
|
editor.post_update(loop_controller);
|
|
|
|
if editor.game_loop_data.lag >= 1.5 * FIXED_TIMESTEP {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if let GraphicsContext::Initialized(ref graphics_context) = editor.engine.graphics_context {
|
|
let window = &graphics_context.window;
|
|
window.set_cursor_icon(translate_cursor_icon(
|
|
editor.engine.user_interfaces.first_mut().cursor(),
|
|
));
|
|
window.request_redraw();
|
|
}
|
|
|
|
if !editor.is_in_preview_mode() {
|
|
editor.update_loop_state.decrease_counter();
|
|
}
|
|
|
|
if loop_controller.exiting() {
|
|
// Kill any active child process on exit.
|
|
match editor.mode {
|
|
Mode::Edit => {}
|
|
Mode::Build {
|
|
ref mut process, ..
|
|
} => {
|
|
if let Some(process) = process {
|
|
let _ = process.kill();
|
|
}
|
|
}
|
|
Mode::Play {
|
|
ref mut process, ..
|
|
} => {
|
|
let _ = process.kill();
|
|
}
|
|
}
|
|
}
|
|
|
|
elapsed
|
|
}
|