Screenshot mode anonymises sender names in the message list. The channel header I added yesterday sat above that list and showed two things it should not. The home world, on the right. It narrows a player down almost as far as the character name does, and the mode exists so a picture can be shared. Worse, the tab name on the left. AutoTellTabsService builds a tell tab's name as "Player@World", so a tell conversation had the partner's name and world set in tracked caps directly above a log where every message had been anonymised. The one place a reader looks first was the one place still naming them. The name is suppressed only where it actually names someone -- a tab with a tell target set. General or Trade stay readable, because they identify nobody, and a self-named tab is the user's own text. Found by asking what the new surface shows rather than by a test failing. Nothing here was failing.
232 lines
8.9 KiB
C#
232 lines
8.9 KiB
C#
using System.Numerics;
|
|
using Dalamud.Bindings.ImGui;
|
|
using Dalamud.Interface;
|
|
using Dalamud.Interface.ManagedFontAtlas;
|
|
using HellionChat.Util;
|
|
|
|
namespace HellionChat.Ui.StyleEngine.Widgets;
|
|
|
|
// The band above the conversation: which channel you are in on the left, where
|
|
// you are and what time it is on the right.
|
|
//
|
|
// Set apart by small caps with wide tracking rather than by size. That is a
|
|
// deliberate departure from the mockup, which asks for one pixel smaller than
|
|
// body text: one pixel would cost a whole additional font handle at full glyph
|
|
// range, because tab names are free user input and can be CJK. Tracking carries
|
|
// the same weight in every palette and costs nothing.
|
|
//
|
|
// Which face draws what is not a style choice here, it is a constraint. The meta
|
|
// face has a glyph range of ASCII plus a middle dot, so only the world name and
|
|
// the clock can use it. The tab name and the translated "no world" stand-in go
|
|
// through the body face, or they come out as rows of question marks.
|
|
internal static class ChannelHeader
|
|
{
|
|
private const float InsetRaw = 14f;
|
|
private const float PadYRaw = 7f;
|
|
private const float TrackRaw = 1.8f;
|
|
private const float DetailTrackRaw = 0.9f;
|
|
private const float IconGapRaw = 8f;
|
|
|
|
// Measured, never a fixed 32px: the band has to hold a line of text, and the
|
|
// font comes from Config, which display scaling does not feed into.
|
|
internal static float Height =>
|
|
ImGui.GetTextLineHeight() + MathF.Round(PadYRaw * 2f * Metrics.Scale);
|
|
|
|
// The body face follows the same switch every other push site follows. Two
|
|
// settings decide it, and reading only one of them is how a window ends up
|
|
// half in the game font and half in the bundled one.
|
|
private static IFontHandle BodyFace(FontManager fonts) =>
|
|
Plugin.Config.FontsEnabled || Plugin.Config.UseHellionFont
|
|
? fonts.RegularFont!
|
|
: fonts.Axis;
|
|
|
|
// Same switch for the meta face. With the game font selected there is no
|
|
// stepped-down variant to fall back to, so the size distinction is simply
|
|
// dropped -- the same honest limitation the sender weight has.
|
|
private static IFontHandle MetaFace(FontManager fonts) =>
|
|
Plugin.Config.FontsEnabled || Plugin.Config.UseHellionFont ? fonts.MetaFont! : fonts.Axis;
|
|
|
|
internal static void Draw(
|
|
Tab tab,
|
|
ChannelHeaderMode mode,
|
|
FontManager fonts,
|
|
ChannelHeaderDetailParts detail,
|
|
float reservedBelow
|
|
)
|
|
{
|
|
var scale = Metrics.Scale;
|
|
var width = ImGui.GetContentRegionAvail().X;
|
|
var height = Height;
|
|
var origin = ImGui.GetCursorScreenPos();
|
|
|
|
var theme = Plugin.Instance.ThemeRegistry.Active;
|
|
var surface = theme.Colors.Surface;
|
|
|
|
var body = BodyFace(fonts);
|
|
var meta = MetaFace(fonts);
|
|
|
|
var icon = Components.Sidebar.ResolveTabIcon(tab);
|
|
var track = TrackRaw * scale;
|
|
var detailTrack = DetailTrackRaw * scale;
|
|
// Screenshot mode hides the name of a tell tab, because that name IS the
|
|
// conversation partner: AutoTellTabsService builds it as "Player@World".
|
|
// Drawing it in tracked caps above a log whose messages are anonymised
|
|
// would give away in the header exactly what the log is hiding.
|
|
var namesAPartner = tab.TellTarget?.IsSet() == true;
|
|
var showName =
|
|
mode is ChannelHeaderMode.Full && !(Plugin.Config.ScreenshotMode && namesAPartner);
|
|
|
|
// ToUpperInvariant allocates, so only where the name is actually drawn.
|
|
var name = showName ? tab.Name.ToUpperInvariant() : string.Empty;
|
|
|
|
Vector2 iconSize;
|
|
using (fonts.FontAwesome.Push())
|
|
iconSize = ImGui.CalcTextSize(icon.ToIconString());
|
|
|
|
var nameRun = 0f;
|
|
if (showName)
|
|
{
|
|
using (body.Push())
|
|
nameRun = DrawListExtensions.MeasureTrackedText(name, track);
|
|
nameRun += iconSize.X + IconGapRaw * scale;
|
|
}
|
|
|
|
// Measured under the face that will draw it, which is the whole point of
|
|
// splitting the detail in two: the stand-in is translated and the meta
|
|
// face cannot render most of those translations.
|
|
float whereRun;
|
|
using ((detail.WhereIsTranslated ? body : meta).Push())
|
|
whereRun = DrawListExtensions.MeasureTrackedText(detail.Where, detailTrack);
|
|
|
|
// No separator with nothing in front of it -- screenshot mode leaves the
|
|
// clock standing alone.
|
|
var rest =
|
|
detail.Where.Length == 0
|
|
? detail.Clock
|
|
: ChannelHeaderDetailParts.Separator + detail.Clock;
|
|
|
|
float restRun;
|
|
using (meta.Push())
|
|
restRun = DrawListExtensions.MeasureTrackedText(rest, detailTrack);
|
|
|
|
var detailRun = whereRun + restRun;
|
|
var inset = InsetRaw * scale;
|
|
|
|
var plan = ChannelHeaderLayout.Plan(
|
|
showName ? ChannelHeaderMode.Full : ChannelHeaderMode.DetailOnly,
|
|
width - inset * 2f,
|
|
ImGui.GetContentRegionAvail().Y - height - reservedBelow,
|
|
nameRun,
|
|
detailRun,
|
|
scale
|
|
);
|
|
|
|
if (!plan.ShowHeader)
|
|
return;
|
|
|
|
var dl = ImGui.GetWindowDrawList();
|
|
var bottomRight = origin + new Vector2(width, height);
|
|
dl.AddRectFilled(origin, bottomRight, ColourUtil.RgbaToAbgr(surface));
|
|
dl.AddLine(
|
|
new Vector2(origin.X, bottomRight.Y - 1f),
|
|
new Vector2(bottomRight.X, bottomRight.Y - 1f),
|
|
ColourUtil.RgbaToAbgr(theme.Colors.Border),
|
|
MathF.Max(1f, scale)
|
|
);
|
|
|
|
var textY = origin.Y + MathF.Round(PadYRaw * scale);
|
|
|
|
if (plan.ShowName)
|
|
{
|
|
var accent = ColourUtil.RgbaToAbgr(
|
|
ColourUtil.EnsureContrast(theme.Colors.Accent, surface, 4.5f)
|
|
);
|
|
var x = origin.X + inset;
|
|
|
|
// Centred against the band, not aligned to the text baseline:
|
|
// FontAwesome is a fixed-width handle built at Dalamud's own size and
|
|
// does not follow Config.FontSizeV2, so the text line height would
|
|
// misplace it at any other body size. Same reasoning as the sidebar.
|
|
using (fonts.FontAwesome.Push())
|
|
dl.AddText(
|
|
new Vector2(x, origin.Y + MetricsMath.CenterY(height, iconSize.Y)),
|
|
accent,
|
|
icon.ToIconString()
|
|
);
|
|
|
|
x += iconSize.X + IconGapRaw * scale;
|
|
|
|
using (body.Push())
|
|
dl.DrawTrackedText(new Vector2(x, textY), name, accent, track);
|
|
}
|
|
|
|
if (plan.ShowDetail)
|
|
{
|
|
var muted = ColourUtil.RgbaToAbgr(
|
|
ColourUtil.EnsureContrast(theme.Colors.TextMuted, surface, 4.5f)
|
|
);
|
|
|
|
var whereFace = detail.WhereIsTranslated ? body : meta;
|
|
var x = bottomRight.X - inset - detailRun;
|
|
|
|
using (whereFace.Push())
|
|
dl.DrawTrackedText(
|
|
new Vector2(x, textY + DropFor(body, whereFace, scale)),
|
|
detail.Where,
|
|
muted,
|
|
detailTrack
|
|
);
|
|
|
|
x += whereRun;
|
|
|
|
using (meta.Push())
|
|
dl.DrawTrackedText(
|
|
new Vector2(x, textY + DropFor(body, meta, scale)),
|
|
rest,
|
|
muted,
|
|
detailTrack
|
|
);
|
|
}
|
|
|
|
// ItemSize rather than a cursor move: it advances the cursor AND extends
|
|
// CursorMaxPos, which is what the enclosing layout measures.
|
|
ImGui.SetCursorScreenPos(origin);
|
|
ImGuiP.ItemSize(new Vector2(width, height - ImGui.GetStyle().ItemSpacing.Y));
|
|
}
|
|
|
|
internal static ChannelHeaderDetailParts CurrentDetail()
|
|
{
|
|
// IsValid guards the row reference, but the case that actually happens is
|
|
// subtler: logged out resolves to row zero, which exists and carries an
|
|
// empty name. Format treats blank as missing, which covers both.
|
|
var world = Plugin.PlayerState.HomeWorld.IsValid
|
|
? Plugin.PlayerState.HomeWorld.Value.Name.ExtractText()
|
|
: null;
|
|
|
|
return ChannelHeaderDetail.Format(
|
|
world,
|
|
DateTimeOffset.Now,
|
|
Plugin.Config.Use24HourClock,
|
|
Resources.HellionStrings.ChannelHeader_NotLoggedIn,
|
|
Plugin.Config.ScreenshotMode
|
|
);
|
|
}
|
|
|
|
// Zero whenever both runs use the same handle, which is the common case.
|
|
private static float DropFor(IFontHandle body, IFontHandle other, float scale)
|
|
{
|
|
if (ReferenceEquals(body, other))
|
|
return 0f;
|
|
|
|
float bodyAscent;
|
|
using (body.Push())
|
|
bodyAscent = ImGui.GetFont().Ascent;
|
|
|
|
float otherAscent;
|
|
using (other.Push())
|
|
otherAscent = ImGui.GetFont().Ascent;
|
|
|
|
return BaselineMath.OffsetFor(bodyAscent, otherAscent, scale);
|
|
}
|
|
}
|