use crate::ecs::EditorWorld;
use nightshade::ecs::material::components::Material;
use nightshade::ecs::material::resources::material_registry_iter;
use nightshade::prelude::*;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
const MATERIALS_RECT: Rect = Rect {
min: Vec2::new(140.0, 140.0),
max: Vec2::new(620.0, 620.0),
};
#[derive(Default, Clone)]
pub struct MaterialsHandles {
pub panel: Entity,
pub filter_input: Entity,
pub status_label: Entity,
pub list_root: Entity,
pub last_filter: String,
pub signature: u64,
pub list_items: Vec<Entity>,
}
pub fn build(tree: &mut UiTreeBuilder) -> MaterialsHandles {
let panel = tree.add_floating_panel("materials_browser", "Materials", MATERIALS_RECT);
ui_set_visible(tree.world_mut(), panel, false);
let content = super::panel_content(tree, panel);
let mut filter_input = Entity::default();
let mut status_label = Entity::default();
let mut list_root = Entity::default();
tree.in_parent(content, |tree| {
filter_input = tree.add_text_input("Search materials");
status_label = status_label_node(tree, "0 materials");
let wrapper = tree.add_node().fill_width().flex_grow(1.0).entity();
let scroll = tree.in_parent(wrapper, |tree| tree.add_scroll_area_fill(4.0, 4.0));
list_root = widget::<UiScrollAreaData>(tree.world_mut(), scroll)
.map(|d| d.content_entity)
.unwrap_or(scroll);
});
MaterialsHandles {
panel,
filter_input,
status_label,
list_root,
last_filter: String::new(),
signature: u64::MAX,
list_items: Vec::new(),
}
}
fn status_label_node(tree: &mut UiTreeBuilder, initial: &str) -> Entity {
let theme = tree
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font = theme.font_size;
let dim = vec4(0.7, 0.7, 0.75, 1.0);
tree.add_node()
.size(100.pct(), (18.0).px())
.with_text(initial, font * 0.85)
.text_left()
.color_raw::<UiBase>(dim)
.entity()
}
pub fn update(editor_world: &mut EditorWorld, world: &mut World) {
let filter_input = editor_world.resources.ui_handles.materials.filter_input;
let status_label = editor_world.resources.ui_handles.materials.status_label;
let list_root = editor_world.resources.ui_handles.materials.list_root;
let filter_text = widget::<UiTextInputData>(world, filter_input)
.map(|data| data.text.clone())
.unwrap_or_default();
let signature = compute_signature(world);
let prior_signature = editor_world.resources.ui_handles.materials.signature;
let prior_filter = editor_world
.resources
.ui_handles
.materials
.last_filter
.clone();
let filter_changed = filter_text != prior_filter;
let signature_changed = signature != prior_signature;
if !filter_changed && !signature_changed {
return;
}
editor_world.resources.ui_handles.materials.signature = signature;
editor_world.resources.ui_handles.materials.last_filter = filter_text.clone();
let prior = std::mem::take(&mut editor_world.resources.ui_handles.materials.list_items);
for entity in prior {
despawn_recursive_immediate(world, entity);
}
let needle = filter_text.to_lowercase();
let mut entries: Vec<MaterialEntry> =
material_registry_iter(&world.resources.assets.material_registry)
.filter(|(name, _)| {
if needle.is_empty() {
return true;
}
name.to_lowercase().contains(&needle)
})
.map(|(name, material)| MaterialEntry::from(name.clone(), material))
.collect();
entries.sort_by(|a, b| a.name.cmp(&b.name));
let total = material_registry_iter(&world.resources.assets.material_registry).count();
let shown = entries.len();
let status_text = if needle.is_empty() {
format!("{total} materials")
} else {
format!("{shown} of {total} materials")
};
ui_set_text(world, status_label, &status_text);
let mut new_items: Vec<Entity> = Vec::with_capacity(entries.len());
{
let mut tree_builder = UiTreeBuilder::from_parent(world, list_root);
for entry in &entries {
let row = build_material_row(&mut tree_builder, entry);
new_items.push(row);
}
tree_builder.finish_subtree();
}
editor_world.resources.ui_handles.materials.list_items = new_items;
}
struct MaterialEntry {
name: String,
base_color: [f32; 4],
metallic: f32,
roughness: f32,
textures: Vec<(&'static str, String)>,
}
impl MaterialEntry {
fn from(name: String, material: &Material) -> Self {
let mut textures: Vec<(&'static str, String)> = Vec::new();
if let Some(path) = &material.base_texture {
textures.push(("base", path.clone()));
}
if let Some(path) = &material.normal_texture {
textures.push(("normal", path.clone()));
}
if let Some(path) = &material.metallic_roughness_texture {
textures.push(("metallic-roughness", path.clone()));
}
if let Some(path) = &material.occlusion_texture {
textures.push(("occlusion", path.clone()));
}
if let Some(path) = &material.emissive_texture {
textures.push(("emissive", path.clone()));
}
if let Some(path) = &material.transmission_texture {
textures.push(("transmission", path.clone()));
}
if let Some(path) = &material.thickness_texture {
textures.push(("thickness", path.clone()));
}
if let Some(path) = &material.specular_texture {
textures.push(("specular", path.clone()));
}
if let Some(path) = &material.specular_color_texture {
textures.push(("specular-color", path.clone()));
}
Self {
name,
base_color: material.base_color,
metallic: material.metallic,
roughness: material.roughness,
textures,
}
}
}
fn compute_signature(world: &World) -> u64 {
let mut hasher = DefaultHasher::new();
let mut count: u64 = 0;
for (name, material) in material_registry_iter(&world.resources.assets.material_registry) {
count += 1;
name.hash(&mut hasher);
for component in material.base_color {
component.to_bits().hash(&mut hasher);
}
material.metallic.to_bits().hash(&mut hasher);
material.roughness.to_bits().hash(&mut hasher);
material.base_texture.hash(&mut hasher);
material.normal_texture.hash(&mut hasher);
material.metallic_roughness_texture.hash(&mut hasher);
material.occlusion_texture.hash(&mut hasher);
material.emissive_texture.hash(&mut hasher);
material.transmission_texture.hash(&mut hasher);
material.thickness_texture.hash(&mut hasher);
material.specular_texture.hash(&mut hasher);
material.specular_color_texture.hash(&mut hasher);
}
count.hash(&mut hasher);
hasher.finish()
}
fn build_material_row(tree: &mut UiTreeBuilder, entry: &MaterialEntry) -> Entity {
let theme = tree
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font = theme.font_size;
let bg = theme.input_background_color;
let border = theme.border_color;
let row_height = 56.0 + entry.textures.len() as f32 * 16.0;
let row = tree
.add_node()
.size(100.pct(), (row_height).px())
.flow(FlowDirection::Vertical, 6.0, 4.0)
.with_rect(4.0, 1.0, border)
.color_raw::<UiBase>(bg)
.entity();
tree.in_parent(row, |tree| {
let header = tree
.add_node()
.size(100.pct(), (20.0).px())
.flow(FlowDirection::Horizontal, 0.0, 8.0)
.entity();
tree.in_parent(header, |tree| {
tree.add_node()
.size((20.0).px(), (20.0).px())
.with_rect(3.0, 1.0, border)
.color_raw::<UiBase>(vec4(
entry.base_color[0],
entry.base_color[1],
entry.base_color[2],
entry.base_color[3],
))
.entity();
tree.add_node()
.flow_child(Rl(vec2(0.0, 100.0)))
.flex_grow(1.0)
.with_text(&entry.name, font * 0.9)
.text_left()
.with_text_overflow(TextOverflow::Ellipsis)
.fg(ThemeColor::Text)
.entity();
});
let params_text = format!(
"metallic {:.2} roughness {:.2} rgba {:.2}, {:.2}, {:.2}, {:.2}",
entry.metallic,
entry.roughness,
entry.base_color[0],
entry.base_color[1],
entry.base_color[2],
entry.base_color[3],
);
tree.add_node()
.size(100.pct(), (16.0).px())
.with_text(¶ms_text, font * 0.8)
.text_left()
.color_raw::<UiBase>(vec4(0.7, 0.7, 0.75, 1.0))
.entity();
for (kind, path) in &entry.textures {
let line = format!("{kind}: {path}");
tree.add_node()
.size(100.pct(), (16.0).px())
.with_text(&line, font * 0.78)
.text_left()
.with_text_overflow(TextOverflow::Ellipsis)
.color_raw::<UiBase>(vec4(0.62, 0.7, 0.82, 1.0))
.entity();
}
});
row
}