using System; using System.Collections.Generic; using UnityEditor; using UnityEngine; namespace Chameleon.Editor.WebAssets { /// /// Reusable catalog browser for importer windows: search field + responsive /// thumbnail grid with lazy, throttled thumbnail loading. The host window owns /// the catalog data and reacts to cell clicks via the onSelect callback. /// public sealed class AssetBrowserGrid { public sealed class Entry { public string Key; // slug / asset id public string Name; // display name (cell label) public string ThumbnailUrl; public string Search; // pre-lowered haystack for filtering } private const int MaxResults = 150; private const int MaxThumbInFlight = 6; private readonly Action _repaint; private List _catalog; private readonly List _filtered = new List(); private string _searchQuery = ""; private Vector2 _gridScroll; private readonly Dictionary _thumbs = new Dictionary(); private readonly HashSet _thumbRequested = new HashSet(); private int _thumbsInFlight; public AssetBrowserGrid(Action repaint) { _repaint = repaint; } public bool HasCatalog => _catalog != null; public void SetCatalog(List catalog) { _catalog = catalog; ApplyFilter(); } /// Draws search field, match count and the grid. Call only when HasCatalog. public void Draw(Action onSelect, Action reloadCatalog, string itemNoun, float gridHeight = 320f) { using (new EditorGUILayout.HorizontalScope()) { EditorGUI.BeginChangeCheck(); _searchQuery = EditorGUILayout.TextField("Search", _searchQuery); if (EditorGUI.EndChangeCheck()) ApplyFilter(); if (reloadCatalog != null && GUILayout.Button("↻", GUILayout.Width(26))) reloadCatalog(); } int shown = Mathf.Min(_filtered.Count, MaxResults); string cap = _filtered.Count > MaxResults ? $", showing first {MaxResults}" : ""; EditorGUILayout.LabelField( $"{_filtered.Count} match(es){cap} of {_catalog.Count} {itemNoun}.", EditorStyles.miniLabel); DrawGrid(shown, onSelect, gridHeight); } private void DrawGrid(int count, Action onSelect, float gridHeight) { const float cell = 104f; const float pad = 6f; float viewW = EditorGUIUtility.currentViewWidth - 24f; int cols = Mathf.Max(1, Mathf.FloorToInt((viewW + pad) / (cell + pad))); _gridScroll = EditorGUILayout.BeginScrollView(_gridScroll, GUILayout.Height(gridHeight)); int rows = Mathf.CeilToInt(count / (float)cols); for (int row = 0; row < rows; row++) { using (new EditorGUILayout.HorizontalScope()) { for (int c = 0; c < cols; c++) { int i = row * cols + c; if (i >= count) { GUILayout.FlexibleSpace(); break; } DrawCell(_filtered[i], cell, onSelect); } } } EditorGUILayout.EndScrollView(); } private void DrawCell(Entry a, float size, Action onSelect) { using (new EditorGUILayout.VerticalScope(GUILayout.Width(size))) { var thumbRect = GUILayoutUtility.GetRect(size, size - 22f, GUILayout.Width(size), GUILayout.Height(size - 22f)); var tex = GetThumb(a); if (tex) GUI.DrawTexture(thumbRect, tex, ScaleMode.ScaleToFit); else EditorGUI.DrawRect(thumbRect, new Color(0f, 0f, 0f, 0.15f)); // Transparent hit target over the thumbnail. if (GUI.Button(thumbRect, new GUIContent("", a.Name), GUIStyle.none)) onSelect?.Invoke(a); EditorGUILayout.LabelField(new GUIContent(a.Name, a.Name), EditorStyles.miniLabel, GUILayout.Width(size)); } } /// Lazy, throttled thumbnail loader — only visible cells request, capped in-flight. private Texture2D GetThumb(Entry a) { if (_thumbs.TryGetValue(a.Key, out var tex)) { if (tex) return tex; // Cached texture was destroyed (e.g. play-mode cleanup) — purge and re-request. _thumbs.Remove(a.Key); _thumbRequested.Remove(a.Key); } // Don't add to _thumbRequested until a slot is free, so it retries next repaint. if (!_thumbRequested.Contains(a.Key) && _thumbsInFlight < MaxThumbInFlight) { _thumbRequested.Add(a.Key); _thumbsInFlight++; EditorWebRequest.GetTexture(a.ThumbnailUrl, t => { _thumbs[a.Key] = t; _thumbsInFlight--; _repaint?.Invoke(); }, _ => { _thumbsInFlight--; _repaint?.Invoke(); }); // no retry-spam on error } return null; } private void ApplyFilter() { _filtered.Clear(); if (_catalog == null) return; var q = _searchQuery?.Trim().ToLowerInvariant(); if (string.IsNullOrEmpty(q)) { _filtered.AddRange(_catalog); return; } var terms = q.Split((char[])null, StringSplitOptions.RemoveEmptyEntries); foreach (var a in _catalog) { bool all = true; foreach (var t in terms) if (a.Search.IndexOf(t, StringComparison.Ordinal) < 0) { all = false; break; } if (all) _filtered.Add(a); } } } }