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;

use super::InteractionSnapshot;
use super::snapshot_interaction;

pub(super) fn handle_scroll_area(
    world: &mut World,
    entity: freecs::Entity,
    interaction: &InteractionSnapshot,
    data: &crate::ecs::ui::components::UiScrollAreaData,
    scroll_delta: Vec2,
    mouse_position: Vec2,
    dpi_scale: f32,
) {
    let visible_height = world
        .ui
        .get_ui_layout_node(entity)
        .map(|node| node.computed_rect.height())
        .unwrap_or(0.0);

    let children: Vec<freecs::Entity> = world
        .resources
        .children_cache
        .get(&data.content_entity)
        .map(|v| v.to_vec())
        .unwrap_or_default();

    let content_flow = world
        .ui
        .get_ui_layout_node(data.content_entity)
        .and_then(|node| node.flow_layout);

    let mut total_content_height = 0.0f32;
    let mut visible_count = 0usize;
    for child in &children {
        if let Some(node) = world.ui.get_ui_layout_node(*child)
            && node.visible
        {
            total_content_height += node.computed_rect.height();
            visible_count += 1;
        }
    }

    if let Some(flow) = content_flow {
        if visible_count > 1 {
            total_content_height += flow.spacing * dpi_scale * (visible_count - 1) as f32;
        }
        total_content_height += flow.padding * dpi_scale * 2.0;
    }

    let max_scroll = (total_content_height - visible_height).max(0.0) / dpi_scale;
    let mut scroll_offset = data.scroll_offset;
    let mut thumb_dragging = data.thumb_dragging;
    let mut thumb_drag_start_offset = data.thumb_drag_start_offset;

    if interaction.hovered && scroll_delta.y.abs() > 0.0 {
        scroll_offset -= scroll_delta.y * 40.0;
        scroll_offset = scroll_offset.clamp(0.0, max_scroll);
    }

    let thumb_interaction = snapshot_interaction(world, data.thumb_entity);

    if thumb_interaction.pressed && !thumb_dragging {
        thumb_dragging = true;
        thumb_drag_start_offset = scroll_offset;
    }

    if thumb_dragging
        && thumb_interaction.pressed
        && let Some(drag_start) = thumb_interaction.drag_start
    {
        let track_rect = world
            .ui
            .get_ui_layout_node(data.track_entity)
            .map(|n| n.computed_rect);
        if let Some(track) = track_rect {
            let track_height = track.height();
            let thumb_ratio = if total_content_height > 0.0 {
                visible_height / total_content_height
            } else {
                1.0
            };
            let scrollable_track = track_height * (1.0 - thumb_ratio);
            if scrollable_track > 0.0 {
                let mouse_delta = mouse_position.y - drag_start.y;
                let scroll_per_pixel = max_scroll / scrollable_track;
                scroll_offset = (thumb_drag_start_offset + mouse_delta * scroll_per_pixel)
                    .clamp(0.0, max_scroll);
            }
        }
    }

    if !thumb_interaction.pressed {
        thumb_dragging = false;
    }

    if let Some(snap) = data.snap_interval
        && snap > 0.0
        && !thumb_dragging
        && scroll_delta.y.abs() < f32::EPSILON
    {
        let target = (scroll_offset / snap).round() * snap;
        let target = target.clamp(0.0, max_scroll);
        let delta_time = world.resources.retained_ui.delta_time;
        let diff = target - scroll_offset;
        if diff.abs() > 0.1 {
            scroll_offset += diff * (10.0 * delta_time).min(1.0);
        } else {
            scroll_offset = target;
        }
    }

    if let Some(content_node) = world.ui.get_ui_layout_node_mut(data.content_entity)
        && let Some(crate::ecs::ui::layout_types::UiLayoutType::Boundary(boundary)) =
            content_node.layouts[crate::ecs::ui::state::UiBase::INDEX].as_mut()
    {
        boundary.position_1 = crate::ecs::ui::units::Ab(Vec2::new(0.0, -scroll_offset)).into();
    }

    let show_scrollbar = total_content_height > visible_height;
    if let Some(track_node) = world.ui.get_ui_layout_node_mut(data.track_entity) {
        track_node.visible = show_scrollbar;
    }

    if show_scrollbar {
        let thumb_ratio = (visible_height / total_content_height).clamp(0.05, 1.0);
        let track_rect = world
            .ui
            .get_ui_layout_node(data.track_entity)
            .map(|n| n.computed_rect);
        if let Some(track) = track_rect {
            let track_height = track.height();
            let thumb_height_physical = (track_height * thumb_ratio).max(20.0 * dpi_scale);
            let scrollable_track = track_height - thumb_height_physical;
            let scroll_ratio = if max_scroll > 0.0 {
                scroll_offset / max_scroll
            } else {
                0.0
            };
            let thumb_y = scroll_ratio * scrollable_track / dpi_scale;
            let thumb_height = thumb_height_physical / dpi_scale;

            if let Some(thumb_node) = world.ui.get_ui_layout_node_mut(data.thumb_entity)
                && let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
                    thumb_node.layouts[crate::ecs::ui::state::UiBase::INDEX].as_mut()
            {
                window.position = crate::ecs::ui::units::Ab(Vec2::new(0.0, thumb_y)).into();
                window.size = crate::ecs::ui::units::Ab(Vec2::new(8.0, thumb_height)).into();
            }
        }
    }

    if let Some(UiWidgetState::ScrollArea(widget_data)) = world.ui.get_ui_widget_state_mut(entity) {
        widget_data.scroll_offset = scroll_offset;
        widget_data.content_height = total_content_height;
        widget_data.visible_height = visible_height;
        widget_data.thumb_dragging = thumb_dragging;
        widget_data.thumb_drag_start_offset = thumb_drag_start_offset;
    }
}

