nightshade 0.43.0

A cross-platform data-oriented game engine.
Documentation
use nalgebra_glm::{Vec2, Vec4};

use crate::ecs::text::components::{TextAlignment, VerticalAlignment};
use crate::ecs::ui::builder::UiTreeBuilder;
use crate::ecs::ui::components::*;
use crate::ecs::ui::layout_types::FlowDirection;
use crate::ecs::ui::state::{UiBase, UiFocused, UiHover, UiPressed};
use crate::ecs::ui::types::Anchor;
use crate::ecs::ui::units::{Ab, Rl};

impl<'a> UiTreeBuilder<'a> {
    pub fn add_button(&mut self, text: &str) -> freecs::Entity {
        let theme = self.active_theme();
        let font_size = theme.font_size;
        let button_height = theme.button_height;
        let corner_radius = theme.corner_radius;
        let border_width = theme.border_width;
        let border_color = theme.border_color;

        let text_slot = self.world_mut().resources.text.cache.add_text(text);

        let button_entity = self
            .add_node()
            .flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
            .with_rect(corner_radius, border_width, border_color)
            .with_theme_border_color(ThemeColor::Border)
            .with_theme_color::<UiBase>(ThemeColor::Background)
            .with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
            .with_theme_color::<UiPressed>(ThemeColor::BackgroundActive)
            .with_theme_effect_role(crate::ecs::ui::components::ThemeEffect::ButtonEffect)
            .with_theme_shadow_role::<UiBase>(crate::ecs::ui::components::ThemeShadow::ButtonShadow)
            .with_theme_shadow_role::<UiHover>(
                crate::ecs::ui::components::ThemeShadow::ButtonShadowHover,
            )
            .with_theme_shadow_role::<UiPressed>(
                crate::ecs::ui::components::ThemeShadow::ButtonShadowPressed,
            )
            .with_theme_shadow_role::<UiFocused>(crate::ecs::ui::components::ThemeShadow::FocusGlow)
            .with_interaction()
            .with_transition::<UiHover>(8.0, 6.0)
            .with_transition::<UiPressed>(12.0, 8.0)
            .with_transition::<UiFocused>(8.0, 6.0)
            .with_state_offset::<UiHover>(Vec2::new(0.0, -2.0))
            .with_state_offset::<UiPressed>(Vec2::new(0.0, 2.0))
            .with_cursor_icon(winit::window::CursorIcon::Pointer)
            .with_children(|tree| {
                tree.add_node()
                    .with_text_slot(text_slot, font_size)
                    .with_theme_color::<UiBase>(ThemeColor::Text)
                    .entity();
            })
            .entity();

        self.world_mut().ui.set_ui_button(
            button_entity,
            UiButtonData {
                clicked: false,
                text_slot: Some(text_slot),
            },
        );
        if let Some(interaction) = self
            .world_mut()
            .ui
            .get_ui_node_interaction_mut(button_entity)
        {
            interaction.accessible_role = Some(AccessibleRole::Button);
        }
        self.assign_tab_index(button_entity);

        button_entity
    }

    pub fn add_button_colored(&mut self, text: &str, color: Vec4) -> freecs::Entity {
        let theme = self.active_theme();
        let font_size = theme.font_size;
        let button_height = theme.button_height;
        let corner_radius = theme.corner_radius;
        let border_width = theme.border_width;
        let border_color = theme.border_color;

        let hover_color = Vec4::new(
            (color.x + 0.1).min(1.0),
            (color.y + 0.1).min(1.0),
            (color.z + 0.1).min(1.0),
            color.w,
        );
        let active_color = Vec4::new(
            (color.x - 0.1).max(0.0),
            (color.y - 0.1).max(0.0),
            (color.z - 0.1).max(0.0),
            color.w,
        );

        let text_slot = self.world_mut().resources.text.cache.add_text(text);

        let button_entity = self
            .add_node()
            .flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
            .with_rect(corner_radius, border_width, border_color)
            .with_theme_border_color(ThemeColor::Border)
            .color_raw::<UiBase>(color)
            .color_raw::<UiHover>(hover_color)
            .color_raw::<UiPressed>(active_color)
            .with_theme_shadow_role::<UiBase>(crate::ecs::ui::components::ThemeShadow::ButtonShadow)
            .with_theme_shadow_role::<UiHover>(
                crate::ecs::ui::components::ThemeShadow::ButtonShadowHover,
            )
            .with_theme_shadow_role::<UiPressed>(
                crate::ecs::ui::components::ThemeShadow::ButtonShadowPressed,
            )
            .with_theme_shadow_role::<UiFocused>(crate::ecs::ui::components::ThemeShadow::FocusGlow)
            .with_interaction()
            .with_transition::<UiHover>(8.0, 6.0)
            .with_transition::<UiPressed>(12.0, 8.0)
            .with_transition::<UiFocused>(8.0, 6.0)
            .with_state_offset::<UiHover>(Vec2::new(0.0, -2.0))
            .with_state_offset::<UiPressed>(Vec2::new(0.0, 2.0))
            .with_cursor_icon(winit::window::CursorIcon::Pointer)
            .with_children(|tree| {
                tree.add_node()
                    .with_text_slot(text_slot, font_size)
                    .with_theme_color::<UiBase>(ThemeColor::Text)
                    .entity();
            })
            .entity();

        self.world_mut().ui.set_ui_button(
            button_entity,
            UiButtonData {
                clicked: false,
                text_slot: Some(text_slot),
            },
        );
        if let Some(interaction) = self
            .world_mut()
            .ui
            .get_ui_node_interaction_mut(button_entity)
        {
            interaction.accessible_role = Some(AccessibleRole::Button);
        }
        self.assign_tab_index(button_entity);

        button_entity
    }

