chore: comments, second pass -- the task codes the first pass missed

The first sweep matched a character class that swallowed the digit, so a bare
B1 slipped through while B1-2 was caught. Searching the whole A-Z space instead
of guessing prefixes turned up 130-odd more: B0 through B6, C2, C3, D1, H2, M6,
P7, P8, T2, W2, plus GP-04, KB-01, OD-1, PM-1, PM-3, SEC-01, TR-4, TR-7, UI-11,
UI-12, XC-8 and API-3.

Kept deliberately: 41 B4 01 is a byte signature, "N0" a format string,
#L119-L128 a source anchor, LS4/LS6 are linkshells, and A=FF B=0C G=41 R=C2
explains a colour-channel order. Those look like codes and are not.

Also translated the eight German comments left in the theme files and
ImGuiUtil. Seven of them described what a palette does to which channel, which
is worth reading -- just not in a second language in an otherwise English
codebase.
This commit is contained in:
2026-08-19 22:03:12 +02:00
parent 5b738e6885
commit 16557213cd
59 changed files with 262 additions and 193 deletions
+4 -4
View File
@@ -23,7 +23,7 @@ internal sealed class AutoTellTabsService : IDisposable
private readonly ILogger<AutoTellTabsService> _logger;
// Tabs-list structure lock now lives on Plugin (neutral owner) so the
// MessageManager refilter can share it. See Plugin.TabsListLock / B3.
// MessageManager refilter can share it. See Plugin.TabsListLock.
private object TabsListLock => _plugin.TabsListLock;
// Bumped whenever something wipes unpinned temp tabs wholesale (logout).
@@ -179,7 +179,7 @@ internal sealed class AutoTellTabsService : IDisposable
}
// Three steps, because building the tab pulls history out of the store and
// that must not happen under TabsListLock (B3 rule; the query sorts the whole
// that must not happen under TabsListLock (the query sorts the whole
// receiver history). Step 1 and 3 are locked, step 2 is not.
int generation;
lock (TabsListLock)
@@ -303,7 +303,7 @@ internal sealed class AutoTellTabsService : IDisposable
internal void DropOldestTempTab()
{
// B3: lock the list-structure ops so the (currently caller-less) Unpin path
// Lock the list-structure ops so the (currently caller-less) Unpin path
// can't race the worker; re-entrant when HandleTell already holds the lock.
lock (TabsListLock)
{
@@ -577,7 +577,7 @@ internal sealed class AutoTellTabsService : IDisposable
// Count and flag under one lock so the cap can't be raced. SaveConfig stays
// OUTSIDE -- holding TabsListLock across a save would put an fsync on the
// click path, which is what B6 just removed elsewhere.
// click path, which is what a later cycle just removed elsewhere.
lock (TabsListLock)
{
if (PinnedTempTabCount >= MaxPinnedTempTabs)
+3 -3
View File
@@ -1,7 +1,7 @@
namespace HellionChat;
// Reduced CJK fallback coverage for the v1.5.3 NotoSansCjk fallback merge (B1).
// Before B1 the fallback merged over the full `Ranges` array (Default + endonyms),
// Reduced CJK fallback coverage for the v1.5.3 NotoSansCjk fallback merge.
// Before that the fallback merged over the full `Ranges` array (Default + endonyms),
// duplicating the Latin/Default work already done by the global/Japanese fonts.
// This is the trimmed remainder the fallback is actually the sole source for:
// - Hangul Syllables (AC00-D7A3): no other merged font ships Korean glyphs.
@@ -9,7 +9,7 @@ namespace HellionChat;
// font is Inter-Light (no CJK), so the fallback is the SOLE Han source. The JpRange
// overlap is harmless (MergeMode: the Japanese font wins for shared kanji).
// Deliberately excluded: ONLY the ASCII/Latin Default block (0x20-0xFF), which the
// global font already owns -- that doubled Latin merge is the B1 waste being removed.
// global font already owns -- that doubled Latin merge is the waste being removed.
// Kept as plain start/end pairs so it is unit-testable without the unsafe ImGui
// glyph-range builder (mirrors FontSizeResolver's split-for-test rationale).
internal static class CjkFallbackRange
+5 -5
View File
@@ -35,7 +35,7 @@ public class ConfigKeyBind
[Serializable]
public class Configuration : IPluginConfiguration
{
internal const int LatestVersion = 26;
internal const int LatestVersion = 27;
public int Version { get; set; } = LatestVersion;
@@ -45,7 +45,7 @@ public class Configuration : IPluginConfiguration
// Global window opacity, applied across all themes.
public float WindowOpacity = 0.85f;
// UI-12: background opacity of the main chat window while unfocused.
// Background opacity of the main chat window while unfocused.
// WindowOpacity above stays the focused value.
public float WindowOpacityInactive = 0.65f;
@@ -161,7 +161,7 @@ public class Configuration : IPluginConfiguration
public bool SeenPopOutHeaderHint;
public bool AutoTellTabsOpenAsPopout;
// UI-7: how sender names are rendered in the chat log.
// How sender names are rendered in the chat log.
public WorldSuffixMode WorldSuffixMode = WorldSuffixMode.OtherWorldOnly;
public NameFormMode NameFormMode = NameFormMode.Full;
@@ -232,7 +232,7 @@ public class Configuration : IPluginConfiguration
// Toast when a tell the user sent could not be delivered.
public bool NotifyFailedTell = true;
// UI-11: warn before sending a message that carries plugin-only glyphs.
// Warn before sending a message that carries plugin-only glyphs.
public bool NotifyPluginDisclosure = true;
public bool KeepInputFocus = true;
public bool Use24HourClock = true;
@@ -462,7 +462,7 @@ public class Tab
[NonSerialized]
internal string? _cachedTellIcon;
// PM-3 hover-lerp state. Default 0f means "not hovered". Sidebar
// hover-lerp state. Default 0f means "not hovered". Sidebar
// path animates per tab; card-mode-border path is tab-aggregate
// (any card-row hover ramps the alpha for all cards in this tab).
// Lerp speed lives in the render loop, not here, so the same field
+6 -6
View File
@@ -94,13 +94,13 @@ public sealed class FontManager : IDisposable
private ushort[] Ranges = [];
private ushort[] JpRange = [];
// B1: trimmed remainder the NotoSansCjk fallback is the sole source for
// Trimmed remainder the NotoSansCjk fallback is the sole source for
// (Hangul + full Han); excludes the Default/Latin block already merged
// by the global font, so the fallback no longer re-merges the full Ranges array.
private ushort[] CjkFallbackGlyphRange = [];
// Report accessor for the ctor self-test: built glyph-range array lengths so
// the step can show the B1 dedup effect (a small trimmed fallback vs the large
// the step can show the dedup effect (a small trimmed fallback vs the large
// primary range) in its on-disk report instead of a bare Pass.
internal (int Ranges, int JpRange, int CjkFallback) GlyphRangeLengths =>
(Ranges.Length, JpRange.Length, CjkFallbackGlyphRange.Length);
@@ -247,7 +247,7 @@ public sealed class FontManager : IDisposable
// Instance method so Ranges / JpRange are reachable without parameter
// plumbing; PascalCase field names follow the existing class style.
// B1: shared CJK + symbols tail for both the regular and italic delegate
// Shared CJK + symbols tail for both the regular and italic delegate
// fonts. Earlier-merged fonts win for shared codepoints (imgui MergeMode),
// so this runs AFTER the primary font is set as config.MergeFont. The CJK
// fallback is the sole Hangul/Simplified-Han source when UseHellionFont=true
@@ -265,7 +265,7 @@ public sealed class FontManager : IDisposable
config.GlyphRanges = JpRange;
AddFontWithFallback(tk, Plugin.Config.JapaneseFontV2.FontId, config, "japanese");
// NotoSansCjk fallback, trimmed to CjkFallbackGlyphRange (B1). Merged last so earlier fonts win.
// NotoSansCjk fallback, trimmed to CjkFallbackGlyphRange. Merged last so earlier fonts win.
config.SizePt = basePt;
config.GlyphRanges = CjkFallbackGlyphRange;
AddFontWithFallback(
@@ -427,7 +427,7 @@ public sealed class FontManager : IDisposable
// Common extras (Axis ingame glyphs, endonyms, enclosed alphanumerics)
// belong to the primary/Japanese ranges only. The trimmed CJK fallback
// (B1) skips them so it stays a pure Hangul/Simplified-Han remainder and
// skips them so it stays a pure Hangul/Simplified-Han remainder and
// does not re-merge the Default-block work the global font already did.
if (includeCommonExtras)
{
@@ -498,7 +498,7 @@ public sealed class FontManager : IDisposable
);
JpRange = BuildRange(GlyphRangesJapanese.GlyphRanges, includeCommonExtras: true);
// B1: the fallback gets only the trimmed Hangul/Simplified-Han remainder.
// The fallback gets only the trimmed Hangul/Simplified-Han remainder.
// No Default block, no endonyms — those are already merged by the global and
// Japanese fonts, so re-merging them on the fallback was wasted atlas work.
CjkFallbackGlyphRange = BuildRange(CjkFallbackRange.Pairs, includeCommonExtras: false);
+7 -7
View File
@@ -507,14 +507,14 @@ internal unsafe class KeybindManager : IDisposable
// Resolve the surface this keybind acts on FIRST: a focused pop-out otherwise
// the main window. Channel-set/REPLY/prefill all write here so the action
// follows the input the user is typing in (C3 full tail rebuild, OD-1).
// follows the input the user is typing in.
var (targetWindow, targetTab) = ResolveKeybindTarget();
// Surface + focus the resolved target ONCE, before routing. Main: ActivateChat
// re-surfaces it from a hide/closed state (the chat-activation entry point
// retired in v1.6.0). Pop-out: arm only its focus — NOT ActivateChat, which
// would yank the main window to front and un-hide it on every pop-out-targeted
// keybind (OD-1: stay where the user types). Exactly one window arms focus per
// keybind: stay where the user types. Exactly one window arms focus per
// keybind, so the next frame has no SetKeyboardFocusHere race.
if (targetWindow is ChannelPopoutWindow)
targetWindow.RequestInputFocus();
@@ -547,7 +547,7 @@ internal unsafe class KeybindManager : IDisposable
// Rotation binds (REPLY / linkshell-cycle). Ported from v1.5.6's
// ChatLogWindow.Activated (1d3b429:240-334) without the ChatActivatedArgs
// indirection (gone in the rewrite). Writes onto the resolved surface's
// tab (C2/C3 shared target), not Plugin.CurrentTab.
// tab, not Plugin.CurrentTab.
if (targetTab is { } rotTab)
{
var targetChannel = (InputChannel?)rotateChannel;
@@ -629,7 +629,7 @@ internal unsafe class KeybindManager : IDisposable
if (info.Permanent)
{
// KB-01 (1.5.6 parity, ChatLogWindow.SetChannel 1d3b429:1476-1479):
// 1.5.6 parity (ChatLogWindow.SetChannel, 1d3b429:1476-1479):
// committing the game channel also pre-targets the game's native input.
// Forward the tab's reply target for Tell so the partner is armed
// game-side (ChangeChatChannel code 17); null for a linkshell —
@@ -654,7 +654,7 @@ internal unsafe class KeybindManager : IDisposable
// Prefill text binds (CMD_COMMAND seeds "/"): the token always goes to the
// main InputBar (the focus contract does not expose pop-out buffers); a
// focused pop-out already received focus above, so only token routing matters
// here (documented scope limit, OD-1).
// here -- a documented scope limit.
if (info.Text is { } text)
Plugin.Instance.InputBar.SetPendingMessage(text);
}
@@ -665,7 +665,7 @@ internal unsafe class KeybindManager : IDisposable
}
// Resolve which chat surface a keybind action targets: the open pop-out whose
// input currently has focus, otherwise the main window. C2/C3 share this so a
// input currently has focus, otherwise the main window. Both paths share it so a
// channel-switch/REPLY/prefill follows the surface the user is typing in. The
// returned tab is that surface's bound tab (pop-out: Bound; main: ActiveTab).
// Null tab => skip the tab-write (early-load window where no tab exists yet).
@@ -682,7 +682,7 @@ internal unsafe class KeybindManager : IDisposable
}
// Tab-delta keybinds (ChatTabForward/Backward) stay main-window-only by design:
// a channel-bound pop-out has no tab list to cycle (OD-1). The focus contract is
// a channel-bound pop-out has no tab list to cycle. The focus contract is
// consumed by the channel-set/REPLY/prefill tail, not here.
private void DispatchTabDelta(int delta)
{
+1 -1
View File
@@ -31,7 +31,7 @@ public sealed class ExtraChat : IDisposable
// volatile: IPC callbacks fire on a Dalamud thread while ImGui reads these.
// Reference assignment is atomic on x64, but the barrier ensures visibility
// across threads (especially Mono/Wine). See AUDIT-2026-05-05 [SEC-01].
// across threads (especially Mono/Wine). Raised in the 2026-05-05 audit.
private volatile Dictionary<string, uint> ChannelCommandColoursInternal = new();
internal IReadOnlyDictionary<string, uint> ChannelCommandColours =>
ChannelCommandColoursInternal;
+3 -3
View File
@@ -163,7 +163,7 @@ internal class MessageManager : IAsyncDisposable
internal void ClearAllTabs()
{
// B3: snapshot the tab LIST under the shared lock so the worker-thread
// Snapshot the tab LIST under the shared lock so the worker-thread
// add/remove can't tear the enumeration; tab.Clear() then runs lock-free
// (each tab's Messages has its own SemaphoreSlim — lock order: list outer).
List<Tab> tabsSnapshot;
@@ -184,8 +184,8 @@ internal class MessageManager : IAsyncDisposable
using var messages = Store.GetMostRecentMessages(CurrentContentId, since);
// TempTabs excluded (live state from AutoTellTabsService). Bucket via the
// pure MapMessagesToTabs so the assignment stays testable outside Dalamud (B3-1).
// B3: snapshot under the shared lock (list copy only — short critical
// pure MapMessagesToTabs so the assignment stays testable outside Dalamud.
// Snapshot under the shared lock (list copy only — short critical
// section). The Store query above and the AddSortPrune writes below stay
// OUTSIDE the lock (lock order: list outer, MessageList inner).
List<Tab> nonTempTabs;
+1 -1
View File
@@ -2,7 +2,7 @@ using HellionChat.Resources;
namespace HellionChat;
// UI-7: how a sender's name is rendered in the chat log. Kept in its own file
// How a sender's name is rendered in the chat log. Kept in its own file
// (no Dalamud usings) so the SenderNameFormatter pure-helper test stays
// AppDomain-isolated (feedback_dalamud_test_isolation).
+1 -1
View File
@@ -264,7 +264,7 @@ internal sealed class PayloadHandler
if (!Sheets.IsInForay())
{
// Single SetPendingMessage call; v1.5.6 used incremental Chat += writes.
// XC-8: shares the /tell builder with the native detours. IsPublic (not
// Shares the /tell builder with the native detours. IsPublic (not
// IsNullOrEmpty) is resolved HERE — a private/null world must NOT leak @World.
_inputBar.SetPendingMessage(
GameFunctions.Chat.BuildTellCommand(
+80 -13
View File
@@ -134,7 +134,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
internal Integrations.HonorificService HonorificService { get; private set; } = null!;
internal Integrations.CustomAudioPlayer CustomAudioPlayer { get; private set; } = null!;
// Ctor-smoke anchors (B0-2). Exposed so the Payload/Chunk ctor-smoke steps
// Ctor-smoke anchors. Exposed so the Payload/Chunk ctor-smoke steps
// can drive the real per-frame Lender path (Borrow()) and the eager
// singletons through the container, never via new(). Mirror of the
// FontManager property pattern — every SelfTest reaches services this way.
@@ -178,7 +178,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
internal bool ChatActivationRequested;
// Set in the first DisposeAsync statement so async callbacks scheduled
// via Framework.RunOnTick (v1.4.8 B3 retention sweep) can early-bail
// via Framework.RunOnTick (v1.4.8 retention sweep) can early-bail
// before they touch state that has already been torn down. Volatile
// because the tick reads it from a different thread than the writer.
private volatile bool _isDisposing;
@@ -188,7 +188,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
// just unloaded belongs to nobody.
internal bool IsDisposing => _isDisposing;
// v1.9.0 B5: last full Draw() wall-time in ms, written once per frame at
// v1.9.0: last full Draw() wall-time in ms, written once per frame at
// the end of the UiBuilder.Draw handler. Covers the GlobalStyleScope push
// and the font push (the first-frame hitch measurement must include atlas/style
// prologue cost), not just WindowSystem.Draw — measuring the inner call
@@ -214,7 +214,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
// must never block doing so.
internal readonly Util.DbOperationGate DbOperations = new();
// B3: neutral owner of the Config.Tabs LIST-structure lock so both the
// Neutral owner of the Config.Tabs LIST-structure lock so both the
// worker-thread mutator (AutoTellTabsService) and the framework-thread
// refilter (MessageManager) share ONE monitor. Lock order: this outer,
// MessageList's SemaphoreSlim inner — never the reverse.
@@ -287,6 +287,37 @@ public sealed class Plugin : IAsyncDalamudPlugin
+ "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.10."
);
}
// 2.0.0 does not migrate, it starts over. Five cycles rebuilt the whole
// window layer, and a config carried through them keeps values chosen
// against surfaces that no longer exist -- an opacity picked for a
// window that has been redrawn twice since, tabs laid out for a sidebar
// that works differently now. Every user of this build is a tester who
// was told this happens, and it is the only way to be sure everyone
// sees the same defaults.
//
// The file is copied aside first. Rolling back to 1.5.6 is a supported
// move here and it stays cheap: the old settings are a file copy away,
// rather than an evening of clicking them back in.
if (Config.Version < 27)
{
BackUpConfigBeforeReset();
Config = Configuration.CreateFresh();
Config.Version = 27;
// Saved immediately: a crash between here and the first user-driven
// save would otherwise run the whole reset again on the next start,
// and the second run would back up the already-reset file over the
// real backup.
SaveConfig();
Log.Information(
"Config reset to defaults for 2.0.0. Previous settings kept as "
+ "HellionChat.json.pre-2.0.0.bak next to the config file."
);
}
else
{
// v23 migration: SidebarTabView was the 1.5.6 sidebar↔top-tabs switch,
// superseded by MainWindowLayoutMode in the v1.6.0 rewrite. A user who
// set it false (only effective in 1.5.6) wanted top tabs — carry that
@@ -341,7 +372,10 @@ public sealed class Plugin : IAsyncDalamudPlugin
var carried = 0;
foreach (var tab in Config.Tabs)
{
if (tab.NameCameFromPartner || (!tab.IsTempTab && tab.TellTarget?.IsSet() != true))
if (
tab.NameCameFromPartner
|| (!tab.IsTempTab && tab.TellTarget?.IsSet() != true)
)
continue;
tab.NameCameFromPartner = true;
@@ -356,14 +390,15 @@ public sealed class Plugin : IAsyncDalamudPlugin
);
}
}
}
Config.Version = 26;
Config.Version = 27;
// Unpinned TempTabs are session-only and dropped on every load. Pinned
// TempTabs survive reload -- tester feedback in v1.4.7.
Config.Tabs.RemoveAll(TabLifecycleHelpers.ShouldStripOnLoad);
// GP-04: clear stale Tab.PopOut flags now — the pool binds further down
// Clear stale Tab.PopOut flags now — the pool binds further down
// (ChannelPopoutPool resolve below), so at this point no tab can own a
// slot. A persisted PopOut=true (notably on surviving pinned TempTabs)
// would otherwise be a flag with no window. Runs after the strip, before
@@ -462,7 +497,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
FirstRunWizard = _host.Services.GetRequiredService<FirstRunWizard>();
ChannelPopoutPool = _host.Services.GetRequiredService<Ui.Windows.ChannelPopoutPool>();
// Ctor-smoke anchors (B0-2). Resolved last, against the fully built
// Ctor-smoke anchors. Resolved last, against the fully built
// container: every MakePayloadHandler dep (MainWindow, InputBar,
// ChunkRenderer, ...) is resolvable here, and the ChunkRenderer resolve
// below just reuses the same cached singleton. These are plain
@@ -732,7 +767,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
return;
// Set before any cleanup so deferred Framework.RunOnTick callbacks
// (B3 retention sweep) see the flag and bail out before they touch
// (the retention sweep) see the flag and bail out before they touch
// MessageManager / Log / static fields that the rest of this method
// is about to tear down.
_isDisposing = true;
@@ -840,6 +875,38 @@ public sealed class Plugin : IAsyncDalamudPlugin
return failure;
}
// Copies the config aside before the 2.0.0 reset overwrites it. Best effort
// by design: a backup that cannot be written must not stop the plugin from
// starting, and the reset itself is what the user was told would happen.
//
// Overwrite=false, so a second run cannot bury the real backup under a copy
// of the already-reset file. The reset saves immediately for the same
// reason, but a crash in between is exactly when this matters.
private static void BackUpConfigBeforeReset()
{
try
{
var dir = Interface.ConfigDirectory.Parent?.FullName;
if (dir is null)
return;
var configFile = Path.Combine(dir, "HellionChat.json");
if (!File.Exists(configFile))
return;
var backup = Path.Combine(dir, "HellionChat.json.pre-2.0.0.bak");
if (File.Exists(backup))
return;
File.Copy(configFile, backup);
Log.Information($"HellionChat: config backed up to {backup} before the 2.0.0 reset");
}
catch (Exception e)
{
Log.Warning(e, "HellionChat: could not back up the config before the 2.0.0 reset");
}
}
private static void MigrateFromChatTwoLayout()
{
var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName;
@@ -1181,7 +1248,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
// Schedule on the next framework tick to avoid the ~194ms
// hitch from blocking with .Wait() while the frame finishes.
// The Config.Tabs enumeration in ClearAllTabs/FilterAllTabs is
// now guarded by the shared Plugin.TabsListLock (B3), so this
// now guarded by the shared Plugin.TabsListLock, so this
// tick scheduling is purely hitch-avoidance, not safety.
// Pattern reference: SimpleTweaks
// Tweaks/Chat/CaseInsensitiveCommands.cs:45.
@@ -1268,7 +1335,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
private void Draw()
{
// v1.9.0 B5: time the whole handler (style + font prologue included).
// v1.9.0: time the whole handler (style + font prologue included).
// Bail before measuring once teardown has begun — a late Draw tick
// must not touch ThemeRegistry / FontManager after DisposeAsync.
if (_isDisposing)
@@ -1277,7 +1344,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
var drawWatch = Stopwatch.StartNew();
try
{
// v1.4.8 B2: pick up external edits of the active custom theme JSON
// v1.4.8: pick up external edits of the active custom theme JSON
// without forcing the user to re-click the picker. The disk-stat is
// 1Hz-throttled inside RefreshActiveIfStale, so this is essentially
// free on built-in themes and ~1 stat/second on custom themes.
@@ -1367,7 +1434,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
// Config.Tabs across the save so JSON includes them. Cloning only the
// unpinned subset keeps the allocation proportional to
// AutoTellTabsLimit (<=15) instead of the full tab list.
// B3: the strip/restore mutates the tab LIST, so it shares TabsListLock
// The strip/restore mutates the tab LIST, so it shares TabsListLock
// with the worker add/remove and the refilter snapshot. Re-entrant: the
// one worker caller (HandleTell) already holds it; framework callers take
// it here. SavePluginConfig runs inside (short, in-memory) — the documented fallback
+1 -1
View File
@@ -5,7 +5,7 @@ using HellionChat.Util;
namespace HellionChat.SelfTests;
// B2: behavioural check that the card path feeds CardClipPlanner AND that a
// Behavioural check that the card path feeds CardClipPlanner AND that a
// layout change clears the height cache — not a non-null check. Pure plan math
// is pinned headless by CardClipPlanTests; this drives the live accessors.
internal sealed class CardClipPlanStep : ISelfTestStep
@@ -15,7 +15,7 @@ namespace HellionChat.SelfTests;
// - a BOUND auto-tell tab keeps its partner by design (leg1) — its binding is
// Tab.TellTarget and is deliberately untouched by the strip.
// The channel label is intentionally NOT asserted: a tell tab re-derives back to
// Tell after the strip (spec TR-7); only the target matters for privacy.
// Tell after the strip ; only the target matters for privacy.
internal sealed class CurrentTabGuidedStep : ISelfTestStep
{
private readonly Plugin _plugin;
+1 -1
View File
@@ -5,7 +5,7 @@ using HellionChat._Helpers;
namespace HellionChat.SelfTests;
// B2-3: proves the plugin-disclosure arm-and-hold wires the (otherwise verwaist)
// Proves the plugin-disclosure arm-and-hold wires the (otherwise verwaist)
// scanner into the REAL send entry InputBar.TrySend. Drives TrySend via the
// arm-test-hook with a PUA glyph in the buffer and NotifyPluginDisclosure on:
// the first send must ARM and HOLD (no send), so PendingMessage stays the probe
@@ -112,7 +112,7 @@ internal sealed class FontManagerCtorSmokeStep : ISelfTestStep
return SelfTestStepResult.Fail;
}
// B1: assert the atlas actually finished building all required handles,
// Assert the atlas actually finished building all required handles,
// not just that the references are non-null. FontsReady is the observable
// state the trimmed-fallback rebuild must still reach; a half-built atlas
// would pass the null/exception checks above but fail here.
@@ -128,7 +128,7 @@ internal sealed class FontManagerCtorSmokeStep : ISelfTestStep
}
// Report what was actually verified rather than a bare Pass.
// The glyph-range entry counts make the B1 dedup visible — the cjk-fallback
// The glyph-range entry counts make the dedup visible — the cjk-fallback
// range is now a small trimmed remainder next to the large primary range.
var counts = fm.GlyphRangeLengths;
var italicState =
@@ -4,7 +4,7 @@ using HellionChat.Ui.Windows;
namespace HellionChat.SelfTests;
// P8 wiring: UserHide() suppresses DrawConditions; both ActivateChat() (Enter) and
// wiring: UserHide() suppresses DrawConditions; both ActivateChat() (Enter) and
// Toggle() (/hellion) restore it. Pure window-state — the focus side is left to smoke.
internal sealed class HideRestoreSelfTestStep : ISelfTestStep
{
+2 -2
View File
@@ -4,7 +4,7 @@ using HellionChat.Ui.Windows;
namespace HellionChat.SelfTests;
// B1-2 window flags. Drives the REAL MainWindow.PreDraw and asserts it wired
// window flags. Drives the REAL MainWindow.PreDraw and asserts it wired
// Window.Flags to ResolveFlags(CanMove, CanResize), then pins the pure
// fresh-base contract: false/false adds NoMove|NoResize, true/true clears them
// -- flags must rebuild from a fresh base, or NoMove sticks after toggling
@@ -72,7 +72,7 @@ internal sealed class MainWindowFlagsStep : ISelfTestStep
return SelfTestStepResult.Fail;
}
// P7 title-bar contract: ShowTitleBar=false adds NoTitleBar from the
// title-bar contract: ShowTitleBar=false adds NoTitleBar from the
// fresh base, true clears it (same no-accumulation guarantee).
var barHidden = MainWindow.ResolveFlags(true, true, false);
var barShown = MainWindow.ResolveFlags(true, true, true);
@@ -3,7 +3,7 @@ using Dalamud.Plugin.SelfTest;
namespace HellionChat.SelfTests;
// UI-12 focus opacity. Pins the pure ResolveBgAlpha contract (focused →
// focus opacity. Pins the pure ResolveBgAlpha contract (focused →
// WindowOpacity, unfocused → WindowOpacityInactive). The PreDraw wiring
// (BgAlpha = ResolveBgAlpha(IsFocused) behind the main-viewport/!docked guard)
// is NOT headless-deterministic — the guard may leave BgAlpha null when
@@ -7,7 +7,7 @@ using HellionChat.Util;
namespace HellionChat.SelfTests;
// B3-3: notification-sound selection. Drives the pure SelectNotificationSound
// Notification-sound selection. Drives the pure SelectNotificationSound
// (the exact pick logic ProcessMessage runs per message) through its SelfTest
// wrapper with local synthetic tabs — Plugin.Config.Tabs is never touched, so
// no real tab gains messages or unread state. The audible preview button is
@@ -3,7 +3,7 @@ using System.IO;
namespace HellionChat.SelfTests;
// Disk sink for the B5 performance baseline. Kept separate from the SelfTest
// Disk sink for the performance baseline. Kept separate from the SelfTest
// step so the per-frame hot path never references file IO. Writes one
// perf-baseline.json into the plugin ConfigDirectory, atomically (tmp + move)
// like ThemeRegistry's theme writer, so a mid-write crash leaves either the
@@ -18,7 +18,7 @@ internal sealed class PerformanceBaselineStep : ISelfTestStep
private const int TargetFrames = 1000;
// Rough draw-call proxy: ImGui emits 6 indices per quad, so vertices/6 is an
// intentional under-count of draw work, not the exact quad count (API-3).
// intentional under-count of draw work, not the exact quad count.
private const int VerticesPerQuadProxy = 6;
private readonly Plugin _plugin;
@@ -3,7 +3,7 @@ using Dalamud.Plugin.SelfTest;
namespace HellionChat.SelfTests;
// B3-5: only the snap decision is headless-testable. Scroll detection + bar +
// Only the snap decision is headless-testable. Scroll detection + bar +
// hit-test are smoke-only (the scroll child exists only in-game; GetScrollY is
// garbage headless). Drives ResolveSnapToBottom via the SelfTest accessor and
// asserts the OR + the request reset invariant.
@@ -5,7 +5,7 @@ using Dalamud.Plugin.SelfTest;
namespace HellionChat.SelfTests;
// B2-1/B2-2: proves the WorldSuffixMode/NameFormMode reformat reaches the REAL
// Proves the WorldSuffixMode/NameFormMode reformat reaches the REAL
// render entry. Drives ChunkRenderer.DrawChunks (a SelfTests/README-sanctioned
// real entry that wires SenderNameDisplay.ForDisplay at ChunkRenderer.cs:54)
// with a synthetic ChunkSource.Sender chunk carrying a PlayerPayload, at a
@@ -4,7 +4,7 @@ using HellionChat.Code;
namespace HellionChat.SelfTests;
// v1.10.0/C3: the active row gets a surface and an accent bar, so exactly the
// v1.10.0: the active row gets a surface and an accent bar, so exactly the
// row the user is on must be marked -- and only that one. Drives the real
// Sidebar.Draw and reads the render-observability counter, so a regression in
// the draw path fails rather than a parallel calculation passing.
@@ -5,7 +5,7 @@ using HellionChat.GameFunctions.Types;
namespace HellionChat.SelfTests;
// B3-2: greeted glyph renders only for temp tabs when the toggle is on. Drives
// Greeted glyph renders only for temp tabs when the toggle is on. Drives
// the REAL Sidebar.Draw (render precedent: HonorificHeaderRenderStep, the only
// real .Draw in this pool — NOT SidebarModeAutoSwitchStep which only calls
// IsExpanded/GetWidth) inside the /xlperf window frame and reads the render
@@ -58,7 +58,7 @@ internal sealed class SidebarModeAutoSwitchStep : ISelfTestStep
return SelfTestStepResult.Fail;
}
// B1-3a: the expanded width must come from Config.SidebarWidth, not the
// The expanded width must come from Config.SidebarWidth, not the
// old fixed 150 constant. Drive the REAL GetWidth (the single source
// Sidebar.Draw consumes) with concrete values and assert the OBSERVED
// effect — in-range passthrough plus clamping — instead of mirroring the
@@ -5,7 +5,7 @@ using HellionChat.GameFunctions.Types;
namespace HellionChat.SelfTests;
// B3-4: section headers render once per non-empty temp-tab pool, and compact
// Section headers render once per non-empty temp-tab pool, and compact
// mode suppresses the header text (separators stay). Drives the REAL
// Sidebar.Draw inside the /xlperf window frame (same render precedent as
// SidebarGreetedGlyphStep) and reads the render observability counter.
@@ -4,7 +4,7 @@ using HellionChat.Ui.Components;
namespace HellionChat.SelfTests;
// B3-1: rename must persist. Drives the real ApplyTabRename (the InputText
// Rename must persist. Drives the real ApplyTabRename (the InputText
// callback path), then SaveConfig + reload from disk and asserts the new name
// survived — a fresh-from-config tab, not the same reference (a reference check
// would pass on a dead roundtrip). Uses a persistent (non-temp) tab: unpinned
@@ -10,7 +10,7 @@ namespace HellionChat.SelfTests;
// The activation strip. Drives the REAL OnTabActivated -- the entry the
// Sidebar/TopTabBar click handlers, the pop-out path and the Draw-seed all call
// — with local probe tabs (Plugin.Config.Tabs is never touched). Asserts the
// five contracts: strip-on-switch, no-strip-on-reclick (TR-4), leg1 preserve,
// five contracts: strip-on-switch, no-strip-on-reclick, leg1 preserve,
// derive, and non-tell untouched.
internal sealed class TellResetOnActivateStep : ISelfTestStep
{
@@ -41,7 +41,7 @@ internal sealed class TellResetOnActivateStep : ISelfTestStep
}
// (b) re-clicking the already-active tab (previous == tab) must NOT strip
// a live game-tell conversation (TR-4 regression guard).
// a live game-tell conversation (regression guard).
var reclick = MakeStaleTellTab(boundTellTarget: false, withLabel: false);
TabLifecycleHelpers.OnTabActivated(reclick, reclick);
if (reclick.CurrentChannel.TellTarget is null)
@@ -4,7 +4,7 @@ using HellionChat.Themes;
namespace HellionChat.SelfTests;
// Verifies the v1.5.4 PM-1 crossfade contract: switching the active
// Verifies the v1.5.4 crossfade contract: switching the active
// theme arms TryGetActiveCrossfade for ~300ms, then the registry
// returns to direct AbgrCache reads. A second switch within 100ms
// keeps the lerped path active (no identity-snap). CleanUp restores
@@ -67,7 +67,7 @@ internal sealed class ThemeCrossfadeSelfTestStep : ISelfTestStep
// it as "saw the start" if more than 300ms have elapsed.
// Skip the mid-crossfade-switch phase in that case -- the
// lerped path is no longer active, so a second switch would
// re-arm a fresh crossfade and not exercise PM-1b's
// re-arm a fresh crossfade and not exercise its
// mid-flight-origin override.
if (Environment.TickCount64 - this.armedAtTickMs > 300)
{
@@ -83,7 +83,7 @@ internal sealed class ThemeCrossfadeSelfTestStep : ISelfTestStep
if (!this.sawMidCrossfadeSwitch)
{
// PM-Test-3 mid-crossfade-switch phase: within ~100ms of the
// mid-crossfade-switch phase: within ~100ms of the
// first observed crossfade, fire a second Switch to a THIRD
// theme. ArmCrossfade must compose the current lerped state
// as the new origin -- TryGetActiveCrossfade still returns
+1 -1
View File
@@ -4,7 +4,7 @@ using HellionChat.Code;
namespace HellionChat.SelfTests;
// v1.10.0/D1: the top-tab strip marks the active tab with a fill plus an accent
// v1.10.0: the top-tab strip marks the active tab with a fill plus an accent
// underline. Drives the real TopTabBar.Draw and reads the render counter.
//
// "At most one", not "exactly one": the strip skips popped-out tabs, so zero
+3 -3
View File
@@ -51,9 +51,9 @@ internal static class EventHorizon
ChatColors: new ThemeChatColors(
new Dictionary<HellionChat.Code.ChatType, uint>
{
// Event Horizon — Cosmic-Purple-Drift: helle Pastelle bekommen
// Lavender-Tinte, Akzent-Channels (Tell) ziehen Richtung Magenta-
// Lila. Channel-Identität bleibt klar erkennbar.
// Cosmic purple drift: the pale pastels take a lavender tint and
// the accent channels (tell) pull towards magenta-violet. Channel
// identity stays readable throughout.
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#E6E0F5"),
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F2C25C"),
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FF9050"),
@@ -51,8 +51,8 @@ internal static class ForgeMerchantman
ChatColors: new ThemeChatColors(
new Dictionary<HellionChat.Code.ChatType, uint>
{
// Forge Merchantman — Patina-Tinte in Party/FC, Bernstein-Tinte in
// Yell/Alliance/CustomEmote. Channel-identity bleibt voll erhalten.
// Patina tint on party and free company, amber on yell, alliance
// and custom emotes. Channel identity is left fully intact.
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"),
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0C060"),
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#E8902C"),
+2 -2
View File
@@ -51,8 +51,8 @@ internal static class HellionArctic
ChatColors: new ThemeChatColors(
new Dictionary<HellionChat.Code.ChatType, uint>
{
// Hellion Arctic — FFXIV-Standard mit dezenter Cyan-Tinte in den
// blauen Channels (Party/FC). Channel-Identität bleibt klar.
// The FFXIV defaults with a restrained cyan tint on the blue
// channels (party, free company). Channel identity stays clear.
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"),
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FFE066"),
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FFA040"),
+4 -3
View File
@@ -51,9 +51,10 @@ internal static class IndigoViolet
ChatColors: new ThemeChatColors(
new Dictionary<HellionChat.Code.ChatType, uint>
{
// Indigo Violet — Lavender-Pink-Drift in Tell und LS6/7. Türkis-
// Mint-Aurora-Counter in Party/FC und LS4. Glitter-Gold in Yell.
// Differenzierung zu Event Horizon: dunkler, dichter, Türkis statt Gold.
// Lavender-pink drift on tell and linkshells 6 and 7, countered by
// a turquoise-mint aurora on party, free company and linkshell 4.
// Glitter gold on yell. What sets it apart from Event Horizon:
// darker, denser, and turquoise where that one goes gold.
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#F0E6FF"),
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0D880"),
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F09A60"),
+3 -3
View File
@@ -51,9 +51,9 @@ internal static class MintGrove
ChatColors: new ThemeChatColors(
new Dictionary<HellionChat.Code.ChatType, uint>
{
// Mint Grove — Naturthemen-Tönung: Honey-Amber in Yell-Familie,
// Mint-Drift in NoviceNetwork und Linkshell. Tell-Pink-Identität
// bleibt erhalten für Erkennbarkeit.
// Nature-themed tint: honey amber across the yell family, a mint
// drift in novice network and linkshell. Tell keeps its pink so
// the channel stays recognisable.
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#E8F5EA"),
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F9D580"),
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F0A050"),
+3 -2
View File
@@ -51,8 +51,9 @@ internal static class NightBlue
ChatColors: new ThemeChatColors(
new Dictionary<HellionChat.Code.ChatType, uint>
{
// Night Blue — Royal-Blue-Tinte in Party/FC, Bronze-Gold in Yell/
// Alliance. Channel-identity (Tell-Pink, NN-Lime) bleibt erhalten.
// Royal blue on party and free company, bronze gold on yell and
// alliance. Channel identity is preserved -- tell stays pink,
// novice network stays lime.
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"),
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FFD060"),
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FFA040"),
@@ -51,8 +51,9 @@ internal static class SynthwaveSunset
ChatColors: new ThemeChatColors(
new Dictionary<HellionChat.Code.ChatType, uint>
{
// Synthwave Sunset — Magenta dominiert die warmen Channels (Yell/Shout/FC),
// Cyan dominiert die kühlen (Tell/Party). Neon-Akzente für Status-nahe Channels.
// Magenta carries the warm channels (yell, shout, free company),
// cyan the cool ones (tell, party). Neon accents on the status-
// adjacent channels.
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#F0DFFF"),
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FF2D95"),
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FF6BB6"),
+5 -5
View File
@@ -10,7 +10,7 @@ public sealed class ThemeRegistry
public const string DefaultSlug = HellionArctic.Slug;
// 1Hz throttle for the v1.4.8 B2 auto-refresh-on-active path. The
// 1Hz throttle for the v1.4.8 auto-refresh-on-active path. The
// Plugin.Draw hook calls RefreshActiveIfStale every frame, but the
// actual File.GetLastWriteTimeUtc disk-stat only runs once per second
// -- 60fps would otherwise mean 3600 stats/min on the same path (more
@@ -24,7 +24,7 @@ public sealed class ThemeRegistry
private readonly string? _customThemesDir;
private Theme _active;
// v1.4.8 B2: source path of the currently active custom theme. Captured
// v1.4.8: source path of the currently active custom theme. Captured
// at Switch() time so RefreshActiveIfStale does not have to reconstruct
// a filename from the slug -- custom theme filenames are not required
// to match the slug they declare in the JSON body. Null when the active
@@ -33,7 +33,7 @@ public sealed class ThemeRegistry
private long _lastActiveStampCheckMs = -ActiveStampPollIntervalMs;
private DateTime _lastActiveStamp = DateTime.MinValue;
// PM-1 crossfade state. Switch() captures the previous AbgrCache as a
// crossfade state. Switch() captures the previous AbgrCache as a
// VALUE-COPY (not a Theme reference) -- the built-in singletons share
// their RecomputeAbgrCache identity, so a reference would mutate
// alongside the new active. _crossfadeStartTickMs == long.MinValue
@@ -115,7 +115,7 @@ public sealed class ThemeRegistry
// ThemeImportExportRow opens this path via Process.Start.
public string? CustomThemesDir => _customThemesDir;
// Read-only enumeration of all built-in theme slugs. T2 ThemePickerCategoryStep
// Read-only enumeration of all built-in theme slugs. ThemePickerCategoryStep
// diffs this set against ThemePicker.CategoryMapSlugs to enforce coverage.
public IEnumerable<string> BuiltinSlugs => _builtIns.Keys;
@@ -293,7 +293,7 @@ public sealed class ThemeRegistry
// (see `Switch` built-in-first lookup), so saving a custom file under
// a built-in slug persists the file but leaves the built-in active —
// looks green, behaves broken. ColorPicker DrawIdleState forks
// built-in themes into a custom slug before BeginEditing, M6
// built-in themes into a custom slug before BeginEditing,
// ImportFromPath renames built-in-colliding imports to <slug>_imported.
// New call-sites must either fork first or rename to a non-built-in slug.
public bool SaveEditingBuffer(out string targetPath)
+1 -1
View File
@@ -1,6 +1,6 @@
namespace HellionChat.Themes;
// Pure stale-check for the v1.4.8 B2 theme-auto-refresh-on-active path.
// Pure stale-check for the v1.4.8 theme-auto-refresh-on-active path.
// Lives in a free helper class so the Build-Suite can exercise the diff
// rules without instantiating ThemeRegistry (which touches the Dalamud
// log proxy and the filesystem). The rules:
+1 -1
View File
@@ -2,7 +2,7 @@ using System.Collections.Generic;
namespace HellionChat.Ui.Components;
// B2 (PERF-B2): variable-height clip plan. ImGuiListClipper needs a constant
// Variable-height clip plan. ImGuiListClipper needs a constant
// row height, and since v1.10.0 neither density has one (compact rows wrap
// too), so both compute a plan from the cached per-row heights: a lead dummy
// for the rows above
+3 -3
View File
@@ -34,11 +34,11 @@ internal sealed class ChunkRenderer
// names change every plugin reload to avoid stable cross-session linkage.
_salt = new Random().Next().ToString();
// Not yet consumed in C2/C3; E-task wiring will likely add log call-sites later.
// No call sites yet; logging here will likely come later.
_ = _logger;
}
// B2-1/B2-2 render-observability: the formatted sender text the real draw
// render-observability: the formatted sender text the real draw
// path actually produced (post-ForDisplay). A SelfTest reads this after
// driving DrawChunks to prove the WorldSuffixMode/NameFormMode reformat
// reached the real render entry — never the helper in isolation. null until
@@ -52,7 +52,7 @@ internal sealed class ChunkRenderer
float lineWidth = 0f
)
{
// UI-7: render a copy with the sender name reformatted per the user's
// Render a copy with the sender name reformatted per the user's
// display options. Skipped in screenshot mode so the name-anonymising
// path in DrawChunk stays reliable (privacy wins). ForDisplay returns
// the list unchanged when nothing applies, so non-sender lists and the
+3 -3
View File
@@ -64,7 +64,7 @@ internal sealed class InputBar
private bool _wasInputTextHovered;
private bool? _isFocusedOverride; // Test-only; null = honour per-frame Draw() value.
// UI-11 plugin-disclosure arm-and-hold: holds the buffer that armed the
// plugin-disclosure arm-and-hold: holds the buffer that armed the
// disclosure warning. null = not armed. Compared by value so an edit
// re-arms and a resend on the identical buffer goes through. 1.5.6 parity
// (ChatInputBar 1d3b429:27).
@@ -196,7 +196,7 @@ internal sealed class InputBar
ImGui.SameLine();
DrawQuickButtons();
// UI-11: yellow inline warning while a plugin-only-glyph message is
// Yellow inline warning while a plugin-only-glyph message is
// armed-and-held (buffer unchanged since it armed). Renders on its own
// line below the input row. 1.5.6 parity (ChatInputBar 1d3b429:93-103).
if (
@@ -589,7 +589,7 @@ internal sealed class InputBar
if (string.IsNullOrEmpty(text))
return;
// UI-11: plugin-disclosure arm-and-hold. Arm + scan on the RAW
// Plugin-disclosure arm-and-hold. Arm + scan on the RAW
// _pendingMessage (NOT the trimmed `text`) so the Draw warning gate
// (_pendingMessage == _disclosureArmedBuffer) matches byte-for-byte even
// when the buffer has leading/trailing whitespace. 1.5.6 armed/held/
+10 -10
View File
@@ -18,13 +18,13 @@ internal sealed class MessageList
private PayloadHandler? _handler;
// B3-5: scroll-to-bottom state. Per-instance, so pop-out windows (own
// Scroll-to-bottom state. Per-instance, so pop-out windows (own
// MessageList instance, PluginHostFactory.cs:263-266) isolate automatically —
// the old 1.5.6 updateScrollState flag is NOT needed here.
private bool _scrolledUp;
private bool _scrollToBottomRequested;
// B2: the height cache is only valid while these inputs are unchanged.
// The height cache is only valid while these inputs are unchanged.
// FontManager's own fingerprint covers font sizes only, not density / the two
// name-display modes / width — a stale height would misplace the clipper dummies.
// Per tab, not per list: the old single field let a width change in tab A mark
@@ -73,12 +73,12 @@ internal sealed class MessageList
return snap;
}
// SelfTest hook (B3-5 reset-invariant, REQUIRED — not optional). Lets
// SelfTest hook (reset-invariant, REQUIRED — not optional). Lets
// ScrollSnapDecisionStep flip the request flag without a real click, so the
// post-snap reset can be asserted; without it only the OR branch is testable.
internal void RequestScrollToBottomForSelfTest() => _scrollToBottomRequested = true;
// SelfTest hook (B2): runs the real planner against a caller fixture so the
// SelfTest hook: runs the real planner against a caller fixture so the
// step asserts the plan without a live scroll child (GetScrollY is garbage headless).
internal CardClipPlan PlanCardClipForSelfTest(
IReadOnlyList<float> heights,
@@ -86,7 +86,7 @@ internal sealed class MessageList
float viewportHeight
) => CardClipPlanner.Plan(heights, scrollY, viewportHeight);
// SelfTest hook (B2): drives the live invalidation, returns the tab's remaining
// SelfTest hook: drives the live invalidation, returns the tab's remaining
// cached-height count so the step can assert the drop. nowMs is a parameter so
// the step can step past the settle window without sleeping (v1.10.0).
internal int RunHeightCacheInvalidationForSelfTest(Tab tab, float contentWidth, long nowMs)
@@ -165,7 +165,7 @@ internal sealed class MessageList
MeasureTimestampColumn(tab);
// B2: drop stale cached heights before the snapshot draw. Both densities
// Drop stale cached heights before the snapshot draw. Both densities
// need this now -- compact rows are not constant height either, they wrap.
// Width read here while it is valid.
InvalidateHeightCacheIfLayoutChanged(
@@ -187,7 +187,7 @@ internal sealed class MessageList
var frozen = _fingerprintGates[tab.Identifier].IsPending;
DrawRows(tab, messages, compact ? _drawCompactRow : _drawCardRow, frozen);
// B3-5: scroll values are frame-constant inside the child, so this
// Scroll values are frame-constant inside the child, so this
// reflects the current frame's state wherever it runs; kept after the
// render to mirror the 1.5.6 end-of-DrawMessageLog placement.
_scrolledUp = ImGui.GetScrollMaxY() - ImGui.GetScrollY() > 1f;
@@ -282,7 +282,7 @@ internal sealed class MessageList
ImGui.SetCursorPos(origin with { X = origin.X + _stampColumnWidth });
}
// B3-5: Discord-style full-width bar pinned to the bottom edge of the
// Discord-style full-width bar pinned to the bottom edge of the
// visible region while the user is scrolled up. Geometry comes from window
// pos + size (visible region), never from the content flow: when scrolled
// up the visible bottom sits above the content bottom, so the
@@ -335,7 +335,7 @@ internal sealed class MessageList
private void DrawCompactRow(Message message, string? previousStamp)
{
// B2-1/B2-2: render the sender through DrawChunks (the name-aware path
// Render the sender through DrawChunks (the name-aware path
// that applies WorldSuffixMode/NameFormMode via ForDisplay), not as a
// flat SenderSource.TextValue string. message.Sender already carries the
// channel brackets/colon as ChunkSource.None wrappers (MessageManager
@@ -566,7 +566,7 @@ internal sealed class MessageList
private void DrawCardRow(Message message, string? previousStamp)
{
// B2-1/B2-2: sender via DrawChunks (name-aware path), on its own line
// Sender via DrawChunks (name-aware path), on its own line
// with content below — 1.5.6 card parity (ChatLogWindow.cs:1913, no
// SameLine after the sender). The 1.5.6 channel-colour push on the
// sender is deferred styling polish (deferred to v1.9.0); plain
@@ -133,7 +133,7 @@ internal sealed class AboutTab
LastHonorificStatusKey = kind.ToString();
var colors = _themes.Active.Colors;
// Null-safety via the `is { } v` pattern, never `.Value` raw (spec SEC-2):
// Null-safety via the `is { } v` pattern, never `.Value` raw :
// the version is bound only on the arms that have it; the impossible
// Detected/Incompatible-without-version state falls through to default.
switch (kind)
@@ -29,7 +29,7 @@ internal sealed class ThemePicker
(HellionStrings.Settings_Theme_Category_Retro, new[] { "synthwave-sunset" }, false),
];
// T2 ThemePickerCategoryStep diffs this against ThemeRegistry.BuiltinSlugs
// ThemePickerCategoryStep diffs this against ThemeRegistry.BuiltinSlugs
// to enforce coverage. Kept on the static map so the test does not pierce instance state.
internal static IEnumerable<string> CategoryMapSlugs => CategoryMap.SelectMany(c => c.Slugs);
+6 -6
View File
@@ -22,7 +22,7 @@ internal sealed class Sidebar
{
public const float IconOnlyWidth = 38f;
// B1-3a: expanded sidebar width is user-configurable (Config.SidebarWidth),
// Expanded sidebar width is user-configurable (Config.SidebarWidth),
// clamped to these bounds (matches the ChannelsTab slider range). Replaces
// the old fixed 150px ExpandedWidth constant.
public const float MinSidebarWidth = 40f;
@@ -38,7 +38,7 @@ internal sealed class Sidebar
// previously active one -- that row was still active when it was painted.
internal int LastRenderedActiveSurfaceCount { get; private set; }
// B3-2 render observability: counts greeted glyphs actually drawn this frame.
// render observability: counts greeted glyphs actually drawn this frame.
// Incremented ONLY in the real glyph branch in DrawRow; reset at Draw start.
// The SelfTest reads it after driving the real Draw — no dead service roundtrip.
internal int LastRenderedGreetedGlyphCount;
@@ -48,7 +48,7 @@ internal sealed class Sidebar
// beside it.
private const float PinGlyphScale = 0.6f;
// B3-4 render observability: section headers actually drawn this frame.
// render observability: section headers actually drawn this frame.
// Incremented only in the real header branch; reset at Draw start.
internal int LastDrawnSectionHeaderCount;
@@ -149,7 +149,7 @@ internal sealed class Sidebar
var dimAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextDim);
var dl = ImGui.GetWindowDrawList();
// B3-4 sectioned render order (1.5.6 parity): persistent → pinned
// sectioned render order (1.5.6 parity): persistent → pinned
// TempTabs → unpinned TempTabs. Only the display sequence regroups;
// the tab list itself stays untouched and every row keeps its
// ORIGINAL list index for PushID, so an open context-menu popup
@@ -250,7 +250,7 @@ internal sealed class Sidebar
// Only split off a separate pop-out hit area when there's room for
// both buttons. Below that, the whole row stays as a single
// selectable strip without the pop-out affordance.
// A3: gate the pop-out affordance on the expanded sidebar too. In
// Gate the pop-out affordance on the expanded sidebar too. In
// icon-only mode avail still clears the width threshold, which used to
// paint the pop-out glyph over the tab icon. The row stays a single
// selectable strip when collapsed; right-click pop-out is unaffected.
@@ -476,7 +476,7 @@ internal sealed class Sidebar
// The hit area sits at the LEFT edge of the row, but the item must
// be submitted AFTER TabContextMenu.Draw — any interactive item
// between the row button and the popup call would steal the
// right-click trigger (B3-1 ordering constraint).
// right-click trigger (ordering constraint).
ImGui.SetCursorScreenPos(origin);
// CheckCircle = greeted, plain Check = still pending (1.5.6 mapping).
+1 -1
View File
@@ -82,7 +82,7 @@ internal static class TabContextMenu
ClearPendingRename();
}
// Per-tab notification sound (B3-3). The checkbox gates the picker so
// Per-tab notification sound. The checkbox gates the picker so
// tabs that never want a sound keep the popup short.
if (
ImGui.Checkbox(
+2 -2
View File
@@ -36,7 +36,7 @@ public class DbViewer : Window
private int CurrentPage = 1;
private string SimpleSearchTerm = "";
// v1.4.8 H2: opt-in full-text search across the whole DB via FTS5.
// v1.4.8: opt-in full-text search across the whole DB via FTS5.
// Transient UI state (per-session), not persisted -- users opt in fresh
// every time so they always see the page-filter as the default mode.
private bool UseFullTextSearch;
@@ -232,7 +232,7 @@ public class DbViewer : Window
tooltipRight: Language.Page_ArrowRight_Tooltip
);
// Full-text search toggle (v1.4.8 H2). IsFtsIndexBuilt is a cached
// Full-text search toggle (v1.4.8). IsFtsIndexBuilt is a cached
// volatile bool in MessageStore -- single field read per frame, no
// SELECT count(*). ImRaii.Disabled blocks any click while the index
// is still being built, so no defensive force-off branch needed
+1 -1
View File
@@ -168,7 +168,7 @@ internal sealed class InputPreview : Window
{
ImGui.TextUnformatted(Language.Options_Preview_Header);
// Primary path (A2) resets the Lender counter in MainWindow.Draw();
// Primary path resets the Lender counter in MainWindow.Draw();
// this fallback covers the edge-case where MainWindow is closed but
// InputPreview is still open, preventing handler pool growth.
if (!_mainWindow.IsOpen)
+1 -1
View File
@@ -44,7 +44,7 @@ internal sealed class ChannelPopoutPool
// A popped tab gets its own input bar, so strip stale tell state first —
// otherwise a popped-out stale-tell tab would be a send surface that
// bypasses the click-path activation strip. Previous = the main window's
// active tab; popping the active tab itself must not strip (TR-4 guard).
// active tab; popping the active tab itself must not strip (guard).
TabLifecycleHelpers.OnTabActivated(tab, Plugin.Instance.MainWindow?.ActiveTab);
var slot = _slots.TryReserve(tab.Identifier);
@@ -103,7 +103,7 @@ internal sealed class ChannelPopoutWindow : Window, IFocusableChatWindow
}
// IFocusableChatWindow — this pop-out's own InputBar carries the focus state
// the keybind tail checks when deciding whether to route at this surface (C3).
// the keybind tail checks when deciding whether to route at this surface.
public bool HasFocusedInput => _input.IsFocused;
// Arm-and-hold the one-frame Activate flag; the pop-out's Draw applies the
+3 -3
View File
@@ -82,14 +82,14 @@ internal sealed class MainWindow : Window, IFocusableChatWindow
RespectCloseHotkey = false;
}
// UI-12: per-window focus-dependent opacity. ResolveBgAlpha stays guard-free
// Per-window focus-dependent opacity. ResolveBgAlpha stays guard-free
// and pure so the self-test can drive it directly; PreDraw owns the guard +
// wiring. 1.5.6 parity (focused → WindowOpacity, unfocused →
// WindowOpacityInactive, ChatLogWindow.PreOpenCheck 1d3b429:724).
internal float ResolveBgAlpha(bool isFocused) =>
isFocused ? Plugin.Config.WindowOpacity : Plugin.Config.WindowOpacityInactive;
// B1-2 / P7: rebuild flags from a fresh base every frame so toggling
// Rebuild flags from a fresh base every frame so toggling
// CanMove/CanResize/ShowTitleBar back on actually CLEARS NoMove/NoResize/
// NoTitleBar (not accumulating). Move/resize/title-bar logic as 1.5.6
// (ChatLogWindow.PreOpenCheck 1d3b429:703-710); base flags = today's
@@ -240,7 +240,7 @@ internal sealed class MainWindow : Window, IFocusableChatWindow
}
// IFocusableChatWindow — the keybind tail resolves which surface owns the
// input focus before routing a channel-set/REPLY/prefill at it (C3).
// input focus before routing a channel-set/REPLY/prefill at it.
public bool HasFocusedInput => _input.IsFocused;
// Arm-and-hold: field writes only, safe from the framework thread; the draw
+1 -1
View File
@@ -9,7 +9,7 @@ using Microsoft.Extensions.Logging;
namespace HellionChat.Ui.Windows;
// `internal` to match the Plugin.SettingsWindow property in W2; `public` here
// `internal` to match the Plugin.SettingsWindow property; `public` here
// would raise CS0053 against the internal members. Matches MainWindow shape.
internal sealed class SettingsWindow : Window
{
+2 -2
View File
@@ -91,7 +91,7 @@ internal static class ColourUtil
}
// Modulates the alpha byte of an ABGR color by a factor in [0, 1].
// RGB stays intact. Used by the PM-3 hover-lerp path where each
// RGB stays intact. Used by the hover-lerp path where each
// frame produces a fractional alpha value but the colour itself
// must not shift.
internal static uint ApplyAlpha(uint abgr, float alphaFactor)
@@ -103,7 +103,7 @@ internal static class ColourUtil
}
// Mixes an ABGR colour's RGB channels toward white (0xFF) by factor t in
// [0, 1]; the alpha byte is left untouched. A1 hover-sheen accent-tint:
// [0, 1]; the alpha byte is left untouched. Hover-sheen accent tint:
// a low factor nudges the sweep toward the element's accent hue without
// going fully saturated (effect level stays "subtle"). RGB-only on
// purpose -- DrawHoverSheen owns the alpha falloff.
+3 -4
View File
@@ -90,10 +90,9 @@ internal static class ImGuiUtil
using (ImRaii.PushColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled]))
ImGui.TextUnformatted("(?)");
// AllowWhenDisabled — ohne das Flag liefert IsItemHovered bei
// ausgegrauten Settings false, der User könnte nicht mehr lesen
// warum eine Option nicht aktiv ist. Genau dann braucht er den
// Hover-Tooltip aber am dringendsten.
// AllowWhenDisabled, because without it IsItemHovered returns false on a
// greyed-out setting -- and a reader who cannot find out why an option is
// inactive is exactly the reader who needs the tooltip most.
if (!ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
return;
+2 -2
View File
@@ -4,7 +4,7 @@ using HellionChat._Helpers;
namespace HellionChat.Util;
// UI-7: produces a render-only view of a chunk list with the sender's name
// Produces a render-only view of a chunk list with the sender's name
// reformatted per the user's WorldSuffixMode / NameFormMode. Called from
// ChatLogWindow.DrawChunks on every draw — it never mutates the input, so the
// stored message (the Sender BLOB in the DB) stays byte-for-byte unchanged and
@@ -76,7 +76,7 @@ internal static class SenderNameDisplay
// text, and any cross-world icon) with one formatted chunk that keeps
// the PlayerPayload link so the name stays clickable. Dropping the
// original sender icon is an accepted trade-off and only happens once
// the user moves UI-7 off its defaults.
// the user moves off its defaults.
// Channel brackets and colons are ChunkSource.None wrappers outside the
// Sender span — they are preserved untouched by the copy loops below.
var copy = new List<Chunk>(chunks.Count);
+2 -2
View File
@@ -72,7 +72,7 @@ internal static class TabLifecycleHelpers
public static bool ShouldStripOnSave(Tab t) => IsInUnpinnedPool(t);
// GP-04: clear every Tab.PopOut at load time. The pool binds later, so at
// Clear every Tab.PopOut at load time. The pool binds later, so at
// load NO tab can own a slot — a persisted PopOut=true is always a stale flag
// with no window. Unconditional (pinned included) because pinned TempTabs
// survive the load and are the main stale-flag source; a !IsPinned filter
@@ -91,7 +91,7 @@ internal static class TabLifecycleHelpers
// partner-name label) so a normal typed line cannot route as a silent /tell
// to the old partner — the same privacy guard StripTellBindingOnPromote
// applies on promote. Re-activating the already-active tab must NOT strip
// (a live game-tell would lose its context, TR-4); a tab carrying its own
// (a live game-tell would lose its context); a tab carrying its own
// Tab.TellTarget is a real tell binding (leg1) and is left intact.
internal static void OnTabActivated(Tab tab, Tab? previous)
{
@@ -1,6 +1,6 @@
namespace HellionChat._Helpers;
// UI-11 pure decision helper: does a message about to be sent carry a glyph
// pure decision helper: does a message about to be sent carry a glyph
// that only renders correctly for players running HellionChat or a similar
// plugin? Those are FFXIV Private-Use-Area icon codepoints (the same range
// SeIconChar covers); a recipient without a plugin sees an empty box.
+1 -1
View File
@@ -2,7 +2,7 @@ using System;
namespace HellionChat._Helpers;
// UI-7 pure decision helper: builds the display string for a sender's name +
// pure decision helper: builds the display string for a sender's name +
// world per the user's NameFormMode / WorldSuffixMode. Dalamud-free so the
// Build Suite can cover every combination; SenderNameDisplay feeds it the name
// and world it pulled from the PlayerPayload.