bevy_pages 0.2.0

A lightweight and elegant framework to upgrade your Bevy UI experience.
Documentation
use crate::events::ElementSpawn;
use crate::widgets::Widget;
use crate::widgets::button::ButtonWidget;
use crate::widgets::checkbox::CheckboxWidget;
use crate::widgets::divider::DividerWidget;
use crate::widgets::dropdown::DropdownWidget;
use crate::widgets::image::ImageWidget;
use crate::widgets::node::NodeWidget;
use crate::widgets::progress_bar::ProgressBarWidget;
use crate::widgets::scroll_view::ScrollViewWidget;
use crate::widgets::slider::SliderWidget;
use crate::widgets::text::TextWidget;
use crate::widgets::text_input::TextInputWidget;
use crate::widgets::tooltip::TooltipWidget;
use bevy::asset::AssetServer;
use bevy::color::Color;
use bevy::ecs::relationship::RelatedSpawnerCommands;
use bevy::prelude::{
    BorderColor, ChildOf, Component, Entity, EntityCommands, GlobalTransform, Interaction,
    Transform,
};
use bevy::ui::{BackgroundColor, Node};
use rustc_hash::FxHashMap;
use smol_str::SmolStr;

/// A UI element representing a node in the UI hierarchy.
///
/// You should generally not try to read this struct for changes,
/// as it only contains the **initial** properties of the element.
///
/// To react to changes and get updated values, look at the individual components of the elements.
#[derive(Clone, Debug, Component)]
pub struct Element {
    /// The core visual, layout, and widget properties.
    pub props: ElementProps,
    /// The element overrides.
    pub overrides: ElementOverrides,
    /// The optional ID of this element.
    pub id: Option<ElementId>,
    /// The children of this element.
    pub children: Vec<Element>,
}

impl Element {
    pub(crate) fn spawn(
        &self,
        parent: &mut RelatedSpawnerCommands<ChildOf>,
        assets: &AssetServer,
        reg: &mut ElementRegistry,
    ) -> Entity {
        let mut entity_cmd = parent.spawn((
            self.clone(),
            self.props.node.clone(),
            Interaction::None,
            Transform::default(),
            GlobalTransform::default(),
            BackgroundColor(self.props.bg_color.unwrap_or(Color::NONE)),
            self.props.border_color.unwrap_or(BorderColor::DEFAULT),
        ));

        if let Some(id) = &self.id {
            entity_cmd.insert(id.clone());
        }

        let root_entity = entity_cmd.id();

        let target_entity = self.props.widget.spawn(&mut entity_cmd, assets);

        if target_entity == root_entity {
            entity_cmd.with_children(|p| {
                for child in &self.children {
                    child.spawn(p, assets, reg);
                }
            });
        } else {
            parent
                .commands_mut()
                .entity(target_entity)
                .with_children(|p| {
                    for child in &self.children {
                        child.spawn(p, assets, reg);
                    }
                });
        }

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

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

        root_entity
    }

    /// Returns the [ElementId] of the element, if it has one.
    pub fn id(&self) -> Option<ElementId> {
        self.id.clone()
    }

    /// Returns a reference to the element's overrides.
    pub fn overrides(&self) -> &ElementOverrides {
        &self.overrides
    }
}

/// Pure visual, layout, and widget properties for a UI element.
#[derive(Clone, Debug)]
pub struct ElementProps {
    /// The node layout settings behind this element.
    pub node: Node,
    /// The background color of this element.
    pub bg_color: Option<Color>,
    /// The border color of this element.
    pub border_color: Option<BorderColor>,
    /// The [Widget] of this [Element].
    pub widget: ElementWidget,
}

/// The [Widget] of an [Element].
#[derive(Clone, Debug)]
pub enum ElementWidget {
    /// A simple container element (equivalent to a `<div>` in HTML).
    Node(NodeWidget),
    /// A button element.
    Button(ButtonWidget),
    /// A text element.
    Text(TextWidget),
    /// An image element.
    Image(ImageWidget),
    /// A checkbox element.
    Checkbox(CheckboxWidget),
    /// A text input element.
    TextInput(TextInputWidget),
    /// A slider element.
    Slider(SliderWidget),
    /// A progress bar element.
    ProgressBar(ProgressBarWidget),
    /// A tooltip element.
    Tooltip(TooltipWidget),
    /// A divider element.
    Divider(DividerWidget),
    /// A scroll view element.
    ScrollView(ScrollViewWidget),
    /// A dropdown element.
    Dropdown(DropdownWidget),
}