    pub fn add_icon_button(
        &mut self,
        texture_index: u32,
        icon_size: Vec2,
        text: &str,
    ) -> freecs::Entity {
        let theme = self.active_theme();
        let font_size = theme.font_size;
        let button_height = theme.button_height;
        let corner_radius = theme.corner_radius;
        let border_width = theme.border_width;
        let border_color = theme.border_color;

        let text_slot = self.world_mut().resources.text.cache.add_text(text);

        let button_entity = self
            .add_node()
            .flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
            .with_rect(corner_radius, border_width, border_color)
            .with_theme_border_color(ThemeColor::Border)
            .with_theme_color::<UiBase>(ThemeColor::Background)
            .with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
            .with_theme_color::<UiPressed>(ThemeColor::BackgroundActive)
            .with_theme_effect_role(crate::ecs::ui::components::ThemeEffect::ButtonEffect)
            .with_theme_shadow_role::<UiBase>(crate::ecs::ui::components::ThemeShadow::ButtonShadow)
            .with_theme_shadow_role::<UiHover>(
                crate::ecs::ui::components::ThemeShadow::ButtonShadowHover,
            )
            .with_theme_shadow_role::<UiPressed>(
                crate::ecs::ui::components::ThemeShadow::ButtonShadowPressed,
            )
            .with_theme_shadow_role::<UiFocused>(crate::ecs::ui::components::ThemeShadow::FocusGlow)
            .with_interaction()
            .with_transition::<UiHover>(8.0, 6.0)
            .with_transition::<UiPressed>(12.0, 8.0)
            .with_transition::<UiFocused>(8.0, 6.0)
            .with_state_offset::<UiHover>(Vec2::new(0.0, -2.0))
            .with_state_offset::<UiPressed>(Vec2::new(0.0, 2.0))
            .with_cursor_icon(winit::window::CursorIcon::Pointer)
            .flow(FlowDirection::Horizontal, 8.0, 8.0)
            .with_children(|tree| {
                tree.add_node()
                    .flow_child(Ab(icon_size))
                    .with_image(texture_index)
                    .with_theme_color::<UiBase>(ThemeColor::Text)
                    .entity();

                tree.add_node()
                    .flow_child(Ab(Vec2::new(0.0, button_height)))
                    .flex_grow(1.0)
                    .with_text_slot(text_slot, font_size)
                    .with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
                    .with_theme_color::<UiBase>(ThemeColor::Text)
                    .entity();
            })
            .entity();

        self.world_mut().ui.set_ui_button(
            button_entity,
            UiButtonData {
                clicked: false,
                text_slot: Some(text_slot),
            },
        );
        if let Some(interaction) = self
            .world_mut()
            .ui
            .get_ui_node_interaction_mut(button_entity)
        {
            interaction.accessible_role = Some(AccessibleRole::Button);
        }
        self.assign_tab_index(button_entity);

        button_entity
    }

