bevy_sprinkles_editor 0.2.0

GPU particle system editor for Bevy
use bevy::prelude::*;
use bevy::reflect::{PartialReflect, ReflectRef};
use bevy_sprinkles::prelude::*;
use bevy_ui_text_input::TextInputQueue;

use crate::io::EditorData;
use crate::state::EditorState;
use crate::ui::components::inspector::FieldKind;
use crate::ui::widgets::checkbox::CheckboxState;
use crate::ui::widgets::color_picker::{
    ColorPickerState, EditorColorPicker, TriggerSwatchMaterial,
};
use crate::ui::widgets::combobox::ComboBoxConfig;
use crate::ui::widgets::curve_edit::{CurveEditState, EditorCurveEdit};
use crate::ui::widgets::gradient_edit::{EditorGradientEdit, GradientEditState};
use crate::ui::widgets::text_edit::{EditorTextEdit, set_text_input_value};
use crate::ui::widgets::variant_edit::{EditorVariantEdit, VariantDefinition, VariantEditConfig};

use super::{
    BoundTo, FieldBinding, FieldValue, InspectedEmitterTracker, format_f32,
    get_variant_index_by_reflection, resolve_binding_data,
};

pub(super) fn bind_text_inputs(
    editor_state: Res<EditorState>,
    assets: Res<Assets<ParticleSystemAsset>>,
    editor_data: Res<EditorData>,
    tracker: Res<InspectedEmitterTracker>,
    new_bindings: Query<Entity, Added<FieldBinding>>,
    new_bound: Query<Entity, Added<BoundTo>>,
    bindings: Query<&FieldBinding>,
    mut text_edits: Query<(&BoundTo, &mut TextInputQueue), With<EditorTextEdit>>,
) {
    if !tracker.is_changed() && new_bindings.is_empty() && new_bound.is_empty() {
        return;
    }

    for (bound, mut queue) in &mut text_edits {
        let Ok(binding) = bindings.get(bound.binding) else {
            continue;
        };

        let Some(data) = resolve_binding_data(binding, &editor_state, &assets, &editor_data) else {
            continue;
        };

        let value = binding.read_value(data);

        if let Some(idx) = bound.component_index {
            if let FieldKind::Vector(suffixes) = &binding.kind {
                if let Some(v) = get_field_value_component(&value, idx) {
                    let text = if suffixes.is_integer() {
                        (v as i32).to_string()
                    } else {
                        format_f32(v)
                    };
                    set_text_input_value(&mut queue, text);
                }
                continue;
            }
        }

        let text = value.to_display_string(&binding.kind).unwrap_or_default();
        set_text_input_value(&mut queue, text);
    }
}