impl ElementWidget {
    /// Spawns the inner widget and returns the target entity for child elements.
    pub fn spawn(&self, commands: &mut EntityCommands, assets: &AssetServer) -> Entity {
        match self {
            ElementWidget::Node(w) => w.spawn(commands, assets),
            ElementWidget::Button(w) => w.spawn(commands, assets),
            ElementWidget::Text(w) => w.spawn(commands, assets),
            ElementWidget::Image(w) => w.spawn(commands, assets),
            ElementWidget::Checkbox(w) => w.spawn(commands, assets),
            ElementWidget::TextInput(w) => w.spawn(commands, assets),
            ElementWidget::Slider(w) => w.spawn(commands, assets),
            ElementWidget::ProgressBar(w) => w.spawn(commands, assets),
            ElementWidget::Tooltip(w) => w.spawn(commands, assets),
            ElementWidget::Divider(w) => w.spawn(commands, assets),
            ElementWidget::ScrollView(w) => w.spawn(commands, assets),
            ElementWidget::Dropdown(w) => w.spawn(commands, assets),
        }
    }

    /// Returns `Some(&NodeWidget)` if this widget is a node, otherwise `None`.
    pub fn as_node(&self) -> Option<&NodeWidget> {
        match self {
            ElementWidget::Node(node) => Some(node),
            _ => None,
        }
    }

    /// Returns `Some(&ButtonWidget)` if this widget is a button, otherwise `None`.
    pub fn as_button(&self) -> Option<&ButtonWidget> {
        match self {
            ElementWidget::Button(button) => Some(button),
            _ => None,
        }
    }

    /// Returns `Some(&TextWidget)` if this widget is a text, otherwise `None`.
    pub fn as_text(&self) -> Option<&TextWidget> {
        match self {
            ElementWidget::Text(text) => Some(text),
            _ => None,
        }
    }

    /// Returns `Some(&ImageWidget)` if this widget is an image, otherwise `None`.
    pub fn as_image(&self) -> Option<&ImageWidget> {
        match self {
            ElementWidget::Image(image) => Some(image),
            _ => None,
        }
    }

    /// Returns `Some(&CheckboxWidget)` if this widget is a checkbox, otherwise `None`.
    pub fn as_checkbox(&self) -> Option<&CheckboxWidget> {
        match self {
            ElementWidget::Checkbox(checkbox) => Some(checkbox),
            _ => None,
        }
    }

    /// Returns `Some(&TextInputWidget)` if this widget is a text input, otherwise `None`.
    pub fn as_text_input(&self) -> Option<&TextInputWidget> {
        match self {
            ElementWidget::TextInput(text_input) => Some(text_input),
            _ => None,
        }
    }

    /// Returns `Some(&SliderWidget)` if this widget is a slider, otherwise `None`.
    pub fn as_slider(&self) -> Option<&SliderWidget> {
        match self {
            ElementWidget::Slider(slider) => Some(slider),
            _ => None,
        }
    }

    /// Returns `Some(&ProgressBarWidget)` if this widget is a progress bar, otherwise `None`.
    pub fn as_progress_bar(&self) -> Option<&ProgressBarWidget> {
        match self {
            ElementWidget::ProgressBar(progress_bar) => Some(progress_bar),
            _ => None,
        }
    }

    /// Returns `Some(&TooltipWidget)` if this widget is a tooltip, otherwise `None`.
    pub fn as_tooltip(&self) -> Option<&TooltipWidget> {
        match self {
            ElementWidget::Tooltip(tooltip) => Some(tooltip),
            _ => None,
        }
    }

    /// Returns `Some(&DividerWidget)` if this widget is a divider, otherwise `None`.
    pub fn as_divider(&self) -> Option<&DividerWidget> {
        match self {
            ElementWidget::Divider(divider) => Some(divider),
            _ => None,
        }
    }

    /// Returns `Some(&ScrollViewWidget)` if this widget is a scroll view, otherwise `None`.
    pub fn as_scroll_view(&self) -> Option<&ScrollViewWidget> {
        match self {
            ElementWidget::ScrollView(scroll_view) => Some(scroll_view),
            _ => None,
        }
    }

    /// Returns `Some(&DropdownWidget)` if this widget is a dropdown, otherwise `None`.
    pub fn as_dropdown(&self) -> Option<&DropdownWidget> {
        match self {
            ElementWidget::Dropdown(dropdown) => Some(dropdown),
            _ => None,
        }
    }
}

/// A unique identifier for an element.
#[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.
    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)
    }
}

/// A set of property overrides enabled during interaction events.
#[derive(Clone, Debug)]
pub struct ElementOverrides {
    /// The default overrides.
    pub default: ElementProps,
    /// Property overrides when hovered on.
    pub on_hover: ElementProps,
    /// Property overrides when clicked.
    pub on_click: ElementProps,
}

#[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()
    }
}