nightshade-api 0.43.0

Procedural high level API for the nightshade game engine
Documentation
//! Screen text anchored to a corner or the center, and 3d labels.

use crate::runner::UI_ROOT_NAME;
use nightshade::ecs::ui::state::UiStateTrait;
use nightshade::ecs::ui::units::UiValue;
use nightshade::prelude::*;

/// Where screen text sits, anchored to a window corner or the center.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScreenAnchor {
    TopLeft,
    TopRight,
    BottomLeft,
    BottomRight,
    Center,
}

/// Spawns screen text at the given anchor. It renders as a retained UI overlay
/// that stays fixed to the window, on top of the scene. Update the content with
/// [`set_text`], restyle it with [`set_text_color`] and [`set_text_size`].
pub fn spawn_text(world: &mut World, text: &str, anchor: ScreenAnchor) -> Entity {
    let root = ui_root(world);
    let (position, anchor_kind, alignment) = anchor_window(anchor);
    let entity = {
        let mut tree = UiTreeBuilder::from_parent(world, root);
        tree.add_node()
            .window(position, Ab(vec2(960.0, 96.0)), anchor_kind)
            .with_text(text, 18.0)
            .with_text_alignment(alignment, VerticalAlignment::Middle)
            .with_text_outline(Vec4::new(0.0, 0.0, 0.0, 1.0), 0.15)
            .color_raw::<UiBase>(Vec4::new(1.0, 1.0, 1.0, 1.0))
            .without_pointer_events()
            .entity()
    };
    ui_mark_render_dirty(world);
    entity
}

/// Spawns 3d text at `position` that always faces the camera.
pub fn spawn_label(world: &mut World, text: &str, position: Vec3) -> Entity {
    spawn_3d_billboard_text_with_properties(
        world,
        text,
        position,
        TextProperties {
            font_size: 24.0,
            alignment: TextAlignment::Center,
            vertical_alignment: VerticalAlignment::Middle,
            ..Default::default()
        },
    )
}

/// Sets a text entity's color as linear RGBA. Works on screen text and 3d
/// labels alike.
pub fn set_text_color(world: &mut World, entity: Entity, color: [f32; 4]) {
    let rgba = Vec4::new(color[0], color[1], color[2], color[3]);
    if is_screen_text(world, entity) {
        if let Some(node_color) = world.ui.get_ui_node_color_mut(entity) {
            node_color.colors[UiBase::INDEX] = Some(rgba);
        }
        ui_mark_render_dirty(world);
        return;
    }
    if let Some(component) = world.core.get_text_mut(entity) {
        component.set_color(rgba);
    }
}

/// Sets a text entity's font size. The same scale [`spawn_text`] and
/// [`spawn_label`] start from, so 18 to 48 reads well.
pub fn set_text_size(world: &mut World, entity: Entity, size: f32) {
    if is_screen_text(world, entity) {
        if let Some(UiNodeContent::Text {
            font_size_override, ..
        }) = world.ui.get_ui_node_content_mut(entity)
        {
            *font_size_override = Some(size);
        }
        ui_mark_render_dirty(world);
        return;
    }
    if let Some(component) = world.core.get_text_mut(entity) {
        component.set_font_size(size);
    }
}

/// Replaces the content of a text entity. Skips all work when the text is
/// unchanged, so calling it every frame with a formatted string is fine.
pub fn set_text(world: &mut World, entity: Entity, text: &str) {
    if let Some(slot) = screen_text_slot(world, entity) {
        if world.resources.text.cache.get_text(slot) == Some(text) {
            return;
        }
        world.resources.text.cache.set_text(slot, text);
        ui_mark_render_dirty(world);
        return;
    }
    let Some(text_index) = world
        .core
        .get_text(entity)
        .map(|component| component.text_index)
    else {
        return;
    };
    if world.resources.text.cache.get_text(text_index) == Some(text) {
        return;
    }
    world.resources.text.cache.set_text(text_index, text);
    if let Some(component) = world.core.get_text_mut(entity) {
        component.dirty = true;
    }
}

fn ui_root(world: &mut World) -> Entity {
    if let Some(&root) = world.resources.entities.names.get(UI_ROOT_NAME)
        && world.ui.get_ui_layout_root(root).is_some()
    {
        return root;
    }
    let root = UiTreeBuilder::new(world).finish();
    world
        .resources
        .entities
        .names
        .insert(UI_ROOT_NAME.to_string(), root);
    root
}

fn is_screen_text(world: &World, entity: Entity) -> bool {
    matches!(
        world.ui.get_ui_node_content(entity),
        Some(UiNodeContent::Text { .. })
    )
}

fn screen_text_slot(world: &World, entity: Entity) -> Option<usize> {
    match world.ui.get_ui_node_content(entity) {
        Some(UiNodeContent::Text { text_slot, .. }) => Some(*text_slot),
        _ => None,
    }
}

fn anchor_window(anchor: ScreenAnchor) -> (UiValue<Vec2>, Anchor, TextAlignment) {
    match anchor {
        ScreenAnchor::TopLeft => (
            Ab(vec2(20.0, 16.0)).into(),
            Anchor::TopLeft,
            TextAlignment::Left,
        ),
        ScreenAnchor::TopRight => (
            Rl(vec2(100.0, 0.0)) + Ab(vec2(-20.0, 16.0)),
            Anchor::TopRight,
            TextAlignment::Right,
        ),
        ScreenAnchor::BottomLeft => (
            Rl(vec2(0.0, 100.0)) + Ab(vec2(20.0, -16.0)),
            Anchor::BottomLeft,
            TextAlignment::Left,
        ),
        ScreenAnchor::BottomRight => (
            Rl(vec2(100.0, 100.0)) + Ab(vec2(-20.0, -16.0)),
            Anchor::BottomRight,
            TextAlignment::Right,
        ),
        ScreenAnchor::Center => (
            Rl(vec2(50.0, 50.0)).into(),
            Anchor::Center,
            TextAlignment::Center,
        ),
    }
}