Commit Graph
31 Commits
Author SHA1 Message Date
JonKazama-Hellion dfc0cda806 fix(chat): four things the header review found, all of them visible
The self-test was the worst of them, because it is the only tool that makes block
A judgeable at all and it destroyed itself on use. It returned Fail while the
atlas was not ready, and its own weight buttons trigger a rebuild -- which is
asynchronous, not synchronous as the comment claimed. Click a weight, watch the
step go red. It waits now, like the two existing steps that had already worked
this out.

The translated stand-in was drawn in the meta face, whose glyph range is ASCII
plus a middle dot. Fifteen of the twenty-five translations reach outside that, so
"not logged in" would have rendered as a row of question marks in Japanese,
Russian, Korean, Greek and eleven others -- and the measured width would have
been the width of the question marks, so the right edge would have drifted too.
The plan said to keep it on the body face and the comment in FontManager says so
as well; the code simply did not. The detail is two parts now rather than one
string, and each part is measured under the face that draws it.

The header was the only place in the UI pushing RegularFont directly, without the
FontsEnabled-or-UseHellionFont check every other push site makes. With both
toggles off the window draws in AXIS and the header would have drawn in
Inter-Light, at a different size, in a band measured against a third one.

And the icon sat on the text baseline. FontAwesome is a fixed-width handle built
at Dalamud's own size and does not follow the plugin's font setting, so at any
other body size it hangs. The sidebar already knew this and centres against the
row; the header does the same now.

Two smaller ones came along: the minimum-height threshold was compared unscaled,
which would have dissolved it at higher display scales, and the height passed
into that check included the input row -- so "is there still room to read" was
measuring the wrong thing. Both callers now say what sits below them.
2026-08-19 10:21:03 +02:00
JonKazama-Hellion 100290ea4d feat(chat): put the channel header above both windows
Drawn before the scroll child in each, so it stays put while the log moves. The
child's height is deliberately left alone: it is given as a negative value, and
ImGui resolves those against the space still available from the current cursor --
which the header has already reduced. Subtracting it a second time would have
opened a gap of exactly the header's height above the input row, in both windows.

Where the name shows follows one rule: never twice on the same screen. The
sidebar layout is the only place nothing else carries it, so that is the only
place the header says it. The top-tab strip carries it, and a pop-out with its
title bar on carries it in the title.

The pop-out's plain title row is gone, replaced by the header. The comment it
left behind is worth keeping in mind -- an earlier header row was removed exactly
because it repeated the tab name one line below the title bar. That warning is
now the rule rather than a reason to have no header at all.

The trailing detail is the home world and the clock, and the clock follows the
same Use24HourClock setting the message timestamps do. Two clock formats in one
window, with the header sitting directly above a column of timestamps, would be a
defect rather than a preference. Culture is pinned for the same reason it is
pinned in the message list.
2026-08-19 09:58:29 +02:00
JonKazama-Hellion 8e38e3e805 refactor(config): delete what was never read, restore what was only orphaned
Block D of v1.12.0. The line between the two is the whole job, and I got
it wrong once on the way: six cache fields on Tab looked dead because
nothing writes them, and nothing writes them because AutoTellTabTint and
TabTintCache went out with the chat window in cf4705e. Deleting the
fields would have cemented a loss instead of recording a decision.

So they are back, and the sidebar uses them again: an auto-tell tab is
tinted and glyphed from its partner, twelve colours against seven icons.
Four open tells are no longer four identical envelopes in one colour.

Their own header promised the same partner keeps its colour "across
sessions" while hashing with string.GetHashCode, which .NET salts per
process -- every game start reshuffled every tab. FNV-1a now, with a
lowbias32 finalizer that is not decoration: without it a probe over 144
similar keys reached six of the twelve colours, because the caller takes
the low bits with a modulo and FNV leaves those correlated. Three pinned
values guard it, which is also the only assertion that can catch a
regression to a salted hash.

