nightshade-editor 0.14.2

Interactive map editor for the Nightshade game engine
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(&params_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
}