use std::collections::HashSet;
use bevy::{input_focus::InputFocus, prelude::*, ui::ui_transform::UiGlobalTransform};
use bevy_monitors::prelude::{Mutation, NotifyChanged};
use jackdaw_feathers::{
context_menu::spawn_context_menu,
icons::IconFont,
text_edit::{self, EditorTextEdit, TextEditCommitEvent, TextEditProps, TextEditValue},
tokens,
tree_view::{ROW_BG, TreeRowStyle, tree_row},
};
use jackdaw_widgets::context_menu::{ContextMenuAction, ContextMenuCloseSet, ContextMenuState};
use jackdaw_widgets::tree_view::{
EntityCategory, TreeChildrenPopulated, TreeFocused, TreeIndex, TreeNode, TreeNodeExpanded,
TreeRowChildren, TreeRowClicked, TreeRowContent, TreeRowDropped, TreeRowDroppedOnRoot,
TreeRowInlineRename, TreeRowLabel, TreeRowRenamed, TreeRowSelected, TreeRowStartRename,
TreeRowVisibilityToggled,
};
use crate::{
EditorEntity, EditorHidden,
commands::{CommandHistory, EditorCommand, ReparentEntity, SetJsnField},
entity_ops,
layout::HierarchyFilter,
selection::{Selected, Selection},
};
use jackdaw_feathers::dialog::{DialogActionEvent, DialogChildrenSlot};
use jackdaw_jsn::BrushGroup;
#[derive(Resource, Default)]
struct PendingTemplateDefaultName(String);
#[derive(Component)]
struct TemplateNameInput;
#[derive(Component)]
#[require(EditorEntity)]
pub struct HierarchyPanel;
#[derive(Component)]
#[require(EditorEntity)]
pub struct HierarchyTreeContainer;
#[derive(Resource, Default)]
pub struct HierarchyShowAll(pub bool);
#[derive(Component)]
pub struct HierarchyShowAllButton;
pub struct HierarchyPlugin;
impl Plugin for HierarchyPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<ContextMenuState>()
.init_resource::<PendingTemplateDefaultName>()
.init_resource::<HierarchyShowAll>()
.add_systems(
OnEnter(crate::AppState::Editor),
(
setup_name_watcher,
rebuild_hierarchy.after(crate::spawn_layout),
),
)
.add_systems(
Update,
(
apply_hierarchy_filter,
cancel_inline_rename,
auto_focus_inline_rename,
handle_hierarchy_right_click.after(ContextMenuCloseSet),
populate_template_dialog,
toggle_show_all_button,
update_show_all_button_appearance,
on_show_all_changed,
jackdaw_feathers::tree_view::tree_keyboard_navigation,
)
.run_if(in_state(crate::AppState::Editor)),
)
.add_observer(handle_inline_rename_commit)
.add_observer(on_root_entity_added)
.add_observer(on_entity_reparented)
.add_observer(on_tree_node_expanded)
.add_observer(on_tree_row_clicked)
.add_observer(on_entity_removed)
.add_observer(on_name_changed)
.add_observer(on_entity_selected)
.add_observer(on_entity_deselected)
.add_observer(on_tree_row_dropped)
.add_observer(on_tree_row_dropped_on_root)
.add_observer(on_tree_row_start_rename)
.add_observer(on_tree_row_renamed)
.add_observer(on_context_menu_action)
.add_observer(on_visibility_toggled)
.add_observer(on_template_dialog_action)
.add_observer(on_entity_hidden);
}
}
fn classify_entity(world: &World, entity: Entity) -> EntityCategory {
if world.get::<BrushGroup>(entity).is_some() {
return EntityCategory::Mesh;
}
if world.get::<Camera>(entity).is_some() {
return EntityCategory::Camera;
}
if world.get::<PointLight>(entity).is_some()
|| world.get::<DirectionalLight>(entity).is_some()
|| world.get::<SpotLight>(entity).is_some()
{
return EntityCategory::Light;
}
if world.get::<Mesh3d>(entity).is_some() {
return EntityCategory::Mesh;
}
if world.get::<SceneRoot>(entity).is_some() {
return EntityCategory::Scene;
}
EntityCategory::Entity
}
fn has_visible_children(world: &World, entity: Entity) -> bool {
let Some(children) = world.get::<Children>(entity) else {
return false;
};
children.iter().any(|child| {
world.get::<EditorEntity>(child).is_none() && world.get::<EditorHidden>(child).is_none()
})
}
fn spawn_single_tree_row(world: &mut World, source: Entity, parent_container: Entity) -> Entity {
let label = world
.get::<Name>(source)
.map(|n| n.as_str().to_string())
.unwrap_or_else(|| format!("Entity {source}"));
let has_children = has_visible_children(world, source);
let category = classify_entity(world, source);
let icon_font = world.resource::<IconFont>().0.clone();
let style = TreeRowStyle { icon_font };
let tree_row_entity = world
.spawn((
tree_row(&label, has_children, false, source, category, &style),
ChildOf(parent_container),
))
.id();
world
.resource_mut::<TreeIndex>()
.insert(source, tree_row_entity);
tree_row_entity
}
fn rebuild_hierarchy(world: &mut World) {
let container = world
.query_filtered::<Entity, With<HierarchyTreeContainer>>()
.iter(world)
.next();
let Some(container) = container else {
return;
};
let roots: Vec<Entity> = world
.query_filtered::<Entity, (
With<Transform>,
Without<EditorEntity>,
Without<EditorHidden>,
Without<ChildOf>,
)>()
.iter(world)
.collect();
let show_all = world.resource::<HierarchyShowAll>().0;
let mut root_data: Vec<(Entity, EntityCategory, String)> = roots
.into_iter()
.filter(|&e| !world.resource::<TreeIndex>().contains(e))
.filter(|&e| show_all || world.get::<Name>(e).is_some())
.map(|e| {
let category = classify_entity(world, e);
let name = world
.get::<Name>(e)
.map(|n| n.as_str().to_string())
.unwrap_or_else(|| format!("Entity {e}"));
(e, category, name)
})
.collect();
root_data.sort_by(|(_, cat_a, name_a), (_, cat_b, name_b)| {
cat_a.cmp(cat_b).then_with(|| name_a.cmp(name_b))
});
for (entity, _category, _name) in root_data {
spawn_single_tree_row(world, entity, container);
}
}
fn on_root_entity_added(
trigger: On<Add, Transform>,
mut commands: Commands,
tree_index: Res<TreeIndex>,
container: Option<Single<Entity, With<HierarchyTreeContainer>>>,
editor_check: Query<(), Or<(With<EditorEntity>, With<EditorHidden>)>>,
child_of_check: Query<(), With<ChildOf>>,
) {
let entity = trigger.event_target();
let Some(container) = container else {
return;
};
if editor_check.contains(entity)
|| child_of_check.contains(entity)
|| tree_index.contains(entity)
{
return;
}
let container = *container;
commands.queue(move |world: &mut World| {
if world.resource::<TreeIndex>().contains(entity) {
return;
}
if world.get::<ChildOf>(entity).is_some() {
return;
}
if world.get::<EditorEntity>(entity).is_some()
|| world.get::<EditorHidden>(entity).is_some()
{
return;
}
if !world.resource::<HierarchyShowAll>().0 && world.get::<Name>(entity).is_none() {
return;
}
spawn_single_tree_row(world, entity, container);
});
}
fn on_name_changed(
trigger: On<Add, Name>,
mut commands: Commands,
name_query: Query<&Name>,
tree_index: Res<TreeIndex>,
tree_nodes: Query<&Children, With<TreeNode>>,
content_query: Query<&Children, With<TreeRowContent>>,
mut label_query: Query<&mut Text, With<TreeRowLabel>>,
container: Option<Single<Entity, With<HierarchyTreeContainer>>>,
editor_check: Query<(), Or<(With<EditorEntity>, With<EditorHidden>)>>,
child_of_check: Query<(), With<ChildOf>>,
) {
let entity = trigger.event_target();
let Ok(name) = name_query.get(entity) else {
return;
};
if let Some(tree_entity) = tree_index.get(entity) {
let Ok(children) = tree_nodes.get(tree_entity) else {
return;
};
for child in children.iter() {
if let Ok(content_children) = content_query.get(child) {
for grandchild in content_children.iter() {
if let Ok(mut text) = label_query.get_mut(grandchild) {
text.0 = name.as_str().to_string();
return;
}
}
}
}
} else {
let Some(container) = container else {
return;
};
if editor_check.contains(entity) || child_of_check.contains(entity) {
return;
}
let container = *container;
commands.queue(move |world: &mut World| {
if world.resource::<TreeIndex>().contains(entity) {
return;
}
if world.get::<ChildOf>(entity).is_some() {
return;
}
if world.get::<EditorEntity>(entity).is_some()
|| world.get::<EditorHidden>(entity).is_some()
{
return;
}
spawn_single_tree_row(world, entity, container);
});
}
}
fn setup_name_watcher(mut commands: Commands) {
commands
.spawn((EditorEntity, NotifyChanged::<Name>::default()))
.observe(on_name_mutated);
}
fn on_name_mutated(
trigger: On<Mutation<Name>>,
name_query: Query<&Name>,
tree_index: Res<TreeIndex>,
tree_nodes: Query<&Children, With<TreeNode>>,
content_query: Query<&Children, With<TreeRowContent>>,
mut label_query: Query<&mut Text, With<TreeRowLabel>>,
) {
let entity = trigger.mutated;
let Ok(name) = name_query.get(entity) else {
return;
};
let Some(tree_entity) = tree_index.get(entity) else {
return;
};
let Ok(children) = tree_nodes.get(tree_entity) else {
return;
};
for child in children.iter() {
let Ok(content_children) = content_query.get(child) else {
continue;
};
for grandchild in content_children.iter() {
if let Ok(mut text) = label_query.get_mut(grandchild) {
text.0 = name.as_str().to_string();
return;
}
}
}
}
fn on_entity_reparented(
trigger: On<Add, ChildOf>,
mut commands: Commands,
tree_index: Res<TreeIndex>,
editor_check: Query<(), Or<(With<EditorEntity>, With<EditorHidden>)>>,
tree_node_check: Query<(), With<TreeNode>>,
child_of_query: Query<&ChildOf>,
children_query: Query<&Children>,
tree_row_children: Query<Entity, With<TreeRowChildren>>,
populated_query: Query<&TreeChildrenPopulated>,
) {
let entity = trigger.event_target();
if editor_check.contains(entity) || tree_node_check.contains(entity) {
return;
}
let Ok(&ChildOf(new_parent)) = child_of_query.get(entity) else {
return;
};
let parent_container = tree_index.get(new_parent).and_then(|parent_tree| {
children_query
.get(parent_tree)
.ok()
.and_then(|children| children.iter().find(|c| tree_row_children.contains(*c)))
});
if let Some(tree_entity) = tree_index.get(entity) {
if let Some(container) = parent_container {
commands.entity(tree_entity).insert(ChildOf(container));
} else {
let source = entity;
commands.queue(move |world: &mut World| {
world.resource_mut::<TreeIndex>().remove(source);
if let Ok(ec) = world.get_entity_mut(tree_entity) {
ec.despawn();
}
});
}
return;
}
let Some(parent_container) = parent_container else {
return;
};
let parent_tree = tree_index.get(new_parent).unwrap(); let populated = populated_query
.get(parent_tree)
.map(|p| p.0)
.unwrap_or(false);
if !populated {
return; }
let container = parent_container;
commands.queue(move |world: &mut World| {
if world.resource::<TreeIndex>().contains(entity) {
return;
}
if !world.resource::<HierarchyShowAll>().0 && world.get::<Name>(entity).is_none() {
return;
}
spawn_single_tree_row(world, entity, container);
});
}
fn on_entity_removed(
trigger: On<Despawn, Name>,
mut commands: Commands,
tree_index: Res<TreeIndex>,
) {
let entity = trigger.event_target();
if let Some(tree_entity) = tree_index.get(entity) {
if let Ok(mut ec) = commands.get_entity(tree_entity) {
ec.despawn();
}
}
}
fn on_entity_hidden(
trigger: On<Add, EditorHidden>,
mut commands: Commands,
tree_index: Res<TreeIndex>,
) {
let entity = trigger.event_target();
if let Some(tree_entity) = tree_index.get(entity) {
if let Ok(mut ec) = commands.get_entity(tree_entity) {
ec.despawn();
}
}
}
fn on_tree_node_expanded(
trigger: On<Mutation<TreeNodeExpanded>>,
mut commands: Commands,
tree_query: Query<(
&TreeNodeExpanded,
&TreeChildrenPopulated,
&TreeNode,
&Children,
)>,
tree_row_children_marker: Query<Entity, With<TreeRowChildren>>,
remote_check: Query<(), With<crate::remote::entity_browser::RemoteEntityProxy>>,
) {
let entity = trigger.event_target();
let Ok((expanded, populated, tree_node, children)) = tree_query.get(entity) else {
return;
};
if !expanded.0 || populated.0 {
return;
}
let source = tree_node.0;
if remote_check.contains(source) {
return;
}
let Some(container) = children
.iter()
.find(|c| tree_row_children_marker.contains(*c))
else {
return;
};
let tree_row_entity = entity;
commands.queue(move |world: &mut World| {
if let Some(pop) = world.get::<TreeChildrenPopulated>(tree_row_entity) {
if pop.0 {
return;
}
}
if let Some(mut pop) = world.get_mut::<TreeChildrenPopulated>(tree_row_entity) {
pop.0 = true;
}
let source_children: Vec<Entity> = world
.get::<Children>(source)
.map(|c| c.iter().collect())
.unwrap_or_default();
let mut child_data: Vec<(Entity, String, EntityCategory)> = Vec::new();
for child in source_children {
if world.get::<EditorEntity>(child).is_some()
|| world.get::<EditorHidden>(child).is_some()
{
continue;
}
if world.resource::<TreeIndex>().contains(child) {
continue;
}
let name = world
.get::<Name>(child)
.map(|n| n.as_str().to_string())
.unwrap_or_else(|| format!("Entity {child}"));
let category = classify_entity(world, child);
child_data.push((child, name, category));
}
child_data.sort_by(|(_, name_a, cat_a), (_, name_b, cat_b)| {
cat_a.cmp(cat_b).then_with(|| name_a.cmp(name_b))
});
for (child_entity, _name, _category) in child_data {
spawn_single_tree_row(world, child_entity, container);
}
});
}
fn on_tree_row_clicked(
event: On<TreeRowClicked>,
mut commands: Commands,
mut selection: ResMut<Selection>,
mut focused: ResMut<TreeFocused>,
keyboard: Res<ButtonInput<KeyCode>>,
parent_query: Query<&ChildOf>,
tree_nodes: Query<Entity, With<TreeNode>>,
remote_check: Query<(), With<crate::remote::entity_browser::RemoteEntityProxy>>,
) {
if remote_check.contains(event.source_entity) {
return;
}
let ctrl = keyboard.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]);
if ctrl {
selection.toggle(&mut commands, event.source_entity);
} else if selection.is_selected(event.source_entity) {
selection.clear(&mut commands);
} else {
selection.select_single(&mut commands, event.source_entity);
}
let content_entity = event.entity;
if let Ok(&ChildOf(tree_row)) = parent_query.get(content_entity) {
if tree_nodes.contains(tree_row) {
focused.0 = Some(tree_row);
}
}
}
fn on_entity_selected(
trigger: On<Add, Selected>,
mut commands: Commands,
tree_index: Res<TreeIndex>,
tree_nodes: Query<&Children, With<TreeNode>>,
tree_row_contents: Query<Entity, With<TreeRowContent>>,
mut bg_query: Query<&mut BackgroundColor>,
mut border_query: Query<&mut BorderColor>,
) {
let entity = trigger.event_target();
let Some(tree_entity) = tree_index.get(entity) else {
return;
};
let Ok(children) = tree_nodes.get(tree_entity) else {
return;
};
for child in children.iter() {
if tree_row_contents.contains(child) {
if let Ok(mut ec) = commands.get_entity(child) {
ec.insert(TreeRowSelected);
}
if let Ok(mut bg) = bg_query.get_mut(child) {
bg.0 = tokens::SELECTED_BG;
}
if let Ok(mut border) = border_query.get_mut(child) {
*border = BorderColor::all(tokens::SELECTED_BORDER);
}
return;
}
}
}
fn on_entity_deselected(
trigger: On<Remove, Selected>,
mut commands: Commands,
tree_index: Res<TreeIndex>,
tree_nodes: Query<&Children, With<TreeNode>>,
tree_row_contents: Query<Entity, With<TreeRowContent>>,
mut bg_query: Query<&mut BackgroundColor>,
mut border_query: Query<&mut BorderColor>,
) {
let entity = trigger.event_target();
let Some(tree_entity) = tree_index.get(entity) else {
return;
};
let Ok(children) = tree_nodes.get(tree_entity) else {
return;
};
for child in children.iter() {
if tree_row_contents.contains(child) {
if let Ok(mut ec) = commands.get_entity(child) {
ec.remove::<TreeRowSelected>();
}
if let Ok(mut bg) = bg_query.get_mut(child) {
bg.0 = ROW_BG;
}
if let Ok(mut border) = border_query.get_mut(child) {
*border = BorderColor::all(Color::NONE);
}
return;
}
}
}
fn on_tree_row_dropped(
event: On<TreeRowDropped>,
mut commands: Commands,
parent_query: Query<&ChildOf>,
) {
let dragged = event.dragged_source;
let target = event.target_source;
if dragged == target {
return;
}
let mut current = target;
while let Ok(&ChildOf(parent)) = parent_query.get(current) {
if parent == dragged {
return;
}
current = parent;
}
let old_parent = parent_query.get(dragged).ok().map(|c| c.0);
let mut cmd = ReparentEntity {
entity: dragged,
old_parent,
new_parent: Some(target),
};
commands.queue(move |world: &mut World| {
cmd.execute(world);
world
.resource_mut::<CommandHistory>()
.undo_stack
.push(Box::new(cmd));
world.resource_mut::<CommandHistory>().redo_stack.clear();
});
}
fn on_tree_row_dropped_on_root(
event: On<TreeRowDroppedOnRoot>,
mut commands: Commands,
parent_query: Query<&ChildOf, Without<EditorEntity>>,
tree_index: Res<TreeIndex>,
container: Single<Entity, With<HierarchyTreeContainer>>,
) {
let dragged = event.dragged_source;
let old_parent = match parent_query.get(dragged) {
Ok(child_of) => Some(child_of.0),
Err(_) => return,
};
let container_entity = *container;
let mut cmd = ReparentEntity {
entity: dragged,
old_parent,
new_parent: None,
};
commands.queue(move |world: &mut World| {
cmd.execute(world);
world
.resource_mut::<CommandHistory>()
.undo_stack
.push(Box::new(cmd));
world.resource_mut::<CommandHistory>().redo_stack.clear();
});
if let Some(tree_entity) = tree_index.get(dragged) {
commands
.entity(tree_entity)
.insert(ChildOf(container_entity));
}
}
fn handle_hierarchy_right_click(
mouse: Res<ButtonInput<MouseButton>>,
mut commands: Commands,
mut state: ResMut<ContextMenuState>,
windows: Query<&Window>,
selection: Res<Selection>,
tree_row_contents: Query<(Entity, &ChildOf), With<TreeRowContent>>,
tree_nodes: Query<&TreeNode>,
computed_nodes: Query<(&ComputedNode, &UiGlobalTransform), With<TreeRowContent>>,
) {
if !mouse.just_pressed(MouseButton::Right) {
return;
}
let Ok(window) = windows.single() else {
return;
};
let Some(cursor_pos) = window.cursor_position() else {
return;
};
if let Some(menu) = state.menu_entity.take() {
if let Ok(mut ec) = commands.get_entity(menu) {
ec.despawn();
}
}
let mut target_source = None;
for (content_entity, child_of) in &tree_row_contents {
let Ok((computed, global_transform)) = computed_nodes.get(content_entity) else {
continue;
};
let size = computed.size();
let (_, _, translation) = global_transform.to_scale_angle_translation();
let pos = translation;
let half = size / 2.0;
let rect = Rect::from_center_half_size(pos, half);
if rect.contains(cursor_pos) {
if let Ok(tree_node) = tree_nodes.get(child_of.0) {
target_source = Some(tree_node.0);
break;
}
}
}
let Some(target) = target_source else {
return;
};
if !selection.is_selected(target) {
commands.queue(move |world: &mut World| {
let old_entities: Vec<Entity> = world.resource::<Selection>().entities.clone();
let mut selection = world.resource_mut::<Selection>();
selection.entities.clear();
selection.entities.push(target);
for &e in &old_entities {
if e != target {
if let Ok(mut ec) = world.get_entity_mut(e) {
ec.remove::<Selected>();
}
}
}
if let Ok(mut ec) = world.get_entity_mut(target) {
ec.insert(Selected);
}
});
}
let menu_items = &[
("hierarchy.focus", "Focus F"),
("hierarchy.rename", "Rename F2"),
("hierarchy.duplicate", "Duplicate Ctrl+D"),
("hierarchy.delete", "Delete Del"),
("---", ""),
("hierarchy.save_template", "Save as Template..."),
("---", ""),
("hierarchy.add_cube", "Add Child Cube"),
("hierarchy.add_sphere", "Add Child Sphere"),
("hierarchy.add_light", "Add Child Light"),
("hierarchy.add_empty", "Add Child Empty"),
];
let items: Vec<(&str, &str)> = menu_items
.iter()
.filter(|(action, _)| *action != "---")
.copied()
.collect();
let menu = spawn_context_menu(&mut commands, cursor_pos, Some(target), &items);
state.menu_entity = Some(menu);
state.target_entity = Some(target);
}
fn on_context_menu_action(
event: On<ContextMenuAction>,
mut commands: Commands,
global_transforms: Query<&GlobalTransform>,
mut camera_query: Query<&mut Transform, With<jackdaw_camera::JackdawCameraSettings>>,
) {
let target_entity = event.target_entity;
match event.action.as_str() {
"hierarchy.focus" => {
if let Some(target) = target_entity {
if let Ok(global_tf) = global_transforms.get(target) {
let target_pos = global_tf.translation();
let scale = global_tf.compute_transform().scale;
let dist = (scale.length() * 3.0).max(5.0);
for mut transform in &mut camera_query {
let forward = transform.forward().as_vec3();
transform.translation = target_pos - forward * dist;
*transform = transform.looking_at(target_pos, Vec3::Y);
}
}
}
}
"hierarchy.rename" => {
if let Some(target) = target_entity {
commands.trigger(TreeRowStartRename {
entity: Entity::PLACEHOLDER,
source_entity: target,
});
}
}
"hierarchy.duplicate" => {
commands.queue(|world: &mut World| {
entity_ops::duplicate_selected(world);
});
}
"hierarchy.delete" => {
commands.queue(|world: &mut World| {
entity_ops::delete_selected(world);
});
}
"hierarchy.add_cube" => {
if let Some(parent) = target_entity {
commands.queue(move |world: &mut World| {
entity_ops::create_entity_in_world(world, entity_ops::EntityTemplate::Cube);
let selection = world.resource::<Selection>();
if let Some(new_entity) = selection.primary() {
world.entity_mut(new_entity).insert(ChildOf(parent));
}
});
}
}
"hierarchy.add_sphere" => {
if let Some(parent) = target_entity {
commands.queue(move |world: &mut World| {
entity_ops::create_entity_in_world(world, entity_ops::EntityTemplate::Sphere);
let selection = world.resource::<Selection>();
if let Some(new_entity) = selection.primary() {
world.entity_mut(new_entity).insert(ChildOf(parent));
}
});
}
}
"hierarchy.add_light" => {
if let Some(parent) = target_entity {
commands.queue(move |world: &mut World| {
entity_ops::create_entity_in_world(
world,
entity_ops::EntityTemplate::PointLight,
);
let selection = world.resource::<Selection>();
if let Some(new_entity) = selection.primary() {
world.entity_mut(new_entity).insert(ChildOf(parent));
}
});
}
}
"hierarchy.add_empty" => {
if let Some(parent) = target_entity {
commands.queue(move |world: &mut World| {
entity_ops::create_entity_in_world(world, entity_ops::EntityTemplate::Empty);
let selection = world.resource::<Selection>();
if let Some(new_entity) = selection.primary() {
world.entity_mut(new_entity).insert(ChildOf(parent));
}
});
}
}
"hierarchy.save_template" => {
if let Some(target) = target_entity {
commands.queue(move |world: &mut World| {
world
.resource_mut::<crate::entity_templates::PendingTemplateSave>()
.entity = Some(target);
let default_name = world
.get::<Name>(target)
.map(|n| n.as_str().to_string())
.unwrap_or_else(|| "template".to_string());
world.resource_mut::<PendingTemplateDefaultName>().0 = default_name;
});
commands.trigger(jackdaw_feathers::dialog::OpenDialogEvent::new(
"Save as Template",
"Save",
));
}
}
_ => {}
}
}
fn on_visibility_toggled(
event: On<TreeRowVisibilityToggled>,
mut commands: Commands,
visibility_query: Query<&Visibility>,
) {
let source = event.source_entity;
let current = visibility_query
.get(source)
.copied()
.unwrap_or(Visibility::Inherited);
let new_visibility = match current {
Visibility::Hidden => Visibility::Inherited,
_ => Visibility::Hidden,
};
let old_json = serde_json::Value::String(format!("{current:?}"));
let new_json = serde_json::Value::String(format!("{new_visibility:?}"));
let cmd = SetJsnField {
entity: source,
type_path: "bevy_camera::visibility::Visibility".to_string(),
field_path: String::new(),
old_value: old_json,
new_value: new_json,
};
commands.queue(move |world: &mut World| {
let mut cmd = Box::new(cmd);
cmd.execute(world);
let mut history = world.resource_mut::<CommandHistory>();
history.undo_stack.push(cmd);
history.redo_stack.clear();
});
}
#[derive(Component)]
struct InlineRenameInput {
label_entity: Entity,
source_entity: Entity,
}
fn cancel_inline_rename(
keyboard: Res<ButtonInput<KeyCode>>,
mut commands: Commands,
rename_query: Query<(Entity, &InlineRenameInput)>,
names: Query<&Name>,
mut input_focus: ResMut<InputFocus>,
) {
if !keyboard.just_pressed(KeyCode::Escape) {
return;
}
for (rename_entity, inline_rename) in &rename_query {
input_focus.clear();
let original_name = names
.get(inline_rename.source_entity)
.map(|n| n.as_str().to_string())
.unwrap_or_default();
commands
.entity(inline_rename.label_entity)
.remove::<TreeRowInlineRename>();
if let Ok(mut ec) = commands.get_entity(inline_rename.label_entity) {
ec.insert(Text::new(original_name));
ec.entry::<Node>().and_modify(|mut node| {
node.display = Display::Flex;
});
}
commands.entity(rename_entity).despawn();
}
}
fn on_tree_row_start_rename(
event: On<TreeRowStartRename>,
mut commands: Commands,
tree_index: Res<TreeIndex>,
tree_nodes: Query<&Children, With<TreeNode>>,
content_query: Query<(Entity, &Children), With<TreeRowContent>>,
label_query: Query<Entity, With<TreeRowLabel>>,
names: Query<&Name>,
rename_check: Query<(), With<InlineRenameInput>>,
) {
let source = event.source_entity;
if !rename_check.is_empty() {
return;
}
let Some(tree_entity) = tree_index.get(source) else {
return;
};
let Ok(children) = tree_nodes.get(tree_entity) else {
return;
};
let mut label_entity = None;
let mut content_entity = None;
for child in children.iter() {
if let Ok((content_e, content_children)) = content_query.get(child) {
for grandchild in content_children.iter() {
if label_query.contains(grandchild) {
label_entity = Some(grandchild);
content_entity = Some(content_e);
break;
}
}
}
}
let Some(label_entity) = label_entity else {
return;
};
let Some(content_entity) = content_entity else {
return;
};
let current_name = names
.get(source)
.map(|n| n.as_str().to_string())
.unwrap_or_default();
commands.entity(label_entity).insert(TreeRowInlineRename);
commands
.entity(label_entity)
.entry::<Node>()
.and_modify(|mut node| {
node.display = Display::None;
});
commands.spawn((
InlineRenameInput {
label_entity,
source_entity: source,
},
text_edit::text_edit(
TextEditProps::default()
.with_default_value(current_name)
.allow_empty(),
),
ChildOf(content_entity),
));
}
fn auto_focus_inline_rename(
rename_inputs: Query<(Entity, &InlineRenameInput, &Children)>,
wrappers: Query<&jackdaw_feathers::text_edit::TextEditConfig>,
wrapper_children: Query<&Children>,
editor_text_edits: Query<Entity, With<EditorTextEdit>>,
mut input_focus: ResMut<InputFocus>,
) {
for (_rename_entity, _inline, children) in &rename_inputs {
for child in children.iter() {
if wrappers.contains(child) {
continue;
}
if let Ok(wrapper_kids) = wrapper_children.get(child) {
for wk in wrapper_kids.iter() {
if editor_text_edits.contains(wk) {
if input_focus.0 != Some(wk) {
input_focus.0 = Some(wk);
}
return;
}
}
}
}
}
}
fn handle_inline_rename_commit(
event: On<TextEditCommitEvent>,
rename_inputs: Query<(Entity, &InlineRenameInput)>,
child_of_query: Query<&ChildOf>,
mut commands: Commands,
mut input_focus: ResMut<InputFocus>,
) {
let mut current = event.entity;
let mut found = None;
for _ in 0..4 {
let Ok(child_of) = child_of_query.get(current) else {
break;
};
if let Ok((rename_entity, inline_rename)) = rename_inputs.get(child_of.parent()) {
found = Some((
rename_entity,
inline_rename.label_entity,
inline_rename.source_entity,
));
break;
}
current = child_of.parent();
}
let Some((rename_entity, label_entity, source_entity)) = found else {
return;
};
input_focus.clear();
commands
.entity(label_entity)
.remove::<TreeRowInlineRename>();
commands
.entity(label_entity)
.insert(Text::new(event.text.clone()));
commands
.entity(label_entity)
.entry::<Node>()
.and_modify(|mut node| {
node.display = Display::Flex;
});
commands.entity(rename_entity).despawn();
commands.trigger(TreeRowRenamed {
entity: label_entity,
source_entity,
new_name: event.text.clone(),
});
}
fn on_tree_row_renamed(event: On<TreeRowRenamed>, mut commands: Commands, names: Query<&Name>) {
let source = event.source_entity;
let new_name = event.new_name.clone();
let old_name = names
.get(source)
.map(|n| n.as_str().to_string())
.unwrap_or_default();
if old_name == new_name {
return;
}
commands.queue(move |world: &mut World| {
let cmd = SetJsnField {
entity: source,
type_path: "bevy_ecs::name::Name".to_string(),
field_path: String::new(),
old_value: serde_json::Value::String(old_name),
new_value: serde_json::Value::String(new_name),
};
let mut cmd = Box::new(cmd);
cmd.execute(world);
let mut history = world.resource_mut::<CommandHistory>();
history.undo_stack.push(cmd);
history.redo_stack.clear();
});
}
fn populate_template_dialog(
mut commands: Commands,
pending: Res<crate::entity_templates::PendingTemplateSave>,
default_name: Res<PendingTemplateDefaultName>,
slots: Query<(Entity, &Children), (With<DialogChildrenSlot>, Changed<Children>)>,
existing_inputs: Query<(), With<TemplateNameInput>>,
) {
if pending.entity.is_none() {
return;
}
if !existing_inputs.is_empty() {
return;
}
for (slot_entity, children) in &slots {
if children.is_empty() {
commands.spawn((
TemplateNameInput,
text_edit::text_edit(
TextEditProps::default()
.with_placeholder("Template name...")
.with_default_value(default_name.0.clone())
.allow_empty(),
),
ChildOf(slot_entity),
));
}
}
}
fn on_template_dialog_action(
_event: On<DialogActionEvent>,
mut commands: Commands,
pending: Res<crate::entity_templates::PendingTemplateSave>,
name_inputs: Query<&TextEditValue, With<TemplateNameInput>>,
) {
let Some(_entity) = pending.entity else {
return;
};
let name = name_inputs
.iter()
.next()
.map(|input| input.0.trim().to_string())
.unwrap_or_default();
if name.is_empty() {
return;
}
commands.queue(move |world: &mut World| {
crate::entity_templates::save_entity_template(world, &name);
world
.resource_mut::<crate::entity_templates::PendingTemplateSave>()
.entity = None;
});
}
fn toggle_show_all_button(
mut show_all: ResMut<HierarchyShowAll>,
interactions: Query<&Interaction, (Changed<Interaction>, With<HierarchyShowAllButton>)>,
) {
for interaction in &interactions {
if *interaction == Interaction::Pressed {
show_all.0 = !show_all.0;
}
}
}
fn update_show_all_button_appearance(
show_all: Res<HierarchyShowAll>,
buttons: Query<&Children, With<HierarchyShowAllButton>>,
mut text_colors: Query<&mut TextColor>,
) {
if !show_all.is_changed() {
return;
}
let color = if show_all.0 {
tokens::TEXT_PRIMARY
} else {
tokens::TEXT_SECONDARY
};
for children in &buttons {
for child in children.iter() {
if let Ok(mut tc) = text_colors.get_mut(child) {
tc.0 = color;
}
}
}
}
fn on_show_all_changed(show_all: Res<HierarchyShowAll>, mut commands: Commands) {
if show_all.is_changed() && !show_all.is_added() {
commands.queue(|world: &mut World| {
clear_all_tree_rows(world);
rebuild_hierarchy(world);
});
}
}
pub fn clear_all_tree_rows(world: &mut World) {
let container = world
.query_filtered::<Entity, With<HierarchyTreeContainer>>()
.iter(world)
.next();
let Some(container) = container else {
return;
};
let tree_rows: Vec<Entity> = world
.get::<Children>(container)
.map(|c| c.iter().collect())
.unwrap_or_default();
for row in tree_rows {
if let Ok(ec) = world.get_entity_mut(row) {
ec.despawn();
}
}
world.resource_mut::<TreeIndex>().clear();
}
fn apply_hierarchy_filter(
filter_input: Query<&TextEditValue, (With<HierarchyFilter>, Changed<TextEditValue>)>,
tree_nodes: Query<(Entity, &TreeNode)>,
names: Query<&Name>,
parent_query: Query<&ChildOf>,
tree_row_children_query: Query<(), With<TreeRowChildren>>,
mut display_query: Query<&mut Node>,
) {
let Ok(text_edit_value) = filter_input.single() else {
return;
};
let filter = text_edit_value.0.trim().to_lowercase();
if filter.is_empty() {
for (tree_entity, _) in &tree_nodes {
if let Ok(mut node) = display_query.get_mut(tree_entity) {
node.display = Display::Flex;
}
}
return;
}
let mut visible_tree_entities: HashSet<Entity> = HashSet::new();
for (tree_entity, tree_node) in &tree_nodes {
let label = names
.get(tree_node.0)
.map(|n| n.as_str().to_lowercase())
.unwrap_or_else(|_| format!("entity {}", tree_node.0).to_lowercase());
let matches = label.contains(&filter);
if matches {
visible_tree_entities.insert(tree_entity);
let mut current = tree_entity;
while let Ok(&ChildOf(parent)) = parent_query.get(current) {
if tree_row_children_query.contains(parent) {
if let Ok(&ChildOf(grandparent)) = parent_query.get(parent) {
visible_tree_entities.insert(grandparent);
current = grandparent;
} else {
break;
}
} else {
break;
}
}
}
}
for (tree_entity, _) in &tree_nodes {
if let Ok(mut node) = display_query.get_mut(tree_entity) {
node.display = if visible_tree_entities.contains(&tree_entity) {
Display::Flex
} else {
Display::None
};
}
}
}