The same question, asked of the three hide conditions this block had
quietly orphaned: HideDuringCutscenes, HideInBattle, HideWhenNotLoggedIn
all had readers in v1.5.6 and lost them in the same commit. Two of them
are states rather than conditions -- a cutscene the user dismissed stays
dismissed until it ends, and combat must not seize a chat that is
already hidden for another reason -- so they come back as a small state
machine with eight pinned transitions, and three toggles whose labels
were already translated in all 25 languages.

Actually deleted, with a reader search each time:

- Six per-tab hide fields. Their reader was the pop-out window and it
  stopped consulting them in cf4705e. Per-tab was the wrong unit anyway:
  "hide during cutscenes" is a statement about the screen.
- Tab.ChatCodes, whose migration the v16 schema gate had already made
  unreachable.
- InactivityHideTimeout and InactivityHideActiveDuringBattle, MaxLinesToRender
  which had stopped bounding anything, and the 155 lines of
  Configuration.UpdateFrom with no caller at all.

Config version 25, at all three places that carry it. No migration step:
the gate only refuses anything under 16 and Json.NET drops keys it does
not know, so the deleted fields simply stop being written.

One thing a review pass caught that matters more than any of the above:
the clone parity guard had gone hollow. It compares collections by
count, ChatCodes was the only collection the probe seeded, and removing
it left the guard comparing zero against zero. Verified by making
Tab.Clone discard both remaining collections and watching every
assertion stay green. The probe seeds them now, and the same sabotage
fails as it should.
2026-08-18 23:36:54 +02:00
JonKazama-Hellion 9b634f54e9 fix(tabs): stop a tell from a popped-out partner hijacking the main window
Reported from the field: a new tell from someone whose tab is popped out
throws the main window onto the General tab, every time.

The router revealed the tab by activating it in the main window. That
window does not display a popped-out tab -- PickMainActiveTab re-anchors
on the next frame, and it anchors to the FIRST non-popped tab in list
order, not to the one the user was reading. So the reveal did nothing it
intended and threw away the active tab on the way.

Nothing needed revealing in the first place: the tab was already on
screen in its own window. That decision now lives in PlanTellReveal,
next to the pop-out helpers it belongs with, pure and pinned by nine
facts -- the mode, the switch and the popped-out state have eight
combinations between them and only two of them should touch anything.

ActivateTab refuses a popped-out tab outright as well. The router is the
caller that got it wrong, but the invariant belongs to the window: its
active tab is never one that something else is drawing.
2026-08-18 22:25:23 +02:00
JonKazama-Hellion 05b203c85c fix(style): dither the surface tint so it stops banding
"Horizontal scanlines" names it exactly. An alpha ramp from 0x34 to 0 has 52
distinct values, so across a 600px pane each one owns a stripe about 12 pixels
tall, and the eye reads those stripes as scanlines. Raising the contrast only
buys more of them, thinner.

So it dithers. Four ramps at a quarter alpha each, every one ending slightly
lower than the last, so they share a starting colour but run at different
slopes. Their step boundaries land on different rows, and where one layer has
stepped up its neighbours have not, which puts the blend between two quantised
values. Same total tint, four times the effective resolution, three extra draw
calls.

Layers are staggered by slope rather than offset on purpose: shifting them would
leave a gap at the top where fewer layers overlap, which reads as a bright band.
That trades one artefact for a worse one.

The tint is roughly twice as strong now, which the settings pane wanted anyway,
and strength is a parameter so the chat log can stay at 45% of it.
2026-08-18 18:09:46 +02:00
JonKazama-Hellion c1f1c0563c fix(style): tint the surface instead of repainting it
Both remaining complaints had one cause. The window paints its own background,
and the backdrop was filling the same area a second time. Two stacked layers
turn a translucent window solid, which is the density over the chat log.

And an opaque fill has to carry the entire gradient by itself. That is where the
banding came from: a shallow ramp across an opaque surface crosses so few 8-bit
values that each one covers a visible stripe. Nothing sits underneath to break
them up.

