bevy_pages 0.1.0

A lightweight and elegant framework to upgrade your Bevy UI experience.
Documentation
use crate::events::ElementSpawn;
use bevy::asset::AssetServer;
use bevy::color::Color;
use bevy::ecs::relationship::RelatedSpawnerCommands;
use bevy::math::Rect;
use bevy::prelude::{
    Button, ChildOf, Component, Entity, FontSize, FontSource, FontStyle, FontWeight, FontWidth,
    ImageNode, Node, NodeImageMode, Text, TextColor, TextFont, VisualBox,
};
use rustc_hash::FxHashMap;
use smol_str::SmolStr;

/// A UI element.
#[derive(Clone, Debug)]
pub enum Element {
    /// A simple element that will spawn a [Node] component among others.
    ///
    /// This is equivalent to a `<div>` in HTML.
    Node {
        /// The node behind this element.
        node: Node,
        /// The optional ID of this element.
        id: Option<ElementId>,
        /// The children of this element.
        children: Vec<Element>,
    },
    /// A button element that will spawn a [Button] component among others.
    Button {
        /// The node behind this element.
        node: Node,
        /// The optional ID of this element.
        id: Option<ElementId>,
        /// The children of this element.
        children: Vec<Element>,
    },
    /// A text element that will spawn a [Text] component among others.
    Text {
        /// The node behind this element.
        node: Node,
        /// The optional ID of this element.
        id: Option<ElementId>,
        /// The content of the text.
        content: String,
        /// An optional font to use. Specify a path to a font file here.
        font: Option<String>,
        /// The optional weight of the font.
        font_weight: Option<FontWeight>,
        /// The optional width of the font.
        font_width: Option<FontWidth>,
        /// The optional size of the font.
        font_size: Option<FontSize>,
        /// The optional style of the font.
        font_style: Option<FontStyle>,
        /// The optional color of the text.
        color: Option<Color>,
    },
    /// An image element that will spawn an [ImageNode] component among others.
    Image {
        /// The node behind this element.
        node: Node,
        /// The optional ID of this element.
        id: Option<ElementId>,
        /// The source of the image. Specify a path to an image file here.
        src: String,
        /// The optional color of the image.
        color: Option<Color>,
        /// The optional flip-x attribute of the image.
        flip_x: Option<bool>,
        /// The optional flip-y attribute of the image.
        flip_y: Option<bool>,
        /// The optional rect to use for the image.
        rect: Option<Rect>,
        /// The optional mode to use for the image.
        mode: Option<NodeImageMode>,
        /// The optional visual box to use for the image.
        visual_box: Option<VisualBox>,
    },
}

impl Element {
    pub(crate) fn spawn(
        &self,
        parent: &mut RelatedSpawnerCommands<ChildOf>,
        assets: &AssetServer,
        reg: &mut ElementRegistry,
    ) -> Entity {
        let entity = match self {
            Element::Node { node, id, children } => {
                let mut entity = parent.spawn((node.clone(), Button));

                if let Some(id) = id {
                    entity.insert(id.clone());
                }

                entity.with_children(|p| {
                    for child in children {
                        child.spawn(p, assets, reg);
                    }
                });

                entity.id()
            }

            Element::Button { node, id, children } => {
                let mut entity = parent.spawn((node.clone(), Button));

                if let Some(id) = id {
                    entity.insert(id.clone());
                }

                entity.with_children(|p| {
                    for child in children {
                        child.spawn(p, assets, reg);
                    }
                });

                entity.id()
            }

            Element::Text {
                node,
                id,
                content,
                font,
                font_weight,
                font_width,
                font_size,
                font_style,
                color,
            } => {
                let text = Text::new(content);

                let font = TextFont {
                    font: if let Some(font) = font {
                        FontSource::Handle(assets.load(font))
                    } else {
                        FontSource::default()
                    },
                    font_size: font_size.unwrap_or_default(),
                    weight: font_weight.unwrap_or_default(),
                    width: font_width.unwrap_or_default(),
                    style: font_style.unwrap_or_default(),
                    ..Default::default()
                };

                let text_color = TextColor(color.unwrap_or(Color::WHITE));

                let mut entity = parent.spawn((node.clone(), Button, text, font, text_color));

                if let Some(id) = id {
                    entity.insert(id.clone());
                }

                entity.id()
            }

            Element::Image {
                node,
                id,
                src,
                color,
                flip_x,
                flip_y,
                rect,
                mode,
                visual_box,
            } => {
                let image = ImageNode {
                    color: color.unwrap_or(Color::WHITE),
                    image: assets.load(src),
                    texture_atlas: None,
                    flip_x: flip_x.unwrap_or_default(),
                    flip_y: flip_y.unwrap_or_default(),
                    rect: *rect,
                    image_mode: mode.clone().unwrap_or_default(),
                    visual_box: visual_box.unwrap_or_default(),
                };

                let mut entity = parent.spawn((node.clone(), Button, image));

                if let Some(id) = id {
                    entity.insert(id.clone());
                }

                entity.id()
            }
        };

        if let Some(id) = self.get_id() {
            reg.register_element(id, entity);
        }

        parent.commands_mut().trigger(ElementSpawn {
            entity,
            id: self.get_id(),
        });

        entity
    }

    /// Returns the [ElementId] of the element, if it has one.
    pub fn get_id(&self) -> Option<ElementId> {
        match self {
            Element::Node { id, .. } => id.clone(),
            Element::Button { id, .. } => id.clone(),
            Element::Text { id, .. } => id.clone(),
            Element::Image { id, .. } => id.clone(),
        }
    }
}

/// A unique identifier for an element.
///
/// Only elements with an `id` attribute have this component.
///
/// The inner type is a cheaper variant of a [String],
/// so you don't need to worry about cloning so much.
#[derive(Clone, Debug, PartialEq, Eq, Hash, Default, Component)]
pub struct ElementId(SmolStr);

impl ElementId {
    /// Creates a new [ElementId].
    pub fn new(s: impl AsRef<str>) -> Self {
        Self(SmolStr::new(s))
    }

    /// Creates a new [ElementId] from a static string.
    ///
    /// Use this, when possible, to avoid allocations.
    pub fn new_static(s: &'static str) -> Self {
        Self(SmolStr::new_static(s))
    }
}

impl From<&'static str> for ElementId {
    fn from(s: &'static str) -> Self {
        Self::new_static(s)
    }
}

#[derive(Clone, Debug)]
pub(crate) struct ElementRegistry {
    ids: FxHashMap<ElementId, Entity>,
}

impl ElementRegistry {
    pub(crate) fn new(cap: usize) -> Self {
        Self {
            ids: FxHashMap::with_capacity_and_hasher(cap, Default::default()),
        }
    }

    pub(crate) fn register_element(&mut self, id: ElementId, entity: Entity) {
        self.ids.insert(id, entity);
    }

    pub(crate) fn get_element(&self, id: ElementId) -> Option<Entity> {
        self.ids.get(&id).cloned()
    }
}