nightshade-editor 0.14.2

Interactive map editor for the Nightshade game engine
use crate::systems::retained_ui::viewport_controls::{self, ViewportControlsHandles};
use nightshade::prelude::*;

const OUTLINE_COLOR: Vec4 = Vec4::new(1.0, 0.647, 0.0, 1.0);
const OUTLINE_WIDTH: f32 = 3.0;

#[derive(Default, Clone)]
pub struct ViewportHandles {
    pub tile_container: Entity,
    pub viewport_pane: TileId,
    pub viewport_content: Entity,
    pub notes_pane: TileId,
    pub notes_content: Entity,
    pub outline: Entity,
    pub controls: Option<ViewportControlsHandles>,
    pub extra_camera_tiles: Vec<ExtraCameraTile>,
}

#[derive(Clone, Copy)]
pub struct ExtraCameraTile {
    pub pane_id: TileId,
    pub camera_entity: Entity,
    pub content_entity: Entity,
    pub controls: ViewportControlsHandles,
}

pub fn configure_viewport_pane(world: &mut World, content_entity: Entity) {
    world.ui.remove_components(
        content_entity,
        nightshade::ecs::world::UI_NODE_COLOR | nightshade::ecs::world::UI_NODE_CONTENT,
    );
    if let Some(node) = world.ui.get_ui_layout_node_mut(content_entity) {
        node.flow_layout = None;
        node.clip_content = false;
    }
}

pub fn build(tree: &mut UiTreeBuilder) -> ViewportHandles {
    let tile_container = tree.add_tile_container(vec2(0.0, 0.0));
    let world = tree.world_mut();

    world.ui.remove_components(
        tile_container,
        nightshade::ecs::world::UI_NODE_COLOR
            | nightshade::ecs::world::UI_NODE_CONTENT
            | nightshade::ecs::world::UI_THEME_BINDING,
    );
    if let Some(node) = world.ui.get_ui_layout_node_mut(tile_container) {
        node.clip_content = false;
    }

    let (viewport_pane, viewport_content) =
        ui_tile_add_pane(world, tile_container, "Viewport").expect("viewport pane");
    configure_viewport_pane(world, viewport_content);

    let (notes_pane, notes_content) =
        ui_tile_add_pane(world, tile_container, "Notes").expect("notes pane");
    let mut sub_tree = UiTreeBuilder::from_parent(world, notes_content);
    sub_tree.add_label("Drag tabs to rearrange. Click a tab to focus.");
    sub_tree.add_label("");
    sub_tree.add_label("The Viewport tab is transparent and reports its rect to");
    sub_tree.add_label("active_viewport_rect, so the camera and picking use");
    sub_tree.add_label("the visible area, not the full window.");
    sub_tree.finish_subtree();

    let outline = tree
        .add_node()
        .window(Ab(vec2(0.0, 0.0)), Ab(vec2(0.0, 0.0)), Anchor::TopLeft)
        .with_rect(0.0, OUTLINE_WIDTH, OUTLINE_COLOR)
        .color_raw::<UiBase>(vec4(0.0, 0.0, 0.0, 0.0))
        .with_layer(UiLayer::DockedPanels)
        .with_depth(UiDepthMode::Set(15.0))
        .entity();
    ui_set_visible(tree.world_mut(), outline, false);

    let controls = tree
        .world_mut()
        .resources
        .active_camera
        .map(|camera_entity| {
            viewport_controls::ensure_shading(tree.world_mut(), camera_entity);
            let world_mut = tree.world_mut();
            if !world_mut
                .core
                .entity_has_components(camera_entity, nightshade::ecs::world::VIEWPORT_UPDATE_MODE)
            {
                world_mut
                    .core
                    .add_components(camera_entity, nightshade::ecs::world::VIEWPORT_UPDATE_MODE);
                world_mut.core.set_viewport_update_mode(
                    camera_entity,
                    nightshade::ecs::camera::components::ViewportUpdateMode::WhenDirty,
                );
            }
            viewport_controls::build(tree, camera_entity)
        });

    ViewportHandles {
        tile_container,
        viewport_pane,
        viewport_content,
        notes_pane,
        notes_content,
        outline,
        controls,
        extra_camera_tiles: Vec::new(),
    }
}

