use crate::ecs::{EditorMode, EditorWorld};
use crate::systems::mode::{is_descendant_of, nearest_prefab_instance};
use crate::systems::retained_ui::{Action, UiHandles};
use nightshade::ecs::ui::components::*;
use nightshade::ecs::ui::layout_types::FlowDirection;
use nightshade::ecs::ui::state::UiHover;
use nightshade::prelude::*;
use std::collections::{HashMap, HashSet};
const ROW_HEIGHT: f32 = 22.0;
const POOL_SIZE: usize = 64;
const INDENT_PER_DEPTH: f32 = 14.0;
const ARROW_WIDTH: f32 = 14.0;
const ICON_WIDTH: f32 = 18.0;
#[derive(Default, Clone)]
pub struct TreeRow {
pub entity: Entity,
pub label: String,
pub depth: usize,
pub has_children: bool,
}
#[derive(Default, Clone, Copy)]
pub struct PoolRow {
pub container: Entity,
pub indent: Entity,
pub arrow: Entity,
pub icon: Entity,
pub label: Entity,
}
#[derive(Default, Clone)]
pub struct TreePanelHandles {
pub panel: Entity,
pub filter_input: Entity,
pub cameras_only_checkbox: Entity,
pub list: Entity,
pub pool_rows: Vec<PoolRow>,
pub all_rows: Vec<TreeRow>,
pub visible_indices: Vec<usize>,
pub expanded: HashSet<u32>,
pub filter_last: String,
pub filter_lower: String,
pub cameras_only: bool,
pub last_signature: u64,
pub last_visible_start: usize,
pub selected_entity: Option<Entity>,
pub selected_entities_len: usize,
pub context_menu: Entity,
pub camera_context_menu: Entity,
pub context_menu_target: Option<Entity>,
}
pub fn build(tree: &mut UiTreeBuilder) -> TreePanelHandles {
let panel = tree.add_docked_panel_left("entity_tree", "Entities", 260.0);
let content = super::panel_content(tree, panel);
let mut filter_input = Entity::default();
let mut cameras_only_checkbox = Entity::default();
let mut list = Entity::default();
tree.in_parent(content, |tree| {
filter_input = tree.add_text_input("Filter entities");
cameras_only_checkbox = tree.add_checkbox("Cameras only", false);
list = tree.add_virtual_list(ROW_HEIGHT, POOL_SIZE);
});
let pool_items = tree
.world_mut()
.ui
.get_ui_virtual_list(list)
.map(|d| d.pool_items.clone())
.unwrap_or_default();
let mut pool_rows: Vec<PoolRow> = Vec::with_capacity(pool_items.len());
for item in pool_items {
if let Some(node) = tree
.world_mut()
.ui
.get_ui_layout_node_mut(item.container_entity)
{
node.flow_layout = Some(FlowLayout {
direction: FlowDirection::Horizontal,
padding: 0.0,
spacing: 4.0,
alignment: FlowAlignment::Start,
cross_alignment: FlowAlignment::Center,
wrap: false,
});
}
let mut indent = Entity::default();
let mut arrow = Entity::default();
let mut icon = Entity::default();
let mut label = Entity::default();
tree.in_parent(item.container_entity, |tree| {
indent = tree.add_node().size((0.0).px(), (ROW_HEIGHT).px()).entity();
arrow = tree
.add_node()
.size((ARROW_WIDTH).px(), (ROW_HEIGHT).px())
.with_text("", ROW_HEIGHT * 0.55)
.text_center()
.fg(ThemeColor::Text)
.entity();
let icon_set = tree.world_mut().resources.retained_ui.icon_set;
let lookup = IconLookup::for_set(icon_set);
icon = tree
.add_node()
.size((ICON_WIDTH).px(), (ROW_HEIGHT).px())
.with_icon(lookup.mesh, ROW_HEIGHT * 0.7)
.fg(ThemeColor::Text)
.entity();
ui_set_text(tree.world_mut(), icon, "");
label = tree
.add_node()
.flow_child(Rl(vec2(0.0, 100.0)))
.flex_grow(1.0)
.with_text("", ROW_HEIGHT * 0.65)
.text_left()
.fg(ThemeColor::Text)
.entity();
});
if let Some(content) = tree
.world_mut()
.ui
.get_ui_node_content_mut(item.container_entity)
{
*content = UiNodeContent::Rect {
corner_radius: 2.0,
border_width: 0.0,
border_color: vec4(0.0, 0.0, 0.0, 0.0),
};
}
if let Some(weights) = tree
.world_mut()
.ui
.get_ui_state_weights_mut(item.container_entity)
{
weights.weights[UiHover::INDEX] = 0.0;
}
pool_rows.push(PoolRow {
container: item.container_entity,
indent,
arrow,
icon,
label,
});
}
let context_menu = tree.add_context_menu_from_builder(
ContextMenuBuilder::new()
.item("Add Child", "")
.item("Duplicate", "Ctrl+D")
.item("Delete", "Del")
.separator()
.item("Convert similar to instanced", ""),
);
let camera_context_menu = tree.add_context_menu_from_builder(
ContextMenuBuilder::new()
.item("View Camera", "")
.item("Add Child", "")
.item("Duplicate", "Ctrl+D")
.item("Delete", "Del"),
);
TreePanelHandles {
panel,
filter_input,
cameras_only_checkbox,
list,
pool_rows,
context_menu,
camera_context_menu,
..Default::default()
}
}
pub fn poll(editor_world: &mut EditorWorld, world: &mut World, handles: &UiHandles) {
let list = handles.tree.list;
let context_menu = handles.tree.context_menu;
let camera_context_menu = handles.tree.camera_context_menu;
let mut clicks: Vec<usize> = Vec::new();
let mut right_clicks: Vec<(usize, Vec2)> = Vec::new();
let mut context_menu_clicks: Vec<usize> = Vec::new();
let mut camera_context_menu_clicks: Vec<usize> = Vec::new();
for event in ui_events(world) {
match event {
UiEvent::VirtualListItemClicked {
entity, item_index, ..
} if *entity == list => {
clicks.push(*item_index);
}
UiEvent::VirtualListItemRightClicked {
entity,
item_index,
screen_position,
} if *entity == list => {
right_clicks.push((*item_index, *screen_position));
}
UiEvent::ContextMenuItemClicked {
entity, item_index, ..
} if *entity == context_menu => {
context_menu_clicks.push(*item_index);
}
UiEvent::ContextMenuItemClicked {
entity, item_index, ..
} if *entity == camera_context_menu => {
camera_context_menu_clicks.push(*item_index);
}
_ => {}
}
}
let shift_held = world
.resources
.input
.keyboard
.is_key_pressed(KeyCode::ShiftLeft)
|| world
.resources
.input
.keyboard
.is_key_pressed(KeyCode::ShiftRight);
let mut select_actions: Vec<Action> = Vec::new();
for visible_idx in clicks {
let Some(row_idx) = editor_world
.resources
.ui_handles
.tree
.visible_indices
.get(visible_idx)
.copied()
else {
continue;
};
let Some(row) = editor_world
.resources
.ui_handles
.tree
.all_rows
.get(row_idx)
.cloned()
else {
continue;
};
if row.has_children {
let entry = editor_world.resources.ui_handles.tree.expanded.clone();
if entry.contains(&row.entity.id) {
editor_world
.resources
.ui_handles
.tree
.expanded
.remove(&row.entity.id);
} else {
editor_world
.resources
.ui_handles
.tree
.expanded
.insert(row.entity.id);
}
editor_world.resources.ui_handles.tree.last_visible_start = usize::MAX;
}
if shift_held {
select_actions.push(Action::ToggleSelectEntity(row.entity));
} else {
select_actions.push(Action::SelectEntity(row.entity));
}
}
editor_world
.resources
.ui_interaction
.actions
.extend(select_actions);
for (visible_idx, position) in right_clicks {
let Some(row_idx) = editor_world
.resources
.ui_handles
.tree
.visible_indices
.get(visible_idx)
.copied()
else {
continue;
};
let Some(row) = editor_world
.resources
.ui_handles
.tree
.all_rows
.get(row_idx)
.cloned()
else {
continue;
};
editor_world.resources.ui_handles.tree.context_menu_target = Some(row.entity);
let menu = if world.core.entity_has_camera(row.entity) {
camera_context_menu
} else {
context_menu
};
ui_show_context_menu(world, menu, position);
}
for item_index in context_menu_clicks {
let Some(target) = editor_world.resources.ui_handles.tree.context_menu_target else {
continue;
};
match item_index {
0 => editor_world
.resources
.ui_interaction
.actions
.push(Action::AddEntityChild(target)),
1 => editor_world
.resources
.ui_interaction
.actions
.push(Action::DuplicateEntity(target)),
2 => editor_world
.resources
.ui_interaction
.actions
.push(Action::DeleteEntity(target)),
3 => {
editor_world
.resources
.ui_interaction
.actions
.push(Action::SelectEntity(target));
editor_world
.resources
.ui_interaction
.actions
.push(Action::ConvertSimilarToInstanced);
}
_ => {}
}
}
for item_index in camera_context_menu_clicks {
let Some(target) = editor_world.resources.ui_handles.tree.context_menu_target else {
continue;
};
let action = match item_index {
0 => Some(Action::ViewCamera(target)),
1 => Some(Action::AddEntityChild(target)),
2 => Some(Action::DuplicateEntity(target)),
3 => Some(Action::DeleteEntity(target)),
_ => None,
};
if let Some(action) = action {
editor_world.resources.ui_interaction.actions.push(action);
}
}
let filter_text = widget::<UiTextInputData>(world, handles.tree.filter_input)
.map(|data| data.text.clone())
.unwrap_or_default();
if filter_text != editor_world.resources.ui_handles.tree.filter_last {
editor_world.resources.ui_handles.tree.filter_lower = filter_text.to_lowercase();
editor_world.resources.ui_handles.tree.filter_last = filter_text;
editor_world.resources.ui_handles.tree.last_visible_start = usize::MAX;
}
let cameras_only = widget::<UiCheckboxData>(world, handles.tree.cameras_only_checkbox)
.map(|data| data.value)
.unwrap_or(false);
if cameras_only != editor_world.resources.ui_handles.tree.cameras_only {
editor_world.resources.ui_handles.tree.cameras_only = cameras_only;
editor_world.resources.ui_handles.tree.last_signature = 0;
editor_world.resources.ui_handles.tree.last_visible_start = usize::MAX;
}
}
pub fn update(editor_world: &mut EditorWorld, world: &mut World) {
let signature = signature_for(world, editor_world.resources.ui_handles.tree.cameras_only);
if signature != editor_world.resources.ui_handles.tree.last_signature {
editor_world.resources.ui_handles.tree.last_signature = signature;
rebuild_rows(editor_world, world);
editor_world.resources.ui_handles.tree.last_visible_start = usize::MAX;
}
let selected = editor_world.resources.ui.selected_entity;
let selected_len = editor_world.resources.ui.selected_entities.len();
if selected != editor_world.resources.ui_handles.tree.selected_entity
|| selected_len != editor_world.resources.ui_handles.tree.selected_entities_len
{
editor_world.resources.ui_handles.tree.selected_entity = selected;
editor_world.resources.ui_handles.tree.selected_entities_len = selected_len;
editor_world.resources.ui_handles.tree.last_visible_start = usize::MAX;
}
refresh_visible_indices(editor_world);
populate_pool(editor_world, world);
}
fn signature_for(world: &World, cameras_only: bool) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
cameras_only.hash(&mut hasher);
world
.core
.query()
.with(nightshade::ecs::world::LOCAL_TRANSFORM | nightshade::ecs::world::GLOBAL_TRANSFORM)
.iter(|entity, _, _| {
entity.id.hash(&mut hasher);
let parent_id = world
.core
.get_parent(entity)
.and_then(|p| p.0)
.map(|p| p.id)
.unwrap_or(0);
parent_id.hash(&mut hasher);
let cam = world.core.entity_has_camera(entity);
cam.hash(&mut hasher);
let name_hash = world
.core
.get_name(entity)
.map(|n| {
let mut h = std::collections::hash_map::DefaultHasher::new();
n.0.hash(&mut h);
h.finish()
})
.unwrap_or(0);
name_hash.hash(&mut hasher);
});
hasher.finish()
}
fn rebuild_rows(editor_world: &mut EditorWorld, world: &World) {
let cameras_only = editor_world.resources.ui_handles.tree.cameras_only;
let mode = editor_world.resources.mode.mode;
editor_world.resources.ui_handles.tree.all_rows.clear();
let mut all_entities: HashSet<Entity> = HashSet::new();
world
.core
.query()
.with(nightshade::ecs::world::LOCAL_TRANSFORM | nightshade::ecs::world::GLOBAL_TRANSFORM)
.iter(|entity, _, _| {
all_entities.insert(entity);
});
if !cameras_only {
all_entities.retain(|entity| match mode {
EditorMode::Object => {
let nearest = nearest_prefab_instance(world, *entity);
match nearest {
Some(root) => root == *entity,
None => true,
}
}
EditorMode::Edit { target } => is_descendant_of(world, *entity, target),
});
}
if cameras_only {
let mut cameras: Vec<Entity> = all_entities
.iter()
.copied()
.filter(|entity| world.core.entity_has_camera(*entity))
.collect();
cameras.sort_by_key(|entity| entity.id);
let rows = cameras
.into_iter()
.map(|entity| TreeRow {
label: label_for(world, entity),
entity,
depth: 0,
has_children: false,
})
.collect();
editor_world.resources.ui_handles.tree.all_rows = rows;
return;
}
let mut children_map: HashMap<Entity, Vec<Entity>> = HashMap::new();
let mut roots: Vec<Entity> = Vec::new();
for entity in &all_entities {
let parent = world
.core
.get_parent(*entity)
.and_then(|p| p.0)
.filter(|parent| all_entities.contains(parent));
match parent {
Some(parent) => children_map.entry(parent).or_default().push(*entity),
None => roots.push(*entity),
}
}
roots.sort_by_key(|entity| entity.id);
for children in children_map.values_mut() {
children.sort_by_key(|entity| entity.id);
}
let mut rows = Vec::new();
let mut visited: HashSet<Entity> = HashSet::new();
let mut stack: Vec<(Entity, usize)> = roots.iter().rev().map(|entity| (*entity, 0)).collect();
while let Some((entity, depth)) = stack.pop() {
if !visited.insert(entity) {
continue;
}
let has_children = children_map
.get(&entity)
.is_some_and(|children| !children.is_empty());
rows.push(TreeRow {
label: label_for(world, entity),
entity,
depth,
has_children,
});
if let Some(children) = children_map.get(&entity) {
for child in children.iter().rev() {
if !visited.contains(child) {
stack.push((*child, depth + 1));
}
}
}
}
editor_world.resources.ui_handles.tree.all_rows = rows;
}
fn label_for(world: &World, entity: Entity) -> String {
world
.core
.get_name(entity)
.map(|n| n.0.clone())
.filter(|n| !n.is_empty())
.unwrap_or_else(|| format!("Entity {}", entity.id))
}
fn refresh_visible_indices(editor_world: &mut EditorWorld) {
let tree = &mut editor_world.resources.ui_handles.tree;
tree.visible_indices.clear();
let filter = tree.filter_lower.clone();
let cameras_only = tree.cameras_only;
let mut skip_until_depth: Option<usize> = None;
for (idx, row) in tree.all_rows.iter().enumerate() {
if let Some(depth) = skip_until_depth {
if row.depth > depth {
continue;
} else {
skip_until_depth = None;
}
}
let collapsed =
!cameras_only && row.has_children && !tree.expanded.contains(&row.entity.id);
let matches_filter = filter.is_empty() || row.label.to_lowercase().contains(&filter);
if matches_filter {
tree.visible_indices.push(idx);
}
if collapsed && filter.is_empty() {
skip_until_depth = Some(row.depth);
}
}
}
fn populate_pool(editor_world: &mut EditorWorld, world: &mut World) {
let total = editor_world.resources.ui_handles.tree.visible_indices.len();
let list = editor_world.resources.ui_handles.tree.list;
ui_virtual_list_set_count(world, list, total);
let visible_start = world
.ui
.get_ui_virtual_list(list)
.map(|d| d.visible_start)
.unwrap_or(0);
let prior_start = editor_world.resources.ui_handles.tree.last_visible_start;
let icon_set = world.resources.retained_ui.icon_set;
let icon_lookup = IconLookup::for_set(icon_set);
let pool_rows = editor_world.resources.ui_handles.tree.pool_rows.clone();
let pool_size = pool_rows.len();
let visible_indices = editor_world
.resources
.ui_handles
.tree
.visible_indices
.clone();
let all_rows = editor_world.resources.ui_handles.tree.all_rows.clone();
let expanded = editor_world.resources.ui_handles.tree.expanded.clone();
let selected = editor_world.resources.ui.selected_entity;
let selected_set: std::collections::HashSet<Entity> = editor_world
.resources
.ui
.selected_entities
.iter()
.copied()
.collect();
let cameras_only = editor_world.resources.ui_handles.tree.cameras_only;
let filter_active = !editor_world
.resources
.ui_handles
.tree
.filter_lower
.is_empty();
let accent = world
.resources
.retained_ui
.theme_state
.active_theme()
.accent_color;
let primary_bg = vec4(accent.x, accent.y, accent.z, 0.3);
let secondary_bg = vec4(accent.x, accent.y, accent.z, 0.15);
let transparent = vec4(0.0, 0.0, 0.0, 0.0);
if visible_start == prior_start && total <= pool_size {
}
for (pool_index, pool) in pool_rows.iter().enumerate() {
let visible_idx = visible_start + pool_index;
let row_idx = visible_indices.get(visible_idx).copied();
let row = row_idx.and_then(|idx| all_rows.get(idx)).cloned();
if let Some(row) = row {
if let Some(node) = world.ui.get_ui_layout_node_mut(pool.indent) {
node.flow_child_size =
Some(Ab(vec2(row.depth as f32 * INDENT_PER_DEPTH, ROW_HEIGHT)).into());
}
let arrow_glyph = if cameras_only || filter_active || !row.has_children {
""
} else if expanded.contains(&row.entity.id) {
"\u{25BC}"
} else {
"\u{25B6}"
};
ui_set_text(world, pool.arrow, arrow_glyph);
let icon_glyph = if world.core.entity_has_camera(row.entity) {
Some(icon_lookup.videocam)
} else if world.core.entity_has_light(row.entity) {
Some(icon_lookup.lightbulb)
} else if world.core.entity_has_render_mesh(row.entity) {
Some(icon_lookup.mesh)
} else {
None
};
match icon_glyph {
Some(glyph) => ui_set_icon(world, pool.icon, glyph),
None => ui_set_text(world, pool.icon, ""),
}
ui_set_text(world, pool.label, &row.label);
let bg = if Some(row.entity) == selected {
primary_bg
} else if selected_set.contains(&row.entity) {
secondary_bg
} else {
transparent
};
if let Some(color) = world.ui.get_ui_node_color_mut(pool.container) {
color.colors[UiBase::INDEX] = Some(bg);
}
} else {
ui_set_text(world, pool.arrow, "");
ui_set_text(world, pool.label, "");
ui_set_text(world, pool.icon, "");
}
}
editor_world.resources.ui_handles.tree.last_visible_start = visible_start;
ui_mark_render_dirty(world);
}