    pub fn add_button_rich(&mut self, spans: &[TextSpan]) -> freecs::Entity {
        let theme = self.active_theme();
        let font_size = theme.font_size;
        let button_height = theme.button_height;
        let corner_radius = theme.corner_radius;
        let border_width = theme.border_width;
        let border_color = theme.border_color;
        let text_color = theme.text_color;

        let button_entity = self
            .add_node()
            .flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
            .with_rect(corner_radius, border_width, border_color)
            .with_theme_border_color(ThemeColor::Border)
            .with_theme_color::<UiBase>(ThemeColor::Background)
            .with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
            .with_theme_color::<UiPressed>(ThemeColor::BackgroundActive)
            .with_theme_effect_role(crate::ecs::ui::components::ThemeEffect::ButtonEffect)
            .with_theme_shadow_role::<UiBase>(crate::ecs::ui::components::ThemeShadow::ButtonShadow)
            .with_theme_shadow_role::<UiHover>(
                crate::ecs::ui::components::ThemeShadow::ButtonShadowHover,
            )
            .with_theme_shadow_role::<UiPressed>(
                crate::ecs::ui::components::ThemeShadow::ButtonShadowPressed,
            )
            .with_theme_shadow_role::<UiFocused>(crate::ecs::ui::components::ThemeShadow::FocusGlow)
            .with_interaction()
            .with_transition::<UiHover>(8.0, 6.0)
            .with_transition::<UiPressed>(12.0, 8.0)
            .with_transition::<UiFocused>(8.0, 6.0)
            .with_state_offset::<UiHover>(Vec2::new(0.0, -2.0))
            .with_state_offset::<UiPressed>(Vec2::new(0.0, 2.0))
            .with_cursor_icon(winit::window::CursorIcon::Pointer)
            .entity();

        self.push_parent(button_entity);

        let span_container = self
            .add_node()
            .flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
            .flow(FlowDirection::Horizontal, 0.0, 8.0)
            .flow_wrap()
            .entity();

        self.push_parent(span_container);

        for span in spans {
            self.build_text_span(span, font_size, text_color, Some(button_height), false);
        }

        self.pop_parent();
        self.pop_parent();

        self.world_mut().ui.set_ui_button(
            button_entity,
            UiButtonData {
                clicked: false,
                text_slot: None,
            },
        );
        if let Some(interaction) = self
            .world_mut()
            .ui
            .get_ui_node_interaction_mut(button_entity)
        {
            interaction.accessible_role = Some(AccessibleRole::Button);
        }
        self.assign_tab_index(button_entity);

        button_entity
    }

    pub fn add_image_node(&mut self, texture_index: u32, size: Vec2) -> freecs::Entity {
        self.add_node()
            .flow_child(Ab(size))
            .with_image(texture_index)
            .color_raw::<UiBase>(Vec4::new(1.0, 1.0, 1.0, 1.0))
            .entity()
    }

    pub fn add_spinner(&mut self) -> freecs::Entity {
        let dot_count = 8;
        let size = 28.0_f32;
        let radius = 9.0_f32;
        let dot_size = 5.0_f32;
        let center = size * 0.5;

        let entity = self
            .add_node()
            .flow_child(Ab(Vec2::new(size, size)))
            .entity();

        self.push_parent(entity);

        let theme = self.active_theme();
        let accent = theme.accent_color;

        let mut dots = Vec::with_capacity(dot_count);
        for index in 0..dot_count {
            let angle = (index as f32 / dot_count as f32) * std::f32::consts::TAU
                - std::f32::consts::FRAC_PI_2;
            let x = center + angle.cos() * radius - dot_size * 0.5;
            let y = center + angle.sin() * radius - dot_size * 0.5;
            let dot = self
                .add_node()
                .window(
                    Ab(Vec2::new(x, y)),
                    Ab(Vec2::new(dot_size, dot_size)),
                    Anchor::TopLeft,
                )
                .with_rect(dot_size * 0.5, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
                .color_raw::<UiBase>(accent)
                .entity();
            dots.push(dot);
        }

        self.pop_parent();

        self.world_mut().ui.set_ui_spinner(
            entity,
            UiSpinnerData {
                dots,
                rotation_speed: 6.5,
                phase: 0.0,
            },
        );

        entity
    }

    pub fn add_selectable_label(&mut self, text: &str, group_id: Option<u32>) -> freecs::Entity {
        let theme = self.active_theme();
        let font_size = theme.font_size;
        let corner_radius = theme.corner_radius;

        let text_slot = self.world_mut().resources.text.cache.add_text(text);

        let label_entity = self
            .add_node()
            .flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, font_size * 1.5)))
            .with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
            .color_raw::<UiBase>(Vec4::new(0.0, 0.0, 0.0, 0.0))
            .with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
            .with_theme_color::<UiPressed>(ThemeColor::BackgroundActive)
            .with_interaction()
            .with_transition::<UiHover>(10.0, 6.0)
            .with_transition::<UiPressed>(14.0, 8.0)
            .with_cursor_icon(winit::window::CursorIcon::Pointer)
            .with_children(|tree| {
                tree.add_node()
                    .boundary(Ab(Vec2::new(8.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
                    .with_text_slot(text_slot, font_size)
                    .with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
                    .with_theme_color::<UiBase>(ThemeColor::Text)
                    .entity();
            })
            .entity();

        self.world_mut().ui.set_ui_selectable_label(
            label_entity,
            UiSelectableLabelData {
                selected: false,
                changed: false,
                text_slot,
                group_id,
            },
        );
        if let Some(gid) = group_id {
            self.world_mut()
                .resources
                .retained_ui
                .groups
                .selectable_labels
                .entry(gid)
                .or_default()
                .push(label_entity);
        }

        label_entity
    }
}