So it tints now. Near-transparent white at the top, near-transparent black at
the bottom, straight over whatever the window already drew. The window colour
stays visible, the ramp only shades it, and the game showing through disperses
what little stepping is left. The sidebar does the same with a flat black wash
rather than a repainted darker surface.

Motes are roughly half as bright everywhere, and the chat log takes 10% of that.

DrawVerticalGradient stays for the segmented control, which paints a surface
that genuinely is its own rather than one already drawn underneath.
2026-08-18 18:05:11 +02:00
JonKazama-Hellion 8b96ffb2ec fix(style): let the backdrop follow the window's own opacity
Three problems from one screenshot, and the same root cause behind two of them.

The pane painted at full opacity regardless of the window it sat in. BgAlpha
only ever reaches WindowBg, so a draw-list fill ignores it entirely, and a
deliberately translucent settings window came out as a solid block beside a
translucent chat window. The backdrop now reads the alpha out of the resolved
WindowBg colour and carries it through the gradient, the accent wash and the
motes.

The banding is 8-bit quantisation, not a rendering fault: a gentle ramp across a
tall surface crosses so few distinct values that each one covers a visible band.
Halving the range leaves fewer and fainter steps, and the translucency now
underneath them breaks up what remains.

Motes over the chat log drop to 18%. A settings page is read in glances and can
carry motion behind it; a chat log is read line by line, where anything drifting
behind the text competes with it. Intensity is a parameter rather than a second
particle system, so the two surfaces share one implementation.
2026-08-18 18:01:07 +02:00
JonKazama-Hellion 8ebed6846d feat(style): put the chat log on the same floor as the settings
The motes were too subtle to register, so: seventy instead of twenty-four, wider
radius range, and each one is a soft halo with a brighter core rather than a
flat disc. At this size a single filled circle reads as a speck of dirt on the
screen; the ring around it is what makes it look lit.

The backdrop moves into its own component and the chat log gets it too. Two
windows in one plugin looking like two separate products was half the reason the
settings window read as untouched -- the chat had been reworked in v1.10.0 and
the settings had not, and no amount of work inside one window closes that gap.

No accent wash over the message list. It works on a settings pane, where the top
of the surface is a heading; over a chat log the oldest visible messages would
sit in a tinted band and read as highlighted.

Registered transient rather than singleton, so two surfaces on screen own
separate mote sets instead of sharing one drift pattern -- which would be
visible the moment a popout sits next to the main window.

MainWindow's constructor changed, so this needs the DI smoke pass.
2026-08-18 17:55:44 +02:00
JonKazama-Hellion 5696eac55a test(selftest): pin the top-tab underline invariant
TopTabBar had no observability at all -- no counter, no self-test reaching it.
It now exposes LastRenderedUnderlineCount and MainWindow hands the component
out the same way it already does for the sidebar.

Three cases: one of two tabs active draws exactly one underline, a null active
tab draws zero, and an active tab that is not in the list also draws zero. The
last one matters because the strip skips popped-out tabs, so the active tab
legitimately need not be among the drawn ones.
2026-08-17 23:57:11 +02:00
JonKazama-Hellion 2b4243599e fix(ui): render each frame from one tab-list snapshot, key widgets by identity
Sidebar, top tabs and status bar each read Config.Tabs on their own, unlocked,
while the worker added or evicted tabs. That gave three independent views of a
moving list: an index built in one place could resolve to a different tab a few
lines later, which showed up either as an out-of-range crash on the draw thread
or -- worse, because it is silent -- as a click landing in someone else's tell.

MainWindow now takes one snapshot under the lock and passes it through the whole
frame. Deliberately a shallow copy: tab identity is compared by reference all
over the draw path, so cloning would break every ReferenceEquals and Contains.
ChangeTabDelta and ResetActiveTabIfRemoved run on the framework thread and keep
their own locked reads instead; ThemeQuickPicker locks its own copy, since
reaching it would mean threading a parameter through InputBar, which popouts
share and which has no tab list.

Widget IDs move from list position to tab.Identifier. ImGui carries popup and
widget state across frames under that ID, so a position-based one re-binds an
open context menu to a different tab as soon as the list shifts -- a snapshot
cannot fix that, it spans frames. This also resolves top tabs visually merging
into each other when the list changed.

