using System; using UnityEditor; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; namespace AibisDream.FrameAnimation.Editor { internal enum FrameAnimationBrowserEntryKind { Header, Clip, Flow, Source } internal enum FrameAnimationBrowserIssueState { None, Warning, Error } internal sealed class FrameAnimationBrowserEntry { public FrameAnimationBrowserEntryKind Kind; public string Title = string.Empty; public string Id = string.Empty; public string TypeLabel = string.Empty; public string Meta = string.Empty; public string StatusLabel = string.Empty; public Color? AccentColor; public FrameAnimationBrowserIssueState IssueState; public FrameAnimationEditorSelection Selection; public bool IsHeader => Kind == FrameAnimationBrowserEntryKind.Header; } internal readonly struct FrameAnimationFlowColorPalette { public readonly Color Raw; public readonly Color Stroke; public readonly Color Header; public readonly Color Muted; public readonly Color Text; public FrameAnimationFlowColorPalette(Color raw, Color stroke, Color header, Color muted, Color text) { Raw = raw; Stroke = stroke; Header = header; Muted = muted; Text = text; } } internal static class FrameAnimationFlowColorUtility { private static readonly Color Panel = new Color32(43, 47, 52, 255); private static readonly Color LightText = new Color32(244, 246, 248, 255); private static readonly Color DarkText = new Color32(24, 27, 30, 255); private static readonly Color Neutral = new Color32(100, 168, 216, 255); internal static Color ResolveRaw(FrameAnimationGraph graph, AnimationFlow flow) { return Opaque(ResolveStored(graph, flow)); } internal static Color ResolveStored(FrameAnimationGraph graph, AnimationFlow flow) { if (graph == null || flow == null) { return Neutral; } var data = graph.EditorData?.FlowEditorData == null ? null : System.Linq.Enumerable.FirstOrDefault(graph.EditorData.FlowEditorData, item => item != null && item.FlowId == flow.Id); return data?.Color ?? Neutral; } internal static FrameAnimationFlowColorPalette CreatePalette(Color value) { var raw = Opaque(value); var luminance = Luminance(raw); var stroke = luminance < 0.30f ? Color.Lerp(raw, Color.white, Mathf.InverseLerp(0.30f, 0.02f, luminance) * 0.42f) : luminance > 0.82f ? Color.Lerp(raw, Color.black, Mathf.InverseLerp(0.82f, 1f, luminance) * 0.22f) : raw; stroke.a = 1f; var header = Color.Lerp(Panel, stroke, 0.68f); header.a = 1f; var muted = Color.Lerp(Panel, stroke, 0.42f); muted.a = 1f; var text = Luminance(header) > 0.22f ? DarkText : LightText; return new FrameAnimationFlowColorPalette(raw, stroke, header, muted, text); } internal static Color Opaque(Color value) => new Color( Mathf.Clamp01(value.r), Mathf.Clamp01(value.g), Mathf.Clamp01(value.b), 1f); internal static float Luminance(Color value) { var linear = value.linear; return linear.r * 0.2126f + linear.g * 0.7152f + linear.b * 0.0722f; } } internal sealed class FrameAnimationResourceRowElement : VisualElement { private readonly VisualElement flowDot; private readonly Label typeBadge; private readonly Label titleLabel; private readonly Label idLabel; private readonly Label metaLabel; private readonly Label statusBadge; internal FrameAnimationResourceRowElement() { AddToClassList("fa-resource-row"); var main = new VisualElement(); main.AddToClassList("fa-resource-row__main"); flowDot = new VisualElement { pickingMode = PickingMode.Ignore }; flowDot.AddToClassList("fa-flow-dot"); typeBadge = new Label { pickingMode = PickingMode.Ignore }; typeBadge.AddToClassList("fa-badge"); titleLabel = new Label { pickingMode = PickingMode.Ignore }; titleLabel.AddToClassList("fa-resource-row__name"); idLabel = new Label { pickingMode = PickingMode.Ignore }; idLabel.AddToClassList("fa-resource-row__id"); statusBadge = new Label { pickingMode = PickingMode.Ignore }; statusBadge.AddToClassList("fa-badge"); main.Add(flowDot); main.Add(typeBadge); main.Add(titleLabel); main.Add(idLabel); main.Add(statusBadge); metaLabel = new Label { pickingMode = PickingMode.Ignore }; metaLabel.AddToClassList("fa-resource-row__meta"); Add(main); Add(metaLabel); } internal void Bind(FrameAnimationBrowserEntry entry, bool selected) { userData = entry; var isHeader = entry?.IsHeader == true; EnableInClassList("fa-resource-group", isHeader); EnableInClassList("fa-resource-row--selected", selected); EnableInClassList("fa-resource-row--error", entry?.IssueState == FrameAnimationBrowserIssueState.Error); EnableInClassList("fa-resource-row--warning", entry?.IssueState == FrameAnimationBrowserIssueState.Warning); if (entry == null) { style.display = DisplayStyle.None; return; } style.display = DisplayStyle.Flex; titleLabel.text = entry.Title; titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold; idLabel.text = entry.Id; metaLabel.text = entry.Meta; typeBadge.text = entry.TypeLabel; typeBadge.style.display = isHeader ? DisplayStyle.None : DisplayStyle.Flex; idLabel.style.display = isHeader ? DisplayStyle.None : DisplayStyle.Flex; metaLabel.style.display = isHeader ? DisplayStyle.None : DisplayStyle.Flex; statusBadge.text = entry.StatusLabel; statusBadge.style.display = isHeader || string.IsNullOrEmpty(entry.StatusLabel) ? DisplayStyle.None : DisplayStyle.Flex; flowDot.style.display = !isHeader && entry.AccentColor.HasValue ? DisplayStyle.Flex : DisplayStyle.None; if (entry.AccentColor.HasValue) { flowDot.style.backgroundColor = FrameAnimationFlowColorUtility.CreatePalette(entry.AccentColor.Value).Stroke; } typeBadge.EnableInClassList("fa-badge--blue", entry.Kind == FrameAnimationBrowserEntryKind.Clip); typeBadge.EnableInClassList("fa-badge--purple", entry.Kind == FrameAnimationBrowserEntryKind.Flow); typeBadge.EnableInClassList("fa-badge--green", entry.Kind == FrameAnimationBrowserEntryKind.Source); statusBadge.EnableInClassList("fa-badge--red", entry.IssueState == FrameAnimationBrowserIssueState.Error); statusBadge.EnableInClassList("fa-badge--yellow", entry.IssueState == FrameAnimationBrowserIssueState.Warning); } } internal sealed class FrameAnimationFrameRowElement : VisualElement { private readonly ObjectField spriteField; private readonly IntegerField durationField; private readonly Label frameNameLabel; private readonly Label sourceIndexLabel; private FrameClip clip; private int index; private bool readOnly; private Action changed; internal FrameAnimationFrameRowElement() { AddToClassList("fa-frame-row"); spriteField = new ObjectField("Sprite") { objectType = typeof(Sprite), allowSceneObjects = false }; durationField = new IntegerField("Duration Ms"); var readOnlyRow = new VisualElement(); readOnlyRow.AddToClassList("fa-frame-row__readonly"); frameNameLabel = new Label(); sourceIndexLabel = new Label(); readOnlyRow.Add(frameNameLabel); readOnlyRow.Add(sourceIndexLabel); Add(spriteField); Add(durationField); Add(readOnlyRow); spriteField.RegisterValueChangedCallback(OnSpriteChanged); durationField.RegisterValueChangedCallback(OnDurationChanged); } internal void Bind(FrameClip value, int itemIndex, bool isReadOnly, Action onChanged) { clip = value; index = itemIndex; readOnly = isReadOnly; changed = onChanged; userData = itemIndex; var frame = clip != null && index >= 0 && index < clip.Frames.Count ? clip.Frames[index] : null; spriteField.SetValueWithoutNotify(frame?.Sprite); durationField.SetValueWithoutNotify(frame?.DurationMs ?? 100); frameNameLabel.text = "Frame " + (frame?.FrameName ?? string.Empty); sourceIndexLabel.text = "Source " + (frame?.SourceIndex.ToString() ?? "-1"); spriteField.SetEnabled(!readOnly); durationField.SetEnabled(!readOnly); } private void OnSpriteChanged(ChangeEvent evt) { if (readOnly || clip == null || index < 0 || index >= clip.Frames.Count) return; var serialized = new SerializedObject(clip); serialized.Update(); var element = serialized.FindProperty("frames").GetArrayElementAtIndex(index); element.FindPropertyRelative("sprite").objectReferenceValue = evt.newValue; serialized.ApplyModifiedProperties(); EditorUtility.SetDirty(clip); changed?.Invoke(); } private void OnDurationChanged(ChangeEvent evt) { if (readOnly || clip == null || index < 0 || index >= clip.Frames.Count) return; var serialized = new SerializedObject(clip); serialized.Update(); var element = serialized.FindProperty("frames").GetArrayElementAtIndex(index); element.FindPropertyRelative("durationMs").intValue = Mathf.Max(1, evt.newValue); serialized.ApplyModifiedProperties(); durationField.SetValueWithoutNotify(Mathf.Max(1, evt.newValue)); EditorUtility.SetDirty(clip); changed?.Invoke(); } } }