Merged Cake and Abel branched into 2.0.3 (#131)

Co-authored-by: azyges <aaaaaa@aaa.aaa>
Co-authored-by: cake <admin@cakeandbanana.nl>
Co-authored-by: defnotken <itsdefnotken@gmail.com>
Reviewed-on: #131
This commit was merged in pull request #131.
This commit is contained in:
2026-01-05 00:45:14 +00:00
parent e0b8070aa8
commit 30717ba200
67 changed files with 13247 additions and 802 deletions

View File

@@ -11,6 +11,7 @@ using LightlessSync.LightlessConfiguration;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.TextureCompression;
using LightlessSync.UI.Models;
using LightlessSync.Utils;
using Microsoft.Extensions.Logging;
using OtterTex;
@@ -34,12 +35,15 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
private const float TextureDetailSplitterWidth = 12f;
private const float TextureDetailSplitterCollapsedWidth = 18f;
private const float SelectedFilePanelLogicalHeight = 90f;
private const float TextureHoverPreviewDelaySeconds = 1.75f;
private const float TextureHoverPreviewSize = 350f;
private static readonly Vector4 SelectedTextureRowTextColor = new(0f, 0f, 0f, 1f);
private readonly CharacterAnalyzer _characterAnalyzer;
private readonly Progress<TextureConversionProgress> _conversionProgress = new();
private readonly IpcManager _ipcManager;
private readonly UiSharedService _uiSharedService;
private readonly LightlessConfigService _configService;
private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
private readonly TransientResourceManager _transientResourceManager;
private readonly TransientConfigService _transientConfigService;
@@ -77,6 +81,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
private string _selectedJobEntry = string.Empty;
private string _filterGamePath = string.Empty;
private string _filterFilePath = string.Empty;
private string _textureHoverKey = string.Empty;
private int _conversionCurrentFileProgress = 0;
private int _conversionTotalJobs;
@@ -87,6 +92,11 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
private bool _textureRowsDirty = true;
private bool _textureDetailCollapsed = false;
private bool _conversionFailed;
private double _textureHoverStartTime = 0;
#if DEBUG
private bool _debugCompressionModalOpen = false;
private TextureConversionProgress? _debugConversionProgress;
#endif
private bool _showAlreadyAddedTransients = false;
private bool _acknowledgeReview = false;
@@ -98,10 +108,12 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
private TextureUsageCategory? _textureCategoryFilter = null;
private TextureMapKind? _textureMapFilter = null;
private TextureCompressionTarget? _textureTargetFilter = null;
private TextureFormatSortMode _textureFormatSortMode = TextureFormatSortMode.None;
public DataAnalysisUi(ILogger<DataAnalysisUi> logger, LightlessMediator mediator,
CharacterAnalyzer characterAnalyzer, IpcManager ipcManager,
PerformanceCollectorService performanceCollectorService, UiSharedService uiSharedService,
LightlessConfigService configService,
PlayerPerformanceConfigService playerPerformanceConfig, TransientResourceManager transientResourceManager,
TransientConfigService transientConfigService, TextureCompressionService textureCompressionService,
TextureMetadataHelper textureMetadataHelper)
@@ -110,6 +122,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
_characterAnalyzer = characterAnalyzer;
_ipcManager = ipcManager;
_uiSharedService = uiSharedService;
_configService = configService;
_playerPerformanceConfig = playerPerformanceConfig;
_transientResourceManager = transientResourceManager;
_transientConfigService = transientConfigService;
@@ -135,21 +148,30 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
private void HandleConversionModal()
{
if (_conversionTask == null)
bool hasConversion = _conversionTask != null;
#if DEBUG
bool showDebug = _debugCompressionModalOpen && !hasConversion;
#else
const bool showDebug = false;
#endif
if (!hasConversion && !showDebug)
{
return;
}
if (_conversionTask.IsCompleted)
if (hasConversion && _conversionTask!.IsCompleted)
{
ResetConversionModalState();
return;
if (!showDebug)
{
return;
}
}
_showModal = true;
if (ImGui.BeginPopupModal("Texture Compression in Progress", ImGuiWindowFlags.AlwaysAutoResize))
if (ImGui.BeginPopupModal("Texture Compression in Progress", UiSharedService.PopupWindowFlags))
{
DrawConversionModalContent();
DrawConversionModalContent(showDebug);
ImGui.EndPopup();
}
else
@@ -164,31 +186,190 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
}
}
private void DrawConversionModalContent()
private void DrawConversionModalContent(bool isDebugPreview)
{
var progress = _lastConversionProgress;
var scale = ImGuiHelpers.GlobalScale;
TextureConversionProgress? progress;
#if DEBUG
progress = isDebugPreview ? _debugConversionProgress : _lastConversionProgress;
#else
progress = _lastConversionProgress;
#endif
var total = progress?.Total ?? Math.Max(_conversionTotalJobs, 1);
var completed = progress != null
? Math.Min(progress.Completed + 1, total)
: _conversionCurrentFileProgress;
var currentLabel = !string.IsNullOrEmpty(_conversionCurrentFileName)
? _conversionCurrentFileName
: "Preparing...";
? Math.Clamp(progress.Completed + 1, 0, total)
: Math.Clamp(_conversionCurrentFileProgress, 0, total);
var percent = total > 0 ? Math.Clamp(completed / (float)total, 0f, 1f) : 0f;
ImGui.TextUnformatted($"Compressing textures ({completed}/{total})");
UiSharedService.TextWrapped("Current file: " + currentLabel);
var job = progress?.CurrentJob;
var inputPath = job?.InputFile ?? string.Empty;
var targetLabel = job != null ? job.TargetType.ToString() : "Unknown";
var currentLabel = !string.IsNullOrEmpty(inputPath)
? Path.GetFileName(inputPath)
: !string.IsNullOrEmpty(_conversionCurrentFileName) ? _conversionCurrentFileName : "Preparing...";
var mapKind = !string.IsNullOrEmpty(inputPath)
? _textureMetadataHelper.DetermineMapKind(inputPath)
: TextureMapKind.Unknown;
if (_conversionFailed)
var accent = UIColors.Get("LightlessPurple");
var accentBg = new Vector4(accent.X, accent.Y, accent.Z, 0.18f);
var accentBorder = new Vector4(accent.X, accent.Y, accent.Z, 0.4f);
var headerHeight = MathF.Max(ImGui.GetTextLineHeightWithSpacing() * 2.6f, 46f * scale);
using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f * scale))
using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, MathF.Max(1f, ImGui.GetStyle().ChildBorderSize)))
using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(12f * scale, 6f * scale)))
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(12f * scale, 2f * scale)))
using (ImRaii.PushColor(ImGuiCol.ChildBg, UiSharedService.Color(accentBg)))
using (ImRaii.PushColor(ImGuiCol.Border, UiSharedService.Color(accentBorder)))
using (var header = ImRaii.Child("compressionHeader", new Vector2(-1f, headerHeight), true, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse))
{
UiSharedService.ColorText("Conversion encountered errors. Please review the log for details.", ImGuiColors.DalamudRed);
if (header)
{
if (ImGui.BeginTable("compressionHeaderTable", 2,
ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.NoBordersInBody | ImGuiTableFlags.NoHostExtendX))
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
DrawCompressionTitle(accent, scale);
var statusText = isDebugPreview ? "Preview mode" : "Working...";
var statusColor = isDebugPreview ? UIColors.Get("LightlessYellow") : ImGuiColors.DalamudGrey;
UiSharedService.ColorText(statusText, statusColor);
ImGui.TableNextColumn();
var progressText = $"{completed}/{total}";
var percentText = $"{percent * 100f:0}%";
var summaryText = $"{progressText} ({percentText})";
var summaryWidth = ImGui.CalcTextSize(summaryText).X;
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + MathF.Max(0f, ImGui.GetColumnWidth() - summaryWidth));
UiSharedService.ColorText(summaryText, ImGuiColors.DalamudGrey);
ImGui.EndTable();
}
}
}
if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel conversion"))
ImGuiHelpers.ScaledDummy(6);
using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 4f * scale))
using (ImRaii.PushStyle(ImGuiStyleVar.FramePadding, new Vector2(0f, 4f * scale)))
using (ImRaii.PushColor(ImGuiCol.FrameBg, UiSharedService.Color(new Vector4(0.15f, 0.15f, 0.18f, 1f))))
using (ImRaii.PushColor(ImGuiCol.PlotHistogram, UiSharedService.Color(accent)))
{
_conversionCancellationTokenSource.Cancel();
ImGui.ProgressBar(percent, new Vector2(-1f, 0f), $"{percent * 100f:0}%");
}
UiSharedService.SetScaledWindowSize(520);
ImGuiHelpers.ScaledDummy(6);
var infoAccent = UIColors.Get("LightlessBlue");
var infoBg = new Vector4(infoAccent.X, infoAccent.Y, infoAccent.Z, 0.12f);
var infoBorder = new Vector4(infoAccent.X, infoAccent.Y, infoAccent.Z, 0.32f);
const int detailRows = 3;
var detailHeight = MathF.Max(ImGui.GetTextLineHeightWithSpacing() * (detailRows + 1.2f), 72f * scale);
using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 5f * scale))
using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, MathF.Max(1f, ImGui.GetStyle().ChildBorderSize)))
using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(10f * scale, 6f * scale)))
using (ImRaii.PushColor(ImGuiCol.ChildBg, UiSharedService.Color(infoBg)))
using (ImRaii.PushColor(ImGuiCol.Border, UiSharedService.Color(infoBorder)))
using (var details = ImRaii.Child("compressionDetail", new Vector2(-1f, detailHeight), true, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse))
{
if (details)
{
if (ImGui.BeginTable("compressionDetailTable", 2,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoBordersInBody | ImGuiTableFlags.PadOuterX))
{
DrawDetailRow("Current file", currentLabel, inputPath);
DrawDetailRow("Target format", targetLabel, null);
DrawDetailRow("Map type", mapKind.ToString(), null);
ImGui.EndTable();
}
}
}
if (_conversionFailed && !isDebugPreview)
{
ImGuiHelpers.ScaledDummy(4);
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, ImGuiColors.DalamudRed);
ImGui.SameLine(0f, 6f * scale);
UiSharedService.TextWrapped("Conversion encountered errors. Please review the log for details.", color: ImGuiColors.DalamudRed);
}
ImGuiHelpers.ScaledDummy(6);
if (!isDebugPreview)
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel conversion"))
{
_conversionCancellationTokenSource.Cancel();
}
}
else
{
#if DEBUG
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Times, "Close preview"))
{
CloseDebugCompressionModal();
}
#endif
}
UiSharedService.SetScaledWindowSize(600);
void DrawDetailRow(string label, string value, string? tooltip)
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey))
{
ImGui.TextUnformatted(label);
}
ImGui.TableNextColumn();
ImGui.TextUnformatted(value);
if (!string.IsNullOrEmpty(tooltip))
{
UiSharedService.AttachToolTip(tooltip);
}
}
void DrawCompressionTitle(Vector4 iconColor, float localScale)
{
const string title = "Texture Compression";
var spacing = 6f * localScale;
var iconText = FontAwesomeIcon.CompressArrowsAlt.ToIconString();
Vector2 iconSize;
using (_uiSharedService.IconFont.Push())
{
iconSize = ImGui.CalcTextSize(iconText);
}
Vector2 titleSize;
using (_uiSharedService.MediumFont.Push())
{
titleSize = ImGui.CalcTextSize(title);
}
var lineHeight = MathF.Max(iconSize.Y, titleSize.Y);
var iconOffsetY = (lineHeight - iconSize.Y) / 2f;
var textOffsetY = (lineHeight - titleSize.Y) / 2f;
var start = ImGui.GetCursorScreenPos();
var drawList = ImGui.GetWindowDrawList();
using (_uiSharedService.IconFont.Push())
{
drawList.AddText(new Vector2(start.X, start.Y + iconOffsetY), UiSharedService.Color(iconColor), iconText);
}
using (_uiSharedService.MediumFont.Push())
{
var textPos = new Vector2(start.X + iconSize.X + spacing, start.Y + textOffsetY);
drawList.AddText(textPos, ImGui.GetColorU32(ImGuiCol.Text), title);
}
ImGui.Dummy(new Vector2(iconSize.X + spacing + titleSize.X, lineHeight));
}
}
private void ResetConversionModalState()
@@ -202,6 +383,41 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
_conversionTotalJobs = 0;
}
#if DEBUG
private void OpenCompressionDebugModal()
{
if (_conversionTask != null && !_conversionTask.IsCompleted)
{
return;
}
_debugCompressionModalOpen = true;
_debugConversionProgress = new TextureConversionProgress(
Completed: 3,
Total: 10,
CurrentJob: new TextureConversionJob(
@"C:\Lightless\Mods\Textures\example_diffuse.tex",
@"C:\Lightless\Mods\Textures\example_diffuse_bc7.tex",
Penumbra.Api.Enums.TextureType.Bc7Tex));
_showModal = true;
_modalOpen = false;
}
private void ResetDebugCompressionModalState()
{
_debugCompressionModalOpen = false;
_debugConversionProgress = null;
}
private void CloseDebugCompressionModal()
{
ResetDebugCompressionModalState();
_showModal = false;
_modalOpen = false;
ImGui.CloseCurrentPopup();
}
#endif
private void RefreshAnalysisCache()
{
if (!_hasUpdate)
@@ -757,6 +973,16 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
ResetTextureFilters();
InvalidateTextureRows();
_conversionFailed = false;
#if DEBUG
ResetDebugCompressionModalState();
#endif
var savedFormatSort = _configService.Current.TextureFormatSortMode;
if (!Enum.IsDefined(typeof(TextureFormatSortMode), savedFormatSort))
{
savedFormatSort = TextureFormatSortMode.None;
}
SetTextureFormatSortMode(savedFormatSort, persist: false);
}
protected override void Dispose(bool disposing)
@@ -1955,6 +2181,17 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
{
InvalidateTextureRows();
}
#if DEBUG
ImGui.SameLine();
using (ImRaii.Disabled(conversionRunning || !UiSharedService.CtrlPressed()))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Cog, "Preview popup (debug)", 200f * scale))
{
OpenCompressionDebugModal();
}
}
UiSharedService.AttachToolTip("Hold CTRL to open the compression popup preview.");
#endif
TextureRow? lastSelected = null;
using (var table = ImRaii.Table("textureDataTable", 9,
@@ -1973,26 +2210,56 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
ImGui.TableSetupColumn("Original", ImGuiTableColumnFlags.PreferSortDescending);
ImGui.TableSetupColumn("Compressed", ImGuiTableColumnFlags.PreferSortDescending);
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableHeadersRow();
DrawTextureTableHeaderRow();
var targets = _textureCompressionService.SelectableTargets;
IEnumerable<TextureRow> orderedRows = rows;
var sortSpecs = ImGui.TableGetSortSpecs();
var sizeSortColumn = -1;
var sizeSortDirection = ImGuiSortDirection.Ascending;
if (sortSpecs.SpecsCount > 0)
{
var spec = sortSpecs.Specs[0];
orderedRows = spec.ColumnIndex switch
if (spec.ColumnIndex is 7 or 8)
{
7 => spec.SortDirection == ImGuiSortDirection.Ascending
? rows.OrderBy(r => r.OriginalSize)
: rows.OrderByDescending(r => r.OriginalSize),
8 => spec.SortDirection == ImGuiSortDirection.Ascending
? rows.OrderBy(r => r.CompressedSize)
: rows.OrderByDescending(r => r.CompressedSize),
_ => rows
};
sizeSortColumn = spec.ColumnIndex;
sizeSortDirection = spec.SortDirection;
}
}
var hasSizeSort = sizeSortColumn != -1;
var indexedRows = rows.Select((row, idx) => (row, idx));
if (_textureFormatSortMode != TextureFormatSortMode.None)
{
bool compressedFirst = _textureFormatSortMode == TextureFormatSortMode.CompressedFirst;
int GroupKey(TextureRow row) => row.IsAlreadyCompressed == compressedFirst ? 0 : 1;
long SizeKey(TextureRow row) => sizeSortColumn == 7 ? row.OriginalSize : row.CompressedSize;
var ordered = indexedRows.OrderBy(pair => GroupKey(pair.row));
if (hasSizeSort)
{
ordered = sizeSortDirection == ImGuiSortDirection.Ascending
? ordered.ThenBy(pair => SizeKey(pair.row))
: ordered.ThenByDescending(pair => SizeKey(pair.row));
}
orderedRows = ordered
.ThenBy(pair => pair.idx)
.Select(pair => pair.row);
}
else if (hasSizeSort)
{
long SizeKey(TextureRow row) => sizeSortColumn == 7 ? row.OriginalSize : row.CompressedSize;
orderedRows = sizeSortDirection == ImGuiSortDirection.Ascending
? indexedRows.OrderBy(pair => SizeKey(pair.row)).ThenBy(pair => pair.idx).Select(pair => pair.row)
: indexedRows.OrderByDescending(pair => SizeKey(pair.row)).ThenBy(pair => pair.idx).Select(pair => pair.row);
}
if (sortSpecs.SpecsCount > 0)
{
sortSpecs.SpecsDirty = false;
}
@@ -2034,6 +2301,79 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
}
}
}
private void DrawTextureTableHeaderRow()
{
ImGui.TableNextRow(ImGuiTableRowFlags.Headers);
DrawHeaderCell(0, "##select");
DrawHeaderCell(1, "Texture");
DrawHeaderCell(2, "Slot");
DrawHeaderCell(3, "Map");
DrawFormatHeaderCell();
DrawHeaderCell(5, "Recommended");
DrawHeaderCell(6, "Target");
DrawHeaderCell(7, "Original");
DrawHeaderCell(8, "Compressed");
}
private static void DrawHeaderCell(int columnIndex, string label)
{
ImGui.TableSetColumnIndex(columnIndex);
ImGui.TableHeader(label);
}
private void DrawFormatHeaderCell()
{
ImGui.TableSetColumnIndex(4);
ImGui.TableHeader(GetFormatHeaderLabel());
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
CycleTextureFormatSortMode();
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Click to cycle sort: normal, compressed first, uncompressed first.");
}
}
private string GetFormatHeaderLabel()
=> _textureFormatSortMode switch
{
TextureFormatSortMode.CompressedFirst => "Format (C)##formatHeader",
TextureFormatSortMode.UncompressedFirst => "Format (U)##formatHeader",
_ => "Format##formatHeader"
};
private void SetTextureFormatSortMode(TextureFormatSortMode mode, bool persist = true)
{
if (_textureFormatSortMode == mode)
{
return;
}
_textureFormatSortMode = mode;
if (persist)
{
_configService.Current.TextureFormatSortMode = mode;
_configService.Save();
}
}
private void CycleTextureFormatSortMode()
{
var nextMode = _textureFormatSortMode switch
{
TextureFormatSortMode.None => TextureFormatSortMode.CompressedFirst,
TextureFormatSortMode.CompressedFirst => TextureFormatSortMode.UncompressedFirst,
_ => TextureFormatSortMode.None
};
SetTextureFormatSortMode(nextMode);
}
private void StartTextureConversion()
{
if (_conversionTask != null && !_conversionTask.IsCompleted)
@@ -2335,11 +2675,30 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
{
if (_texturePreviews.TryGetValue(key, out var state))
{
var loadTask = state.LoadTask;
if (loadTask is { IsCompleted: false })
{
_ = loadTask.ContinueWith(_ =>
{
state.Texture?.Dispose();
}, TaskScheduler.Default);
}
state.Texture?.Dispose();
_texturePreviews.Remove(key);
}
}
private void ClearHoverPreview(TextureRow row)
{
if (string.Equals(_selectedTextureKey, row.Key, StringComparison.Ordinal))
{
return;
}
ResetPreview(row.Key);
}
private TextureResolutionInfo? GetTextureResolution(TextureRow row)
{
if (_textureResolutionCache.TryGetValue(row.Key, out var cached))
@@ -2440,7 +2799,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
UiSharedService.AttachToolTip("Already stored in a compressed format; additional compression is disabled.");
}
DrawSelectableColumn(isSelected, () =>
var nameHovered = DrawSelectableColumn(isSelected, () =>
{
var selectableLabel = $"{row.DisplayName}##texName{index}";
if (ImGui.Selectable(selectableLabel, isSelected))
@@ -2448,20 +2807,20 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
_selectedTextureKey = isSelected ? string.Empty : key;
}
return () => UiSharedService.AttachToolTip($"{row.PrimaryFilePath}{UiSharedService.TooltipSeparator}{string.Join(Environment.NewLine, row.GamePaths)}");
return null;
});
DrawSelectableColumn(isSelected, () =>
_ = DrawSelectableColumn(isSelected, () =>
{
ImGui.TextUnformatted(row.Slot);
return null;
});
DrawSelectableColumn(isSelected, () =>
_ = DrawSelectableColumn(isSelected, () =>
{
ImGui.TextUnformatted(row.MapKind.ToString());
return null;
});
DrawSelectableColumn(isSelected, () =>
_ = DrawSelectableColumn(isSelected, () =>
{
Action? tooltipAction = null;
ImGui.TextUnformatted(row.Format);
@@ -2475,7 +2834,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
return tooltipAction;
});
DrawSelectableColumn(isSelected, () =>
_ = DrawSelectableColumn(isSelected, () =>
{
if (row.SuggestedTarget.HasValue)
{
@@ -2537,19 +2896,21 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
UiSharedService.AttachToolTip("This texture is already compressed and cannot be processed again.");
}
DrawSelectableColumn(isSelected, () =>
_ = DrawSelectableColumn(isSelected, () =>
{
ImGui.TextUnformatted(UiSharedService.ByteToString(row.OriginalSize));
return null;
});
DrawSelectableColumn(isSelected, () =>
_ = DrawSelectableColumn(isSelected, () =>
{
ImGui.TextUnformatted(UiSharedService.ByteToString(row.CompressedSize));
return null;
});
DrawTextureRowHoverTooltip(row, nameHovered);
}
private static void DrawSelectableColumn(bool isSelected, Func<Action?> draw)
private static bool DrawSelectableColumn(bool isSelected, Func<Action?> draw)
{
ImGui.TableNextColumn();
if (isSelected)
@@ -2558,6 +2919,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
}
var after = draw();
var hovered = ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled | ImGuiHoveredFlags.AllowWhenBlockedByActiveItem);
if (isSelected)
{
@@ -2565,6 +2927,127 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
}
after?.Invoke();
return hovered;
}
private void DrawTextureRowHoverTooltip(TextureRow row, bool isHovered)
{
if (!isHovered)
{
if (string.Equals(_textureHoverKey, row.Key, StringComparison.Ordinal))
{
_textureHoverKey = string.Empty;
_textureHoverStartTime = 0;
ClearHoverPreview(row);
}
return;
}
var now = ImGui.GetTime();
if (!string.Equals(_textureHoverKey, row.Key, StringComparison.Ordinal))
{
_textureHoverKey = row.Key;
_textureHoverStartTime = now;
}
var elapsed = now - _textureHoverStartTime;
if (elapsed < TextureHoverPreviewDelaySeconds)
{
var progress = (float)Math.Clamp(elapsed / TextureHoverPreviewDelaySeconds, 0f, 1f);
DrawTextureRowTextTooltip(row, progress);
return;
}
DrawTextureRowPreviewTooltip(row);
}
private void DrawTextureRowTextTooltip(TextureRow row, float progress)
{
ImGui.BeginTooltip();
ImGui.SetWindowFontScale(1f);
DrawTextureRowTooltipBody(row);
ImGuiHelpers.ScaledDummy(4);
DrawTextureHoverProgressBar(progress, GetTooltipContentWidth());
ImGui.EndTooltip();
}
private void DrawTextureRowPreviewTooltip(TextureRow row)
{
ImGui.BeginTooltip();
ImGui.SetWindowFontScale(1f);
DrawTextureRowTooltipBody(row);
ImGuiHelpers.ScaledDummy(4);
var previewSize = new Vector2(TextureHoverPreviewSize * ImGuiHelpers.GlobalScale);
var (previewTexture, previewLoading, previewError) = GetTexturePreview(row);
if (previewTexture != null)
{
ImGui.Image(previewTexture.Handle, previewSize);
}
else
{
using (ImRaii.Child("textureHoverPreview", previewSize, true))
{
UiSharedService.TextWrapped(previewLoading ? "Loading preview..." : previewError ?? "Preview unavailable.");
}
}
ImGui.EndTooltip();
}
private static void DrawTextureRowTooltipBody(TextureRow row)
{
var text = row.GamePaths.Count > 0
? $"{row.PrimaryFilePath}{UiSharedService.TooltipSeparator}{string.Join(Environment.NewLine, row.GamePaths)}"
: row.PrimaryFilePath;
var wrapWidth = GetTextureHoverTooltipWidth();
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + wrapWidth);
if (text.Contains(UiSharedService.TooltipSeparator, StringComparison.Ordinal))
{
var splitText = text.Split(UiSharedService.TooltipSeparator, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < splitText.Length; i++)
{
ImGui.TextUnformatted(splitText[i]);
if (i != splitText.Length - 1)
{
ImGui.Separator();
}
}
}
else
{
ImGui.TextUnformatted(text);
}
ImGui.PopTextWrapPos();
}
private static void DrawTextureHoverProgressBar(float progress, float width)
{
var scale = ImGuiHelpers.GlobalScale;
var barHeight = 4f * scale;
var barWidth = width > 0f ? width : -1f;
using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 3f * scale))
using (ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero))
using (ImRaii.PushColor(ImGuiCol.PlotHistogram, UiSharedService.Color(UIColors.Get("LightlessPurple"))))
{
ImGui.ProgressBar(progress, new Vector2(barWidth, barHeight), string.Empty);
}
}
private static float GetTextureHoverTooltipWidth()
=> ImGui.GetFontSize() * 35f;
private static float GetTooltipContentWidth()
{
var min = ImGui.GetWindowContentRegionMin();
var max = ImGui.GetWindowContentRegionMax();
var width = max.X - min.X;
if (width <= 0f)
{
width = ImGui.GetContentRegionAvail().X;
}
return width;
}
private static void ApplyTextureRowBackground(TextureRow row, bool isSelected)