nightshade 0.14.1

A cross-platform data-oriented game engine.
Documentation
use nalgebra_glm::Vec2;
use winit::keyboard::KeyCode;

use crate::ecs::world::World;

pub(super) fn handle_context_menu(
    world: &mut World,
    entity: freecs::Entity,
    data: &crate::ecs::ui::components::UiContextMenuData,
) {
    if !data.open {
        if let Some(widget_data) = world.ui.get_ui_context_menu_mut(entity) {
            widget_data.clicked_item = None;
        }
        return;
    }

    let use_defs = !data.item_defs.is_empty();
    let mut clicked_command = None;
    let mut should_close = false;

    if use_defs {
        clicked_command = check_defs_for_click(world, &data.item_defs);

        handle_submenu_hover(world, entity, &data.item_defs);
    } else {
        for (index, item_entity) in data.item_entities.iter().enumerate() {
            if *item_entity == freecs::Entity::default() {
                continue;
            }
            let item_clicked = world
                .ui
                .get_ui_node_interaction(*item_entity)
                .map(|i| i.clicked)
                .unwrap_or(false);
            if item_clicked {
                clicked_command = Some(index);
                should_close = true;
                break;
            }
        }
    }

    if clicked_command.is_some() {
        should_close = true;
    }

    let frame_keys = &world.resources.retained_ui.input.frame_keys;
    for (key, is_pressed) in frame_keys {
        if *is_pressed && *key == KeyCode::Escape {
            should_close = true;
        }
    }

    let hovered = world
        .resources
        .retained_ui
        .interaction_for_active_mut()
        .hovered_entity;
    let mouse_state = world.resources.input.mouse.state;
    let left_just_pressed =
        mouse_state.contains(crate::ecs::input::resources::MouseState::LEFT_JUST_PRESSED);

    if left_just_pressed {
        let is_inside = match hovered {
            Some(hovered_ent) => {
                let mut inside =
                    data.item_entities.contains(&hovered_ent) || hovered_ent == data.popup_entity;
                if !inside && use_defs {
                    inside = is_entity_in_submenu_popups(&data.item_defs, hovered_ent, world);
                }
                inside
            }
            None => false,
        };
        if !is_inside {
            should_close = true;
        }
    }

    if should_close {
        if let Some(node) = world.ui.get_ui_layout_node_mut(data.popup_entity) {
            node.visible = false;
        }
        close_submenu_popups(world, &data.item_defs);
        if let Some(widget_data) = world.ui.get_ui_context_menu_mut(entity) {
            widget_data.open = false;
            widget_data.clicked_item = clicked_command;
            close_submenu_defs_state(&mut widget_data.item_defs);
        }
        world.resources.retained_ui.overlays.active_context_menu = None;

        if let Some(cmd_id) = clicked_command {
            toggle_checkable_for_command(world, entity, cmd_id);
            let tag = lookup_tag(&data.item_defs, cmd_id);
            world.resources.retained_ui.events_for_active_mut().push(
                crate::ecs::ui::resources::UiEvent::ContextMenuItemClicked {
                    entity,
                    item_index: cmd_id,
                    tag,
                },
            );
        }
    }
}

fn toggle_checkable_for_command(world: &mut World, menu_entity: freecs::Entity, cmd_id: usize) {
    let Some(data) = world.ui.get_ui_context_menu_mut(menu_entity) else {
        return;
    };
    toggle_checkable_in_defs(&mut data.item_defs, cmd_id, &mut world.resources.text.cache);
}

fn toggle_checkable_in_defs(
    defs: &mut [crate::ecs::ui::components::ContextMenuItemDef],
    cmd_id: usize,
    text_cache: &mut crate::ecs::text::resources::TextCache,
) -> bool {
    for def in defs.iter_mut() {
        let command_id = def.command_id;
        if let crate::ecs::ui::components::ContextMenuItemKind::Checkable {
            checked,
            check_text_slot,
        } = &mut def.kind
        {
            if command_id == Some(cmd_id) {
                *checked = !*checked;
                text_cache.set_text(*check_text_slot, if *checked { "\u{2713}" } else { "" });
                return true;
            }
        } else if let crate::ecs::ui::components::ContextMenuItemKind::Submenu { children, .. } =
            &mut def.kind
            && toggle_checkable_in_defs(children, cmd_id, text_cache)
        {
            return true;
        }
    }
    false
}

fn lookup_tag(defs: &[crate::ecs::ui::components::ContextMenuItemDef], cmd_id: usize) -> u32 {
    for def in defs {
        match &def.kind {
            crate::ecs::ui::components::ContextMenuItemKind::Action
            | crate::ecs::ui::components::ContextMenuItemKind::Checkable { .. }
                if def.command_id == Some(cmd_id) =>
            {
                return def.tag;
            }
            crate::ecs::ui::components::ContextMenuItemKind::Submenu { children, .. } => {
                let tag = lookup_tag(children, cmd_id);
                if tag != 0 {
                    return tag;
                }
            }
            _ => {}
        }
    }
    0
}

fn check_defs_for_click(
    world: &World,
    defs: &[crate::ecs::ui::components::ContextMenuItemDef],
) -> Option<usize> {
    for def in defs {
        match &def.kind {
            crate::ecs::ui::components::ContextMenuItemKind::Action
            | crate::ecs::ui::components::ContextMenuItemKind::Checkable { .. } => {
                if let Some(cmd_id) = def.command_id {
                    let clicked = world
                        .ui
                        .get_ui_node_interaction(def.row_entity)
                        .map(|i| i.clicked)
                        .unwrap_or(false);
                    if clicked {
                        return Some(cmd_id);
                    }
                }
            }
            crate::ecs::ui::components::ContextMenuItemKind::Submenu { children, .. } => {
                if let Some(cmd_id) = check_defs_for_click(world, children) {
                    return Some(cmd_id);
                }
            }
            _ => {}
        }
    }
    None
}

