Files
HellionChat/HellionChat/Ui/StatusBar.cs
T
JonKazama-Hellion c4c85cf4b8 docs: unify documentation and streamline code comments
- Translated project documentation (LEARNING-JOURNEY, CONTRIBUTORS, AI_DISCLOSURE) to English for better accessibility.
- Standardized internal code documentation by converting XML-doc blocks to standard comment format.
- Cleaned up inline comments and removed redundant versioning metadata across the codebase.
- Refactored non-functional text elements to improve readability and maintain a consistent style.
2026-05-11 00:52:15 +02:00

177 lines
5.9 KiB
C#

using System.Globalization;
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using HellionChat.Code;
using HellionChat.Resources;
using HellionChat.Util;
namespace HellionChat.Ui;
// Bottom status bar, 22px tall. Slots left to right: channel indicator,
// privacy badge, counts, tells (hidden at 0), version (right-aligned).
// Updates at 1Hz; format strings are cached between updates.
internal sealed class StatusBar
{
public const float Height = 22f;
private const long UpdateIntervalMs = 1000;
// Initially outdated so the first frame always computes fresh.
private long _lastUpdateMs = -UpdateIntervalMs;
private string _cachedCountsText = string.Empty;
private string _cachedTellsText = string.Empty;
// Pure string logic, testable without ImGui init.
public static string FormatCounts(int tabs, int messages)
{
// InvariantCulture so locale doesn't affect the format (e.g. de_DE "1,2k").
var msgPart =
messages >= 1000
? string.Format(CultureInfo.InvariantCulture, "{0:0.0}k msg", messages / 1000.0)
: $"{messages} msg";
var tabsPart = $"{tabs} {(tabs == 1 ? "tab" : "tabs")}";
return $"{tabsPart} · {msgPart}";
}
// Pure string logic, testable without ImGui init. Returns empty string at 0 tells.
public static string FormatTells(int count)
{
if (count <= 0)
return string.Empty;
return $"{count} {(count == 1 ? "tell" : "tells")}";
}
// Single-pass replacement for a LINQ Sum+Count pair. Pure helper for unit testing.
internal static (int messages, int tells) AggregateForStatusBar(IList<Tab> tabs)
{
int messages = 0,
tells = 0;
foreach (var t in tabs)
{
messages += t.Messages.Count;
if (t.IsTempTab)
tells++;
}
return (messages, tells);
}
// Test hook to verify cache logic without a real time source.
internal (string counts, string tells) SnapshotForTest(
long now,
int tabs,
int messages,
int tells
)
{
UpdateCacheIfDue(now, tabs, messages, tells);
return (_cachedCountsText, _cachedTellsText);
}
private void UpdateCacheIfDue(long now, int tabs, int messages, int tells)
{
if (now - _lastUpdateMs < UpdateIntervalMs)
return;
_cachedCountsText = FormatCounts(tabs, messages);
_cachedTellsText = FormatTells(tells);
_lastUpdateMs = now;
}
public void Draw(Plugin plugin)
{
var theme = plugin.ThemeRegistry.Active;
var now = Environment.TickCount64;
if (now - _lastUpdateMs >= UpdateIntervalMs)
{
var (messages, tells) = AggregateForStatusBar(Plugin.Config.Tabs);
UpdateCacheIfDue(now, Plugin.Config.Tabs.Count, messages, tells);
}
// Border top via DrawList -- ImGui.Separator has too much padding.
var cursorY = ImGui.GetCursorScreenPos().Y;
var winLeft = ImGui.GetWindowPos().X;
var winRight = winLeft + ImGui.GetWindowSize().X;
ImGui
.GetWindowDrawList()
.AddLine(
new Vector2(winLeft, cursorY),
new Vector2(winRight, cursorY),
ColourUtil.RgbaToAbgr(theme.Colors.Border),
1f
);
ImGui.Dummy(new Vector2(0, 2));
// Slot 1: active channel indicator
var inputCh = plugin.CurrentTab?.CurrentChannel?.Channel ?? InputChannel.Invalid;
var hasChannel = inputCh != InputChannel.Invalid;
var chatType = inputCh.ToChatType();
var channelName = hasChannel ? chatType.Name() : "—";
var channelColor = hasChannel
? (plugin.Functions.Chat.GetChannelColor(chatType) ?? theme.Colors.TextMuted)
: theme.Colors.TextMuted;
DrawDot(channelColor);
ImGui.SameLine();
ImGui.TextUnformatted(channelName);
// Slot 2: privacy badge
ImGui.SameLine();
DrawSeparator();
ImGui.SameLine();
using (plugin.FontManager.FontAwesome.Push())
{
ImGui.TextUnformatted(FontAwesomeIcon.Lock.ToIconString());
}
ImGui.SameLine();
var privacyLabel = Plugin.Config.PrivacyFilterEnabled
? HellionStrings.StatusBar_Privacy_Enabled
: HellionStrings.StatusBar_Privacy_Open;
ImGui.TextUnformatted(privacyLabel);
// Slot 3: counts
ImGui.SameLine();
DrawSeparator();
ImGui.SameLine();
ImGui.TextUnformatted(_cachedCountsText);
// Slot 4: tells (hidden at 0)
if (!string.IsNullOrEmpty(_cachedTellsText))
{
ImGui.SameLine();
DrawSeparator();
ImGui.SameLine();
ImGui.TextUnformatted(_cachedTellsText);
}
// Slot 5: version, right-aligned, muted
var versionText = $"v{Plugin.Interface.Manifest.AssemblyVersion} · Hellion";
var versionWidth = ImGui.CalcTextSize(versionText).X;
var contentRegionMax = ImGui.GetContentRegionMax().X;
ImGui.SameLine(contentRegionMax - versionWidth);
using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(theme.Colors.TextMuted)))
{
ImGui.TextUnformatted(versionText);
}
}
private static void DrawDot(uint rgba)
{
var pos = ImGui.GetCursorScreenPos();
const float radius = 4f;
ImGui
.GetWindowDrawList()
.AddCircleFilled(
new Vector2(pos.X + radius, pos.Y + ImGui.GetTextLineHeight() / 2f),
radius,
ColourUtil.RgbaToAbgr(rgba)
);
ImGui.Dummy(new Vector2(radius * 2 + 4, ImGui.GetTextLineHeight()));
}
private static void DrawSeparator()
{
ImGui.TextDisabled("·");
}
}