pub fn update(
    world: &mut World,
    handles: &super::UiHandles,
    extra_camera_tiles: &[ExtraCameraTile],
) {
    let viewport_size = world
        .resources
        .window
        .cached_viewport_size
        .map(|(width, height)| vec2(width as f32, height as f32));
    let Some(viewport_size) = viewport_size else {
        world.resources.window.active_viewport_rect = None;
        world.resources.window.camera_tile_rects.clear();
        world.resources.user_interface.required_cameras.clear();
        ui_set_visible(world, handles.viewport.outline, false);
        return;
    };

    let reserved = ui_reserved_areas(world).clone();
    let dpi = world.resources.window.cached_scale_factor.max(0.0001);
    let center_min = vec2(reserved.left, reserved.top);
    let center_size = vec2(
        (viewport_size.x - reserved.left - reserved.right).max(0.0),
        (viewport_size.y - reserved.top - reserved.bottom).max(0.0),
    );

    if let Some(node) = world
        .ui
        .get_ui_layout_node_mut(handles.viewport.tile_container)
        && let Some(nightshade::ecs::ui::layout_types::UiLayoutType::Window(window)) =
            node.base_layout.as_mut()
    {
        window.position = nightshade::ecs::ui::units::Ab(center_min / dpi).into();
        window.size = nightshade::ecs::ui::units::Ab(center_size / dpi).into();
    }

    let viewport_active = ui_tile_active_pane(
        world,
        handles.viewport.tile_container,
        handles.viewport.viewport_pane,
    );
    let active_pane_rect = pane_rect(
        world,
        handles.viewport.tile_container,
        handles.viewport.viewport_pane,
    );

    let camera_tile_rects = &mut world.resources.window.camera_tile_rects;
    camera_tile_rects.clear();

    let active_camera = world.resources.active_camera;
    if let Some(camera) = active_camera {
        let pane_layout = world
            .ui
            .get_ui_layout_node(handles.viewport.viewport_content)
            .map(|node| (node.computed_rect, node.visible));
        if let Some((rect, visible)) = pane_layout
            && visible
            && rect.width() > 0.0
            && rect.height() > 0.0
        {
            world.resources.window.camera_tile_rects.insert(
                camera,
                ViewportRect {
                    x: rect.min.x,
                    y: rect.min.y,
                    width: rect.width(),
                    height: rect.height(),
                },
            );
        }
    }

    for tile in extra_camera_tiles {
        if Some(tile.camera_entity) == active_camera {
            continue;
        }
        let pane_layout = world
            .ui
            .get_ui_layout_node(tile.content_entity)
            .map(|node| (node.computed_rect, node.visible));
        if let Some((rect, visible)) = pane_layout
            && visible
            && rect.width() > 0.0
            && rect.height() > 0.0
        {
            world.resources.window.camera_tile_rects.insert(
                tile.camera_entity,
                ViewportRect {
                    x: rect.min.x,
                    y: rect.min.y,
                    width: rect.width(),
                    height: rect.height(),
                },
            );
        }
    }

    if let Some(controls) = handles.viewport.controls {
        viewport_controls::update(
            world,
            &controls,
            world
                .ui
                .get_ui_layout_node(handles.viewport.viewport_content)
                .filter(|node| node.visible)
                .map(|node| node.computed_rect),
        );
        viewport_controls::sync(world, &controls);
    }

    for tile in extra_camera_tiles {
        viewport_controls::update(
            world,
            &tile.controls,
            world
                .ui
                .get_ui_layout_node(tile.content_entity)
                .filter(|node| node.visible)
                .map(|node| node.computed_rect),
        );
        viewport_controls::sync(world, &tile.controls);
    }

    let required = &mut world.resources.user_interface.required_cameras;
    required.clear();
    if let Some(active) = active_camera
        && world
            .resources
            .window
            .camera_tile_rects
            .contains_key(&active)
    {
        required.push(active);
    }
    for camera in world.resources.window.camera_tile_rects.keys() {
        if Some(*camera) != active_camera {
            required.push(*camera);
        }
    }

    if viewport_active {
        if let Some(camera) = active_camera
            && let Some(rect) = world
                .resources
                .window
                .camera_tile_rects
                .get(&camera)
                .copied()
        {
            world.resources.window.active_viewport_rect = Some(rect);
        } else {
            world.resources.window.active_viewport_rect = None;
        }
    } else {
        world.resources.window.active_viewport_rect = None;
    }

    if let Some(rect) = active_pane_rect
        && rect.width() > 1.0
        && rect.height() > 1.0
    {
        ui_set_visible(world, handles.viewport.outline, true);
        if let Some(node) = world.ui.get_ui_layout_node_mut(handles.viewport.outline)
            && let Some(nightshade::ecs::ui::layout_types::UiLayoutType::Window(window)) =
                node.base_layout.as_mut()
        {
            window.position = nightshade::ecs::ui::units::Ab(rect.min / dpi).into();
            window.size = nightshade::ecs::ui::units::Ab(rect.size() / dpi).into();
        }
    } else {
        ui_set_visible(world, handles.viewport.outline, false);
    }
}

fn pane_rect(
    world: &World,
    container: Entity,
    pane_id: TileId,
) -> Option<nightshade::ecs::ui::types::Rect> {
    let data = world.ui.get_ui_tile_container(container)?;
    data.rects.get(pane_id.0).copied()
}