The sidebar section headers counted over the live list while the rows came from
BuildRenderOrder, which skips popped-out tabs. Both sides take the same
predicate now, so the count matches what is drawn.
2026-08-17 07:27:34 +02:00
JonKazama-Hellion 6733c7f92e Restore pop-out exclusivity: hide popped tabs from main window + keybind/unread parity 2026-06-16 18:30:23 +02:00
JonKazama-Hellion fa20b53455 C2/C3: restore rotation keybinds (REPLY/LS-cycle) + focus contract routing on the focused chat surface 2026-06-16 14:09:28 +02:00
JonKazama-Hellion 6f71b09331 fix(closeout): address closure-review findings
- gate keybind pill-sync on IsChannelOrExistingLinkshell so an empty
  linkshell slot no longer desyncs the pill from the real send channel
- close manually-popped pop-out windows on logout via an IsOpen filter
  instead of the PopOut flag (which manual pops never set)
- read the router's tell-tab lookup through a lock-wrapped accessor so the
  framework thread cannot enumerate Config.Tabs mid worker-thread mutation
- add a "switch on every tell" toggle (default on) and make the auto-open
  mode pick the matching layout, so Sidebar vs Top-tab are distinct
- comment corrections (stale/contradictory text, TEST-MIRROR path depth)
2026-06-16 09:04:18 +02:00
JonKazama-Hellion 3878869904 feat(keybind): cycle tabs and switch channel with pill sync 2026-06-16 01:21:05 +02:00
JonKazama-Hellion a73f4d0d0c feat(window): restore hide-chat-window + Enter-to-restore (1.5.6)
The eye/hide button now hides the HellionChat window (runtime-only, via a new
DrawConditions gate) instead of toggling native-chat suppression, matching 1.5.6.
The chat-activation keybind (Enter / "/"), whose dispatch was a dead stub in the
KeybindManager since the v1.6.0 rewrite, is re-wired to MainWindow.ActivateChat:
it un-hides, opens if closed, brings the window to front and focuses the input --
so the chat reacts to Enter again from any state. /hellion is a reliable one-press
recovery (Toggle now clears the hide), and the window always shows on login
(start state no longer read from the persisted flag). Adds HideRestoreSelfTestStep.
2026-06-15 20:49:14 +02:00
JonKazama-Hellion b397591ba4 feat(window): restore the title-bar, hide-button, and 24-hour-clock toggles
Re-wires four 1.5.6 settings that survived the v1.6.0 rewrite as dormant config
fields but lost their UI + consumers:
- ShowTitleBar / ShowPopOutTitleBar: gate ImGuiWindowFlags.NoTitleBar on the main
  window (ResolveFlags) and pop-out windows (new PreDraw). Inverted logic matches
  1.5.6 (flag set only when the toggle is off).
- ShowHideButton: gate the input-bar hide button on the toggle.
- Use24HourClock: add the toggle (MessageList already consumes the field).
New 'Window style' section in WindowTab; Use24HourClock in ChatTab display modes.
MainWindowFlagsStep extended with the NoTitleBar fresh-base contract.
2026-06-15 20:19:59 +02:00
JonKazama-Hellion 6813b80d58 feat(themes): restore the header theme/tab quick-picker
Brings back the 1.5.4 quick-picker lost in the v1.6.0 rewrite: a palette button
in the input-bar button row (left of the cog) opens a popup that switches the
theme (built-in + custom, active row checked) and jumps between chat tabs without
opening settings. Theme switch mirrors the settings ThemePicker; the tab jump
routes through a new MainWindow.ActivateTab that replays the click path
(previous -> set -> OnTabActivated) so tell/unread handling is unchanged. Main
window only -- pop-out InputBars get a null picker. Adds QuickPickerSelfTestStep.
2026-06-15 19:53:31 +02:00
JonKazama-Hellion 5dfe8e3b49 fix(unread): restore the tab unread badge and fix the post-F2 unread decision
The v1.8.x sidebar/top-bar rebuild never re-rendered the unread dot, so inactive tabs showed no badge even though the counter was tracked. Draw it again top-right of the tab icon in both Sidebar and TopTabBar, gated on !active && UnreadMode != None && Unread > 0, and zero the active tab's counter every frame (1.5.6 convention) so the dot only ever shows on tabs you are not looking at.