#[allow(clippy::too_many_arguments)]
pub(super) fn bind_widget_values(
    editor_state: Res<EditorState>,
    assets: Res<Assets<ParticleSystemAsset>>,
    editor_data: Res<EditorData>,
    tracker: Res<InspectedEmitterTracker>,
    new_bindings: Query<Entity, Added<FieldBinding>>,
    new_variant_edits: Query<Entity, Added<EditorVariantEdit>>,
    new_bound: Query<Entity, Added<BoundTo>>,
    bindings: Query<&FieldBinding>,
    mut checkbox_states: Query<(&FieldBinding, &mut CheckboxState)>,
    mut curve_edits: Query<(&FieldBinding, &mut CurveEditState), With<EditorCurveEdit>>,
    mut gradient_edits: Query<(&FieldBinding, &mut GradientEditState), With<EditorGradientEdit>>,
    mut colocated_comboboxes: Query<(&FieldBinding, &mut ComboBoxConfig)>,
    mut bound_comboboxes: Query<(&BoundTo, &mut ComboBoxConfig), Without<FieldBinding>>,
    mut variant_edits: Query<(&FieldBinding, &mut VariantEditConfig), With<EditorVariantEdit>>,
) {
    if !tracker.is_changed()
        && new_bindings.is_empty()
        && new_variant_edits.is_empty()
        && new_bound.is_empty()
    {
        return;
    }

    for (binding, mut state) in &mut checkbox_states {
        let Some(data) = resolve_binding_data(binding, &editor_state, &assets, &editor_data) else {
            continue;
        };
        let value = binding.read_value(data);
        if let Some(checked) = value.to_bool() {
            state.checked = checked;
        }
    }

    for (binding, mut state) in &mut curve_edits {
        if binding.kind != FieldKind::Curve {
            continue;
        }
        let Some(data) = resolve_binding_data(binding, &editor_state, &assets, &editor_data) else {
            continue;
        };
        let Some(reflected) = binding.read_reflected(data) else {
            continue;
        };
        if let Some(ct) = reflected.try_downcast_ref::<CurveTexture>() {
            state.set_curve(ct.clone());
        } else if let Some(opt) = reflected.try_downcast_ref::<Option<CurveTexture>>() {
            state.set_curve(opt.clone().unwrap_or_default());
        }
    }

    for (binding, mut state) in &mut gradient_edits {
        if binding.kind != FieldKind::Gradient {
            continue;
        }
        let Some(data) = resolve_binding_data(binding, &editor_state, &assets, &editor_data) else {
            continue;
        };
        let Some(reflected) = binding.read_reflected(data) else {
            continue;
        };
        if let Some(gradient) = reflected.try_downcast_ref::<ParticleGradient>() {
            state.gradient = gradient.clone();
        }
    }

    for (binding, mut config) in &mut colocated_comboboxes {
        if !matches!(binding.kind, FieldKind::ComboBox { .. }) {
            continue;
        }
        let Some(data) = resolve_binding_data(binding, &editor_state, &assets, &editor_data) else {
            continue;
        };
        let value = binding.read_value(data);
        if let FieldValue::U32(index) = value {
            config.selected = index as usize;
        }
    }

    for (bound, mut config) in &mut bound_comboboxes {
        let Ok(binding) = bindings.get(bound.binding) else {
            continue;
        };
        if !matches!(binding.kind, FieldKind::ComboBox { .. }) {
            continue;
        }
        let Some(data) = resolve_binding_data(binding, &editor_state, &assets, &editor_data) else {
            continue;
        };
        let value = binding.read_value(data);
        if let FieldValue::U32(index) = value {
            config.selected = index as usize;
        }
    }

    for (binding, mut config) in &mut variant_edits {
        let Some(data) = resolve_binding_data(binding, &editor_state, &assets, &editor_data) else {
            continue;
        };
        let new_index = if binding.is_variant() {
            let Some(reflected) = binding.read_reflected(data) else {
                continue;
            };
            get_nested_variant_index(reflected, &config.variants)
        } else {
            let Some(idx) = get_variant_index_by_reflection(data, binding.path(), &config.variants)
            else {
                continue;
            };
            idx
        };
        config.selected_index = new_index;
    }
}

pub(super) fn bind_color_pickers(
    mut commands: Commands,
    editor_state: Res<EditorState>,
    assets: Res<Assets<ParticleSystemAsset>>,
    editor_data: Res<EditorData>,
    mut color_pickers: Query<
        (Entity, &mut ColorPickerState, &FieldBinding),
        (With<EditorColorPicker>, Without<BindingInitialized>),
    >,
    trigger_swatches: Query<&TriggerSwatchMaterial>,
) {
    for (entity, mut state, binding) in &mut color_pickers {
        if !matches!(binding.kind, FieldKind::Color) {
            continue;
        }

        let trigger_ready = trigger_swatches.iter().any(|swatch| swatch.0 == entity);
        if !trigger_ready {
            continue;
        }

        let Some(data) = resolve_binding_data(binding, &editor_state, &assets, &editor_data) else {
            continue;
        };

        let value = binding.read_value(data);
        let Some(color) = value.to_color() else {
            continue;
        };

        state.set_from_rgba(color);
        commands.entity(entity).try_insert(BindingInitialized);
    }
}

#[derive(Component)]
pub(super) struct BindingInitialized;

fn get_nested_variant_index(value: &dyn PartialReflect, variants: &[VariantDefinition]) -> usize {
    let ReflectRef::Enum(enum_ref) = value.reflect_ref() else {
        return 0;
    };

    let variant_name = enum_ref.variant_name();

    if let Some(pos) = find_variant_index_by_name(variant_name, variants) {
        return pos;
    }

    if variant_name == "Some" {
        if let Some(inner) = enum_ref.field_at(0) {
            if let ReflectRef::Enum(inner_enum) = inner.reflect_ref() {
                let inner_name = inner_enum.variant_name();
                if let Some(pos) = find_variant_index_by_name(inner_name, variants) {
                    return pos;
                }
            }
        }
    }

    0
}

fn find_variant_index_by_name(name: &str, variants: &[VariantDefinition]) -> Option<usize> {
    variants
        .iter()
        .position(|v| v.name == name || v.aliases.iter().any(|a| a == name))
}

fn get_field_value_component(value: &FieldValue, index: usize) -> Option<f32> {
    match value {
        FieldValue::Vec2(vec) => match index {
            0 => Some(vec.x),
            1 => Some(vec.y),
            _ => None,
        },
        FieldValue::Vec3(vec) => match index {
            0 => Some(vec.x),
            1 => Some(vec.y),
            2 => Some(vec.z),
            _ => None,
        },
        FieldValue::Range(min, max) => match index {
            0 => Some(*min),
            1 => Some(*max),
            _ => None,
        },
        _ => None,
    }
}