Codes like POP-1c or B4b-2 name a task in a planning document, not anything in the code. A reader has no way to resolve them and they age into noise the moment the document is closed. Where a code was used as a reference, the sentence now names the function it meant.
190 lines
7.0 KiB
C#
190 lines
7.0 KiB
C#
using System.Numerics;
|
|
using Dalamud.Bindings.ImGui;
|
|
using HellionChat.Themes;
|
|
using HellionChat.Ui.StyleEngine;
|
|
using HellionChat.Ui.StyleEngine.Widgets;
|
|
using HellionChat.Util;
|
|
|
|
namespace HellionChat.Ui.Components;
|
|
|
|
// Horizontal tab strip — the alternative MainWindow layout to the Sidebar.
|
|
// Selection drives the same shared EnsureCurrentChannel path; pop-out is the
|
|
// same pool.TryOpen affordance as the sidebar (right-click context menu).
|
|
internal sealed class TopTabBar
|
|
{
|
|
private readonly Windows.ChannelPopoutPool _pool;
|
|
private readonly ThemeRegistry _themes;
|
|
private readonly WidgetPalette _palette;
|
|
|
|
// Slightly smaller than the sidebar default: the strip is horizontal, so
|
|
// every pixel of badge width costs a tab.
|
|
private static readonly BadgeStyle TabBadge = new() { Height = 12f, PaddingX = 3f };
|
|
|
|
// Render observability. At most one underline per frame, except in the frame
|
|
// a click lands: a tab drawn before the clicked one was still the active tab
|
|
// when it was painted, so that frame legitimately shows two.
|
|
internal int LastRenderedUnderlineCount { get; private set; }
|
|
|
|
public TopTabBar(Windows.ChannelPopoutPool pool, ThemeRegistry themes, TokenResolver resolver)
|
|
{
|
|
_pool = pool;
|
|
_themes = themes;
|
|
_palette = new WidgetPalette(resolver);
|
|
}
|
|
|
|
public void Draw(IReadOnlyList<Tab> tabs, ref Tab? activeTab)
|
|
{
|
|
LastRenderedUnderlineCount = 0;
|
|
|
|
var colors = _themes.Active.Colors;
|
|
var surfaceActive = _palette.Abgr(Token.SurfaceActive, colors);
|
|
var surfaceHover = _palette.Abgr(Token.SurfaceHover, colors);
|
|
var accent = _palette.Abgr(Token.AccentPrimary, colors);
|
|
var textAbgr = _palette.Abgr(Token.Text, colors);
|
|
var mutedAbgr = _palette.Abgr(Token.TextMuted, colors);
|
|
var borderAbgr = _palette.Abgr(Token.Border, colors);
|
|
|
|
var height = Metrics.TopTabHeight;
|
|
var padX = Metrics.TopTabPaddingX;
|
|
var dl = ImGui.GetWindowDrawList();
|
|
|
|
var firstDrawn = true;
|
|
for (var i = 0; i < tabs.Count; i++)
|
|
{
|
|
var tab = tabs[i];
|
|
// A popped-out tab is owned by its pop-out window, not the main
|
|
// strip (1.5.6 exclusivity). Gate on the pool, not Tab.PopOut (stale flag).
|
|
if (_pool.IsOpen(tab.Identifier))
|
|
continue;
|
|
|
|
if (!firstDrawn)
|
|
ImGui.SameLine();
|
|
firstDrawn = false;
|
|
|
|
var selected = ReferenceEquals(tab, activeTab);
|
|
var origin = ImGui.GetCursorScreenPos();
|
|
|
|
// The badge has to be part of the width, not painted over it. Padding
|
|
// alone is 10px and the badge is at least 14 wide, so placing it in
|
|
// the trailing padding covered the label on every tab that had one.
|
|
var showUnread =
|
|
!ReferenceEquals(tab, activeTab)
|
|
&& tab.UnreadMode != UnreadMode.None
|
|
&& tab.Unread > 0;
|
|
// Resolved once: measuring one string and drawing another would size
|
|
// every tab wrong the moment screenshot mode is on.
|
|
var label = Util.TabDisplayName.Resolve(
|
|
tab.Name,
|
|
tab.NameCameFromPartner,
|
|
Plugin.Config.ScreenshotMode
|
|
);
|
|
|
|
var unread = showUnread ? (int)Math.Min(tab.Unread, int.MaxValue) : 0;
|
|
var badgeSize = showUnread ? Badge.CalcSize(unread, TabBadge) : Vector2.Zero;
|
|
|
|
var width =
|
|
ImGui.CalcTextSize(label).X
|
|
+ padX * 2f
|
|
+ (showUnread ? badgeSize.X + Metrics.TopTabUnreadInset : 0f);
|
|
var size = WidgetGeometry.IconButton(width, height);
|
|
|
|
// The ### keeps the ImGui id stable across a rename; without it the
|
|
// context menu loses its binding the moment the label changes. Built
|
|
// once and reused for the hover key, since GetID hashes the same
|
|
// string the button registers under.
|
|
var buttonId = $"###hellion_toptab_{tab.Identifier}";
|
|
var hoverId = ImGui.GetID(buttonId);
|
|
var pressed = ImGui.InvisibleButton(buttonId, size);
|
|
var hovered = ImGui.IsItemHovered();
|
|
var hoverAmount = HoverState.Query(hoverId, hovered);
|
|
|
|
if (pressed)
|
|
{
|
|
var previous = activeTab;
|
|
activeTab = tab;
|
|
TabLifecycleHelpers.OnTabActivated(tab, previous);
|
|
selected = true;
|
|
}
|
|
|
|
DrawTab(
|
|
dl,
|
|
origin,
|
|
size,
|
|
label,
|
|
selected,
|
|
hoverAmount,
|
|
surfaceActive,
|
|
surfaceHover,
|
|
accent,
|
|
selected || hovered ? textAbgr : mutedAbgr,
|
|
padX
|
|
);
|
|
|
|
if (selected)
|
|
LastRenderedUnderlineCount++;
|
|
|
|
// Clicking a tab clears its marker in the same frame, like the
|
|
// sidebar: showUnread was resolved before the click was handled, so
|
|
// re-check against the post-click selection.
|
|
if (showUnread && !ReferenceEquals(tab, activeTab))
|
|
Badge.Draw(
|
|
new Vector2(
|
|
origin.X + size.X - badgeSize.X - Metrics.TopTabUnreadInset,
|
|
origin.Y + MetricsMath.CenterY(size.Y, badgeSize.Y)
|
|
),
|
|
unread,
|
|
_palette.Abgr(Token.AccentEmber, colors),
|
|
textAbgr,
|
|
TabBadge
|
|
);
|
|
|
|
TabContextMenu.Draw(tab, $"toptab_ctx_{tab.Identifier}", _pool);
|
|
}
|
|
|
|
LineDivider.Draw(null, borderAbgr, mutedAbgr);
|
|
}
|
|
|
|
// Keeps the fill Selectable used to provide (ImGuiCol.Header) and adds the
|
|
// underline on top. Dropping the fill for the underline alone would make the
|
|
// active tab harder to spot, not easier.
|
|
private static void DrawTab(
|
|
ImDrawListPtr dl,
|
|
Vector2 origin,
|
|
Vector2 size,
|
|
string label,
|
|
bool selected,
|
|
float hoverAmount,
|
|
uint surfaceActive,
|
|
uint surfaceHover,
|
|
uint accent,
|
|
uint labelAbgr,
|
|
float padX
|
|
)
|
|
{
|
|
var max = origin + size;
|
|
|
|
if (selected)
|
|
dl.AddRectFilled(origin, max, surfaceActive);
|
|
|
|
if (hoverAmount > 0f)
|
|
{
|
|
var a = (uint)
|
|
Math.Clamp(MathF.Round(((surfaceHover >> 24) & 0xFF) * hoverAmount), 0f, 255f);
|
|
dl.AddRectFilled(origin, max, (surfaceHover & 0x00FFFFFFu) | (a << 24));
|
|
}
|
|
|
|
if (selected)
|
|
{
|
|
var thickness = Metrics.TopTabUnderline;
|
|
dl.AddRectFilled(new Vector2(origin.X, max.Y - thickness), max, accent);
|
|
}
|
|
|
|
var textSize = ImGui.CalcTextSize(label);
|
|
dl.AddText(
|
|
new Vector2(origin.X + padX, origin.Y + MetricsMath.CenterY(size.Y, textSize.Y)),
|
|
labelAbgr,
|
|
label
|
|
);
|
|
}
|
|
}
|