bevy_sprinkles_editor 0.3.0

GPU particle system editor for Bevy
use bevy::prelude::*;
use bevy::text::FontSourceTemplate;

use crate::ui::icons::{ICON_ADD, ICON_ARROW_DOWN};
use crate::ui::tokens::{BORDER_COLOR, FONT_PATH, TEXT_DISPLAY_COLOR, TEXT_SIZE};
use crate::ui::widgets::button::{ButtonClickEvent, ButtonVariant, IconButtonProps, icon_button};

pub fn plugin(app: &mut App) {
    app.add_systems(Update, setup_panel_section_buttons);
}

#[derive(Component, Default, Clone)]
pub struct EditorPanelSection;

#[derive(Component, Default, Clone)]
struct PanelSectionHeader;

#[derive(Component, Default, Clone)]
struct PanelSectionButtonsContainer;

#[derive(Component)]
pub struct PanelSectionAddButton(pub Entity);

#[derive(Component)]
struct PanelSectionCollapseButton(Entity);

#[derive(Component, Default, Clone)]
struct Collapsed(bool);

#[derive(Component, Default, Clone)]
struct PanelSectionState {
    has_add_button: bool,
    collapsible: bool,
}

#[derive(Default, Clone, Copy)]
pub enum PanelSectionSize {
    #[default]
    MD,
    XL,
}

impl PanelSectionSize {
    fn padding(&self) -> UiRect {
        match self {
            Self::MD => UiRect::all(px(12)),
            Self::XL => UiRect::new(px(24), px(24), px(14), px(24)),
        }
    }
}

#[derive(Default)]
pub struct PanelSectionProps {
    pub title: String,
    pub size: PanelSectionSize,
    pub has_add_button: bool,
    pub collapsible: bool,
}

impl PanelSectionProps {
    pub fn new(title: impl Into<String>) -> Self {
        Self {
            title: title.into(),
            ..default()
        }
    }

    pub fn with_size(mut self, size: PanelSectionSize) -> Self {
        self.size = size;
        self
    }

    pub fn with_add_button(mut self) -> Self {
        self.has_add_button = true;
        self
    }

    pub fn collapsible(mut self) -> Self {
        self.collapsible = true;
        self
    }
}

pub fn panel_section(props: PanelSectionProps) -> impl Scene {
    let PanelSectionProps {
        title,
        size,
        has_add_button,
        collapsible,
    } = props;
    let padding = size.padding();

    bsn! {
        EditorPanelSection
        Collapsed
        Node {
            width: percent(100),
            flex_direction: { FlexDirection::Column },
            row_gap: px(12),
            padding: { padding },
            border: { UiRect::bottom(px(1)) },
        }
        template_value(BorderColor::all(BORDER_COLOR))
        template_value(PanelSectionState {
            has_add_button,
            collapsible,
        })
        Children [
            (
                PanelSectionHeader
                Node {
                    width: percent(100),
                    justify_content: { JustifyContent::SpaceBetween },
                    align_items: { AlignItems::Center },
                }
                Children [
                    (
                        Text({ title })
                        TextFont {
                            font: { FontSourceTemplate::Handle(FONT_PATH.into()) },
                            font_size: TEXT_SIZE,
                            weight: { FontWeight::SEMIBOLD },
                        }
                        TextColor({ TEXT_DISPLAY_COLOR })
                    ),
                    (
                        PanelSectionButtonsContainer
                        Node {
                            align_items: { AlignItems::Center },
                        }
                    ),
                ]
            )
        ]
    }
}

fn setup_panel_section_buttons(
    mut commands: Commands,
    _asset_server: Res<AssetServer>,
    new_sections: Query<(Entity, &PanelSectionState, &Children), Added<EditorPanelSection>>,
    headers: Query<&Children, With<PanelSectionHeader>>,
    containers: Query<Entity, With<PanelSectionButtonsContainer>>,
) {
    for (section_entity, state, section_children) in &new_sections {
        let Some(&header_entity) = section_children.first() else {
            continue;
        };
        let Ok(header_children) = headers.get(header_entity) else {
            continue;
        };
        let Some(&container_entity) = header_children.get(1) else {
            continue;
        };
        if containers.get(container_entity).is_err() {
            continue;
        }

        if state.has_add_button {
            let add_entity = commands
                .spawn_scene(icon_button(
                    IconButtonProps::new(ICON_ADD).variant(ButtonVariant::Ghost),
                ))
                .insert(PanelSectionAddButton(section_entity))
                .observe(on_add_click)
                .id();
            commands.entity(container_entity).add_child(add_entity);
        }

        if state.collapsible {
            let collapse_entity = commands
                .spawn_scene(icon_button(
                    IconButtonProps::new(ICON_ARROW_DOWN).variant(ButtonVariant::Ghost),
                ))
                .insert((
                    PanelSectionCollapseButton(section_entity),
                    UiTransform {
                        rotation: Rot2::degrees(180.0),
                        ..default()
                    },
                ))
                .observe(on_collapse_click)
                .id();
            commands.entity(container_entity).add_child(collapse_entity);
        }
    }
}

fn on_add_click(
    event: On<ButtonClickEvent>,
    add_buttons: Query<&PanelSectionAddButton>,
    mut commands: Commands,
) {
    let Ok(add_button) = add_buttons.get(event.entity) else {
        return;
    };
    commands.trigger(ButtonClickEvent {
        entity: add_button.0,
    });
}

fn on_collapse_click(
    event: On<ButtonClickEvent>,
    collapse_buttons: Query<&PanelSectionCollapseButton>,
    mut sections: Query<(&mut Collapsed, &Children), With<EditorPanelSection>>,
    mut nodes: Query<&mut Node, Without<PanelSectionHeader>>,
    headers: Query<Entity, With<PanelSectionHeader>>,
    mut button_transforms: Query<&mut UiTransform>,
) {
    let button_entity = event.entity;
    let Ok(collapse_button) = collapse_buttons.get(button_entity) else {
        return;
    };

    let Ok((mut collapsed, section_children)) = sections.get_mut(collapse_button.0) else {
        return;
    };

    collapsed.0 = !collapsed.0;

    for child in section_children.iter() {
        if headers.get(child).is_ok() {
            continue;
        }
        if let Ok(mut node) = nodes.get_mut(child) {
            node.display = if collapsed.0 {
                Display::None
            } else {
                Display::Flex
            };
        }
    }

    if let Ok(mut transform) = button_transforms.get_mut(button_entity) {
        transform.rotation = if collapsed.0 {
            Rot2::degrees(0.0)
        } else {
            Rot2::degrees(180.0)
        };
    }
}