mod actor_inspector; mod animation_inspector; mod asset_browser; mod audio_inspector; mod build; pub mod component_registry; mod diagnostics; mod dock_tabs; mod fonts; pub(crate) mod helpers; mod hierarchy; pub mod hierarchy_ops; pub mod hierarchy_state; pub(crate) mod inspector; mod layout; mod menu; pub(crate) mod navigation_inspector; mod play_controls; mod post_process_volume_ui; mod scene_tabs; pub mod selection_ops; mod status_bar; pub(crate) mod theme; mod toolbar; mod viewport_chrome; mod widgets; use std::collections::HashSet; use bevy::prelude::*; use bevy::window::{CursorGrabMode, CursorOptions, PrimaryWindow}; use bevy_egui::{egui, EguiContext, EguiPrimaryContextPass, PrimaryEguiContext}; use bevy_inspector_egui::bevy_inspector::hierarchy::SelectedEntities; use egui_dock::{DockArea, DockState}; use egui_phosphor_icons::icons; use serde::{Deserialize, Serialize}; use crate::project_io::UserPreferences; use crate::selection::SelectedEntity; use crate::state::EditorMode; pub use build::BuildPanel; pub use diagnostics::{BrushDiagnosticsPanel, DiagnosticsPanel}; pub use layout::LayoutSaveTimer; pub use viewport_chrome::ViewportUiState; use diagnostics::diagnostics_window; use dock_tabs::PanelNodes; use hierarchy_state::HierarchyPanelState; use layout::{load_dock_layout, mark_layout_dirty_if_changed, tick_layout_save}; use menu::top_menu_bar; use status_bar::status_bar_ui; use theme::{apply_editor_theme, editor_dock_style}; use toolbar::editor_toolbar_panel; use viewport_chrome::viewport_tab_ui; use crate::settings_ui::{project_settings_window, ProjectSettingsPanel}; #[derive(Resource)] pub struct UiState { pub dock_state: DockState, pub viewport_rect: egui::Rect, pub pointer_in_viewport: bool, /// Pointer position inside the viewport image (logical screen coords), for picking. pub viewport_pointer_pos: Option, pub selected_entities: SelectedEntities, pub renaming_entity: Option, pub rename_buffer: String, pub(crate) inspector_collapsed_components: HashSet, pub(crate) panel_nodes: PanelNodes, last_mode_tab: Option, } pub fn egui_captures_keyboard(ctx: &egui::Context) -> bool { ctx.egui_wants_keyboard_input() } pub fn egui_captures_keyboard_from_world(world: &mut World) -> bool { let Ok(egui_context) = world .query_filtered::<&mut EguiContext, With>() .single(world) else { return false; }; let mut egui_context = egui_context.clone(); egui_captures_keyboard(egui_context.get_mut()) } pub fn viewport_keyboard_shortcuts_active( ui_state: &UiState, ctx: &egui::Context, buttons: &ButtonInput, ) -> bool { ui_state.pointer_in_viewport && !egui_captures_keyboard(ctx) && !buttons.any_pressed([MouseButton::Right, MouseButton::Middle]) } impl UiState { pub fn default_layout() -> Self { Self::from_prefs(&UserPreferences::default()) } pub fn from_prefs(prefs: &UserPreferences) -> Self { let layout = load_dock_layout(prefs); let fallback_nodes = layout.panel_nodes.unwrap_or(PanelNodes { main_view: layout.main_view_node, hierarchy: layout.main_view_node, inspector: layout.main_view_node, bottom: layout.main_view_node, }); let panel_nodes = PanelNodes::discover(&layout.dock_state, fallback_nodes); Self { dock_state: layout.dock_state, viewport_rect: egui::Rect::NOTHING, pointer_in_viewport: false, viewport_pointer_pos: None, selected_entities: SelectedEntities::default(), renaming_entity: None, rename_buffer: String::new(), inspector_collapsed_components: HashSet::new(), panel_nodes, last_mode_tab: None, } } #[expect( deprecated, reason = "bevy_egui exposes a Context during EguiPrimaryContextPass, so one top-level panel must bridge to the new Ui-based panel API" )] fn ui(&mut self, world: &mut World, ctx: &mut egui::Context, dt: f32) { apply_editor_theme(ctx); let selected_before_ui = world.resource::().0; let mode = *world.resource::>().get(); if self.last_mode_tab != Some(mode) { self.last_mode_tab = Some(mode); if mode == EditorMode::Playing { dock_tabs::ensure_viewport_for_play(&mut self.dock_state, &self.panel_nodes); } } self.pointer_in_viewport = false; self.viewport_pointer_pos = None; let playing = mode == EditorMode::Playing; let cursor_grabbed = world .query_filtered::<&CursorOptions, With>() .single(world) .is_ok_and(|cursor| cursor.grab_mode != CursorGrabMode::None); if playing && cursor_grabbed { ctx.input_mut(|input| { input.focused = false; }); } egui::CentralPanel::no_frame().show(ctx, |root_ui| { top_menu_bar( world, root_ui, &self.selected_entities, &mut self.dock_state, &mut self.panel_nodes, ); editor_toolbar_panel(world, root_ui, &mut self.selected_entities); status_bar_ui(world, root_ui, &self.selected_entities, mode); let mut viewer = TabViewer { world, viewport_rect: &mut self.viewport_rect, pointer_in_viewport: &mut self.pointer_in_viewport, viewport_pointer_pos: &mut self.viewport_pointer_pos, playing, cursor_grabbed, selected_entities: &mut self.selected_entities, renaming_entity: &mut self.renaming_entity, rename_buffer: &mut self.rename_buffer, }; DockArea::new(&mut self.dock_state) .style(editor_dock_style(ctx)) .show_inside(root_ui, &mut viewer); }); self.panel_nodes = PanelNodes::discover(&self.dock_state, self.panel_nodes); mark_layout_dirty_if_changed( world, &self.dock_state, self.panel_nodes.main_view(), self.panel_nodes, ); tick_layout_save(world, dt); let mut diagnostics_open = world.resource::().open; diagnostics_window(world, ctx, &mut diagnostics_open); world.resource_mut::().open = diagnostics_open; let mut brush_diagnostics_open = world.resource::().open; diagnostics::brush_diagnostics_window(world, ctx, &mut brush_diagnostics_open); world.resource_mut::().open = brush_diagnostics_open; let mut rendering_open = world .resource::() .open; crate::rendering_diagnostics::rendering_diagnostics_window(world, ctx, &mut rendering_open); world .resource_mut::() .open = rendering_open; crate::session::session_resume_window(world, ctx); world.resource_scope(|world, mut panel: Mut| { project_settings_window(world, ctx, &mut panel); }); if ctx.input(|input| input.key_pressed(egui::Key::F1)) { world.resource_mut::().shortcuts_open = true; } let requested_selection = world.resource::().0; reconcile_operator_selection( &mut self.selected_entities, selected_before_ui, requested_selection, ); let selected = self.selected_entities.as_slice().first().copied(); world.resource_mut::().0 = selected; } } fn reconcile_operator_selection( selected_entities: &mut SelectedEntities, selected_before_ui: Option, requested_selection: Option, ) { let selected_after_ui = selected_entities.as_slice().first().copied(); if requested_selection == selected_before_ui || selected_after_ui != selected_before_ui { return; } selected_entities.clear(); if let Some(entity) = requested_selection { selected_entities.select_replace(entity); } } fn tab_title(icon: egui_phosphor_icons::Icon, label: &str) -> egui::WidgetText { let mut job = egui::text::LayoutJob::default(); job.append( icon.as_str(), 0.0, egui::TextFormat { font_id: egui::FontId::new(13.0, egui::FontFamily::Name("phosphor-regular".into())), color: theme::TEXT, ..Default::default() }, ); job.append( &format!(" {label}"), 0.0, egui::TextFormat { font_id: egui::FontId::new(13.0, egui::FontFamily::Proportional), color: theme::TEXT, ..Default::default() }, ); job.into() } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum EditorTab { Viewport, /// Legacy saved-layout tab. Loaded layouts are migrated to [`EditorTab::Viewport`]. GameView, Hierarchy, Inspector, Toolbar, AssetBrowser, Build, } pub struct EditorUiPlugin; impl Plugin for EditorUiPlugin { fn build(&self, app: &mut App) { app.insert_resource(UiState::default_layout()) .add_plugins(fonts::EditorFontsPlugin) .init_resource::() .init_resource::() .init_resource::() .init_resource::() .init_resource::() .init_resource::() .init_resource::() .init_resource::() .init_resource::() .init_resource::() .init_resource::() .init_resource::() .add_systems( Startup, init_hierarchy_panel_from_prefs .after(crate::project_io::load_user_preferences_on_startup), ) .add_systems(Startup, validate_editor_component_registry) .add_systems( Startup, apply_ui_state_from_prefs .after(crate::project_io::load_user_preferences_on_startup), ) .add_systems(EguiPrimaryContextPass, show_ui_system) .add_systems( Update, ( hierarchy_state::tick_hierarchy_prefs_save, build::tick_build_job, navigation_inspector::sync_navigation_state, ), ); } } fn validate_editor_component_registry(world: &mut World) { let errors = world .resource::() .validate(world) .err(); if let Some(errors) = errors { panic!( "invalid authoring component registry:\n{}", errors.join("\n") ); } } fn apply_ui_state_from_prefs(mut ui_state: ResMut, prefs: Res) { *ui_state = UiState::from_prefs(&prefs); } fn init_hierarchy_panel_from_prefs(mut commands: Commands, prefs: Res) { commands.insert_resource(HierarchyPanelState::from_prefs(&prefs)); } pub(crate) fn show_ui_system(world: &mut World) { let Ok(egui_context) = world .query_filtered::<&mut EguiContext, With>() .single(world) else { return; }; let mut egui_context = egui_context.clone(); let dt = world .get_resource::