nightshade 0.13.1

A cross-platform data-oriented game engine.
Documentation
use nalgebra_glm::Vec4;

use crate::ecs::text::components::{TextAlignment, TextProperties, VerticalAlignment};
use crate::ecs::text::resources::FontAtlasData;
use crate::ecs::ui::components::{DropZone, SplitDirection, TileId, TileNode, UiWidgetState};
use crate::ecs::ui::render_sync::select_best_font;
use crate::ecs::ui::types::Rect;
use crate::ecs::ui::types::UiTextInstance;
use crate::ecs::world::World;
use crate::render::wgpu::passes::geometry::{UiLayer, UiRect};
use crate::render::wgpu::text_mesh::generate_text_mesh;

pub(super) fn emit_tile_containers(world: &mut World, dpi_scale: f32) {
    let tile_entities: Vec<freecs::Entity> = world
        .ui
        .query_entities(crate::ecs::world::UI_WIDGET_STATE)
        .collect();

    struct TileEmit {
        rects: Vec<UiRect>,
        texts: Vec<(String, nalgebra_glm::Vec2, f32, Vec4)>,
        overlay_texts: Vec<(String, nalgebra_glm::Vec2, f32, Vec4)>,
        container_depth: f32,
        clip_rect: Option<Rect>,
        layer: UiLayer,
    }

    let mut emits = Vec::new();

    for entity in tile_entities {
        let (data, depth, clip_rect, layer) = {
            let is_tile = matches!(
                world.ui.get_ui_widget_state(entity),
                Some(UiWidgetState::TileContainer(_))
            );
            if !is_tile {
                continue;
            }

            let node = match world.ui.get_ui_layout_node(entity) {
                Some(node)
                    if node.visible
                        && (node.computed_rect.width() > 0.0
                            || node.computed_rect.height() > 0.0) =>
                {
                    node
                }
                _ => continue,
            };

            let depth = node.computed_depth;
            let clip = node.computed_clip_rect;
            let node_layer = node.computed_layer.unwrap_or(UiLayer::Background);

            let data = match world.ui.get_ui_widget_state(entity) {
                Some(UiWidgetState::TileContainer(data)) => data.clone(),
                _ => continue,
            };

            (data, depth, clip, node_layer)
        };

        let theme = world.resources.retained_ui.theme_state.active_theme();
        let border_color = theme.border_color;
        let panel_header_color = theme.panel_header_color;
        let accent_color = theme.accent_color;
        let panel_color = theme.panel_color;
        let text_color = theme.text_color;
        let font_size = theme.font_size;

        let z_index = (depth * 100.0) as i32 + 150;
        let mut rects = Vec::new();
        let mut texts = Vec::new();
        let mut overlay_texts = Vec::new();

        let total_panes = data
            .tiles
            .iter()
            .filter(|t| matches!(t, Some(TileNode::Pane { .. })))
            .count();

        for (index, tile) in data.tiles.iter().enumerate() {
            if let Some(TileNode::Split {
                direction,
                children,
                ..
            }) = tile
            {
                let child0_rect = match data.rects.get(children[0].0) {
                    Some(r) => *r,
                    None => continue,
                };

                let (splitter_pos, splitter_size) = match direction {
                    SplitDirection::Horizontal => (
                        nalgebra_glm::Vec2::new(child0_rect.max.x, child0_rect.min.y),
                        nalgebra_glm::Vec2::new(data.splitter_width, child0_rect.height()),
                    ),
                    SplitDirection::Vertical => (
                        nalgebra_glm::Vec2::new(child0_rect.min.x, child0_rect.max.y),
                        nalgebra_glm::Vec2::new(child0_rect.width(), data.splitter_width),
                    ),
                };

                rects.push(UiRect {
                    position: splitter_pos,
                    size: splitter_size,
                    color: border_color,
                    corner_radius: 0.0,
                    border_width: 0.0,
                    border_color: Vec4::new(0.0, 0.0, 0.0, 0.0),
                    rotation: 0.0,
                    clip_rect,
                    layer,
                    z_index,
                });
            }

            if let Some(TileNode::Tabs { panes, active }) = tile {
                let show_close = total_panes >= 2;

                let tabs_rect = match data.rects.get(index) {
                    Some(r) => *r,
                    None => continue,
                };

                let tab_bar_rect = Rect {
                    min: tabs_rect.min,
                    max: nalgebra_glm::Vec2::new(
                        tabs_rect.max.x,
                        tabs_rect.min.y + data.tab_bar_height,
                    ),
                };

                rects.push(UiRect {
                    position: tab_bar_rect.min,
                    size: tab_bar_rect.size(),
                    color: panel_header_color,
                    corner_radius: 0.0,
                    border_width: 0.0,
                    border_color: Vec4::new(0.0, 0.0, 0.0, 0.0),
                    rotation: 0.0,
                    clip_rect,
                    layer,
                    z_index,
                });

                if !panes.is_empty() {
                    let tab_width = tab_bar_rect.width() / panes.len() as f32;
                    for (tab_index, &pane_id) in panes.iter().enumerate() {
                        let tab_x = tab_bar_rect.min.x + tab_index as f32 * tab_width;
                        let is_active = tab_index == *active;

                        let tab_bg = if is_active { accent_color } else { panel_color };

                        rects.push(UiRect {
                            position: nalgebra_glm::Vec2::new(
                                tab_x + 1.0,
                                tab_bar_rect.min.y + 1.0,
                            ),
                            size: nalgebra_glm::Vec2::new(
                                tab_width - 2.0,
                                data.tab_bar_height - 2.0,
                            ),
                            color: tab_bg,
                            corner_radius: 3.0 * dpi_scale,
                            border_width: 0.0,
                            border_color: Vec4::new(0.0, 0.0, 0.0, 0.0),
                            rotation: 0.0,
                            clip_rect,
                            layer,
                            z_index: z_index + 1,
                        });

                        if let Some(TileNode::Pane { title, .. }) = data.get(pane_id) {
                            let close_w = data.tab_close_width();
                            let title_offset = if show_close { close_w * 0.5 } else { 0.0 };
                            let text_x = tab_x + (tab_width - title_offset) * 0.5;
                            let text_y = tab_bar_rect.min.y + data.tab_bar_height * 0.5;
                            texts.push((
                                title.clone(),
                                nalgebra_glm::Vec2::new(text_x, text_y),
                                font_size * 0.8,
                                text_color,
                            ));

                            if show_close {
                                let close_x = tab_x + tab_width - close_w * 0.5 - 1.0;
                                let close_y = tab_bar_rect.min.y + data.tab_bar_height * 0.5;
                                let is_hovered =
                                    if let Some((hover_tabs, hover_idx)) = data.hovered_close {
                                        hover_tabs == TileId(index) && hover_idx == tab_index
                                    } else {
                                        false
                                    };
                                let alpha = if is_hovered {
                                    text_color.w
                                } else {
                                    text_color.w * 0.5
                                };
                                let close_color =
                                    Vec4::new(text_color.x, text_color.y, text_color.z, alpha);
                                texts.push((
                                    "\u{00D7}".to_string(),
                                    nalgebra_glm::Vec2::new(close_x, close_y),
                                    font_size * 0.75,
                                    close_color,
                                ));
                            }
                        }
                    }
                }

                rects.push(UiRect {
                    position: nalgebra_glm::Vec2::new(
                        tabs_rect.min.x,
                        tabs_rect.min.y + data.tab_bar_height - 1.0,
                    ),
                    size: nalgebra_glm::Vec2::new(tabs_rect.width(), 1.0),
                    color: border_color,
                    corner_radius: 0.0,
                    border_width: 0.0,
                    border_color: Vec4::new(0.0, 0.0, 0.0, 0.0),
                    rotation: 0.0,
                    clip_rect,
                    layer,
                    z_index: z_index + 2,
                });
            }
        }

        if let Some(ref preview) = data.drop_preview {
            let target_rect = data.rects.get(preview.target_tile.0).copied();
            if let Some(target_rect) = target_rect {
                let (preview_pos, preview_size) = match preview.zone {
                    DropZone::Left => (
                        target_rect.min,
                        nalgebra_glm::Vec2::new(target_rect.width() * 0.5, target_rect.height()),
                    ),
                    DropZone::Right => (
                        nalgebra_glm::Vec2::new(
                            target_rect.min.x + target_rect.width() * 0.5,
                            target_rect.min.y,
                        ),
                        nalgebra_glm::Vec2::new(target_rect.width() * 0.5, target_rect.height()),
                    ),
                    DropZone::Top => (
                        target_rect.min,
                        nalgebra_glm::Vec2::new(target_rect.width(), target_rect.height() * 0.5),
                    ),
                    DropZone::Bottom => (
                        nalgebra_glm::Vec2::new(
                            target_rect.min.x,
                            target_rect.min.y + target_rect.height() * 0.5,
                        ),
                        nalgebra_glm::Vec2::new(target_rect.width(), target_rect.height() * 0.5),
                    ),
                    DropZone::Center => (target_rect.min, target_rect.size()),
                };

                let preview_color = Vec4::new(accent_color.x, accent_color.y, accent_color.z, 0.2);
                rects.push(UiRect {
                    position: preview_pos,
                    size: preview_size,
                    color: preview_color,
                    corner_radius: 0.0,
                    border_width: 2.0 * dpi_scale,
                    border_color: Vec4::new(accent_color.x, accent_color.y, accent_color.z, 0.6),
                    rotation: 0.0,
                    clip_rect: None,
                    layer: UiLayer::DockIndicator,
                    z_index: z_index + 10,
                });
            }
        }

        if let Some((source_tabs_id, tab_index, _)) = data.dragging_tab {
            let mouse_pos = world.resources.input.mouse.position;
            if let Some(TileNode::Tabs { panes, .. }) = data.get(source_tabs_id)
                && tab_index < panes.len()
            {
                let pane_id = panes[tab_index];
                if let Some(TileNode::Pane { title, .. }) = data.get(pane_id) {
                    let ghost_width = 120.0;
                    let ghost_height = data.tab_bar_height;

                    rects.push(UiRect {
                        position: nalgebra_glm::Vec2::new(
                            mouse_pos.x - ghost_width * 0.5,
                            mouse_pos.y - ghost_height * 0.5,
                        ),
                        size: nalgebra_glm::Vec2::new(ghost_width, ghost_height),
                        color: Vec4::new(accent_color.x, accent_color.y, accent_color.z, 0.8),
                        corner_radius: 4.0 * dpi_scale,
                        border_width: 1.0 * dpi_scale,
                        border_color,
                        rotation: 0.0,
                        clip_rect: None,
                        layer: UiLayer::DockIndicator,
                        z_index: z_index + 20,
                    });

                    overlay_texts.push((
                        title.clone(),
                        nalgebra_glm::Vec2::new(mouse_pos.x, mouse_pos.y),
                        font_size * 0.8,
                        text_color,
                    ));
                }
            }
        }

        emits.push(TileEmit {
            rects,
            texts,
            overlay_texts,
            container_depth: depth,
            clip_rect,
            layer,
        });
    }

    let bitmap_fonts: Vec<FontAtlasData> = world
        .resources
        .text_cache
        .font_manager
        .bitmap_fonts
        .iter()
        .map(|arc| arc.as_ref().clone())
        .collect();

    for emit in emits {
        world.resources.retained_ui.frame_rects.extend(emit.rects);

        let base_z = (emit.container_depth * 100.0) as i32;
        for (text, position, size, color) in &emit.texts {
            let physical_size = *size * dpi_scale;
            if let Some((tile_font_idx, font_data)) = select_best_font(&bitmap_fonts, physical_size)
            {
                let properties = TextProperties {
                    font_size: physical_size,
                    color: *color,
                    alignment: TextAlignment::Center,
                    vertical_alignment: VerticalAlignment::Middle,
                    ..TextProperties::default()
                };
                let mesh = generate_text_mesh(text, font_data, &properties);
                world
                    .resources
                    .retained_ui
                    .frame_text_meshes
                    .push(UiTextInstance {
                        mesh,
                        position: *position,
                        color: *color,
                        outline_color: Vec4::new(0.0, 0.0, 0.0, 0.0),
                        outline_width: 0.0,
                        font_size: physical_size,
                        font_index: tile_font_idx,
                        clip_rect: emit.clip_rect,
                        layer: emit.layer,
                        z_index: base_z + 155,
                        character_colors: None,
                        character_background_colors: None,
                    });
            }
        }
        for (text, position, size, color) in &emit.overlay_texts {
            let physical_size = *size * dpi_scale;
            if let Some((tile_font_idx, font_data)) = select_best_font(&bitmap_fonts, physical_size)
            {
                let properties = TextProperties {
                    font_size: physical_size,
                    color: *color,
                    alignment: TextAlignment::Center,
                    vertical_alignment: VerticalAlignment::Middle,
                    ..TextProperties::default()
                };
                let mesh = generate_text_mesh(text, font_data, &properties);
                world
                    .resources
                    .retained_ui
                    .frame_text_meshes
                    .push(UiTextInstance {
                        mesh,
                        position: *position,
                        color: *color,
                        outline_color: Vec4::new(0.0, 0.0, 0.0, 0.0),
                        outline_width: 0.0,
                        font_size: physical_size,
                        font_index: tile_font_idx,
                        clip_rect: None,
                        layer: UiLayer::DockIndicator,
                        z_index: base_z + 175,
                        character_colors: None,
                        character_background_colors: None,
                    });
            }
        }
    }
}