bevy_sprinkles_editor 0.3.0

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

use crate::state::{ActiveSidebarTab, SidebarTab};
use crate::ui::tokens::{
    BACKGROUND_COLOR, BORDER_COLOR, CORNER_RADIUS_LG, FONT_PATH, PRIMARY_COLOR, TEXT_BODY_COLOR,
    TEXT_SIZE_SM,
};
use crate::ui::widgets::separator::EditorSeparator;

use super::data_panel::EditorDataPanel;

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

#[derive(Component, Default, Clone, Copy)]
struct SidebarButton(SidebarTab);

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

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

pub fn plugin(app: &mut App) {
    app.add_systems(
        Update,
        (
            setup_sidebar,
            handle_sidebar_click,
            update_sidebar_buttons,
            toggle_data_panel,
        ),
    );
}

pub fn sidebar() -> impl Scene {
    bsn! {
        EditorSidebar
        Node {
            width: px(72),
            flex_direction: { FlexDirection::Column },
            padding: { UiRect::all(px(12)) },
            row_gap: px(12),
            border: { UiRect::right(px(1)) },
        }
        BackgroundColor(BACKGROUND_COLOR)
        template_value(BorderColor::all(BORDER_COLOR))
    }
}

fn sidebar_button(tab: SidebarTab) -> impl Scene {
    bsn! {
        SidebarButton(tab)
        Button
        Hovered
        Node {
            width: percent(100),
            flex_direction: { FlexDirection::Column },
            align_items: { AlignItems::Center },
            row_gap: px(2),
        }
        Children [
            (
                SidebarButton(tab)
                SidebarButtonIcon
                Node {
                    width: px(28),
                    height: px(28),
                    justify_content: { JustifyContent::Center },
                    align_items: { AlignItems::Center },
                    border_radius: { BorderRadius::all(CORNER_RADIUS_LG) },
                }
                BackgroundColor({ Color::NONE })
                Children [
                    (
                        SidebarButton(tab)
                        SidebarButtonImage
                        ImageNode {
                            image: { tab.icon() },
                            color: { Color::Srgba(TEXT_BODY_COLOR) },
                        }
                        Node {
                            width: px(16),
                            height: px(16),
                        }
                    )
                ]
            ),
            (
                Text({ tab.label() })
                TextFont {
                    font: { FontSourceTemplate::Handle(FONT_PATH.into()) },
                    font_size: TEXT_SIZE_SM,
                }
                TextColor(TEXT_BODY_COLOR)
            )
        ]
    }
}

fn setup_sidebar(mut commands: Commands, sidebars: Query<Entity, Added<EditorSidebar>>) {
    for entity in &sidebars {
        commands
            .spawn_scene(sidebar_button(SidebarTab::Project))
            .insert(ChildOf(entity));
        commands
            .spawn_scene(sidebar_button(SidebarTab::Outliner))
            .insert(ChildOf(entity));
        commands
            .spawn_scene(EditorSeparator::horizontal())
            .insert(ChildOf(entity));
        commands
            .spawn_scene(sidebar_button(SidebarTab::Settings))
            .insert(ChildOf(entity));
    }
}

fn handle_sidebar_click(
    interactions: Query<(&Interaction, &SidebarButton), Changed<Interaction>>,
    mut active_tab: ResMut<ActiveSidebarTab>,
) {
    for (interaction, sidebar_btn) in &interactions {
        if *interaction == Interaction::Pressed {
            active_tab.0 = sidebar_btn.0;
        }
    }
}

fn update_sidebar_buttons(
    active_tab: Res<ActiveSidebarTab>,
    buttons: Query<(&SidebarButton, &Hovered), (With<Button>, Without<SidebarButtonIcon>)>,
    changed_hover: Query<(), (Changed<Hovered>, With<SidebarButton>)>,
    mut icon_containers: Query<
        (&SidebarButton, &mut BackgroundColor),
        (With<SidebarButtonIcon>, Without<Button>),
    >,
    mut images: Query<(&SidebarButton, &mut ImageNode), With<SidebarButtonImage>>,
) {
    if !active_tab.is_changed() && changed_hover.is_empty() {
        return;
    }

    for (sidebar_btn, hovered) in &buttons {
        let is_active = active_tab.0 == sidebar_btn.0;
        let is_hovered = hovered.get();

        let (bg_base, bg_alpha) = match (is_active, is_hovered) {
            (false, false) => (TEXT_BODY_COLOR, 0.0),
            (false, true) => (TEXT_BODY_COLOR, 0.05),
            (true, false) => (PRIMARY_COLOR, 0.1),
            (true, true) => (PRIMARY_COLOR, 0.15),
        };

        let icon_color = if is_active {
            PRIMARY_COLOR.lighter(0.05)
        } else {
            TEXT_BODY_COLOR
        };

        for (icon_btn, mut bg) in &mut icon_containers {
            if icon_btn.0 == sidebar_btn.0 {
                bg.0 = bg_base.with_alpha(bg_alpha).into();
            }
        }

        for (img_btn, mut image) in &mut images {
            if img_btn.0 == sidebar_btn.0 {
                image.color = Color::Srgba(icon_color);
            }
        }
    }
}

fn toggle_data_panel(
    active_tab: Res<ActiveSidebarTab>,
    mut data_panels: Query<&mut Node, With<EditorDataPanel>>,
) {
    if !active_tab.is_changed() {
        return;
    }

    let display = if active_tab.0 == SidebarTab::Outliner {
        Display::Flex
    } else {
        Display::None
    };

    for mut node in &mut data_panels {
        node.display = display;
    }
}