From 907d923149ed50cdd944cdc4d44fecee67592dfc Mon Sep 17 00:00:00 2001 From: AdiPrk Date: Sat, 6 Jun 2026 10:05:46 -0700 Subject: [PATCH 1/2] ui --- Directory.Build.props | 2 +- src/General/UI/ReplayUI.Actions.cs | 378 ++++++-------- src/General/UI/ReplayUI.Build.cs | 488 ++++++------------ src/General/UI/ReplayUI.Columns.cs | 359 ------------- src/General/UI/ReplayUI.ConfigTab.cs | 303 +++++++++++ src/General/UI/ReplayUI.LeaderboardTab.cs | 19 + src/General/UI/ReplayUI.RunsTab.cs | 222 ++++++++ src/General/UI/ReplayUI.SceneList.cs | 130 +++++ src/General/UI/ReplayUI.Widgets.cs | 93 +++- src/General/UI/ReplayUI.cs | 361 +++++++------ src/General/UIStyle.cs | 55 +- .../ReplayTimerMod.HK.1221.csproj | 2 +- .../ReplayTimerMod.HK.1578.csproj | 2 +- src/HollowKnight/ReplayTimerModHK.cs | 1 + 14 files changed, 1277 insertions(+), 1138 deletions(-) delete mode 100644 src/General/UI/ReplayUI.Columns.cs create mode 100644 src/General/UI/ReplayUI.ConfigTab.cs create mode 100644 src/General/UI/ReplayUI.LeaderboardTab.cs create mode 100644 src/General/UI/ReplayUI.RunsTab.cs create mode 100644 src/General/UI/ReplayUI.SceneList.cs diff --git a/Directory.Build.props b/Directory.Build.props index 947f80c..6ade7a6 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -11,7 +11,7 @@ It should follow the format major.minor.patch (semantic versioning). If you publish your mod as a library to NuGet, this version will also be used as the package version. --> - 0.4.0 + 1.0.0 $(DefaultItemExcludes);obj\**;obj\ss\**;obj\hk\** diff --git a/src/General/UI/ReplayUI.Actions.cs b/src/General/UI/ReplayUI.Actions.cs index 17d440c..e8c6136 100644 --- a/src/General/UI/ReplayUI.Actions.cs +++ b/src/General/UI/ReplayUI.Actions.cs @@ -1,35 +1,32 @@ using System.Linq; using UnityEngine; +using UnityEngine.UI; namespace ReplayTimerMod { public partial class ReplayUI { - // ── Copy all (header) - clipboard ──────────────────────────────────── + // -- Config tab: Copy all -- private void OnExportAllClicked() { var all = PBManager.AllPBs().Select(p => p.Value).ToList(); if (all.Count == 0) { - ShowExportFeedback("Nothing to copy", UIStyle.Subtext); + ShowButtonFeedback(exportAllCfgLbl, exportAllCfgBg, "Nothing to copy", UIStyle.Subtext); return; } GUIUtility.systemCopyBuffer = ReplayShareEncoder.EncodeCollection(all); - ShowExportFeedback($"{all.Count} copied", UIStyle.Accent); + ShowButtonFeedback(exportAllCfgLbl, exportAllCfgBg, all.Count + " copied", UIStyle.Accent); Log.LogInfo($"[ReplayUI] Copied {all.Count} replays to clipboard"); } - // ── Download all (header) - writes file to disk ─────────────────────── + // -- Config tab: Export All (save to disk) -- private void OnDownloadAllClicked() { var all = PBManager.AllPBs().Select(p => p.Value).ToList(); - if (all.Count == 0) - { - ShowDownloadFeedback("Nothing to save", UIStyle.Subtext); - return; - } + if (all.Count == 0) return; try { @@ -39,20 +36,15 @@ private void OnDownloadAllClicked() "export"); System.IO.Directory.CreateDirectory(dir); - string datePart = System.DateTime.Now.ToString("yyyy-MM-dd_HHmm"); - string countPart = $"{all.Count}"; - string fileName = $"Re_{datePart}_{countPart}.rtmc.txt"; - + string stamp = System.DateTime.Now.ToString("yyyy-MM-dd_HHmm"); + string fileName = $"Re_{stamp}_{all.Count}.rtmc.txt"; string path = System.IO.Path.Combine(dir, fileName); System.IO.File.WriteAllText(path, ReplayShareEncoder.EncodeCollection(all)); - - ShowDownloadFeedback($"Saved {all.Count} to /export/", UIStyle.Accent); Log.LogInfo($"[ReplayUI] Saved {all.Count} replays to {path}"); } catch (System.Exception ex) { - ShowDownloadFeedback("Save failed", UIStyle.Red); Log.LogError($"[ReplayUI] Download all failed: {ex.Message}"); } } @@ -65,38 +57,49 @@ private void OnOpenExportFolderClicked() "export"); if (System.IO.Directory.Exists(dir)) - { System.Diagnostics.Process.Start(dir); - } - else + } + + // -- Config tab: Clear all (two-click confirm) -- + + private void OnClearAllClicked() + { + if (!clearAllPending) { - ShowDownloadFeedback("Nothing Exported", UIStyle.Red); + clearAllPending = true; + ShowButtonFeedback(clearAllCfgLbl, clearAllCfgBg, "Are you sure?", UIStyle.Red); + if (clearAllCfgBg != null) clearAllCfgBg.color = UIStyle.Red with { a = 0.55f }; + return; } + + PBManager.DeleteAll(); + selectedScene = null; + clearAllPending = false; + RefreshCurrentView(); + Log.LogInfo("[ReplayUI] All replays cleared"); } - private void ShowExportFeedback(string msg, Color color) + private void ResetClearAllConfirm() { - if (exportAllBtnLbl == null) return; - exportAllBtnLbl.text = msg; - exportAllBtnLbl.color = color; - if (exportAllBtnImg != null) exportAllBtnImg.color = color with { a = 0.30f }; + clearAllPending = false; + ShowButtonFeedback(clearAllCfgLbl, clearAllCfgBg, "Clear all data", UIStyle.Red); + if (clearAllCfgBg != null) clearAllCfgBg.color = UIStyle.Red with { a = 0.15f }; + ShowButtonFeedback(exportAllCfgLbl, exportAllCfgBg, "Copy all", UIStyle.Accent); + if (exportAllCfgBg != null) exportAllCfgBg.color = UIStyle.Accent with { a = 0.18f }; } - private void ShowDownloadFeedback(string msg, Color color) + private static void ShowButtonFeedback(Text? label, Image? bg, string msg, Color color) { - if (downloadAllBtnLbl == null) return; - downloadAllBtnLbl.text = msg; - downloadAllBtnLbl.color = color; - if (downloadAllBtnImg != null) downloadAllBtnImg.color = color with { a = 0.30f }; + if (label != null) { label.text = msg; label.color = color; } } - // ── Export scene (sub-header) ───────────────────────────────────────── + // -- Scene-level actions -- private void OnExportSceneClicked() { if (selectedScene == null) { - ShowPasteStatus("Select a scene first", UIStyle.Subtext); + ShowPasteStatus("Select a room first", UIStyle.Subtext); return; } var entries = PBManager.AllPBs() @@ -109,107 +112,38 @@ private void OnExportSceneClicked() return; } GUIUtility.systemCopyBuffer = ReplayShareEncoder.EncodeCollection(entries); - ShowPasteStatus($"{entries.Count} routes copied", UIStyle.Accent); + ShowPasteStatus(entries.Count + " routes copied", UIStyle.Accent); Log.LogInfo($"[ReplayUI] Exported {entries.Count} routes for {selectedScene}"); } - // ── Per-snapshot ────────────────────────────────────────────────────── - - private void CopyReplay(RoomKey key, string snapshotId) - { - var snapshot = PBManager.GetHistory(key) - .FirstOrDefault(s => s.SnapshotId == snapshotId); - if (snapshot == null) - { - Log.LogWarning($"[ReplayUI] No snapshot for {key}#{snapshotId}"); - return; - } - - GUIUtility.systemCopyBuffer = snapshot.EncodedData; - Log.LogInfo($"[ReplayUI] Copied {key}#{snapshotId}"); - } - - private void DeleteSnapshot(RoomKey key, string snapshotId) - { - PBManager.DeleteSnapshot(key, snapshotId); - if (selectedScene != null) RebuildRight(selectedScene); - else RefreshSettingsBar(); - RebuildLeft(); - } - - private void DeleteRoute(RoomKey key) - { - PBManager.DeletePB(key); - if (selectedScene != null) RebuildRight(selectedScene); - else RefreshSettingsBar(); - RebuildLeft(); - } - - // ── Clear scene (sub-header) ────────────────────────────────────────── - private void OnClearSceneClicked() { if (selectedScene == null) return; PBManager.DeleteScene(selectedScene); selectedScene = null; - ClearRight(); - RebuildLeft(); - RefreshSettingsBar(); - } - - // ── Global clear-all (header) - two-click confirm ───────────────────── - - private void OnClearAllClicked() - { - if (!clearAllPending) - { - clearAllPending = true; - if (clearAllBtnLbl != null) clearAllBtnLbl.text = "Are you sure?"; - if (clearAllBtnImg != null) clearAllBtnImg.color = UIStyle.Red with { a = 0.55f }; - } - else - { - PBManager.DeleteAll(); - selectedScene = null; - ClearRight(); - RebuildLeft(); - RefreshSettingsBar(); - ResetClearAllConfirm(); - Log.LogInfo("[ReplayUI] All replays cleared"); - } - } - - private void ResetClearAllConfirm() - { - clearAllPending = false; - if (clearAllBtnLbl != null) clearAllBtnLbl.text = "Clear all"; - if (clearAllBtnImg != null) clearAllBtnImg.color = UIStyle.Red with { a = 0.22f }; - if (exportAllBtnLbl != null) exportAllBtnLbl.text = "Copy all"; - if (exportAllBtnImg != null) exportAllBtnImg.color = UIStyle.Accent with { a = 0.22f }; - if (downloadAllBtnLbl != null) downloadAllBtnLbl.text = "Download all"; - if (downloadAllBtnImg != null) downloadAllBtnImg.color = UIStyle.Accent with { a = 0.15f }; + ClearSelectedScene(); + RebuildSceneList(); } - // ── Paste ───────────────────────────────────────────────────────────── + // -- Paste -- private void OnPasteClicked() { string clip = GUIUtility.systemCopyBuffer ?? ""; if (string.IsNullOrEmpty(clip)) { - ShowPasteStatus("✕ Clipboard empty", UIStyle.Red); + ShowPasteStatus("Clipboard empty", UIStyle.Red); return; } var rooms = ReplayShareEncoder.DecodeShareString(clip); if (rooms.Count == 0) { - ShowPasteStatus("✕ Invalid data", UIStyle.Red); + ShowPasteStatus("Invalid data", UIStyle.Red); return; } - int imported = 0; - int duplicates = 0; + int imported = 0, duplicates = 0; foreach (var room in rooms) { if (PBManager.ImportPB(room)) imported++; @@ -217,19 +151,17 @@ private void OnPasteClicked() } selectedScene = rooms[0].Key.SceneName; - RebuildLeft(); - RebuildRight(selectedScene); - RefreshSettingsBar(); + RebuildSceneList(); + UpdateRightSubHeader(); + RebuildRightContent(); string status; if (rooms.Count == 1) - { status = imported > 0 ? rooms[0].Key.SceneName : "Duplicate replay"; - } else { - status = imported > 0 ? $"{imported} imported" : "No new replays"; - if (duplicates > 0) status += $" ({duplicates} duplicate)"; + status = imported > 0 ? imported + " imported" : "No new replays"; + if (duplicates > 0) status += " (" + duplicates + " dup)"; } ShowPasteStatus(status, imported > 0 ? UIStyle.Gold : UIStyle.Subtext); @@ -238,218 +170,216 @@ private void OnPasteClicked() private void ShowPasteStatus(string msg, Color color) { - if (pasteStatus == null) return; - pasteStatus.text = msg; - pasteStatus.color = color; + if (pasteStatusLbl != null) + { + pasteStatusLbl.text = msg; + pasteStatusLbl.color = color; + } } - // ── Jump to current room (left sub-header) ──────────────────────────── + // -- Snapshot actions -- - private void OnJumpToCurrentClicked() + private void CopyReplay(RoomKey key, string snapshotId) { - string scene = RoomTracker.CurrentScene; - - if (string.IsNullOrEmpty(scene)) + var snapshot = PBManager.GetHistory(key) + .FirstOrDefault(s => s.SnapshotId == snapshotId); + if (snapshot == null) { - ShowJumpFeedback("Not in a room", UIStyle.Subtext); + Log.LogWarning($"[ReplayUI] No snapshot for {key}#{snapshotId}"); return; } + GUIUtility.systemCopyBuffer = snapshot.EncodedData; + Log.LogInfo($"[ReplayUI] Copied {key}#{snapshotId}"); + } + + private void DeleteSnapshot(RoomKey key, string snapshotId) + { + PBManager.DeleteSnapshot(key, snapshotId); + RebuildSceneList(); + if (selectedScene != null && activeTab == TabKind.Runs) + RebuildRightContent(); + } - bool hasPB = PBManager.AllPBs().Any(p => p.Key.SceneName == scene); - if (!hasPB) + private void DeleteRoute(RoomKey key) + { + PBManager.DeletePB(key); + RebuildSceneList(); + if (selectedScene != null && activeTab == TabKind.Runs) + RebuildRightContent(); + } + + private void SelectSnapshotForEditing(RoomKey key, string snapshotId) + { + var snapshot = PBManager.GetSnapshot(key, snapshotId); + if (snapshot == null) return; + + if (!snapshot.HasVisualOverride) { - ShowJumpFeedback($"No PB", UIStyle.Subtext); - return; + Color color = snapshot.ResolveGhostColor(CurrentGlobalGhostColor); + if (!PBManager.UpdateSnapshotVisuals(key, snapshotId, true, color)) + return; } - ResetJumpFeedback(); - SelectScene(scene); - ScrollToScene(scene); - - Log.LogInfo($"[ReplayUI] Jumped to current room: {scene}"); + SelectionState?.SelectSnapshot(snapshotId); + if (activeTab == TabKind.Runs && selectedScene == key.SceneName) + RebuildRightContent(); + if (activeTab == TabKind.Config) + RefreshConfigValues(); } - private void ShowJumpFeedback(string msg, Color color) + private void ToggleSnapshotPlayback(RoomKey key, string snapshotId) { - if (jumpToCurrentBtnLbl == null) return; - jumpToCurrentBtnLbl.text = msg; - jumpToCurrentBtnLbl.color = color; - if (jumpToCurrentBtnImg != null) - jumpToCurrentBtnImg.color = color with { a = 0.18f }; + SelectionState?.TogglePlayback(snapshotId); + if (activeTab == TabKind.Runs && selectedScene == key.SceneName) + RebuildRightContent(); } - private void ResetJumpFeedback() + // -- Jump navigation -- + + private void OnJumpToCurrentClicked() { - if (jumpToCurrentBtnLbl == null) return; - jumpToCurrentBtnLbl.text = "Current"; - jumpToCurrentBtnLbl.color = UIStyle.Gold; - if (jumpToCurrentBtnImg != null) - jumpToCurrentBtnImg.color = UIStyle.Gold with { a = 0.18f }; + string scene = RoomTracker.CurrentScene; + if (string.IsNullOrEmpty(scene)) + { + ShowButtonFeedback(jumpCurrentLbl, jumpCurrentBg, "Not in a room", UIStyle.Subtext); + return; + } + if (!PBManager.AllPBs().Any(p => p.Key.SceneName == scene)) + { + ShowButtonFeedback(jumpCurrentLbl, jumpCurrentBg, "No PB", UIStyle.Subtext); + return; + } + + ResetJumpFeedback(); + if (activeTab != TabKind.Runs) + SwitchTab(TabKind.Runs); + SelectScene(scene); + ScrollToScene(scene); } - // ── Jump to previous room (left sub-header) ─────────────────────────── - private void OnJumpToLastClicked() { string scene = RoomTracker.PreviousScene; - if (string.IsNullOrEmpty(scene)) { - ShowJumpLastFeedback("No previous", UIStyle.Subtext); + ShowButtonFeedback(jumpPreviousLbl, jumpPreviousBg, "No previous", UIStyle.Subtext); return; } - - bool hasPB = PBManager.AllPBs().Any(p => p.Key.SceneName == scene); - if (!hasPB) + if (!PBManager.AllPBs().Any(p => p.Key.SceneName == scene)) { - ShowJumpLastFeedback($"No PB", UIStyle.Subtext); + ShowButtonFeedback(jumpPreviousLbl, jumpPreviousBg, "No PB", UIStyle.Subtext); return; } ResetJumpLastFeedback(); + if (activeTab != TabKind.Runs) + SwitchTab(TabKind.Runs); SelectScene(scene); ScrollToScene(scene); - - Log.LogInfo($"[ReplayUI] Jumped to previous room: {scene}"); } - private void ShowJumpLastFeedback(string msg, Color color) + private void ResetJumpFeedback() { - if (jumpToLastBtnLbl == null) return; - jumpToLastBtnLbl.text = msg; - jumpToLastBtnLbl.color = color; - if (jumpToLastBtnImg != null) - jumpToLastBtnImg.color = color with { a = 0.18f }; + if (jumpCurrentLbl != null) { jumpCurrentLbl.text = "Current"; jumpCurrentLbl.color = UIStyle.Gold; } + if (jumpCurrentBg != null) jumpCurrentBg.color = UIStyle.Gold with { a = 0.18f }; } private void ResetJumpLastFeedback() { - if (jumpToLastBtnLbl == null) return; - jumpToLastBtnLbl.text = "Previous"; - jumpToLastBtnLbl.color = UIStyle.Accent; - if (jumpToLastBtnImg != null) - jumpToLastBtnImg.color = UIStyle.Accent with { a = 0.18f }; + if (jumpPreviousLbl != null) { jumpPreviousLbl.text = "Previous"; jumpPreviousLbl.color = UIStyle.Accent; } + if (jumpPreviousBg != null) jumpPreviousBg.color = UIStyle.Accent with { a = 0.18f }; } - // ── Ghost settings ──────────────────────────────────────────────────── + // -- Settings toggles -- private void OnTrackingToggle() { GhostSettings.TrackingEnabled = !GhostSettings.TrackingEnabled; - RefreshSettingsBar(); + if (activeTab == TabKind.Config) RefreshConfigValues(); } private void OnGhostToggle() { GhostSettings.GhostEnabled = !GhostSettings.GhostEnabled; - RefreshSettingsBar(); + if (activeTab == TabKind.Config) RefreshConfigValues(); } private void OnSavePolicyToggle() { GhostSettings.SaveAllRunsEnabled = !GhostSettings.SaveAllRunsEnabled; - RefreshSettingsBar(); + if (activeTab == TabKind.Config) RefreshConfigValues(); } - private void OnEditGlobalContext() - { - SelectionState?.SelectSnapshot(null); - RefreshSettingsBar(); - if (selectedScene != null) RebuildRight(selectedScene); - } + private void OnMaxSavedReplaysMinus() => AdjustMaxSaved(-1); + private void OnMaxSavedReplaysPlus() => AdjustMaxSaved(1); - private void SelectSnapshotForEditing(RoomKey key, string snapshotId) + private void AdjustMaxSaved(int delta) { - var snapshot = PBManager.GetSnapshot(key, snapshotId); - if (snapshot == null) - return; - - if (!snapshot.HasVisualOverride) - { - Color color = snapshot.ResolveGhostColor(CurrentGlobalGhostColor); - if (!PBManager.UpdateSnapshotVisuals(key, snapshotId, true, color)) - return; - } - - SelectionState?.SelectSnapshot(snapshotId); - RefreshSettingsBar(); - if (selectedScene == key.SceneName) - RebuildRight(key.SceneName); + GhostSettings.MaxSavedReplaysPerRoute += delta; + PBManager.PruneAllHistories(GhostSettings.MaxSavedReplaysPerRoute, persist: true); + if (activeTab == TabKind.Config) RefreshConfigValues(); + if (activeTab == TabKind.Runs && selectedScene != null) + RebuildRightContent(); } - private void ToggleSnapshotPlayback(RoomKey key, string snapshotId) + private void OnEditGlobalContext() { - SelectionState?.TogglePlayback(snapshotId); - if (selectedScene == key.SceneName) - RebuildRight(key.SceneName); - else - RefreshSettingsBar(); + SelectionState?.SelectSnapshot(null); + if (activeTab == TabKind.Config) RefreshConfigValues(); + if (activeTab == TabKind.Runs && selectedScene != null) + RebuildRightContent(); } private void OnAlphaMinus() => AdjustAlpha(-0.05f); - private void OnAlphaPlus() => AdjustAlpha(0.05f); private void AdjustAlpha(float delta) { if (TryGetSelectedSnapshot(out var key, out var snapshot) && snapshot != null) { - if (!snapshot.HasVisualOverride) - return; + if (!snapshot.HasVisualOverride) return; Color color = snapshot.ResolveGhostColor(CurrentGlobalGhostColor); color.a = Mathf.Clamp01(Mathf.Round((color.a + delta) * 20f) / 20f); - if (PBManager.UpdateSnapshotVisuals(key, snapshot.SnapshotId, true, color) - && selectedScene == key.SceneName) - RebuildRight(key.SceneName); + PBManager.UpdateSnapshotVisuals(key, snapshot.SnapshotId, true, color); } else { GhostSettings.GhostAlpha = Mathf.Round((GhostSettings.GhostAlpha + delta) * 20f) / 20f; - if (selectedScene != null) - RebuildRight(selectedScene); } - RefreshSettingsBar(); + if (activeTab == TabKind.Config) RefreshConfigValues(); + if (activeTab == TabKind.Runs && selectedScene != null) + RebuildRightContent(); } private void OnColorSwatch(Color rgb) { if (TryGetSelectedSnapshot(out var key, out var snapshot) && snapshot != null) { - if (!snapshot.HasVisualOverride) - return; + if (!snapshot.HasVisualOverride) return; Color color = snapshot.ResolveGhostColor(CurrentGlobalGhostColor); color.r = rgb.r; color.g = rgb.g; color.b = rgb.b; - if (PBManager.UpdateSnapshotVisuals(key, snapshot.SnapshotId, true, color) - && selectedScene == key.SceneName) - RebuildRight(key.SceneName); + PBManager.UpdateSnapshotVisuals(key, snapshot.SnapshotId, true, color); } else { GhostSettings.GhostColor = new Color(rgb.r, rgb.g, rgb.b, GhostSettings.GhostAlpha); - if (selectedScene != null) - RebuildRight(selectedScene); } - RefreshSettingsBar(); + if (activeTab == TabKind.Config) RefreshConfigValues(); + if (activeTab == TabKind.Runs && selectedScene != null) + RebuildRightContent(); } - private static string AlphaString() => - GhostSettings.GhostAlpha.ToString("0.00"); - - // ── Timer HUD settings ──────────────────────────────────────────────── private void OnTimerToggleClicked() { GhostSettings.TimerHudEnabled = !GhostSettings.TimerHudEnabled; - if (!GhostSettings.TimerHudEnabled && timerHud != null) - { - timerHud.Disarm(); - } - RefreshSettingsBar(); + if (!GhostSettings.TimerHudEnabled) timerHud?.Disarm(); + if (activeTab == TabKind.Config) RefreshConfigValues(); } } } \ No newline at end of file diff --git a/src/General/UI/ReplayUI.Build.cs b/src/General/UI/ReplayUI.Build.cs index 4887a75..46eefcf 100644 --- a/src/General/UI/ReplayUI.Build.cs +++ b/src/General/UI/ReplayUI.Build.cs @@ -5,7 +5,6 @@ namespace ReplayTimerMod { public partial class ReplayUI { - // ── Tab button (bottom-left ≡) ──────────────────────────────────────── private void BuildTab() { tabGO = MakeGO("ReplayTab", canvasGO!.transform); @@ -16,13 +15,12 @@ private void BuildTab() rt.anchorMin = rt.anchorMax = Vector2.zero; rt.pivot = Vector2.zero; rt.anchoredPosition = new Vector2(M, M); - rt.sizeDelta = new Vector2(TW, TH); + rt.sizeDelta = new Vector2(UIStyle.TabBtnWidth, UIStyle.TabBtnHeight); - MakeLbl(tabGO.transform, "≡", UIStyle.FontSizeLg, + MakeLbl(tabGO.transform, "\u2261", UIStyle.FontSizeLg, UIStyle.Text, TextAnchor.MiddleCenter, fill: true); } - // ── Panel ───────────────────────────────────────────────────────────── private void BuildPanel() { panelGO = MakeGO("ReplayPanel", canvasGO!.transform); @@ -31,378 +29,194 @@ private void BuildPanel() var rt = panelGO.GetComponent(); rt.anchorMin = rt.anchorMax = Vector2.zero; rt.pivot = Vector2.zero; - rt.anchoredPosition = new Vector2(M, M + TH + M); + rt.anchoredPosition = new Vector2(M, M + UIStyle.TabBtnHeight + M); rt.sizeDelta = new Vector2(PW, PH); - BuildPanelHeader(); + int HDR = UIStyle.HeaderHeight; + int SRCH = UIStyle.SearchBarHeight; + int TABH = UIStyle.TabBarHeight; + int SUBH = UIStyle.SubHeaderHeight; + int FOOT = UIStyle.FooterHeight; + + BuildPanelHeader(HDR); + HLine(panelGO.transform, 0, HDR, PW); int bodyY = HDR + 1; - int bodyH = PH - bodyY - 1 - STGSH; - int scrollBodyH = bodyH - SUBHDR - 1; - BuildLeftSubHeader(bodyY); - BuildRightSubHeader(bodyY); - HLine(panelGO.transform, 0, bodyY + SUBHDR, PW); + // Left panel + int searchY = bodyY; + int footerY = PH - FOOT; + int sceneListY = searchY + SRCH + 1; + int sceneListH = footerY - 1 - sceneListY; + + BuildSearchBar(searchY, SRCH); + HLine(panelGO.transform, 0, sceneListY - 1, LW); + + sceneListContent = BuildScrollArea(panelGO.transform, "SceneListScroll", + 0, sceneListY, LW, sceneListH); + sceneListScroll = sceneListContent.parent.parent.GetComponent(); + + HLine(panelGO.transform, 0, footerY - 1, LW); + BuildLeftFooter(footerY, FOOT); - VLine(panelGO.transform, LW, bodyY, bodyH); + // Vertical divider + VLine(panelGO.transform, LW, bodyY, PH - bodyY); - leftContent = BuildScrollArea(panelGO.transform, "LeftScroll", - 0, bodyY + SUBHDR + 1, LW, scrollBodyH); - leftScrollRect = leftContent.parent.parent.GetComponent(); + // Right panel + int tabBarY = bodyY; + int rightSubY = tabBarY + TABH + 1; + int rightContentY = rightSubY + SUBH + 1; + int rightContentH = PH - rightContentY; - rightContent = BuildScrollArea(panelGO.transform, "RightScroll", - LW + 1, bodyY + SUBHDR + 1, RW, scrollBodyH); + BuildTabBar(tabBarY, TABH); + HLine(panelGO.transform, LW + 1, tabBarY + TABH, RW); + BuildRightSubHeader(rightSubY, SUBH); + HLine(panelGO.transform, LW + 1, rightContentY - 1, RW); - HLine(panelGO.transform, 0, PH - STGSH - 1, PW); - BuildSettingsBar(); + rightContent = BuildScrollArea(panelGO.transform, "RightContentScroll", + LW + 1, rightContentY, RW, rightContentH); } - // ── Panel header: "Replay Times" [Export all] [Clear all] [-] ────── - private void BuildPanelHeader() + private void BuildPanelHeader(int height) { var hdr = MakeGO("Header", panelGO!.transform); Img(hdr, UIStyle.Surface); - Rect(hdr, 0, 0, PW, HDR); - HLine(panelGO.transform, 0, HDR, PW); - - var collBtn = MakeGO("Collapse", hdr.transform); - Img(collBtn, UIStyle.Overlay); - Btn(collBtn, TogglePanel); - Rect(collBtn, PW - HDR, 0, HDR, HDR); - MakeLbl(collBtn.transform, "-", UIStyle.FontSizeLg, - UIStyle.Text, TextAnchor.MiddleCenter, fill: true); + Rect(hdr, 0, 0, PW, height); - int btnH = UIStyle.H(22); - int btnY = (HDR - btnH) / 2; - - int clearW = UIStyle.W(76); - int clearX = PW - HDR - M - clearW; - var clearGO = MakeGO("ClearAll", hdr.transform); - Rect(clearGO, clearX, btnY, clearW, btnH); - clearAllBtnImg = clearGO.AddComponent(); - clearAllBtnImg.color = UIStyle.Red with { a = 0.22f }; - clearGO.AddComponent