Fix scoped editor UI action state
This commit is contained in:
parent
4b33f32357
commit
9e23ae731f
@ -78,7 +78,12 @@ impl EditorCommandRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom inspector body for level objects (game crates register via [`register_actor_inspector_section`]).
|
||||
/// Custom inspector body for level objects (game crates register via
|
||||
/// [`register_actor_inspector_section`]). Synchronous [`Self::ui`] callbacks run while
|
||||
/// [`UiState`] is scoped out of `World`; implementations must not request that resource. Keep
|
||||
/// extension-owned transient state in independently registered resources, use
|
||||
/// [`crate::ui::request_ui_selection`] and [`crate::ui::request_editor_tab`] for host UI changes,
|
||||
/// or defer other cross-panel work through extension-owned commands or events.
|
||||
pub trait ActorInspectorSection: Send + Sync {
|
||||
fn id(&self) -> &str;
|
||||
fn title(&self) -> &str;
|
||||
|
||||
@ -19,7 +19,6 @@ use shared::{
|
||||
|
||||
use crate::operators::{ActiveOperator, OperatorPhase, OperatorStatus};
|
||||
use crate::scene_io::{SceneIo, SceneIoRequest};
|
||||
use crate::selection::SelectedEntity;
|
||||
use crate::state::scene_tools_active;
|
||||
use crate::ui::hierarchy_ops::{
|
||||
apply_sibling_change, apply_sibling_change_new, hierarchy_drop_violation, next_sibling_index,
|
||||
@ -2872,32 +2871,15 @@ fn despawn_entity(world: &mut World, entity: Option<Entity>) {
|
||||
}
|
||||
|
||||
fn select_one(world: &mut World, entity: Entity) {
|
||||
if let Some(mut ui_state) = world.get_resource_mut::<UiState>() {
|
||||
ui_state.selected_entities.clear();
|
||||
ui_state.selected_entities.select_replace(entity);
|
||||
}
|
||||
world.resource_mut::<SelectedEntity>().0 = Some(entity);
|
||||
crate::ui::request_ui_selection(world, &[entity]);
|
||||
}
|
||||
|
||||
fn select_many(world: &mut World, entities: &[Entity]) {
|
||||
if let Some(mut ui_state) = world.get_resource_mut::<UiState>() {
|
||||
ui_state.selected_entities.clear();
|
||||
for (index, entity) in entities.iter().enumerate() {
|
||||
if index == 0 {
|
||||
ui_state.selected_entities.select_replace(*entity);
|
||||
} else {
|
||||
ui_state.selected_entities.select_maybe_add(*entity, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
world.resource_mut::<SelectedEntity>().0 = entities.first().copied();
|
||||
crate::ui::request_ui_selection(world, entities);
|
||||
}
|
||||
|
||||
pub fn clear_selection(world: &mut World) {
|
||||
if let Some(mut ui_state) = world.get_resource_mut::<UiState>() {
|
||||
ui_state.selected_entities.clear();
|
||||
}
|
||||
world.resource_mut::<SelectedEntity>().0 = None;
|
||||
crate::ui::request_ui_selection(world, &[]);
|
||||
}
|
||||
|
||||
pub fn is_level_object(world: &World, entity: Entity) -> bool {
|
||||
@ -2939,6 +2921,28 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::operators::test_harness::{assert_undo_redo_round_trip, OperatorInvariantHarness};
|
||||
use crate::operators::{ActiveOperator, OperatorPhase};
|
||||
use crate::selection::SelectedEntity;
|
||||
use crate::ui::PendingUiSelection;
|
||||
|
||||
#[test]
|
||||
fn history_select_many_queues_exact_selection_while_ui_state_is_scoped_out() {
|
||||
let first = Entity::from_bits(1);
|
||||
let second = Entity::from_bits(2);
|
||||
let mut world = World::new();
|
||||
world.init_resource::<SelectedEntity>();
|
||||
world.init_resource::<PendingUiSelection>();
|
||||
world.insert_resource(UiState::default_layout());
|
||||
|
||||
world.resource_scope::<UiState, _>(|world, _ui_state| {
|
||||
assert!(!world.contains_resource::<UiState>());
|
||||
select_many(world, &[first, second]);
|
||||
assert_eq!(
|
||||
world.resource::<PendingUiSelection>().0.as_deref(),
|
||||
Some([first, second].as_slice())
|
||||
);
|
||||
assert_eq!(world.resource::<SelectedEntity>().0, Some(first));
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Component, Reflect, Default, Debug, Clone, PartialEq)]
|
||||
#[reflect(Component, Default)]
|
||||
|
||||
@ -47,6 +47,11 @@ pub struct EditorComponentRegistry {
|
||||
inspectors: HashMap<&'static str, ComponentInspectorFn>,
|
||||
}
|
||||
|
||||
/// Synchronous inspector callbacks run while [`crate::ui::UiState`] is scoped out of `World`.
|
||||
/// They must not request that resource. Keep extension-owned transient state in independently
|
||||
/// registered resources, use [`crate::ui::request_ui_selection`] and
|
||||
/// [`crate::ui::request_editor_tab`] for host UI changes, or defer other cross-panel work through
|
||||
/// extension-owned commands or events.
|
||||
pub type ComponentInspectorFn = fn(&mut World, &mut egui::Ui, Entity);
|
||||
|
||||
impl EditorComponentRegistry {
|
||||
|
||||
@ -13,8 +13,6 @@ use crate::history::set_brush_with_history;
|
||||
use crate::history::EditorHistory;
|
||||
use crate::project_io::ProjectWorkspace;
|
||||
use crate::scene_io::{SceneIo, SceneIoEventSeverity};
|
||||
use crate::selection::SelectedEntity;
|
||||
use crate::ui::UiState;
|
||||
use crate::viewport::collider_diagnostics::{
|
||||
ColliderDiagnosticSeverity, ColliderDiagnosticsState, ColliderOverlayStatus,
|
||||
};
|
||||
@ -123,11 +121,7 @@ pub fn diagnostics_ui(world: &mut World, ui: &mut egui::Ui) {
|
||||
.iter(world)
|
||||
.find_map(|(entity, id)| (id.0 == actor_id).then_some(entity))
|
||||
{
|
||||
world.resource_mut::<SelectedEntity>().0 = Some(entity);
|
||||
world
|
||||
.resource_mut::<UiState>()
|
||||
.selected_entities
|
||||
.select_replace(entity);
|
||||
request_diagnostic_selection(world, entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -250,11 +244,7 @@ fn collider_diagnostics_ui(world: &mut World, ui: &mut egui::Ui) {
|
||||
ui.colored_label(color, row.shape_label);
|
||||
ui.label(&row.actor_name);
|
||||
if ui.small_button("Select").clicked() {
|
||||
world.resource_mut::<SelectedEntity>().0 = Some(row.entity);
|
||||
world
|
||||
.resource_mut::<UiState>()
|
||||
.selected_entities
|
||||
.select_replace(row.entity);
|
||||
request_diagnostic_selection(world, row.entity);
|
||||
}
|
||||
});
|
||||
for diagnostic in &row.diagnostics {
|
||||
@ -304,11 +294,7 @@ fn brush_diagnostics_ui(world: &mut World, ui: &mut egui::Ui) {
|
||||
ui.colored_label(color, row.status_label());
|
||||
ui.label(row.name.as_str());
|
||||
if ui.button("Select").clicked() {
|
||||
world.resource_mut::<SelectedEntity>().0 = Some(row.entity);
|
||||
world
|
||||
.resource_mut::<UiState>()
|
||||
.selected_entities
|
||||
.select_replace(row.entity);
|
||||
request_diagnostic_selection(world, row.entity);
|
||||
}
|
||||
if ui
|
||||
.add_enabled(row.error_count > 0, egui::Button::new("Reset Cube"))
|
||||
@ -323,6 +309,10 @@ fn brush_diagnostics_ui(world: &mut World, ui: &mut egui::Ui) {
|
||||
}
|
||||
}
|
||||
|
||||
fn request_diagnostic_selection(world: &mut World, entity: Entity) {
|
||||
crate::ui::request_ui_selection(world, &[entity]);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BrushDiagnosticRow {
|
||||
entity: Entity,
|
||||
@ -397,3 +387,37 @@ pub fn diagnostics_window(world: &mut World, ctx: &egui::Context, open: &mut boo
|
||||
diagnostics_ui(world, ui);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::selection::SelectedEntity;
|
||||
use crate::ui::{PendingUiSelection, UiState};
|
||||
|
||||
#[test]
|
||||
fn diagnostic_selection_reconciles_while_ui_state_is_scoped_out() {
|
||||
let before = Entity::from_bits(1);
|
||||
let requested = Entity::from_bits(2);
|
||||
let mut world = World::new();
|
||||
world.init_resource::<SelectedEntity>();
|
||||
world.init_resource::<PendingUiSelection>();
|
||||
world.resource_mut::<SelectedEntity>().0 = Some(before);
|
||||
|
||||
let mut ui_state = UiState::default_layout();
|
||||
ui_state.selected_entities.select_replace(before);
|
||||
world.insert_resource(ui_state);
|
||||
|
||||
world.resource_scope::<UiState, _>(|world, mut ui_state| {
|
||||
assert!(!world.contains_resource::<UiState>());
|
||||
request_diagnostic_selection(world, requested);
|
||||
let pending = world.resource_mut::<PendingUiSelection>().0.take();
|
||||
super::super::reconcile_operator_selection(
|
||||
&mut ui_state.selected_entities,
|
||||
&[before],
|
||||
world.resource::<SelectedEntity>().0,
|
||||
pending.as_deref(),
|
||||
);
|
||||
assert_eq!(ui_state.selected_entities.as_slice(), &[requested]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,7 +37,6 @@ use crate::history::{
|
||||
set_primitive_with_history, set_rigid_body_with_history, set_static_mesh_renderer_with_history,
|
||||
EditorEntitySnapshot,
|
||||
};
|
||||
use crate::selection::SelectedEntity;
|
||||
use crate::ui::theme::{
|
||||
panel_heading, BORDER, ELEVATED_BG, PANEL_BG_DARK, SELECTION_BG_MUTED, TEXT_DIM, TEXT_MUTED,
|
||||
WIDGET_BG,
|
||||
@ -51,9 +50,8 @@ use crate::viewport::collider_diagnostics::{
|
||||
use super::component_registry::{
|
||||
EditorComponentCategory, EditorComponentDescriptor, EditorComponentRegistry,
|
||||
};
|
||||
use super::dock_tabs::open_and_focus_tab;
|
||||
use super::helpers::create_scene_sun_override_from_project_settings;
|
||||
use super::{EditorTab, UiState};
|
||||
use super::EditorTab;
|
||||
use crate::assets::asset_db::{find_asset_by_path, AssetRegistry};
|
||||
use crate::assets::static_mesh::{
|
||||
load_static_mesh_manifest, material_id_from_label, part_id_from_label,
|
||||
@ -135,6 +133,7 @@ pub(crate) struct InspectorPanelState {
|
||||
add_component_scroll_selected: bool,
|
||||
add_component_selected_index: usize,
|
||||
add_component_target: Option<Entity>,
|
||||
collapsed_components: HashSet<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@ -276,13 +275,9 @@ pub(crate) fn component_card_context(
|
||||
state_key,
|
||||
);
|
||||
let collapsed = world
|
||||
.get_resource::<UiState>()
|
||||
.map(|state| {
|
||||
state
|
||||
.inspector_collapsed_components
|
||||
.contains(&component_card_key(world, entity, options.type_name))
|
||||
})
|
||||
.unwrap_or(false);
|
||||
.resource::<InspectorPanelState>()
|
||||
.collapsed_components
|
||||
.contains(&component_card_key(world, entity, options.type_name));
|
||||
let pasteable = options.copyable
|
||||
&& world
|
||||
.get_resource::<InspectorClipboard>()
|
||||
@ -326,12 +321,11 @@ fn apply_component_card_response_for_type(
|
||||
) {
|
||||
if let Some(collapsed) = response.collapsed {
|
||||
let key = component_card_key(world, entity, type_name);
|
||||
if let Some(mut ui_state) = world.get_resource_mut::<UiState>() {
|
||||
if collapsed {
|
||||
ui_state.inspector_collapsed_components.insert(key);
|
||||
} else {
|
||||
ui_state.inspector_collapsed_components.remove(&key);
|
||||
}
|
||||
let mut panel_state = world.resource_mut::<InspectorPanelState>();
|
||||
if collapsed {
|
||||
panel_state.collapsed_components.insert(key);
|
||||
} else {
|
||||
panel_state.collapsed_components.remove(&key);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2362,18 +2356,15 @@ fn locate_asset_ref(
|
||||
return;
|
||||
};
|
||||
|
||||
reveal_asset_in_browser(world, &candidate.folder_path, &candidate.selection);
|
||||
}
|
||||
|
||||
fn reveal_asset_in_browser(world: &mut World, folder_path: &str, selection: &AssetSelection) {
|
||||
if let Some(mut assets) = world.get_resource_mut::<EditorAssets>() {
|
||||
assets.current_folder = candidate.folder_path.clone();
|
||||
assets.select(candidate.selection.clone());
|
||||
}
|
||||
if let Some(mut ui_state) = world.get_resource_mut::<UiState>() {
|
||||
let panel_nodes = ui_state.panel_nodes;
|
||||
open_and_focus_tab(
|
||||
&mut ui_state.dock_state,
|
||||
EditorTab::AssetBrowser,
|
||||
&panel_nodes,
|
||||
);
|
||||
assets.current_folder = folder_path.to_string();
|
||||
assets.select(selection.clone());
|
||||
}
|
||||
super::request_editor_tab(world, EditorTab::AssetBrowser);
|
||||
}
|
||||
|
||||
fn skinned_mesh_renderer_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
|
||||
@ -4625,10 +4616,7 @@ pub fn project_sun_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
|
||||
|
||||
if create_override {
|
||||
let sun = create_scene_sun_override_from_project_settings(world);
|
||||
world.resource_mut::<SelectedEntity>().0 = Some(sun);
|
||||
if let Some(mut ui_state) = world.get_resource_mut::<crate::ui::UiState>() {
|
||||
ui_state.selected_entities.select_replace(sun);
|
||||
}
|
||||
crate::ui::request_ui_selection(world, &[sun]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4881,18 +4869,7 @@ fn locate_texture_asset(
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(mut assets) = world.get_resource_mut::<EditorAssets>() {
|
||||
assets.current_folder = candidate.folder_path.clone();
|
||||
assets.select(candidate.selection.clone());
|
||||
}
|
||||
if let Some(mut ui_state) = world.get_resource_mut::<UiState>() {
|
||||
let panel_nodes = ui_state.panel_nodes;
|
||||
open_and_focus_tab(
|
||||
&mut ui_state.dock_state,
|
||||
EditorTab::AssetBrowser,
|
||||
&panel_nodes,
|
||||
);
|
||||
}
|
||||
reveal_asset_in_browser(world, &candidate.folder_path, &candidate.selection);
|
||||
}
|
||||
|
||||
fn option_string_ui(ui: &mut egui::Ui, label: &str, value: &mut Option<String>) -> bool {
|
||||
@ -4924,6 +4901,80 @@ fn option_string_ui(ui: &mut egui::Ui, label: &str, value: &mut Option<String>)
|
||||
mod collider_inspector_tests {
|
||||
use super::*;
|
||||
use crate::history::{apply_command_undo, EditorHistory};
|
||||
use crate::ui::{DockTabRequest, UiState};
|
||||
|
||||
#[test]
|
||||
fn component_collapse_persists_while_ui_state_is_scoped_out() {
|
||||
let mut world = World::new();
|
||||
world.init_resource::<EditorComponentRegistry>();
|
||||
world.init_resource::<InspectorPanelState>();
|
||||
let entity = world
|
||||
.spawn((ActorId::new("collapsed-component"), Transform::IDENTITY))
|
||||
.id();
|
||||
world.insert_resource(UiState::default_layout());
|
||||
|
||||
world.resource_scope::<UiState, _>(|world, _ui_state| {
|
||||
assert!(!world.contains_resource::<UiState>());
|
||||
apply_component_card_response(
|
||||
world,
|
||||
entity,
|
||||
ComponentCardResponse {
|
||||
type_name: COMPONENT_TRANSFORM,
|
||||
collapsed: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let context = component_card_context(
|
||||
world,
|
||||
entity,
|
||||
ComponentCardOptions::fixed(COMPONENT_TRANSFORM, "Transform", icons::CUBE),
|
||||
);
|
||||
assert!(context.collapsed);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn locate_texture_requests_asset_browser_while_ui_state_is_scoped_out() {
|
||||
let path = "assets/textures/scoped-locate.png";
|
||||
let folder = "assets/textures";
|
||||
let selection = AssetSelection::File(path.to_string());
|
||||
let mut world = World::new();
|
||||
world.insert_resource(EditorAssets {
|
||||
folders: Vec::new(),
|
||||
assets: vec![EditorAsset {
|
||||
label: "Scoped Locate".into(),
|
||||
path: Some(path.into()),
|
||||
folder_path: folder.into(),
|
||||
kind: EditorAssetKind::Texture,
|
||||
}],
|
||||
current_folder: crate::assets::ASSETS_ROOT.into(),
|
||||
selected: None,
|
||||
dragging: None,
|
||||
status: String::new(),
|
||||
});
|
||||
world.init_resource::<DockTabRequest>();
|
||||
world.insert_resource(UiState::default_layout());
|
||||
let candidate = TextureAssetCandidate {
|
||||
label: "Scoped Locate".into(),
|
||||
path: path.into(),
|
||||
folder_path: folder.into(),
|
||||
selection: selection.clone(),
|
||||
texture_id: None,
|
||||
};
|
||||
|
||||
world.resource_scope::<UiState, _>(|world, _ui_state| {
|
||||
assert!(!world.contains_resource::<UiState>());
|
||||
locate_texture_asset(world, Some(path), &[candidate]);
|
||||
});
|
||||
|
||||
let assets = world.resource::<EditorAssets>();
|
||||
assert_eq!(assets.current_folder, folder);
|
||||
assert_eq!(assets.selected.as_ref(), Some(&selection));
|
||||
assert_eq!(
|
||||
world.resource::<DockTabRequest>().0,
|
||||
Some(EditorTab::AssetBrowser)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_switch_preserves_dimensions_and_is_one_undoable_edit() {
|
||||
|
||||
@ -40,6 +40,7 @@ pub fn top_menu_bar(
|
||||
selected: &SelectedEntities,
|
||||
dock_state: &mut DockState<EditorTab>,
|
||||
panel_nodes: &mut PanelNodes,
|
||||
viewport_rect: egui::Rect,
|
||||
) {
|
||||
egui::Panel::top("editor_menu_bar").show_inside(root_ui, |ui| {
|
||||
egui::MenuBar::new().ui(ui, |ui| {
|
||||
@ -209,7 +210,7 @@ pub fn top_menu_bar(
|
||||
("Create Link", NavigationActorType::Link),
|
||||
] {
|
||||
if menu_item(ui, label, None, true).clicked() {
|
||||
spawn_navigation_actor(world, actor_type);
|
||||
spawn_navigation_actor(world, actor_type, viewport_rect);
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,8 +26,6 @@ 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};
|
||||
@ -71,7 +69,6 @@ pub struct UiState {
|
||||
pub selected_entities: SelectedEntities,
|
||||
pub renaming_entity: Option<Entity>,
|
||||
pub rename_buffer: String,
|
||||
pub(crate) inspector_collapsed_components: HashSet<String>,
|
||||
pub(crate) panel_nodes: PanelNodes,
|
||||
last_mode_tab: Option<EditorMode>,
|
||||
}
|
||||
@ -79,6 +76,56 @@ pub struct UiState {
|
||||
#[derive(Resource, Default)]
|
||||
struct DockTabRequest(Option<EditorTab>);
|
||||
|
||||
#[derive(Resource, Default, Debug)]
|
||||
pub(crate) struct PendingUiSelection(pub(crate) Option<Vec<Entity>>);
|
||||
|
||||
fn replace_selected_entities(selected: &mut SelectedEntities, entities: &[Entity]) {
|
||||
selected.clear();
|
||||
for (index, entity) in entities.iter().enumerate() {
|
||||
if index == 0 {
|
||||
selected.select_replace(*entity);
|
||||
} else {
|
||||
selected.select_maybe_add(*entity, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Requests an editor selection change from synchronous UI or extension callbacks.
|
||||
///
|
||||
/// Returns `false` when the editor selection resources are not installed. A normally configured
|
||||
/// editor provides them through [`crate::EditorPluginGroup`].
|
||||
pub fn request_ui_selection(world: &mut World, entities: &[Entity]) -> bool {
|
||||
{
|
||||
let Some(mut selected) = world.get_resource_mut::<SelectedEntity>() else {
|
||||
return false;
|
||||
};
|
||||
selected.0 = entities.first().copied();
|
||||
}
|
||||
if world.contains_resource::<UiState>() {
|
||||
{
|
||||
let mut ui_state = world.resource_mut::<UiState>();
|
||||
replace_selected_entities(&mut ui_state.selected_entities, entities);
|
||||
}
|
||||
if let Some(mut pending) = world.get_resource_mut::<PendingUiSelection>() {
|
||||
pending.0 = None;
|
||||
}
|
||||
} else if let Some(mut pending) = world.get_resource_mut::<PendingUiSelection>() {
|
||||
pending.0 = Some(entities.to_vec());
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Requests that an editor tab be opened and focused after the current synchronous UI pass.
|
||||
///
|
||||
/// Returns `false` when [`EditorUiPlugin`] is not installed.
|
||||
pub fn request_editor_tab(world: &mut World, tab: EditorTab) -> bool {
|
||||
let Some(mut request) = world.get_resource_mut::<DockTabRequest>() else {
|
||||
return false;
|
||||
};
|
||||
request.0 = Some(tab);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn egui_captures_keyboard(ctx: &egui::Context) -> bool {
|
||||
ctx.egui_wants_keyboard_input()
|
||||
}
|
||||
@ -126,7 +173,6 @@ impl UiState {
|
||||
selected_entities: SelectedEntities::default(),
|
||||
renaming_entity: None,
|
||||
rename_buffer: String::new(),
|
||||
inspector_collapsed_components: HashSet::new(),
|
||||
panel_nodes,
|
||||
last_mode_tab: None,
|
||||
}
|
||||
@ -138,7 +184,7 @@ impl UiState {
|
||||
)]
|
||||
fn ui(&mut self, world: &mut World, ctx: &mut egui::Context, dt: f32) {
|
||||
apply_editor_theme(ctx);
|
||||
let selected_before_ui = world.resource::<SelectedEntity>().0;
|
||||
let selected_before_ui: Vec<Entity> = self.selected_entities.iter().collect();
|
||||
|
||||
let mode = *world.resource::<State<EditorMode>>().get();
|
||||
if self.last_mode_tab != Some(mode) {
|
||||
@ -169,9 +215,15 @@ impl UiState {
|
||||
&self.selected_entities,
|
||||
&mut self.dock_state,
|
||||
&mut self.panel_nodes,
|
||||
self.viewport_rect,
|
||||
);
|
||||
|
||||
editor_toolbar_panel(world, root_ui, &mut self.selected_entities);
|
||||
editor_toolbar_panel(
|
||||
world,
|
||||
root_ui,
|
||||
&mut self.selected_entities,
|
||||
self.viewport_rect,
|
||||
);
|
||||
status_bar_ui(world, root_ui, &self.selected_entities, mode);
|
||||
|
||||
let mut viewer = TabViewer {
|
||||
@ -233,11 +285,13 @@ impl UiState {
|
||||
world.resource_mut::<ViewportUiState>().shortcuts_open = true;
|
||||
}
|
||||
|
||||
let pending_selection = world.resource_mut::<PendingUiSelection>().0.take();
|
||||
let requested_selection = world.resource::<SelectedEntity>().0;
|
||||
reconcile_operator_selection(
|
||||
&mut self.selected_entities,
|
||||
selected_before_ui,
|
||||
&selected_before_ui,
|
||||
requested_selection,
|
||||
pending_selection.as_deref(),
|
||||
);
|
||||
let selected = self.selected_entities.as_slice().first().copied();
|
||||
world.resource_mut::<SelectedEntity>().0 = selected;
|
||||
@ -246,16 +300,24 @@ impl UiState {
|
||||
|
||||
fn reconcile_operator_selection(
|
||||
selected_entities: &mut SelectedEntities,
|
||||
selected_before_ui: Option<Entity>,
|
||||
selected_before_ui: &[Entity],
|
||||
requested_selection: Option<Entity>,
|
||||
pending_selection: Option<&[Entity]>,
|
||||
) {
|
||||
let selected_after_ui = selected_entities.as_slice().first().copied();
|
||||
if requested_selection == selected_before_ui || selected_after_ui != selected_before_ui {
|
||||
if selected_entities.as_slice() != selected_before_ui {
|
||||
return;
|
||||
}
|
||||
if let Some(entities) = pending_selection {
|
||||
replace_selected_entities(selected_entities, entities);
|
||||
return;
|
||||
}
|
||||
if requested_selection == selected_before_ui.first().copied() {
|
||||
return;
|
||||
}
|
||||
selected_entities.clear();
|
||||
if let Some(entity) = requested_selection {
|
||||
selected_entities.select_replace(entity);
|
||||
replace_selected_entities(selected_entities, &[entity]);
|
||||
} else {
|
||||
selected_entities.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@ -310,6 +372,7 @@ impl Plugin for EditorUiPlugin {
|
||||
.init_resource::<material_library::MaterialLibraryState>()
|
||||
.init_resource::<material_library::MaterialLibraryCatalog>()
|
||||
.init_resource::<DockTabRequest>()
|
||||
.init_resource::<PendingUiSelection>()
|
||||
.init_resource::<inspector::InspectorClipboard>()
|
||||
.init_resource::<inspector::InspectorPanelState>()
|
||||
.init_resource::<animation_inspector::AnimationInspectorState>()
|
||||
@ -476,7 +539,7 @@ mod tests {
|
||||
let mut selected = SelectedEntities::default();
|
||||
selected.select_replace(before);
|
||||
|
||||
reconcile_operator_selection(&mut selected, Some(before), Some(spawned));
|
||||
reconcile_operator_selection(&mut selected, &[before], Some(spawned), None);
|
||||
|
||||
assert_eq!(selected.as_slice(), &[spawned]);
|
||||
}
|
||||
@ -489,8 +552,62 @@ mod tests {
|
||||
let mut selected = SelectedEntities::default();
|
||||
selected.select_replace(clicked);
|
||||
|
||||
reconcile_operator_selection(&mut selected, Some(before), Some(requested));
|
||||
reconcile_operator_selection(
|
||||
&mut selected,
|
||||
&[before],
|
||||
Some(requested),
|
||||
Some(&[requested, Entity::from_bits(4)]),
|
||||
);
|
||||
|
||||
assert_eq!(selected.as_slice(), &[clicked]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_ui_secondary_selection_change_beats_pending_operator_selection() {
|
||||
let a = Entity::from_bits(1);
|
||||
let b = Entity::from_bits(2);
|
||||
let c = Entity::from_bits(3);
|
||||
let d = Entity::from_bits(4);
|
||||
let e = Entity::from_bits(5);
|
||||
let mut selected = SelectedEntities::default();
|
||||
selected.select_replace(a);
|
||||
selected.select_maybe_add(c, true);
|
||||
|
||||
reconcile_operator_selection(&mut selected, &[a, b], Some(d), Some(&[d, e]));
|
||||
|
||||
assert_eq!(selected.as_slice(), &[a, c]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn menu_scope_multi_selection_request_preserves_all_entities() {
|
||||
let before = Entity::from_bits(1);
|
||||
let first_duplicate = Entity::from_bits(2);
|
||||
let second_duplicate = Entity::from_bits(3);
|
||||
let mut world = World::new();
|
||||
world.init_resource::<SelectedEntity>();
|
||||
world.init_resource::<PendingUiSelection>();
|
||||
world.resource_mut::<SelectedEntity>().0 = Some(before);
|
||||
|
||||
let mut ui_state = UiState::default_layout();
|
||||
ui_state.selected_entities.select_replace(before);
|
||||
world.insert_resource(ui_state);
|
||||
|
||||
world.resource_scope::<UiState, _>(|world, mut ui_state| {
|
||||
assert!(!world.contains_resource::<UiState>());
|
||||
request_ui_selection(world, &[first_duplicate, second_duplicate]);
|
||||
|
||||
let pending = world.resource_mut::<PendingUiSelection>().0.take();
|
||||
reconcile_operator_selection(
|
||||
&mut ui_state.selected_entities,
|
||||
&[before],
|
||||
world.resource::<SelectedEntity>().0,
|
||||
pending.as_deref(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ui_state.selected_entities.as_slice(),
|
||||
&[first_duplicate, second_duplicate]
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -858,10 +858,16 @@ pub enum NavigationActorType {
|
||||
Link,
|
||||
}
|
||||
|
||||
pub fn spawn_navigation_actor(world: &mut World, actor_type: NavigationActorType) -> Entity {
|
||||
let rect = world.resource::<crate::ui::UiState>().viewport_rect;
|
||||
pub fn spawn_navigation_actor(
|
||||
world: &mut World,
|
||||
actor_type: NavigationActorType,
|
||||
viewport_rect: egui::Rect,
|
||||
) -> Entity {
|
||||
let settings = world.resource::<ViewportSettings>().clone();
|
||||
let translation = snap_translation(editor_spawn_ground_position(world, rect), &settings);
|
||||
let translation = snap_translation(
|
||||
editor_spawn_ground_position(world, viewport_rect),
|
||||
&settings,
|
||||
);
|
||||
let (name, bounds, obstacle, area, link) = match actor_type {
|
||||
NavigationActorType::Bounds => (
|
||||
"Navigation Bounds",
|
||||
@ -1019,8 +1025,49 @@ pub fn sync_navigation_state(world: &mut World) {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::history::EditorHistory;
|
||||
use crate::scene_io::SceneIo;
|
||||
use crate::selection::SelectedEntity;
|
||||
use crate::ui::{PendingUiSelection, UiState};
|
||||
use scene::navigation::NavigationGeometryInput;
|
||||
use shared::{ActorKind, LevelObject};
|
||||
|
||||
#[test]
|
||||
fn navigation_spawn_works_while_ui_state_is_scoped_out_of_world() {
|
||||
let mut world = World::new();
|
||||
world.init_resource::<EditorHistory>();
|
||||
world.init_resource::<SceneIo>();
|
||||
world.init_resource::<SelectedEntity>();
|
||||
world.init_resource::<PendingUiSelection>();
|
||||
world.init_resource::<ViewportSettings>();
|
||||
|
||||
let mut ui_state = UiState::default_layout();
|
||||
ui_state.viewport_rect =
|
||||
egui::Rect::from_min_size(egui::pos2(100.0, 80.0), egui::vec2(1280.0, 720.0));
|
||||
world.insert_resource(ui_state);
|
||||
|
||||
let entity = world.resource_scope::<UiState, _>(|world, mut ui_state| {
|
||||
assert!(!world.contains_resource::<UiState>());
|
||||
let entity =
|
||||
spawn_navigation_actor(world, NavigationActorType::Bounds, ui_state.viewport_rect);
|
||||
let pending = world.resource_mut::<PendingUiSelection>().0.take();
|
||||
super::super::reconcile_operator_selection(
|
||||
&mut ui_state.selected_entities,
|
||||
&[],
|
||||
world.resource::<SelectedEntity>().0,
|
||||
pending.as_deref(),
|
||||
);
|
||||
assert_eq!(ui_state.selected_entities.as_slice(), &[entity]);
|
||||
entity
|
||||
});
|
||||
|
||||
assert!(world.contains_resource::<UiState>());
|
||||
assert!(world.get::<LevelObject>(entity).is_some());
|
||||
assert!(world.get::<NavigationBounds>(entity).is_some());
|
||||
assert_eq!(world.get::<ActorKind>(entity), Some(&ActorKind::Navigation));
|
||||
assert_eq!(world.resource::<SelectedEntity>().0, Some(entity));
|
||||
assert_eq!(world.resource::<EditorHistory>().undo_depth(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn world_affine_uses_current_authored_parent_chain() {
|
||||
|
||||
@ -8,7 +8,6 @@ use egui_phosphor_icons::icons;
|
||||
use crate::history::spawn_with_history;
|
||||
use crate::scene_io::{SceneIo, SceneIoRequest};
|
||||
use crate::state::{EditorMode, PlayPaused, PlayPossession};
|
||||
use crate::ui::UiState;
|
||||
use crate::viewport::brush_tool::start_draw_brush_tool;
|
||||
use crate::viewport::{editor_spawn_ground_position, snap_translation, ViewportSettings};
|
||||
use shared::PrimitiveShape;
|
||||
@ -30,15 +29,21 @@ pub fn editor_toolbar_panel(
|
||||
world: &mut World,
|
||||
root_ui: &mut egui::Ui,
|
||||
selected: &mut SelectedEntities,
|
||||
viewport_rect: egui::Rect,
|
||||
) {
|
||||
egui::Panel::top("editor_toolbar")
|
||||
.exact_size(TOOLBAR_HEIGHT)
|
||||
.show_inside(root_ui, |ui| {
|
||||
toolbar_ui(world, ui, selected);
|
||||
toolbar_ui(world, ui, selected, viewport_rect);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn toolbar_ui(world: &mut World, ui: &mut egui::Ui, selected: &mut SelectedEntities) {
|
||||
pub fn toolbar_ui(
|
||||
world: &mut World,
|
||||
ui: &mut egui::Ui,
|
||||
selected: &mut SelectedEntities,
|
||||
viewport_rect: egui::Rect,
|
||||
) {
|
||||
let (toolbar_rect, _) = ui.allocate_exact_size(
|
||||
egui::vec2(ui.available_width(), TOOLBAR_HEIGHT - 2.0),
|
||||
egui::Sense::hover(),
|
||||
@ -65,7 +70,7 @@ pub fn toolbar_ui(world: &mut World, ui: &mut egui::Ui, selected: &mut SelectedE
|
||||
.layout(egui::Layout::left_to_right(egui::Align::Center)),
|
||||
);
|
||||
left_ui.spacing_mut().item_spacing.x = 6.0;
|
||||
left_toolbar(world, &mut left_ui);
|
||||
left_toolbar(world, &mut left_ui, viewport_rect);
|
||||
|
||||
let mut transport_ui = ui.new_child(
|
||||
egui::UiBuilder::new()
|
||||
@ -100,7 +105,7 @@ pub fn toolbar_ui(world: &mut World, ui: &mut egui::Ui, selected: &mut SelectedE
|
||||
);
|
||||
}
|
||||
|
||||
fn left_toolbar(world: &mut World, ui: &mut egui::Ui) {
|
||||
fn left_toolbar(world: &mut World, ui: &mut egui::Ui, viewport_rect: egui::Rect) {
|
||||
brand_mark_frame().show(ui, |ui| {
|
||||
ui.set_min_width(118.0);
|
||||
ui.set_min_height(30.0);
|
||||
@ -126,16 +131,16 @@ fn left_toolbar(world: &mut World, ui: &mut egui::Ui) {
|
||||
toolbar_group_frame().show(ui, |ui| {
|
||||
panel_toolbar_row(ui, |ui| {
|
||||
if icon_button(ui, icons::CUBE, "Spawn cube").clicked() {
|
||||
spawn_primitive_at_camera(world, PrimitiveShape::Box, "Cube");
|
||||
spawn_primitive_at_camera(world, PrimitiveShape::Box, "Cube", viewport_rect);
|
||||
}
|
||||
if icon_button(ui, icons::MOUNTAINS, "Spawn ramp").clicked() {
|
||||
spawn_primitive_at_camera(world, PrimitiveShape::Ramp, "Ramp");
|
||||
spawn_primitive_at_camera(world, PrimitiveShape::Ramp, "Ramp", viewport_rect);
|
||||
}
|
||||
if icon_button(ui, icons::SPHERE, "Spawn sphere").clicked() {
|
||||
spawn_primitive_at_camera(world, PrimitiveShape::Sphere, "Sphere");
|
||||
spawn_primitive_at_camera(world, PrimitiveShape::Sphere, "Sphere", viewport_rect);
|
||||
}
|
||||
if icon_button(ui, icons::LIGHTBULB, "Spawn point light").clicked() {
|
||||
let pos = camera_ground_spawn(world);
|
||||
let pos = camera_ground_spawn(world, viewport_rect);
|
||||
spawn_with_history(world, light_snapshot("Point Light", pos));
|
||||
}
|
||||
if icon_button(ui, icons::PENCIL_SIMPLE_LINE, "Draw brush (B)").clicked() {
|
||||
@ -157,6 +162,7 @@ fn left_toolbar(world: &mut World, ui: &mut egui::Ui) {
|
||||
super::navigation_inspector::spawn_navigation_actor(
|
||||
world,
|
||||
super::navigation_inspector::NavigationActorType::Bounds,
|
||||
viewport_rect,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -232,14 +238,68 @@ fn transport_controls(world: &mut World, ui: &mut egui::Ui) {
|
||||
}
|
||||
}
|
||||
|
||||
fn camera_ground_spawn(world: &mut World) -> Vec3 {
|
||||
let rect = world.resource::<UiState>().viewport_rect;
|
||||
fn camera_ground_spawn(world: &mut World, viewport_rect: egui::Rect) -> Vec3 {
|
||||
let settings = world.resource::<ViewportSettings>().clone();
|
||||
let pos = editor_spawn_ground_position(world, rect);
|
||||
let pos = editor_spawn_ground_position(world, viewport_rect);
|
||||
snap_translation(pos, &settings)
|
||||
}
|
||||
|
||||
fn spawn_primitive_at_camera(world: &mut World, shape: PrimitiveShape, name: &str) {
|
||||
let pos = camera_ground_spawn(world);
|
||||
spawn_with_history(world, primitive_snapshot(name, shape, pos));
|
||||
fn spawn_primitive_at_camera(
|
||||
world: &mut World,
|
||||
shape: PrimitiveShape,
|
||||
name: &str,
|
||||
viewport_rect: egui::Rect,
|
||||
) -> Entity {
|
||||
let pos = camera_ground_spawn(world, viewport_rect);
|
||||
spawn_with_history(world, primitive_snapshot(name, shape, pos))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::history::EditorHistory;
|
||||
use crate::scene_io::SceneIo;
|
||||
use crate::selection::SelectedEntity;
|
||||
use crate::ui::{PendingUiSelection, UiState};
|
||||
use shared::{LevelObject, Primitive};
|
||||
|
||||
#[test]
|
||||
fn primitive_spawn_works_while_ui_state_is_scoped_out_of_world() {
|
||||
let mut world = World::new();
|
||||
world.init_resource::<EditorHistory>();
|
||||
world.init_resource::<SceneIo>();
|
||||
world.init_resource::<SelectedEntity>();
|
||||
world.init_resource::<PendingUiSelection>();
|
||||
world.init_resource::<ViewportSettings>();
|
||||
|
||||
let mut ui_state = UiState::default_layout();
|
||||
ui_state.viewport_rect =
|
||||
egui::Rect::from_min_size(egui::pos2(100.0, 80.0), egui::vec2(1280.0, 720.0));
|
||||
world.insert_resource(ui_state);
|
||||
|
||||
let entity = world.resource_scope::<UiState, _>(|world, mut ui_state| {
|
||||
assert!(!world.contains_resource::<UiState>());
|
||||
let entity = spawn_primitive_at_camera(
|
||||
world,
|
||||
PrimitiveShape::Box,
|
||||
"Cube",
|
||||
ui_state.viewport_rect,
|
||||
);
|
||||
let pending = world.resource_mut::<PendingUiSelection>().0.take();
|
||||
super::super::reconcile_operator_selection(
|
||||
&mut ui_state.selected_entities,
|
||||
&[],
|
||||
world.resource::<SelectedEntity>().0,
|
||||
pending.as_deref(),
|
||||
);
|
||||
assert_eq!(ui_state.selected_entities.as_slice(), &[entity]);
|
||||
entity
|
||||
});
|
||||
|
||||
assert!(world.contains_resource::<UiState>());
|
||||
assert!(world.get::<LevelObject>(entity).is_some());
|
||||
assert!(world.get::<Primitive>(entity).is_some());
|
||||
assert_eq!(world.resource::<SelectedEntity>().0, Some(entity));
|
||||
assert_eq!(world.resource::<EditorHistory>().undo_depth(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user