fn handle_submenu_hover(
    world: &mut World,
    menu_entity: freecs::Entity,
    defs: &[crate::ecs::ui::components::ContextMenuItemDef],
) {
    let hovered = world
        .resources
        .retained_ui
        .interaction_for_active_mut()
        .hovered_entity;

    let mut submenu_indices = Vec::new();
    for (index, def) in defs.iter().enumerate() {
        if let crate::ecs::ui::components::ContextMenuItemKind::Submenu { .. } = &def.kind {
            submenu_indices.push(index);
        }
    }

    for &sub_index in &submenu_indices {
        let def = &defs[sub_index];
        let row_hovered = hovered == Some(def.row_entity);

        if let crate::ecs::ui::components::ContextMenuItemKind::Submenu {
            popup_entity,
            open,
            children,
            ..
        } = &def.kind
        {
            let child_popup = *popup_entity;
            let was_open = *open;

            if row_hovered && !was_open {
                let row_rect = world
                    .ui
                    .get_ui_layout_node(def.row_entity)
                    .map(|n| n.computed_rect);
                if let Some(rect) = row_rect {
                    let dpi_scale = crate::ecs::window::resources::window_scale_factor(world);
                    let pos = Vec2::new(rect.max.x, rect.min.y) / dpi_scale;
                    if let Some(node) = world.ui.get_ui_layout_node_mut(child_popup)
                        && let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
                            node.base_layout.as_mut()
                    {
                        window.position = crate::ecs::ui::units::Ab(pos).into();
                        node.visible = true;
                    }
                }
                if let Some(menu_data) = world.ui.get_ui_context_menu_mut(menu_entity)
                    && let Some(item_def) = menu_data.item_defs.get_mut(sub_index)
                    && let crate::ecs::ui::components::ContextMenuItemKind::Submenu { open, .. } =
                        &mut item_def.kind
                {
                    *open = true;
                }
            } else if !row_hovered && was_open {
                let popup_hovered = hovered == Some(child_popup);
                let child_item_hovered =
                    is_entity_in_submenu_popups(children, hovered.unwrap_or_default(), world)
                        || popup_hovered;

                if !child_item_hovered {
                    if let Some(node) = world.ui.get_ui_layout_node_mut(child_popup) {
                        node.visible = false;
                    }
                    close_submenu_popups(world, children);
                    if let Some(menu_data) = world.ui.get_ui_context_menu_mut(menu_entity)
                        && let Some(item_def) = menu_data.item_defs.get_mut(sub_index)
                        && let crate::ecs::ui::components::ContextMenuItemKind::Submenu {
                            open,
                            children,
                            ..
                        } = &mut item_def.kind
                    {
                        *open = false;
                        close_submenu_defs_state(children);
                    }
                }
            }

            if was_open {
                let current_defs =
                    if let Some(menu_data) = world.ui.get_ui_context_menu(menu_entity) {
                        if let Some(item_def) = menu_data.item_defs.get(sub_index) {
                            if let crate::ecs::ui::components::ContextMenuItemKind::Submenu {
                                children,
                                ..
                            } = &item_def.kind
                            {
                                Some(children.clone())
                            } else {
                                None
                            }
                        } else {
                            None
                        }
                    } else {
                        None
                    };
                if let Some(child_defs) = current_defs {
                    handle_submenu_hover(world, menu_entity, &child_defs);
                }
            }
        }
    }
}

fn is_entity_in_submenu_popups(
    defs: &[crate::ecs::ui::components::ContextMenuItemDef],
    target: freecs::Entity,
    world: &World,
) -> bool {
    for def in defs {
        if def.row_entity == target {
            return true;
        }
        if let crate::ecs::ui::components::ContextMenuItemKind::Submenu {
            popup_entity,
            children,
            ..
        } = &def.kind
        {
            if *popup_entity == target {
                return true;
            }
            let popup_rect = world
                .ui
                .get_ui_layout_node(*popup_entity)
                .map(|n| n.computed_rect);
            if let Some(rect) = popup_rect {
                let mouse_pos = world.resources.input.mouse.position;
                if rect.contains(mouse_pos) {
                    return true;
                }
            }
            if is_entity_in_submenu_popups(children, target, world) {
                return true;
            }
        }
    }
    false
}

pub(super) fn close_submenu_popups(
    world: &mut World,
    defs: &[crate::ecs::ui::components::ContextMenuItemDef],
) {
    for def in defs {
        if let crate::ecs::ui::components::ContextMenuItemKind::Submenu {
            popup_entity,
            children,
            ..
        } = &def.kind
        {
            if let Some(node) = world.ui.get_ui_layout_node_mut(*popup_entity) {
                node.visible = false;
            }
            close_submenu_popups(world, children);
        }
    }
}

pub(super) fn close_submenu_defs_state(
    defs: &mut [crate::ecs::ui::components::ContextMenuItemDef],
) {
    for def in defs.iter_mut() {
        if let crate::ecs::ui::components::ContextMenuItemKind::Submenu { open, children, .. } =
            &mut def.kind
        {
            *open = false;
            close_submenu_defs_state(children);
        }
    }
}