diff --git a/CHANGELOG.md b/CHANGELOG.md index a794666..30dbd7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Default excluded paths are now empty** - Excluded path presets (e.g., VRCFury temp) are no longer applied by default; they remain available via the UI "Add Path" menu - **Texture filter flags no longer managed by presets** - `ProcessMainTextures`, `ProcessNormalMaps`, `ProcessEmissionMaps`, and `ProcessOtherTextures` are now per-component settings that persist across preset changes, consistent with other filter settings (Data Protection, Path Exclusions) +- **Separate search boxes for Frozen Textures and Preview sections** - Each section now owns its own search state, allowing independent filtering; search boxes use Unity's standard `SearchField` control +- **Texture type label relocated in preview entries** - Moved below the thumbnail for better visual grouping - **Analysis backends return raw complexity scores** - `ITextureAnalysisBackend.AnalyzeBatch` now returns `Dictionary` (0–1 scores) instead of fully built `TextureAnalysisResult`; score→divisor→resolution conversion moved to the service layer via `AnalysisResultHelper` - `CpuAnalysisBackend`, `GpuAnalysisBackend`, `AnalysisBackendFactory`, and `TextureAnalyzer` no longer depend on `ComplexityCalculator` - Backend contract documented: returned keys must be a subset of input keys; analysis failures on valid textures return a default score rather than dropping the entry @@ -29,6 +31,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Resolve original asset path via ObjectRegistry for textures replaced by other NDMF plugins - Collect all material references for shared textures regardless of per-property type filter +- Disable preview entry when frozen state changes in either direction (freeze or unfreeze) since the previous scan +- Frozen texture search matches the GUID fallback when the asset path cannot be resolved, consistent with the displayed text +- Invalidate the GUID-to-path cache on Unity project changes so renamed/moved assets resolve correctly +- Freeze/Unfreeze button in Preview and Frozen entries is no longer pushed off-screen by long texture names; entries are capped to the inspector view width and the texture name label clips when it would overlap the button ## [v0.7.0] - 2026-03-27 diff --git a/Editor/Common/UI/Controls/SearchBoxControl.cs b/Editor/Common/UI/Controls/SearchBoxControl.cs index f5e0378..4c7fe70 100644 --- a/Editor/Common/UI/Controls/SearchBoxControl.cs +++ b/Editor/Common/UI/Controls/SearchBoxControl.cs @@ -2,19 +2,21 @@ using System.Collections.Generic; using dev.limitex.avatar.compressor.editor; using UnityEditor; +using UnityEditor.IMGUI.Controls; using UnityEngine; namespace dev.limitex.avatar.compressor.editor.ui { /// - /// A reusable search box control with fuzzy search support and result caching. + /// Reusable search control with fuzzy search support. + /// Holds search state, draws the UI via Unity's , and provides matching logic. /// public class SearchBoxControl { /// /// Current search text. /// - public string SearchText { get; private set; } = ""; + public string SearchText { get; private set; } /// /// Whether fuzzy search is enabled. @@ -26,131 +28,126 @@ public class SearchBoxControl /// public bool IsSearching => !string.IsNullOrEmpty(SearchText); + private readonly SearchField _searchField = new(); + + // Count cache — avoids recomputing every IMGUI frame + private int _cachedCount; + private string _cachedCountSearchText; + private bool _cachedCountUseFuzzy; + private object _cachedSourceRef; + private int _cachedSourceCount; + private object _cachedPredicate; + /// - /// Event fired when search text or fuzzy search option changes. + /// Creates a new SearchBoxControl with optional initial state. /// - public event Action OnSearchChanged; - - // Cache state - private string _cachedSearchText; - private bool _cachedUseFuzzySearch; - private HashSet _cachedMatchIndices; - private Func _matchFunction; - private int _cachedItemCount; + /// Initial search text. + /// Initial fuzzy search state. + public SearchBoxControl(string searchText = "", bool useFuzzySearch = false) + { + SearchText = searchText; + UseFuzzySearch = useFuzzySearch; + } /// - /// Draws the search box UI. + /// Draws the search box UI and updates internal state. /// + /// Number of matched items (-1 to hide the summary). + /// Total number of items (-1 to hide the summary). /// True if search parameters changed. - public bool Draw() + public bool Draw(int matchedCount = -1, int totalCount = -1) { bool changed = false; - EditorGUILayout.BeginVertical(EditorStyles.helpBox); - EditorGUILayout.BeginHorizontal(); - - // Search icon - var searchIcon = EditorGUIUtility.IconContent("d_Search Icon"); - if (searchIcon != null && searchIcon.image != null) - { - GUILayout.Label(searchIcon, GUILayout.Width(20), GUILayout.Height(18)); - } - - // Text field with placeholder - var textFieldRect = EditorGUILayout.GetControlRect(); - GUI.SetNextControlName("SearchField"); - EditorGUI.BeginChangeCheck(); - var newSearchText = EditorGUI.TextField(textFieldRect, SearchText); - if (EditorGUI.EndChangeCheck()) + var newText = _searchField.OnGUI(SearchText); + if (newText != SearchText) { - SearchText = newSearchText; + SearchText = newText; changed = true; } - // Draw placeholder when empty and not focused - if (string.IsNullOrEmpty(SearchText) && GUI.GetNameOfFocusedControl() != "SearchField") - { - var placeholderRect = textFieldRect; - placeholderRect.x += 3; - EditorGUI.LabelField( - placeholderRect, - "Search textures...", - EditorStylesCache.PlaceholderStyle - ); - } - - // Clear button - EditorGUI.BeginDisabledGroup(string.IsNullOrEmpty(SearchText)); - if (GUILayout.Button("Clear", GUILayout.Width(50))) - { - SearchText = ""; - GUI.FocusControl(null); - changed = true; - } - EditorGUI.EndDisabledGroup(); - - EditorGUILayout.EndHorizontal(); - - // Show options when searching - if (!string.IsNullOrEmpty(SearchText)) + if (IsSearching) { EditorGUILayout.BeginHorizontal(); - GUILayout.Space(30); - // Fuzzy search toggle EditorGUI.BeginChangeCheck(); - var newUseFuzzySearch = EditorGUILayout.ToggleLeft( + var fuzzy = EditorGUILayout.ToggleLeft( "Fuzzy", UseFuzzySearch, GUILayout.Width(55) ); if (EditorGUI.EndChangeCheck()) { - UseFuzzySearch = newUseFuzzySearch; + UseFuzzySearch = fuzzy; changed = true; } - EditorGUILayout.EndHorizontal(); - } + GUILayout.FlexibleSpace(); - EditorGUILayout.EndVertical(); + if (totalCount >= 0) + { + GUILayout.Label( + $"Showing {matchedCount} of {totalCount}", + EditorStyles.miniLabel + ); + } - if (changed) - { - InvalidateCache(); - OnSearchChanged?.Invoke(); + EditorGUILayout.EndHorizontal(); } return changed; } /// - /// Draws the hit count display. + /// Returns the number of items matching the current search, with caching. + /// When not searching, returns items.Count without invoking the predicate. + /// The cache is keyed on (SearchText, UseFuzzySearch, collection reference, items.Count, predicate) + /// and is automatically invalidated when any of these change. /// - /// Total number of matching items. - /// Number of frozen texture matches. - /// Number of preview texture matches. - public void DrawHitCount(int totalHits, int frozenHits, int previewHits) + /// Source collection. + /// Per-item match function (only called on cache miss). + public int CountMatches(IReadOnlyCollection items, Func predicate) { - if (string.IsNullOrEmpty(SearchText)) - return; + if (!IsSearching) + return items.Count; - string hitText = totalHits == 1 ? "1 hit" : $"{totalHits} hits"; - if (frozenHits > 0 || previewHits > 0) + if ( + SearchText == _cachedCountSearchText + && UseFuzzySearch == _cachedCountUseFuzzy + && ReferenceEquals(items, _cachedSourceRef) + && items.Count == _cachedSourceCount + && Equals(predicate, _cachedPredicate) + ) + { + return _cachedCount; + } + + int count = 0; + foreach (var item in items) { - hitText += $" (Frozen: {frozenHits}, Preview: {previewHits})"; + if (predicate(item)) + count++; } - EditorGUILayout.LabelField(hitText, EditorStylesCache.HitCountStyle); + + _cachedCount = count; + _cachedCountSearchText = SearchText; + _cachedCountUseFuzzy = UseFuzzySearch; + _cachedSourceRef = items; + _cachedSourceCount = items.Count; + _cachedPredicate = predicate; + return count; } /// - /// Draws a hidden count indicator. + /// Invalidates the count cache, forcing to recount + /// on its next call. Use when the source data changes without a count change + /// (e.g. preview regeneration). /// - /// Number of hidden items. - public static void DrawHiddenCount(int hiddenCount) + public void InvalidateCountCache() { - string hiddenText = hiddenCount == 1 ? "1 hidden" : $"{hiddenCount} hidden"; - EditorGUILayout.LabelField(hiddenText, EditorStylesCache.HiddenCountStyle); + _cachedCountSearchText = null; + _cachedSourceRef = null; + _cachedPredicate = null; } /// @@ -160,61 +157,52 @@ public void Clear() { SearchText = ""; UseFuzzySearch = false; - InvalidateCache(); + InvalidateCountCache(); } /// - /// Invalidates the search cache, forcing recalculation on next access. + /// Checks if a string matches the current search. + /// When not searching, always returns true (all items visible). + /// When searching, returns false for null/empty text (no content to match against). /// - public void InvalidateCache() + /// Text to match against. + /// True if text matches search, or true if not searching. + public bool MatchesSearch(string text) { - _cachedMatchIndices = null; - _cachedSearchText = null; - _cachedItemCount = 0; + if (!IsSearching) + return true; + return MatchesCore(text); } /// - /// Checks if an item at the given index matches the search. + /// Checks if either of the provided strings matches the current search. /// - /// Item index. - /// Total item count. - /// Function that returns true if item at index matches search. - /// True if item matches or no search is active. - public bool IsMatch(int index, int itemCount, Func matchFunction) + public bool MatchesSearchAny(string text1, string text2) { if (!IsSearching) return true; - UpdateCache(itemCount, matchFunction); - return _cachedMatchIndices?.Contains(index) ?? false; + return MatchesCore(text1) || MatchesCore(text2); } /// - /// Counts the number of matching items. + /// Checks if any of the three provided strings matches the current search. /// - /// Total item count. - /// Function that returns true if item at index matches search. - /// Number of matching items. - public int CountMatches(int itemCount, Func matchFunction) + public bool MatchesSearchAny(string text1, string text2, string text3) { if (!IsSearching) - return itemCount; + return true; - UpdateCache(itemCount, matchFunction); - return _cachedMatchIndices?.Count ?? 0; + return MatchesCore(text1) || MatchesCore(text2) || MatchesCore(text3); } /// - /// Checks if a string matches the current search. + /// Core matching logic. Assumes IsSearching is true. /// - /// Text to match against. - /// True if text matches search. - public bool MatchesSearch(string text) + private bool MatchesCore(string text) { if (string.IsNullOrEmpty(text)) return false; - if (!IsSearching) - return true; if (UseFuzzySearch) { @@ -225,54 +213,5 @@ public bool MatchesSearch(string text) return text.IndexOf(SearchText, StringComparison.OrdinalIgnoreCase) >= 0; } } - - /// - /// Checks if any of the provided strings matches the current search. - /// - /// Texts to match against. - /// True if any text matches search. - public bool MatchesSearchAny(params string[] texts) - { - if (!IsSearching) - return true; - - foreach (var text in texts) - { - if (MatchesSearch(text)) - return true; - } - return false; - } - - private void UpdateCache(int itemCount, Func matchFunction) - { - // Check if cache is valid - if ( - _cachedSearchText == SearchText - && _cachedUseFuzzySearch == UseFuzzySearch - && _cachedItemCount == itemCount - && _matchFunction == matchFunction - && _cachedMatchIndices != null - ) - { - return; - } - - // Update cache state - _cachedSearchText = SearchText; - _cachedUseFuzzySearch = UseFuzzySearch; - _cachedItemCount = itemCount; - _matchFunction = matchFunction; - - // Rebuild cache - _cachedMatchIndices = new HashSet(); - for (int i = 0; i < itemCount; i++) - { - if (matchFunction(i)) - { - _cachedMatchIndices.Add(i); - } - } - } } } diff --git a/Editor/Common/UI/Styles/EditorStylesCache.cs b/Editor/Common/UI/Styles/EditorStylesCache.cs index ca8c219..481aad2 100644 --- a/Editor/Common/UI/Styles/EditorStylesCache.cs +++ b/Editor/Common/UI/Styles/EditorStylesCache.cs @@ -11,9 +11,9 @@ namespace dev.limitex.avatar.compressor.editor.ui public static class EditorStylesCache { private static GUIStyle _centeredLabel; + private static GUIStyle _centeredBoldLabel; + private static GUIStyle _clippedBoldLabel; private static GUIStyle _linkStyle; - private static GUIStyle _placeholderStyle; - private static GUIStyle _hitCountStyle; private static GUIStyle _hiddenCountStyle; private static GUIStyle _modifiedStatusStyle; private static GUIStyle _syncedStatusStyle; @@ -25,9 +25,9 @@ public static class EditorStylesCache private static void ClearCacheOnDomainReload() { _centeredLabel = null; + _centeredBoldLabel = null; + _clippedBoldLabel = null; _linkStyle = null; - _placeholderStyle = null; - _hitCountStyle = null; _hiddenCountStyle = null; _modifiedStatusStyle = null; _syncedStatusStyle = null; @@ -43,36 +43,35 @@ private static void ClearCacheOnDomainReload() }; /// - /// Link style for clickable text (no underline, uses GUI.color for coloring). + /// Centered bold mini label style. /// - public static GUIStyle LinkStyle => _linkStyle ??= new GUIStyle(EditorStyles.miniLabel); + public static GUIStyle CenteredBoldLabel => + _centeredBoldLabel ??= new GUIStyle(EditorStyles.miniBoldLabel) + { + alignment = TextAnchor.MiddleCenter, + }; /// - /// Placeholder text style (italic, gray). + /// Bold label style that clips overflow. /// - public static GUIStyle PlaceholderStyle => - _placeholderStyle ??= new GUIStyle(EditorStyles.label) + public static GUIStyle ClippedBoldLabel => + _clippedBoldLabel ??= new GUIStyle(EditorStyles.boldLabel) { - fontStyle = FontStyle.Italic, - normal = { textColor = new Color(0.5f, 0.5f, 0.5f) }, + clipping = TextClipping.Clip, }; /// - /// Hit count style (right-aligned mini label). + /// Link style for clickable text (no underline, uses GUI.color for coloring). /// - public static GUIStyle HitCountStyle => - _hitCountStyle ??= new GUIStyle(EditorStyles.miniLabel) - { - alignment = TextAnchor.MiddleRight, - }; + public static GUIStyle LinkStyle => _linkStyle ??= new GUIStyle(EditorStyles.miniLabel); /// - /// Hidden count style (right-aligned, gray mini label). + /// Hidden count style (center-aligned, gray mini label). /// public static GUIStyle HiddenCountStyle => _hiddenCountStyle ??= new GUIStyle(EditorStyles.miniLabel) { - alignment = TextAnchor.MiddleRight, + alignment = TextAnchor.MiddleCenter, normal = { textColor = new Color(0.6f, 0.6f, 0.6f) }, }; diff --git a/Editor/Common/UI/Utils/EditorDrawUtils.cs b/Editor/Common/UI/Utils/EditorDrawUtils.cs index 9a8a6c1..7ddb311 100644 --- a/Editor/Common/UI/Utils/EditorDrawUtils.cs +++ b/Editor/Common/UI/Utils/EditorDrawUtils.cs @@ -46,6 +46,16 @@ public static void DrawSeparator() EditorGUILayout.Space(5); } + /// + /// Draws a hidden count indicator for filtered lists. + /// + /// Number of hidden items. + public static void DrawHiddenCount(int hiddenCount) + { + string hiddenText = $"{hiddenCount} hidden"; + EditorGUILayout.LabelField(hiddenText, EditorStylesCache.HiddenCountStyle); + } + /// /// Draws a colored button with selection highlight. /// diff --git a/Editor/Common/UI/Utils/GuidPathCache.cs b/Editor/Common/UI/Utils/GuidPathCache.cs new file mode 100644 index 0000000..4c96bf6 --- /dev/null +++ b/Editor/Common/UI/Utils/GuidPathCache.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using UnityEditor; + +namespace dev.limitex.avatar.compressor.editor.ui +{ + /// + /// Shared cache of GUID-to-asset-path lookups. Auto-clears on Unity project changes. + /// + [InitializeOnLoad] + public static class GuidPathCache + { + private static readonly Dictionary s_cache = new(); + + static GuidPathCache() + { + EditorApplication.projectChanged += Clear; + } + + /// + /// Returns the asset path for the given GUID, using the cache to avoid repeated lookups. + /// + public static string GetPath(string guid) + { + if (string.IsNullOrEmpty(guid)) + return ""; + + if (!s_cache.TryGetValue(guid, out var path)) + { + path = AssetDatabase.GUIDToAssetPath(guid); + s_cache[guid] = path; + } + return path; + } + + /// + /// Clears all cached entries. + /// + public static void Clear() + { + s_cache.Clear(); + } + } +} diff --git a/Editor/Common/UI/Utils/GuidPathCache.cs.meta b/Editor/Common/UI/Utils/GuidPathCache.cs.meta new file mode 100644 index 0000000..d08e1a0 --- /dev/null +++ b/Editor/Common/UI/Utils/GuidPathCache.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d1be2709c3c342dc781e3730351443d7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/TextureCompressor/TextureCompressorEditor.cs b/Editor/TextureCompressor/TextureCompressorEditor.cs index 317997b..eba55ab 100644 --- a/Editor/TextureCompressor/TextureCompressorEditor.cs +++ b/Editor/TextureCompressor/TextureCompressorEditor.cs @@ -1,5 +1,4 @@ using dev.limitex.avatar.compressor.editor.texture.ui; -using dev.limitex.avatar.compressor.editor.ui; using UnityEditor; using UnityEngine; @@ -11,19 +10,17 @@ namespace dev.limitex.avatar.compressor.editor.texture [CustomEditor(typeof(TextureCompressor))] public class TextureCompressorEditor : CompressorEditorBase { - // State-holding components - private SearchBoxControl _searchBox; + // Section components (each owns its own search and UI state) + private FrozenTexturesSection _frozenSection; private PreviewSection _previewSection; - // UI state + // UI state for sections that don't own their own private bool _showExclusionsSection; private bool _showTextureFiltersSection; - private bool _showFrozenSection = true; - private Vector2 _frozenScrollPosition; private void OnEnable() { - _searchBox = new SearchBoxControl(); + _frozenSection = new FrozenTexturesSection(); _previewSection = new PreviewSection(); } @@ -49,21 +46,12 @@ protected override void DrawInspectorContent() FilterSection.DrawExclusions(config, ref _showExclusionsSection); EditorGUILayout.Space(15); - // Search box - _searchBox.Draw(); - EditorGUILayout.Space(10); - // Frozen textures - FrozenTexturesSection.Draw( - config, - _searchBox, - ref _showFrozenSection, - ref _frozenScrollPosition - ); + _frozenSection.Draw(config); EditorGUILayout.Space(15); // Preview section - _previewSection.Draw(config, _searchBox); + _previewSection.Draw(config); } } } diff --git a/Editor/TextureCompressor/UI/Preview/PreviewSection.cs b/Editor/TextureCompressor/UI/Preview/PreviewSection.cs index d5ec4d2..2c1b7c6 100644 --- a/Editor/TextureCompressor/UI/Preview/PreviewSection.cs +++ b/Editor/TextureCompressor/UI/Preview/PreviewSection.cs @@ -8,10 +8,12 @@ namespace dev.limitex.avatar.compressor.editor.texture.ui { /// /// Draws the preview section with texture analysis results. + /// Owns its own search box. /// public class PreviewSection { private readonly PreviewGenerator _generator = new PreviewGenerator(); + private readonly SearchBoxControl _searchBox = new(); private TexturePreviewData[] _previewData; private int _previewSettingsHash; private bool _showPreview; @@ -20,7 +22,7 @@ public class PreviewSection /// /// Draws the preview section. /// - public void Draw(TextureCompressor config, SearchBoxControl search) + public void Draw(TextureCompressor config) { bool isOutdated = IsPreviewOutdated(config); @@ -28,7 +30,6 @@ public void Draw(TextureCompressor config, SearchBoxControl search) { GeneratePreview(config); _showPreview = true; - search.InvalidateCache(); } if (_showPreview && _previewData != null && _previewData.Length > 0) @@ -40,7 +41,7 @@ public void Draw(TextureCompressor config, SearchBoxControl search) MessageType.Warning ); } - DrawPreview(config, search); + DrawPreview(config); } else if (_showPreview && (_previewData == null || _previewData.Length == 0)) { @@ -56,20 +57,12 @@ public void Draw(TextureCompressor config, SearchBoxControl search) } } - /// - /// Invalidates the preview cache. - /// - public void InvalidateCache() - { - _previewData = null; - _previewSettingsHash = 0; - } - private void GeneratePreview(TextureCompressor config) { var backend = AvatarCompressorPreferences.AnalysisBackend; _previewSettingsHash = PreviewGenerator.ComputeSettingsHash(config, backend); _previewData = _generator.Generate(config, backend); + _searchBox.InvalidateCountCache(); } private bool IsPreviewOutdated(TextureCompressor config) @@ -83,13 +76,13 @@ private bool IsPreviewOutdated(TextureCompressor config) ) != _previewSettingsHash; } - private void DrawPreview(TextureCompressor config, SearchBoxControl search) + private void DrawPreview(TextureCompressor config) { EditorGUILayout.Space(10); - bool isSearching = search.IsSearching; + bool isSearching = _searchBox.IsSearching; int totalCount = _previewData.Length; - int filteredCount = isSearching ? CountPreviewMatches(search) : totalCount; + int filteredCount = _searchBox.CountMatches(_previewData, MatchesPreviewSearch); // Build header text string frozenInfo = @@ -150,6 +143,9 @@ private void DrawPreview(TextureCompressor config, SearchBoxControl search) EditorGUILayout.EndVertical(); + // Search box for preview textures + _searchBox.Draw(filteredCount, totalCount); + // Show "no results" message when searching with no matches if (isSearching && filteredCount == 0) { @@ -174,7 +170,7 @@ private void DrawPreview(TextureCompressor config, SearchBoxControl search) var data = _previewData[i]; // Skip items that don't match search - if (isSearching && !MatchesPreviewSearch(data, search)) + if (isSearching && !MatchesPreviewSearch(data)) continue; // Section headers @@ -201,7 +197,7 @@ private void DrawPreview(TextureCompressor config, SearchBoxControl search) hasDrawnSkippedHeader = true; } - DrawPreviewEntry(config, data, search); + DrawPreviewEntry(config, data); } EditorGUILayout.EndScrollView(); @@ -209,7 +205,7 @@ private void DrawPreview(TextureCompressor config, SearchBoxControl search) // Show hidden count when searching if (isSearching && filteredCount < totalCount) { - SearchBoxControl.DrawHiddenCount(totalCount - filteredCount); + EditorDrawUtils.DrawHiddenCount(totalCount - filteredCount); } } @@ -222,30 +218,40 @@ private void DrawPreview(TextureCompressor config, SearchBoxControl search) } } - private void DrawPreviewEntry( - TextureCompressor config, - TexturePreviewData data, - SearchBoxControl search - ) + private void DrawPreviewEntry(TextureCompressor config, TexturePreviewData data) { bool isSkipped = !data.IsProcessed; - bool isFrozenNow = !data.IsFrozen && config.IsFrozen(data.Guid); + bool frozenStateChanged = data.IsFrozen != config.IsFrozen(data.Guid); - if (isSkipped || isFrozenNow) + if (isSkipped || frozenStateChanged) { EditorGUI.BeginDisabledGroup(true); } EditorGUILayout.BeginHorizontal(EditorStyles.helpBox); - // Thumbnail + EditorGUILayout.BeginVertical(GUILayout.Width(45)); + EditorGUILayout.Space(2); + EditorGUILayout.BeginHorizontal(GUILayout.Width(45)); + GUILayout.FlexibleSpace(); ThumbnailControl.DrawClickable(data.Texture); + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.LabelField( + data.TextureType.ToString(), + EditorStylesCache.CenteredBoldLabel, + GUILayout.Width(45) + ); + EditorGUILayout.EndVertical(); EditorGUILayout.BeginVertical(); EditorGUILayout.BeginHorizontal(); - EditorGUILayout.LabelField(data.Texture.name, EditorStyles.boldLabel); - EditorGUILayout.LabelField($"[{data.TextureType.ToString()}]", GUILayout.Width(60)); + EditorGUILayout.LabelField( + data.Texture.name, + EditorStylesCache.ClippedBoldLabel, + GUILayout.MinWidth(0) + ); // Freeze/Unfreeze button if (data.IsProcessed) @@ -259,7 +265,6 @@ SearchBoxControl search Undo.RecordObject(config, "Unfreeze Texture"); config.UnfreezeTexture(data.Guid); EditorUtility.SetDirty(config); - search.InvalidateCache(); } GUI.backgroundColor = savedColor; } @@ -276,7 +281,6 @@ SearchBoxControl search ); config.SetFrozenSettings(data.Guid, frozenSettings); EditorUtility.SetDirty(config); - search.InvalidateCache(); } } } @@ -295,7 +299,7 @@ SearchBoxControl search EditorGUILayout.EndVertical(); EditorGUILayout.EndHorizontal(); - if (isSkipped || isFrozenNow) + if (isSkipped || frozenStateChanged) { EditorGUI.EndDisabledGroup(); } @@ -329,7 +333,7 @@ private void DrawProcessedDetails(TexturePreviewData data) sizeText = $"{data.OriginalSize.x}x{data.OriginalSize.y} (unchanged){manualIndicator}"; } - EditorGUILayout.LabelField(sizeText); + EditorGUILayout.LabelField(sizeText, GUILayout.MinWidth(0)); EditorGUILayout.EndHorizontal(); // Format @@ -353,7 +357,11 @@ private void DrawProcessedDetails(TexturePreviewData data) GUI.color = formatColor; EditorGUILayout.LabelField(formatName, EditorStyles.boldLabel, GUILayout.Width(70)); GUI.color = savedGuiColor; - EditorGUILayout.LabelField(formatInfo, EditorStyles.miniLabel); + EditorGUILayout.LabelField( + formatInfo, + EditorStyles.miniLabel, + GUILayout.MinWidth(0) + ); } else { @@ -387,26 +395,12 @@ private void DrawSkippedDetails(TexturePreviewData data) EditorGUILayout.EndHorizontal(); } - private int CountPreviewMatches(SearchBoxControl search) - { - if (_previewData == null) - return 0; - - int count = 0; - foreach (var data in _previewData) - { - if (MatchesPreviewSearch(data, search)) - count++; - } - return count; - } - - private bool MatchesPreviewSearch(TexturePreviewData data, SearchBoxControl search) + private bool MatchesPreviewSearch(TexturePreviewData data) { - string assetPath = AssetDatabase.GUIDToAssetPath(data.Guid); + string assetPath = GuidPathCache.GetPath(data.Guid); string textureName = data.Texture != null ? data.Texture.name : ""; - return search.MatchesSearchAny(textureName, assetPath, data.TextureType.ToString()); + return _searchBox.MatchesSearchAny(textureName, assetPath, data.TextureType.ToString()); } } } diff --git a/Editor/TextureCompressor/UI/Sections/FrozenTexturesSection.cs b/Editor/TextureCompressor/UI/Sections/FrozenTexturesSection.cs index ab6e2dc..0168b73 100644 --- a/Editor/TextureCompressor/UI/Sections/FrozenTexturesSection.cs +++ b/Editor/TextureCompressor/UI/Sections/FrozenTexturesSection.cs @@ -1,3 +1,4 @@ +using System.IO; using dev.limitex.avatar.compressor; using dev.limitex.avatar.compressor.editor.ui; using UnityEditor; @@ -7,51 +8,22 @@ namespace dev.limitex.avatar.compressor.editor.texture.ui { /// /// Draws the frozen textures section for manual texture overrides. + /// Owns its own search box and all UI state. /// - public static class FrozenTexturesSection + public class FrozenTexturesSection { - private static readonly System.Collections.Generic.Dictionary< - string, - string - > _guidPathCache = new(); - - /// - /// Gets the asset path for a GUID, using cache to avoid repeated lookups. - /// - private static string GetAssetPathCached(string guid) - { - if (string.IsNullOrEmpty(guid)) - return ""; - - if (!_guidPathCache.TryGetValue(guid, out var path)) - { - path = AssetDatabase.GUIDToAssetPath(guid); - _guidPathCache[guid] = path; - } - return path; - } - - /// - /// Clears the GUID-to-path cache. Call when assets may have changed. - /// - public static void InvalidateCache() - { - _guidPathCache.Clear(); - } + private readonly SearchBoxControl _searchBox = new(); + private bool _showSection = true; + private Vector2 _scrollPosition; /// /// Draws the frozen textures section with search filtering. /// - public static void Draw( - TextureCompressor config, - SearchBoxControl search, - ref bool showSection, - ref Vector2 scrollPos - ) + public void Draw(TextureCompressor config) { int frozenCount = config.FrozenTextures.Count; - bool isSearching = search.IsSearching; - int filteredCount = isSearching ? CountMatches(config, search) : frozenCount; + bool isSearching = _searchBox.IsSearching; + int filteredCount = _searchBox.CountMatches(config.FrozenTextures, MatchesFrozenSearch); // Show filtered count in header when searching string headerText = @@ -59,9 +31,9 @@ ref Vector2 scrollPos ? $"Frozen Textures ({filteredCount}/{frozenCount})" : $"Frozen Textures ({frozenCount})"; - showSection = EditorGUILayout.Foldout(showSection, headerText, true); + _showSection = EditorGUILayout.Foldout(_showSection, headerText, true); - if (!showSection) + if (!_showSection) return; if (frozenCount == 0) @@ -73,6 +45,9 @@ ref Vector2 scrollPos return; } + // Search box for frozen textures + _searchBox.Draw(filteredCount, frozenCount); + // Show "no results" message when searching with no matches if (isSearching && filteredCount == 0) { @@ -88,7 +63,10 @@ ref Vector2 scrollPos if (useScrollView) { - scrollPos = EditorGUILayout.BeginScrollView(scrollPos, GUILayout.MaxHeight(250)); + _scrollPosition = EditorGUILayout.BeginScrollView( + _scrollPosition, + GUILayout.MaxHeight(250) + ); } for (int i = config.FrozenTextures.Count - 1; i >= 0; i--) @@ -96,7 +74,7 @@ ref Vector2 scrollPos var frozen = config.FrozenTextures[i]; // Skip items that don't match search - if (isSearching && !MatchesFrozenSearch(frozen, search)) + if (isSearching && !MatchesFrozenSearch(frozen)) continue; DrawFrozenTextureEntry(config, frozen, i); @@ -110,11 +88,11 @@ ref Vector2 scrollPos // Show hidden count when searching if (isSearching && filteredCount < frozenCount) { - SearchBoxControl.DrawHiddenCount(frozenCount - filteredCount); + EditorDrawUtils.DrawHiddenCount(frozenCount - filteredCount); } } - private static void DrawFrozenTextureEntry( + private void DrawFrozenTextureEntry( TextureCompressor config, FrozenTextureSettings frozen, int index @@ -125,13 +103,19 @@ int index EditorGUILayout.BeginHorizontal(EditorStyles.helpBox); // Resolve path from GUID for display and loading (cached) - string assetPath = GetAssetPathCached(frozen.TextureGuid); + string assetPath = GuidPathCache.GetPath(frozen.TextureGuid); var texture = !string.IsNullOrEmpty(assetPath) ? AssetDatabase.LoadAssetAtPath(assetPath) : null; - // Thumbnail + EditorGUILayout.BeginVertical(GUILayout.Width(45)); + EditorGUILayout.Space(2); + EditorGUILayout.BeginHorizontal(GUILayout.Width(45)); + GUILayout.FlexibleSpace(); ThumbnailControl.DrawClickable(texture); + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); EditorGUILayout.BeginVertical(); @@ -139,9 +123,13 @@ int index EditorGUILayout.BeginHorizontal(); string textureName = !string.IsNullOrEmpty(assetPath) - ? System.IO.Path.GetFileName(assetPath) + ? Path.GetFileName(assetPath) : frozen.TextureGuid; - EditorGUILayout.LabelField(textureName, EditorStyles.boldLabel); + EditorGUILayout.LabelField( + textureName, + EditorStylesCache.ClippedBoldLabel, + GUILayout.MinWidth(0) + ); if (GUILayout.Button("Unfreeze", GUILayout.Width(70))) { @@ -282,28 +270,14 @@ private static void DrawLegacyPathMigrationUI(TextureCompressor config) EditorGUILayout.EndHorizontal(); } - private static int CountMatches(TextureCompressor config, SearchBoxControl search) - { - int count = 0; - foreach (var frozen in config.FrozenTextures) - { - if (MatchesFrozenSearch(frozen, search)) - count++; - } - return count; - } - - private static bool MatchesFrozenSearch( - FrozenTextureSettings frozen, - SearchBoxControl search - ) + private bool MatchesFrozenSearch(FrozenTextureSettings frozen) { - string assetPath = GetAssetPathCached(frozen.TextureGuid); + string assetPath = GuidPathCache.GetPath(frozen.TextureGuid); string textureName = !string.IsNullOrEmpty(assetPath) - ? assetPath.Substring(assetPath.LastIndexOf('/') + 1) - : ""; + ? Path.GetFileName(assetPath) + : frozen.TextureGuid; - return search.MatchesSearchAny(textureName, assetPath); + return _searchBox.MatchesSearchAny(textureName, assetPath); } } } diff --git a/Tests/Editor/UI/GuidPathCacheTests.cs b/Tests/Editor/UI/GuidPathCacheTests.cs new file mode 100644 index 0000000..4f88759 --- /dev/null +++ b/Tests/Editor/UI/GuidPathCacheTests.cs @@ -0,0 +1,107 @@ +using System.Collections.Generic; +using System.IO; +using dev.limitex.avatar.compressor.editor.ui; +using NUnit.Framework; +using UnityEditor; +using UnityEngine; + +namespace dev.limitex.avatar.compressor.tests +{ + [TestFixture] + public class GuidPathCacheTests + { + private const string TestAssetFolder = "Assets/_LAC_TMP_GuidPathCache"; + private List _createdAssetPaths; + + [SetUp] + public void SetUp() + { + _createdAssetPaths = new List(); + GuidPathCache.Clear(); + + if (!AssetDatabase.IsValidFolder(TestAssetFolder)) + { + AssetDatabase.CreateFolder("Assets", "_LAC_TMP_GuidPathCache"); + } + } + + [TearDown] + public void TearDown() + { + foreach (var path in _createdAssetPaths) + { + if ( + !string.IsNullOrEmpty(path) + && AssetDatabase.LoadAssetAtPath(path) != null + ) + { + AssetDatabase.DeleteAsset(path); + } + } + _createdAssetPaths.Clear(); + + if (AssetDatabase.IsValidFolder(TestAssetFolder)) + { + var remaining = AssetDatabase.FindAssets("", new[] { TestAssetFolder }); + if (remaining.Length == 0) + { + AssetDatabase.DeleteAsset(TestAssetFolder); + } + } + } + + [Test] + public void GetPath_AfterClear_RefreshesCachedPath() + { + string originalPath = CreateImportedTextureAsset("GuidPathCacheTexture"); + string guid = AssetDatabase.AssetPathToGUID(originalPath); + + Assert.That(GuidPathCache.GetPath(guid), Is.EqualTo(originalPath)); + + string renameError = AssetDatabase.RenameAsset( + originalPath, + "GuidPathCacheTextureRenamed" + ); + Assert.That(renameError, Is.Empty); + + string renamedPath = $"{TestAssetFolder}/GuidPathCacheTextureRenamed.png"; + ReplaceTrackedPath(originalPath, renamedPath); + + GuidPathCache.Clear(); + + Assert.That(GuidPathCache.GetPath(guid), Is.EqualTo(renamedPath)); + } + + private string CreateImportedTextureAsset(string name) + { + var texture = new Texture2D(2, 2, TextureFormat.RGBA32, false); + texture.SetPixels32( + new[] + { + new Color32(255, 0, 0, 255), + new Color32(0, 255, 0, 255), + new Color32(0, 0, 255, 255), + new Color32(255, 255, 255, 255), + } + ); + texture.Apply(); + + string assetPath = $"{TestAssetFolder}/{name}.png"; + File.WriteAllBytes(assetPath, texture.EncodeToPNG()); + _createdAssetPaths.Add(assetPath); + Object.DestroyImmediate(texture); + + AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceSynchronousImport); + return assetPath; + } + + private void ReplaceTrackedPath(string originalPath, string renamedPath) + { + int index = _createdAssetPaths.IndexOf(originalPath); + if (index >= 0) + { + _createdAssetPaths[index] = renamedPath; + } + } + } +} diff --git a/Tests/Editor/UI/GuidPathCacheTests.cs.meta b/Tests/Editor/UI/GuidPathCacheTests.cs.meta new file mode 100644 index 0000000..fdb48ec --- /dev/null +++ b/Tests/Editor/UI/GuidPathCacheTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4cd75768a58b40fd8d5f8011ff6b1884 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/UI/SearchBoxControlTests.cs b/Tests/Editor/UI/SearchBoxControlTests.cs index b52e734..d011db6 100644 --- a/Tests/Editor/UI/SearchBoxControlTests.cs +++ b/Tests/Editor/UI/SearchBoxControlTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using dev.limitex.avatar.compressor.editor.ui; using NUnit.Framework; @@ -14,124 +15,193 @@ public void SetUp() _searchBox = new SearchBoxControl(); } - #region Initial State Tests + #region Constructor Tests [Test] - public void NewInstance_SearchTextIsEmpty() + public void Constructor_Default_IsEmptyAndNotSearching() { Assert.That(_searchBox.SearchText, Is.Empty); + Assert.That(_searchBox.IsSearching, Is.False); + Assert.That(_searchBox.UseFuzzySearch, Is.False); } [Test] - public void NewInstance_IsSearchingIsFalse() + public void Constructor_WithSearchText_SetsState() { - Assert.That(_searchBox.IsSearching, Is.False); + var search = new SearchBoxControl("test"); + + Assert.That(search.SearchText, Is.EqualTo("test")); + Assert.That(search.IsSearching, Is.True); + Assert.That(search.UseFuzzySearch, Is.False); } [Test] - public void NewInstance_UseFuzzySearchIsFalse() + public void Constructor_WithFuzzySearch_SetsState() { - Assert.That(_searchBox.UseFuzzySearch, Is.False); + var search = new SearchBoxControl("test", useFuzzySearch: true); + + Assert.That(search.SearchText, Is.EqualTo("test")); + Assert.That(search.UseFuzzySearch, Is.True); } #endregion - #region MatchesSearch Tests + #region MatchesSearch Tests (Not Searching) [Test] - public void MatchesSearch_EmptySearchText_ReturnsTrue() + public void MatchesSearch_NotSearching_ReturnsTrue() { - // When not searching, everything matches Assert.That(_searchBox.MatchesSearch("any text"), Is.True); } [Test] - public void MatchesSearch_NullText_ReturnsFalse() + public void MatchesSearch_NullText_NotSearching_ReturnsTrue() { - Assert.That(_searchBox.MatchesSearch(null), Is.False); + Assert.That(_searchBox.MatchesSearch(null), Is.True); } [Test] - public void MatchesSearch_EmptyText_ReturnsFalse() + public void MatchesSearch_EmptyText_NotSearching_ReturnsTrue() { - Assert.That(_searchBox.MatchesSearch(""), Is.False); + Assert.That(_searchBox.MatchesSearch(""), Is.True); } #endregion - #region MatchesSearchAny Tests + #region MatchesSearch Tests (Active Search) [Test] - public void MatchesSearchAny_NotSearching_ReturnsTrue() + public void MatchesSearch_SubstringMatch_ReturnsTrue() { - Assert.That(_searchBox.MatchesSearchAny("text1", "text2"), Is.True); + var search = new SearchBoxControl("avatar"); + + Assert.That(search.MatchesSearch("my_avatar_texture"), Is.True); + } + + [Test] + public void MatchesSearch_CaseInsensitive_ReturnsTrue() + { + var search = new SearchBoxControl("AVATAR"); + + Assert.That(search.MatchesSearch("my_avatar_texture"), Is.True); + } + + [Test] + public void MatchesSearch_NoMatch_ReturnsFalse() + { + var search = new SearchBoxControl("xyz"); + + Assert.That(search.MatchesSearch("avatar_texture"), Is.False); + } + + [Test] + public void MatchesSearch_ExactMatch_ReturnsTrue() + { + var search = new SearchBoxControl("texture"); + + Assert.That(search.MatchesSearch("texture"), Is.True); + } + + [Test] + public void MatchesSearch_NullText_WhileSearching_ReturnsFalse() + { + var search = new SearchBoxControl("test"); + + Assert.That(search.MatchesSearch(null), Is.False); + } + + [Test] + public void MatchesSearch_EmptyText_WhileSearching_ReturnsFalse() + { + var search = new SearchBoxControl("test"); + + Assert.That(search.MatchesSearch(""), Is.False); } + #endregion + + #region MatchesSearchAny Tests (Not Searching) + [Test] - public void MatchesSearchAny_EmptyArray_ReturnsTrue_WhenNotSearching() + public void MatchesSearchAny_NotSearching_ReturnsTrue() { - Assert.That(_searchBox.MatchesSearchAny(), Is.True); + Assert.That(_searchBox.MatchesSearchAny("text1", "text2"), Is.True); } #endregion - #region Clear Tests + #region MatchesSearchAny Tests (Two Args) [Test] - public void Clear_ResetsSearchText() + public void MatchesSearchAny_TwoArgs_FirstMatches_ReturnsTrue() { - _searchBox.Clear(); + var search = new SearchBoxControl("avatar"); - Assert.That(_searchBox.SearchText, Is.Empty); + Assert.That(search.MatchesSearchAny("my_avatar", "no_match"), Is.True); } [Test] - public void Clear_ResetsFuzzySearch() + public void MatchesSearchAny_TwoArgs_SecondMatches_ReturnsTrue() { - _searchBox.Clear(); + var search = new SearchBoxControl("avatar"); - Assert.That(_searchBox.UseFuzzySearch, Is.False); + Assert.That(search.MatchesSearchAny("no_match", "my_avatar"), Is.True); } [Test] - public void Clear_IsSearchingBecomesFalse() + public void MatchesSearchAny_TwoArgs_NoneMatch_ReturnsFalse() { - _searchBox.Clear(); + var search = new SearchBoxControl("xyz"); - Assert.That(_searchBox.IsSearching, Is.False); + Assert.That(search.MatchesSearchAny("avatar", "texture"), Is.False); } #endregion - #region InvalidateCache Tests + #region MatchesSearchAny Tests (Three Args) [Test] - public void InvalidateCache_DoesNotThrow() + public void MatchesSearchAny_ThreeArgs_LastMatches_ReturnsTrue() { - Assert.DoesNotThrow(() => _searchBox.InvalidateCache()); + var search = new SearchBoxControl("normal"); + + Assert.That(search.MatchesSearchAny("diffuse", "specular", "normal_map"), Is.True); } [Test] - public void InvalidateCache_CanBeCalledMultipleTimes() + public void MatchesSearchAny_ThreeArgs_NoneMatch_ReturnsFalse() { - Assert.DoesNotThrow(() => - { - _searchBox.InvalidateCache(); - _searchBox.InvalidateCache(); - _searchBox.InvalidateCache(); - }); + var search = new SearchBoxControl("xyz"); + + Assert.That(search.MatchesSearchAny("avatar", "texture", "normal"), Is.False); } #endregion - #region IsMatch Tests + #region Fuzzy Search Tests [Test] - public void IsMatch_NotSearching_ReturnsTrue() + public void MatchesSearch_FuzzyEnabled_MatchesNonContiguous() { - bool result = _searchBox.IsMatch(0, 10, i => false); + var search = new SearchBoxControl("avt", useFuzzySearch: true); - Assert.That(result, Is.True); + Assert.That(search.MatchesSearch("avatar_texture"), Is.True); + } + + [Test] + public void MatchesSearch_FuzzyDisabled_DoesNotMatchNonContiguous() + { + var search = new SearchBoxControl("avt"); + + Assert.That(search.MatchesSearch("avatar_texture"), Is.False); + } + + [Test] + public void MatchesSearchAny_FuzzyEnabled_MatchesAny() + { + var search = new SearchBoxControl("avt", useFuzzySearch: true); + + Assert.That(search.MatchesSearchAny("no_match", "avatar_texture"), Is.True); } #endregion @@ -141,23 +211,140 @@ public void IsMatch_NotSearching_ReturnsTrue() [Test] public void CountMatches_NotSearching_ReturnsItemCount() { - int result = _searchBox.CountMatches(10, i => false); + var items = new List { "a", "b", "c" }; + + int result = _searchBox.CountMatches(items, _ => false); - Assert.That(result, Is.EqualTo(10)); + Assert.That(result, Is.EqualTo(3)); + } + + [Test] + public void CountMatches_Searching_CountsMatchingItems() + { + var search = new SearchBoxControl("test"); + var items = new List { "test1", "test2", "other" }; + + int result = search.CountMatches(items, s => search.MatchesSearch(s)); + + Assert.That(result, Is.EqualTo(2)); + } + + [Test] + public void CountMatches_Searching_NoMatches_ReturnsZero() + { + var search = new SearchBoxControl("xyz"); + var items = new List { "alpha", "beta", "gamma" }; + + int result = search.CountMatches(items, s => search.MatchesSearch(s)); + + Assert.That(result, Is.EqualTo(0)); + } + + [Test] + public void CountMatches_CachesResult_SkipsPredicateOnSecondCall() + { + var search = new SearchBoxControl("test"); + var items = new List { "test1", "other" }; + int callCount = 0; + System.Func predicate = s => + { + callCount++; + return s.Contains("test"); + }; + + search.CountMatches(items, predicate); + int firstCallCount = callCount; + + search.CountMatches(items, predicate); + + Assert.That(callCount, Is.EqualTo(firstCallCount)); + } + + [Test] + public void CountMatches_InvalidateCountCache_ForcesRecount() + { + var search = new SearchBoxControl("test"); + var items = new List { "test1", "other" }; + int callCount = 0; + System.Func predicate = s => + { + callCount++; + return s.Contains("test"); + }; + + search.CountMatches(items, predicate); + int firstCallCount = callCount; + + search.InvalidateCountCache(); + search.CountMatches(items, predicate); + + Assert.That(callCount, Is.GreaterThan(firstCallCount)); + } + + [Test] + public void CountMatches_CacheInvalidatedByCountChange() + { + var search = new SearchBoxControl("test"); + var items = new List { "test1", "other" }; + int callCount = 0; + System.Func predicate = s => + { + callCount++; + return s.Contains("test"); + }; + + search.CountMatches(items, predicate); + int firstCallCount = callCount; + + items.Add("test2"); + search.CountMatches(items, predicate); + + Assert.That(callCount, Is.GreaterThan(firstCallCount)); + } + + [Test] + public void CountMatches_WorksWithArray() + { + var search = new SearchBoxControl("test"); + var items = new[] { "test1", "test2", "other" }; + + int result = search.CountMatches(items, s => search.MatchesSearch(s)); + + Assert.That(result, Is.EqualTo(2)); } #endregion - #region OnSearchChanged Event Tests + #region Clear Tests [Test] - public void OnSearchChanged_CanSubscribe() + public void Clear_ResetsSearchText() { - bool eventFired = false; - _searchBox.OnSearchChanged += () => eventFired = true; + var search = new SearchBoxControl("test", useFuzzySearch: true); + + search.Clear(); + + Assert.That(search.SearchText, Is.Empty); + } + + [Test] + public void Clear_ResetsFuzzySearch() + { + var search = new SearchBoxControl("test", useFuzzySearch: true); + + search.Clear(); + + Assert.That(search.UseFuzzySearch, Is.False); + } + + [Test] + public void Clear_IsSearchingBecomesFalse() + { + var search = new SearchBoxControl("test"); + + search.Clear(); - // Event should be subscribed without throwing - Assert.That(eventFired, Is.False); + Assert.That(search.IsSearching, Is.False); } #endregion