The unread decision moves to MessageManager.ShouldCountUnread and snapshots the active tab + whether it shows the message once before the loop: Unseen suppresses unread on an inactive tab only when the active (real, post-F2) tab also shows that message. Adds SidebarUnreadDotStep (render) and UnreadDecisionStep (decision) self-tests (step count 32 -> 34).
2026-06-13 18:49:27 +02:00
JonKazama-Hellion 0ca8513065 fix(tell): couple CurrentTab to the active tab, retire LastTab
Plugin.CurrentTab now delegates to MainWindow.ActiveTab (fallback Tabs[0]) instead of the never-assigned LastTab index, so the game hooks, unread tracking, notification sounds, InputDisabled and Foray/Eureka paths all operate on the tab the user actually has selected. The dead LastTab/WantedTab fields and both WantedTab writes are removed.

A reference-based MainWindow.ResetActiveTabIfRemoved repairs the active-tab reference on eviction/logout (immune to the SaveConfig temp-tab strip window). The worker-thread eviction path marshals it onto the framework thread so the strip mutation serializes with Draw; logout is already framework-thread. The Draw-seed gains a lazy re-seed for a wholesale config swap. Adds CurrentTabCouplingStep (headless) and the interactive CurrentTabGuidedStep self-test (step count 30 -> 32).
2026-06-13 16:09:59 +02:00
JonKazama-Hellion ad635c77c1 fix(tell): strip stale tell state on tab activation
OnTabActivated clears the runtime tell state the game-side detour leaves on a tab (CurrentChannel tell target + partner label) when a DIFFERENT tab becomes the input surface, so a normal typed line can no longer route as a silent /tell to the old partner. Re-clicking the active tab and tabs carrying their own Tab.TellTarget binding (leg1) are preserved.

All four activation paths route through it: Sidebar, TopTabBar, ChannelPopoutPool.TryOpen, and the MainWindow draw-seed. EnsureCurrentChannel becomes a pure derive-helper reached only via OnTabActivated. Adds the TellResetOnActivateStep self-test (step count 29 -> 30).
2026-06-13 15:09:38 +02:00
JonKazama-Hellion c28e3f72a1 feat(messages): restore scroll-to-bottom bar with snap decision 2026-06-10 16:55:26 +02:00
JonKazama-Hellion caacb87a6a feat(window): wire move/resize flags and consolidate the duplicate toggle 2026-05-30 17:54:15 +02:00
JonKazama-Hellion 336f722eef feat(window): wire inactive opacity to main window focus state 2026-05-30 16:57:59 +02:00
JonKazama-Hellion ad892cbcb6 feat(layout): add top-tabs layout mode and shared channel resolver 2026-05-29 14:06:41 +02:00
JonKazama-Hellion b954a19b67 fix(payload-handler): popup-pfad in MessageList-Child-Scope verschieben
Seit dem v1.7.0-Components-Layer-Refactor lebte der PayloadHandler-
Popup-Render in MainWindow.Draw als _messages.DrawHandlerPopups()-
Aufruf nach dem ##hellion-body-Child-Close. ImGui.OpenPopup (in
RightClickPayload, innerhalb ##hellion-main-area-Child) und
ImGui.BeginPopup (in PayloadHandler.DrawPopups, im MainWindow-Root
nach Child-Close) hashed die Popup-ID per g.CurrentWindow->GetID(...)
window-relativ — also unterschiedlich. OpenPopupStack-Eintrag wurde
nie gefunden, popup.Success blieb false, _popup wurde auf null
zurückgesetzt. Alle vier Popup-Switch-Cases waren tot: URL-Rechtsklick,
Player, Item (inkl. EventItem-Subpfad), Status.

