#![allow(deprecated)] #![allow(irrefutable_let_patterns)]
#![allow(clippy::too_many_arguments)]
#![allow(clippy::large_enum_variant)]
#![allow(clippy::mixed_read_write_in_expression)]
#![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 ui_scene;
pub mod utils;
pub mod world;
#[cfg(test)]
mod test;
pub use fyrox;
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::{
algebra::{Matrix3, Vector2},
color::Color,
log::{Log, MessageKind},
make_relative_path,
parking_lot::Mutex,
pool::Handle,
task::TaskPool,
uuid::Uuid,
watcher::FileSystemWatcher,
SafeLock, TypeUuidProvider,
},
core::{info, uuid},
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::UiStatisticsPlugin,
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_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();
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))
};
}
static GIZMO_SHADER: LazyLock<ShaderResource> = LazyLock::new(|| {
ShaderResource::from_str(
Uuid::new_v4(),
include_str!("../resources/shaders/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 {
pub working_directory: PathBuf,
pub scenes: Vec<PathBuf>,
pub named_objects: bool,
}
#[derive(Debug)]
pub enum SaveSceneConfirmationDialogAction {
None,
OpenLoadSceneDialog,
LoadScene(PathBuf),
MakeNewScene,
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() {
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 {
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 {
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 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>,
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 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 {
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(
&engine.serialization_context,
&engine.widget_constructors,
ctx,
)));
let widget_context_menu = Rc::new(RefCell::new(WidgetContextMenu::new(
&engine.widget_constructors,
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 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(UiStatisticsPlugin::default())
.with(CurveEditorPlugin::default())
.with(ReflectionProbePlugin::default())
.with(inspector_plugin),
focused: true,
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,
};
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 scene in data.scenes {
if scene != PathBuf::default() {
editor.message_sender.send(Message::LoadScene(scene));
}
}
} else {
editor.engine.user_interfaces.first().send(
editor.configurator.window,
WindowMessage::Open {
alignment: WindowAlignment::Center,
modal: true,
focus_content: true,
},
);
}
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) {
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_uuid(),
));
} else if hot_key == key_bindings.enable_move_mode {
sender.send(Message::SetInteractionMode(MoveInteractionMode::type_uuid()));
} else if hot_key == key_bindings.enable_rotate_mode {
sender.send(Message::SetInteractionMode(
RotateInteractionMode::type_uuid(),
));
} else if hot_key == key_bindings.enable_scale_mode {
sender.send(Message::SetInteractionMode(
ScaleInteractionMode::type_uuid(),
));
} else if hot_key == key_bindings.enable_navmesh_mode {
sender.send(Message::SetInteractionMode(EditNavmeshMode::type_uuid()));
} else if hot_key == key_bindings.enable_terrain_mode {
sender.send(Message::SetInteractionMode(
TerrainInteractionMode::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.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.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));
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, .. } => {
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, 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;
}
};
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),
},
);
}
}
}
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 {
self.scene_viewer
.set_render_target(engine.user_interfaces.first(), None);
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) {
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) {
let ui = &self.engine.user_interfaces.first();
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.menu.on_scene_changed(&*entry.controller, ui);
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));
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 {
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 {
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);
}
*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 {
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();
if let Some(game_scene) = entry.controller.downcast_ref::<GameScene>() {
self.node_removal_dialog.open(
&entry.selection,
game_scene,
&self.engine,
)
}
}
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());
}
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();
}
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()),
);
}
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)
}
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());
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)
|| self.engine.user_interfaces.first().captured_node().is_some()
||!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")),
);
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) => {
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);
}
}
_ => (),
}
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,
},
);
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) {
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;
}
}
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() {
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);
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() {
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
}