nightshade 0.13.0

A cross-platform data-oriented game engine.
Documentation
use crate::ecs::ui::state::UiStateTrait as _;
use nalgebra_glm::Vec2;
use winit::keyboard::KeyCode;

use crate::ecs::ui::components::UiWidgetState;
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(UiWidgetState::ContextMenu(widget_data)) =
            world.ui.get_ui_widget_state_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.frame_keys;
    for (key, is_pressed) in frame_keys {
        if *is_pressed && *key == KeyCode::Escape {
            should_close = true;
        }
    }

    let hovered = world.resources.retained_ui.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 Some(hovered_ent) = hovered {
        let mut is_inside =
            data.item_entities.contains(&hovered_ent) || hovered_ent == data.popup_entity;
        if !is_inside && use_defs {
            is_inside = is_entity_in_submenu_popups(&data.item_defs, hovered_ent, world);
        }
        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(UiWidgetState::ContextMenu(widget_data)) =
            world.ui.get_ui_widget_state_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.active_context_menu = None;

        if let Some(cmd_id) = clicked_command {
            world.resources.retained_ui.frame_events.push(
                crate::ecs::ui::resources::UiEvent::ContextMenuItemClicked {
                    entity,
                    item_index: cmd_id,
                },
            );
        }
    }
}

fn check_defs_for_click(
    world: &World,
    defs: &[crate::ecs::ui::components::ContextMenuItemDef],
) -> Option<usize> {
    for def in defs {
        if let crate::ecs::ui::components::ContextMenuItemKind::Action = &def.kind
            && 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);
            }
        }
        if let crate::ecs::ui::components::ContextMenuItemKind::Submenu { children, .. } = &def.kind
            && 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.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 = world.resources.window.cached_scale_factor;
                    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.layouts[crate::ecs::ui::state::UiBase::INDEX].as_mut()
                    {
                        window.position = crate::ecs::ui::units::Ab(pos).into();
                        node.visible = true;
                    }
                }
                if let Some(UiWidgetState::ContextMenu(menu_data)) =
                    world.ui.get_ui_widget_state_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(UiWidgetState::ContextMenu(menu_data)) =
                        world.ui.get_ui_widget_state_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(UiWidgetState::ContextMenu(menu_data)) =
                    world.ui.get_ui_widget_state(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);
        }
    }
}