Fix nach v1.5.6/ChatTwo-Pattern: _handler?.Draw() ans Ende von
MessageList.Draw() verschieben. MessageList läuft im
##hellion-main-area-Scope und öffnet selbst kein Child, also teilen
OpenPopup und BeginPopup denselben Window-Stack. ID-Hash matched,
Popup rendert.

DrawHandlerPopups-Wrapper aus MessageList und der Aufruf in
MainWindow.Draw entfallen — kein toter Code mehr (grep
DrawHandlerPopups: 0 Treffer).

Hypothese verifiziert gegen imgui.h:845 + imgui.cpp:12282+12528
(beide BeginPopup-Hash und OpenPopup-Hash sind window-relativ),
v1.5.6 ChatLogWindow.cs:1667 (handler.Draw im
##chat2-messages-Child), ChatTwo ChatLog.Window.cs:620 (identisches
Pattern). Reader-Lock auf tab.Messages bleibt während DrawPopups
gehalten — identisch zu v1.5.6-Semantik.

Verifiziert in-game (Flo): Linksklick auf URL öffnet Browser direkt
(v1.5.6-konform), Rechtsklick öffnet wieder das Kontext-Popup. dotnet
build clean, dotnet csharpier check clean.

Plan-Runde 1 dieses Cycles (4-LOC-Reroute LeftClick → RightClickPayload)
wurde verworfen weil empirischer Test zeigte dass auch Rechtsklick
broken war — der Reroute hätte das Symptom nur sichtbarer gemacht
ohne die Root-Cause zu adressieren.
2026-05-28 15:25:05 +02:00
JonKazama-Hellion 29fb4b92eb fix(input-preview): wire Inside-mode + Tooltip-mode render paths
InputPreview was only rendered for PreviewPosition.Top/Bottom (the
DrawConditions IsWindowMode gate). Inside-mode (the default) and
Tooltip-mode had no caller at all because v1.5.6's inline-render path
lived on the deleted ChatLogWindow and was not migrated to the v1.7.0
Components-Layer.

Wire Inside-mode by calling CalculatePreviewHeight + DrawPreview
inline from MainWindow.DrawMainArea between the message-list child
and the input bar, with the message-list height reserved for the
preview block. Wire Tooltip-mode by sampling IsItemHovered() on the
input text widget inside InputBar.DrawInputField (analog to the
existing _isFocused = ImGui.IsItemFocused() idiom on the same line)
and exposing it as WasInputTextHovered; MainWindow opens the tooltip
after _input.Draw when both the hover-flag and PreviewPosition.Tooltip
are active.

Plan-drift acknowledged: the plan stated Plugin.InputPreview is
statically reachable, but the property was declared as an instance
member on Plugin.cs:101. Hoisted to internal static to match the
plan's intention (analog to Plugin.Config); updated the single
external instance-access site in PluginLifecycle.RegisterWindows
to the type-qualified form.

Verified in-game: Inside-mode preview block appears between message
list and input bar on first keystroke; tooltip-mode shows preview on
text-field hover only; Top/Bottom-mode unchanged; empty buffer hides
the preview in all modes. dotnet build clean, dotnet csharpier check
clean.
2026-05-28 13:21:59 +02:00
JonKazama-Hellion d1bfddd9b8 feat(main-window): wire Lender + handler.Draw() (A2, MessageList popups)
A2 completes the deferred Lender-cycle from A1 and addresses the
handler.Draw() gap identified in I code-quality-review:

- MainWindow ctor takes Lender<PayloadHandler> as new param (DI-reg
  extended in PluginHostFactory); _handlerLender.ResetCounter() called
  at top of Draw() as primary pool-reset path (InputPreview has the
  secondary defensive fallback for MainWindow-closed edge case)

- MessageList.DrawHandlerPopups() new passthrough method
  (=> _handler?.Draw()) provides the per-frame popup-tick that
  PayloadHandler needs to render the right-click context popup;
  MainWindow.Draw() calls it after the message-list body renders

