concinnity-dev 0.19.119

The Concinnity dev tooling library: world authoring, the in-engine editor, the debug server, docs and packaging
//! EditorHook: the Content panel's data assembly and click handling. Items come
//! from the cooked asset tree (authored + expansion assets), narrowed to the
//! visual types, filtered by the panel's type chip, and ranked by the search
//! query; cells bind baked thumbnails through `editor/thumbs.rs` and fall back
//! to typed icon chips.

use concinnity_cook::authoring::registry::RegisteredType;
use concinnity_core::ecs::World;

use crate::editor::filter;
use crate::editor::hook::{EditorHook, scroll_step};
use crate::editor::panels::content_panel::{self, ContentAction};
use crate::editor::thumbs;
use crate::editor::widget;

// The asset types the grid shows, the type-chip cycle order. "All" leads.
pub(in crate::editor::hook) const VISUAL_TYPES: [RegisteredType; 7] = [
    RegisteredType::EnvironmentMap,
    RegisteredType::Material,
    RegisteredType::Mesh,
    RegisteredType::Model,
    RegisteredType::Prefab,
    RegisteredType::ProceduralMesh,
    RegisteredType::Texture,
];

impl EditorHook {
    // The type chip's caption for the current cycle position.
    pub(in crate::editor::hook) fn content_type_caption(&self) -> &'static str {
        match self.content.type_chip {
            0 => "All",
            i => VISUAL_TYPES[(i - 1).min(VISUAL_TYPES.len() - 1)].as_str(),
        }
    }

    pub(in crate::editor::hook) fn cycle_content_type(&mut self) {
        self.content.type_chip = (self.content.type_chip + 1) % (VISUAL_TYPES.len() + 1);
        self.content.scroll = 0;
    }

    // Every visual asset under the current type filter and search query, best
    // match first: `(name, type)` pairs over the cooked tree.
    pub(super) fn content_items(&self, world: &World) -> Vec<(String, RegisteredType)> {
        let mut items: Vec<(String, RegisteredType)> = self
            .assets
            .groups
            .iter()
            .flat_map(|g| g.assets.iter())
            .filter(|a| VISUAL_TYPES.contains(&a.asset_type))
            .filter(|a| match self.content.type_chip {
                0 => true,
                i => a.asset_type == VISUAL_TYPES[i - 1],
            })
            .map(|a| (a.name.clone(), a.asset_type))
            .collect();
        items.sort_by(|a, b| (&a.0, a.1.as_str()).cmp(&(&b.0, b.1.as_str())));
        items.dedup();
        let query = widget::field_text(world, content_panel::SEARCH_INPUT);
        if query.trim().is_empty() {
            return items;
        }
        filter::ranked(items.iter().map(|(n, t)| (n.as_str(), t.as_str())), &query)
            .into_iter()
            .map(|i| items[i].clone())
            .collect()
    }

    // The visible cell window plus the filtered total, for the panel draw.
    pub(in crate::editor::hook) fn content_cells(
        &self,
        world: &World,
    ) -> (Vec<content_panel::CellView>, usize) {
        let items = self.content_items(world);
        let thumbs = thumbs::injected();
        let selected = self.selected_names();
        let first = self.content.scroll * content_panel::COLS;
        let cells = items
            .iter()
            .skip(first)
            .take(content_panel::CELLS)
            .map(|(name, ty)| content_panel::CellView {
                thumb: thumbs.get(name),
                selected: selected.contains(name),
                name: name.clone(),
                asset_type: *ty,
            })
            .collect();
        (cells, items.len())
    }

    pub(in crate::editor::hook) fn scroll_content(&mut self, delta: f32, world: &World) {
        let total = self.content_items(world).len();
        let rows = total.div_ceil(content_panel::COLS);
        let max = rows.saturating_sub(content_panel::GRID_ROWS);
        self.content.scroll = scroll_step(self.content.scroll, delta, max);
    }

    pub(in crate::editor::hook) fn apply_content_action(
        &mut self,
        action: ContentAction,
        world: &mut World,
        at: [f32; 2],
    ) {
        match action {
            ContentAction::FocusSearch => {
                self.content.search_focus = true;
                let text = widget::field_text(world, content_panel::SEARCH_INPUT);
                widget::focus_field_with(world, content_panel::SEARCH_INPUT, &text);
            }
            ContentAction::CycleType => {
                self.content.search_focus = false;
                self.cycle_content_type();
            }
            ContentAction::SelectCell(slot) => {
                self.content.search_focus = false;
                let items = self.content_items(world);
                let index = self.content.scroll * content_panel::COLS + slot;
                if let Some((name, ty)) = items.get(index) {
                    if self.shift_held {
                        self.toggle_named(name);
                    } else {
                        self.select_named(name);
                        // A plain press may also become a drag-out placement.
                        self.arm_content_drag(name.clone(), *ty, at);
                    }
                }
            }
            ContentAction::Consume => self.content.search_focus = false,
        }
    }
}