pub(super) fn handle_virtual_list(
    world: &mut World,
    entity: freecs::Entity,
    data: &crate::ecs::ui::components::UiVirtualListData,
    dpi_scale: f32,
    frame_keys: &[(KeyCode, bool)],
    focused_entity: Option<freecs::Entity>,
) {
    let scroll_offset = if let Some(UiWidgetState::ScrollArea(scroll_data)) =
        world.ui.get_ui_widget_state(data.scroll_entity)
    {
        scroll_data.scroll_offset
    } else {
        0.0
    };

    let visible_start = if data.item_height > 0.0 {
        (scroll_offset / data.item_height).floor() as usize
    } else {
        0
    };
    let visible_start = visible_start.min(data.total_items.saturating_sub(data.pool_size));
    let visible_end = data.total_items.min(visible_start + data.pool_size);

    let top_height = visible_start as f32 * data.item_height;
    let bottom_height = data.total_items.saturating_sub(visible_end) as f32 * data.item_height;

    if let Some(top_node) = world.ui.get_ui_layout_node_mut(data.top_spacer) {
        top_node.flow_child_size = Some(
            crate::ecs::ui::units::Rl(Vec2::new(100.0, 0.0))
                + crate::ecs::ui::units::Ab(Vec2::new(0.0, top_height)),
        );
    }
    if let Some(bottom_node) = world.ui.get_ui_layout_node_mut(data.bottom_spacer) {
        bottom_node.flow_child_size = Some(
            crate::ecs::ui::units::Rl(Vec2::new(100.0, 0.0))
                + crate::ecs::ui::units::Ab(Vec2::new(0.0, bottom_height)),
        );
    }

    let mut selection = data.selection;
    let mut selection_changed = false;

    for (pool_index, item) in data.pool_items.iter().enumerate() {
        let item_index = visible_start + pool_index;
        let is_visible = item_index < data.total_items;

        if let Some(node) = world.ui.get_ui_layout_node_mut(item.container_entity) {
            node.visible = is_visible;
        }

        if is_visible {
            let interaction = snapshot_interaction(world, item.container_entity);
            if interaction.clicked {
                selection = Some(item_index);
                selection_changed = true;
                world.resources.retained_ui.frame_events.push(
                    crate::ecs::ui::resources::UiEvent::VirtualListItemClicked {
                        entity,
                        item_index,
                    },
                );
            }

            let accent_color = world
                .resources
                .retained_ui
                .theme_state
                .active_theme()
                .accent_color;
            let is_selected = selection == Some(item_index);
            if let Some(crate::ecs::ui::components::UiNodeContent::Rect {
                border_width,
                border_color,
                ..
            }) = world.ui.get_ui_node_content_mut(item.container_entity)
            {
                if is_selected {
                    *border_width = 1.0 / dpi_scale;
                    *border_color = accent_color;
                } else {
                    *border_width = 0.0;
                }
            }
        }
    }

    let is_focused = focused_entity == Some(entity)
        || data
            .pool_items
            .iter()
            .any(|item| focused_entity == Some(item.container_entity));
    if is_focused && data.total_items > 0 {
        for &(key, pressed) in frame_keys {
            if !pressed {
                continue;
            }
            match key {
                KeyCode::ArrowUp => {
                    let current = selection.unwrap_or(0);
                    if current > 0 {
                        selection = Some(current - 1);
                        selection_changed = true;
                    }
                }
                KeyCode::ArrowDown => {
                    let current = selection.map_or(0, |s| s + 1);
                    if current < data.total_items {
                        selection = Some(current);
                        selection_changed = true;
                    }
                }
                KeyCode::Home => {
                    selection = Some(0);
                    selection_changed = true;
                }
                KeyCode::End => {
                    selection = Some(data.total_items - 1);
                    selection_changed = true;
                }
                _ => {}
            }
        }
    }

    if let Some(UiWidgetState::VirtualList(widget_data)) = world.ui.get_ui_widget_state_mut(entity)
    {
        widget_data.visible_start = visible_start;
        widget_data.selection = selection;
        widget_data.selection_changed = selection_changed;
    }
}