Without this fix, right-clicking a player/item/status in the chat log
would silently fail to open a popup (handler.Draw() never fired for the
MessageList's _handler). Phase 3 smoke steps 3/4/5 unblocked.

Polish-Sweep + Smoke-Gate are the last cycle-tasks.
2026-05-27 23:29:18 +02:00
JonKazama-Hellion 94fdef38ad feat(main-window): track per-frame window pos/size/viewport for PayloadHandler
Replaces the v1.5.6 `LogWindow.LastViewport/LastWindowPos/LastWindowSize`
window-instance state with public/internal MainWindow surfaces refreshed
at the top of Draw() each frame. PayloadHandler.MoveTooltip in Phase 2
will read these to filter cross-viewport AddonLifecycle events and to
reposition the native item tooltip away from the chat window.

LastViewport is `internal unsafe` (not public) — the only consumer is
PayloadHandler.MoveTooltip in the same assembly; keeping the raw pointer
out of the public surface is the safer default.

Split from Sub-Task A — Lender injection lives in A2 (after F's DI-reg).
2026-05-27 08:08:29 +02:00
JonKazama-Hellion 2f099fd4e1 fix(ui): clickable channel pill, auto-seed channel, kill outer scrollbar
Three smoke bugs from the second in-game test:

1. The channel pill was draw-list only, so it didn't react to clicks
   and there was no way to switch channels inside a tab. The pill now
   has a hit area on top and opens a popup that lists every ChatType
   in tab.SelectedChannels with a ToInputChannel mapping; selecting
   one writes through CurrentChannel.SetChannel.

2. Switching to Allgemein / Gruppe / Linkshell still showed "—"
   because tab.CurrentChannel.Channel stayed at Invalid until somebody
   set it. The sidebar now seeds CurrentChannel on tab activation by
   walking SelectedChannels for the first key with a valid mapping,
   so every tab opens with its own real channel instead of inheriting
   the FC default.

3. MainWindow still surfaced an outer scrollbar next to the message
   list's own scroll. Adding NoScrollbar + NoScrollWithMouse to the
   window flags strips the second bar — the body child owns scroll on
   its own.

Plus the system-icon path: System / BattleSystem / GatheringSystem /
Error / Notice / LootNotice all map to fa-cog now, so the System tab
renders the gear instead of falling back to the generic comment.
2026-05-23 20:58:50 +02:00
JonKazama-Hellion 0109bfd222 test(selftests): add v1.6.0 SelfTest steps from sub-spec
Six new ISelfTestStep entries register with the Dalamud SelfTestRegistry:
SidebarModeAutoSwitch probes the width threshold both above and below the
exact boundary so the >= contract stays pinned; ColorEditorBuffer is a
placeholder until the picker arrives; ConfigMigrationV20 asserts the
schema stamp and the five v20 field defaults; HoverSheenAlloc drives 100
hovered frames against three constant keys and a final un-hover sweep to
exercise the cleanup branch; HonorificHeaderRender runs one Draw call on
the live component to catch IPC fallback crashes; PerformanceBaseline
prints a JSON line of the IO counters so the cycle notes can pick up a
snapshot. MainWindow gets internal accessors so the probes can reach the
sidebar and honorific header without widening the public surface.
2026-05-23 20:35:01 +02:00
JonKazama-Hellion 576cd6dafd feat(ui): assemble MainWindow from the components layer
Top-level chat window composes HonorificHeader, Sidebar, MessageList,
InputBar and StatusBar in the layout from the master spec: header row,
horizontal body (sidebar + main area with messages + input), status
strip pinned to the bottom. Component types are fully qualified through
the Ui.Components prefix so the v1.5.6 Ui.StatusBar type cannot shadow
the new layer through parent-namespace resolution before it is removed.
Toggle is a new-shadow on Window.Toggle so the open path also writes
Config.MainWindowOpen; OnClose covers the close path through the base
behaviour. InputBar.Height is now public so the layout math can reach
it from outside the components folder.
2026-05-23 19:52:37 +02:00