using System;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using Ashwild.Crafting;
using Ashwild.Inventory;
namespace Ashwild.EditorTools
{
///
/// The custom editor for a CraftingRecipe shown in the right pane of the Ashwild Database. The
/// editor IS the recipe equation: a row of interactive ingredient cards ("+"-joined) leading to
/// "=" and the result card. Each card's icon opens an item picker to set/change the item, a
/// −/+ stepper adjusts its quantity, and ingredient cards carry a remove button; a dashed "+" tile
/// appends a new ingredient. A compact name field sits above it. All edits write straight to the
/// asset and repaint the strip.
///
public class RecipeEditorView
{
#region State
public VisualElement Root { get; }
private const int RecipePickerControlId = 0x41534852;
private readonly CraftingRecipe recipe;
private readonly SerializedObject so;
private readonly SerializedProperty ingredientsProp;
private readonly Action onMetaChanged;
private VisualElement equation;
private Action pickHandler;
private bool pickCommitsOnCloseOnly;
#endregion
#region Construction
///
/// Builds the recipe editor tree. is raised when the recipe
/// name or result changes so the left list can refresh that row.
///
public RecipeEditorView(CraftingRecipe recipe, Action onMetaChanged)
{
this.recipe = recipe;
this.onMetaChanged = onMetaChanged;
so = new SerializedObject(recipe);
ingredientsProp = so.FindProperty("ingredients");
Root = new VisualElement();
Root.Add(BuildNameField());
Root.Add(BuildEquation());
Root.Add(BuildPickerProxy());
RefreshEquation();
}
#endregion
#region Header
///
/// Builds the recipe-name field shown above the equation, styled as the large editable title
/// (same look as an item's hero name field); renaming refreshes the list.
///
private VisualElement BuildNameField()
{
TextField nameField = new TextField();
nameField.AddToClassList("ash-hero__name");
nameField.AddToClassList("ash-recipe-name");
nameField.BindProperty(so.FindProperty("recipeName"));
nameField.RegisterValueChangedCallback(_ => onMetaChanged?.Invoke());
return nameField;
}
#endregion
#region Equation
///
/// Builds the container for the interactive equation; its cards are filled by RefreshEquation.
///
private VisualElement BuildEquation()
{
equation = new VisualElement();
equation.AddToClassList("ash-equation");
return equation;
}
///
/// Repaints the whole equation: one interactive card per ingredient (joined by "+"), the dashed
/// add tile, then "=", then the result card.
///
private void RefreshEquation()
{
equation.Clear();
int count = ingredientsProp.arraySize;
for (int i = 0; i < count; i++)
{
if (i > 0) equation.Add(Operator("+"));
equation.Add(BuildIngredientCard(i));
}
if (count > 0) equation.Add(Operator("+"));
equation.Add(BuildAddTile());
equation.Add(Operator("="));
equation.Add(BuildResultCard());
}
///
/// Builds a large "+"/"=" operator glyph between cards.
///
private static Label Operator(string glyph)
{
Label op = new Label(glyph);
op.AddToClassList("ash-equation__op");
return op;
}
#endregion
#region Cards
///
/// Builds the interactive card for the ingredient at : a clickable icon
/// (change item), the name, a −/+ quantity stepper, and a remove button.
///
private VisualElement BuildIngredientCard(int index)
{
SerializedProperty element = ingredientsProp.GetArrayElementAtIndex(index);
ItemData item = element.FindPropertyRelative("item").objectReferenceValue as ItemData;
int quantity = element.FindPropertyRelative("quantity").intValue;
VisualElement card = MakeCard(item, () => OpenItemPicker(item, true, picked => SetIngredientItem(index, picked)));
card.Add(Stepper(quantity, delta => AdjustIngredientQuantity(index, delta)));
Button remove = new Button(() => RemoveIngredient(index)) { text = "✕" };
remove.AddToClassList("ash-rchip__remove");
card.Add(remove);
return card;
}
///
/// Builds the result card: a clickable icon (change result item) and a −/+ quantity stepper.
///
private VisualElement BuildResultCard()
{
VisualElement card = MakeCard(recipe.ResultItem, () => OpenItemPicker(recipe.ResultItem, true, SetResultItem));
card.AddToClassList("ash-rchip--result");
card.Add(Stepper(recipe.ResultQuantity, AdjustResultQuantity));
return card;
}
///
/// Builds the dashed "+" tile that appends a new ingredient once an item is picked.
///
private VisualElement BuildAddTile()
{
VisualElement tile = new VisualElement();
tile.AddToClassList("ash-rchip");
tile.AddToClassList("ash-rchip--add");
tile.tooltip = "Add an ingredient";
tile.RegisterCallback(_ => OpenItemPicker(null, false, AddIngredient));
Label plus = new Label("+");
plus.AddToClassList("ash-rchip__plus");
tile.Add(plus);
return tile;
}
///
/// Builds the shared card body (icon + name) for an item, with the icon wired to open a picker.
/// The icon shows the item's sprite, or a neutral placeholder when unset.
///
private VisualElement MakeCard(ItemData item, Action onIconClicked)
{
VisualElement card = new VisualElement();
card.AddToClassList("ash-rchip");
VisualElement icon = new VisualElement();
icon.AddToClassList("ash-rchip__icon");
icon.tooltip = "Click to choose an item";
if (item != null && item.Icon != null) icon.style.backgroundImage = new StyleBackground(item.Icon);
icon.RegisterCallback(_ => onIconClicked());
card.Add(icon);
Label name = new Label(item != null ? item.ItemName : "Choose…");
name.AddToClassList("ash-rchip__name");
card.Add(name);
return card;
}
///
/// Builds a −/+ quantity stepper showing the current value; receives
/// -1 or +1.
///
private VisualElement Stepper(int quantity, Action onDelta)
{
VisualElement stepper = new VisualElement();
stepper.AddToClassList("ash-rchip__stepper");
Button minus = new Button(() => onDelta(-1)) { text = "−" };
minus.AddToClassList("ash-rchip__step");
stepper.Add(minus);
Label value = new Label(quantity.ToString());
value.AddToClassList("ash-rchip__qty");
stepper.Add(value);
Button plus = new Button(() => onDelta(1)) { text = "+" };
plus.AddToClassList("ash-rchip__step");
stepper.Add(plus);
return stepper;
}
#endregion
#region Mutations
///
/// Sets the item of an existing ingredient and repaints.
///
private void SetIngredientItem(int index, ItemData item)
{
if (index < 0 || index >= ingredientsProp.arraySize) return;
ingredientsProp.GetArrayElementAtIndex(index).FindPropertyRelative("item").objectReferenceValue = item;
so.ApplyModifiedProperties();
RefreshEquation();
}
///
/// Changes an ingredient's quantity by a delta, clamped to a minimum of one, and repaints.
///
private void AdjustIngredientQuantity(int index, int delta)
{
if (index < 0 || index >= ingredientsProp.arraySize) return;
SerializedProperty quantity = ingredientsProp.GetArrayElementAtIndex(index).FindPropertyRelative("quantity");
quantity.intValue = Mathf.Max(1, quantity.intValue + delta);
so.ApplyModifiedProperties();
RefreshEquation();
}
///
/// Appends a new ingredient (quantity 1) for the picked item and repaints; ignores a null pick
/// (e.g. the picker was cancelled).
///
private void AddIngredient(ItemData item)
{
if (item == null) return;
int index = ingredientsProp.arraySize;
ingredientsProp.arraySize++;
SerializedProperty element = ingredientsProp.GetArrayElementAtIndex(index);
element.FindPropertyRelative("item").objectReferenceValue = item;
element.FindPropertyRelative("quantity").intValue = 1;
so.ApplyModifiedProperties();
RefreshEquation();
}
///
/// Removes the ingredient at the given index and repaints.
///
private void RemoveIngredient(int index)
{
if (index < 0 || index >= ingredientsProp.arraySize) return;
ingredientsProp.DeleteArrayElementAtIndex(index);
so.ApplyModifiedProperties();
RefreshEquation();
}
///
/// Sets the recipe's result item, repaints, and refreshes the list row (icon/name follow it).
///
private void SetResultItem(ItemData item)
{
so.FindProperty("resultItem").objectReferenceValue = item;
so.ApplyModifiedProperties();
RefreshEquation();
onMetaChanged?.Invoke();
}
///
/// Changes the result quantity by a delta, clamped to a minimum of one, and repaints.
///
private void AdjustResultQuantity(int delta)
{
SerializedProperty quantity = so.FindProperty("resultQuantity");
quantity.intValue = Mathf.Max(1, quantity.intValue + delta);
so.ApplyModifiedProperties();
RefreshEquation();
}
#endregion
#region Item Picker
///
/// Builds the hidden IMGUI proxy that relays Unity's object-picker commands to the active pick
/// handler (the editor window is UI Toolkit, which can't receive those commands directly).
///
private VisualElement BuildPickerProxy()
{
IMGUIContainer proxy = new IMGUIContainer(HandlePickerCommands);
proxy.style.position = Position.Absolute;
proxy.style.width = 1;
proxy.style.height = 1;
return proxy;
}
///
/// Opens Unity's item picker seeded with the current item. When
/// is true the pick applies on every highlight (live preview, e.g. changing an existing item);
/// otherwise it applies only when the picker closes (e.g. adding a new ingredient — commit once).
///
private void OpenItemPicker(ItemData seed, bool commitLive, Action onPicked)
{
pickHandler = onPicked;
pickCommitsOnCloseOnly = !commitLive;
EditorGUIUtility.ShowObjectPicker(seed, false, string.Empty, RecipePickerControlId);
}
///
/// Forwards the picker's selection to the active handler, honouring the live-vs-on-close policy.
///
private void HandlePickerCommands()
{
Event evt = Event.current;
if (evt == null || evt.type != EventType.ExecuteCommand) return;
if (EditorGUIUtility.GetObjectPickerControlID() != RecipePickerControlId) return;
bool updated = evt.commandName == "ObjectSelectorUpdated";
bool closed = evt.commandName == "ObjectSelectorClosed";
if (!updated && !closed) return;
if (updated && pickCommitsOnCloseOnly) return;
pickHandler?.Invoke(EditorGUIUtility.GetObjectPickerObject() as ItemData);
}
#endregion
}
}