Files
HellionChat/HellionChat/Ui/Windows/WidgetGalleryWindow.cs
T
JonKazama-Hellion 56a9f3f474 fix(privacy): the header gave away what the log was hiding
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.
2026-08-19 11:42:13 +02:00

487 lines
16 KiB
C#

#if DEBUG
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using HellionChat.Themes;
using HellionChat.Ui.StyleEngine;
using HellionChat.Ui.StyleEngine.Widgets;
namespace HellionChat.Ui.Windows;
// Every widget in Ui/StyleEngine/Widgets shown in its states, so they can be
// checked one at a time before they land in real components. The v2.x style
// engine grew three primitives that were never wired to a call site
// (DrawGlowBorder, DrawSlipPolygon, DrawHonorificHeader) -- this is the cheap
// way to notice that before a cycle closes.
//
// DEBUG-only, like SeStringDebugger: it is a verification aid, not a feature.
internal sealed class WidgetGalleryWindow : Window
{
private readonly Plugin _plugin;
private readonly WidgetPalette _palette;
private int _badgeCount = 3;
private Tab? _headerSample;
private int _headerMode;
private bool _headerLoggedOut;
private bool _rowActive = true;
private bool _toggleA = true;
private bool _toggleB;
internal WidgetGalleryWindow(Plugin plugin, TokenResolver resolver)
: base("Widget Gallery###hellion-widget-gallery")
{
_plugin = plugin;
_palette = new WidgetPalette(resolver);
Size = new Vector2(460, 560);
SizeCondition = ImGuiCond.FirstUseEver;
}
public override void Draw()
{
var c = _plugin.ThemeRegistry.Active.Colors;
ImGui.TextDisabled(
$"GlobalScale {Metrics.Scale:0.00} · hover entries {HoverState.TrackedCount}"
);
ImGui.Separator();
DrawSectionHeaderSection(c);
DrawRowSection(c);
DrawToggleSection(c);
DrawSegmentedSection(c);
DrawSettingRowSection(c);
DrawBadgeSection(c);
DrawPillSection(c);
DrawIconButtonSection(c);
DrawDividerSection(c);
DrawChannelHeaderSection(c);
}
// Unlike every other entry here the header is not a stateless primitive -- it
// needs a tab and the font handles. It is in the gallery anyway: this window
// exists because three style-engine pieces once shipped with no call site at
// all, and a header nobody can look at outside a live chat is exactly how
// that happens again.
private void DrawChannelHeaderSection(ThemeColors c)
{
ImGui.TextUnformatted("ChannelHeader");
var fonts = _plugin.FontManager;
if (fonts is null || !fonts.FontsReady)
{
ImGui.TextDisabled("fonts not ready");
return;
}
_headerSample ??= new Tab { Name = "General", Icon = "comment" };
ImGui.RadioButton("full##hdr", ref _headerMode, 0);
ImGui.SameLine();
ImGui.RadioButton("detail only##hdr", ref _headerMode, 1);
ImGui.SliderFloat("sender weight##hdr", ref FontManager.SenderWeight, 1.0f, 1.6f, "%.2f");
ImGui.SameLine();
if (ImGui.Button("apply##hdr-weight"))
fonts.RebuildDelegateFonts();
var mode = _headerMode == 0 ? ChannelHeaderMode.Full : ChannelHeaderMode.DetailOnly;
// Both detail shapes, because they take different faces: a known world
// rides the meta face, the translated stand-in has to use the body face.
ImGui.Checkbox("logged out##hdr", ref _headerLoggedOut);
var detail = ChannelHeaderDetail.Format(
_headerLoggedOut ? null : "Ravana",
DateTimeOffset.Now,
Plugin.Config.Use24HourClock,
Resources.HellionStrings.ChannelHeader_NotLoggedIn,
Plugin.Config.ScreenshotMode
);
ChannelHeader.Draw(_headerSample, mode, fonts, detail, 0f);
ImGui.Spacing();
}
private void DrawRowSection(ThemeColors c)
{
ImGui.TextUnformatted("Row");
ImGui.Checkbox("active##row", ref _rowActive);
// Clamped: this window demonstrates the guard, it should not be the one
// that asserts when dragged narrow.
var rowSize = HellionChat.Util.WidgetGeometry.IconButton(
ImGui.GetContentRegionAvail().X,
Metrics.SidebarRowHeight
);
var width = rowSize.X;
var height = rowSize.Y;
for (var i = 0; i < 3; i++)
{
var origin = ImGui.GetCursorScreenPos();
var id = ImGui.GetID($"gallery.row.{i}");
ImGui.InvisibleButton($"##gallery-row-{i}", new Vector2(width, height));
var hovered = ImGui.IsItemHovered();
Row.Draw(
origin,
new Vector2(width, height),
new RowVisualState
{
IsActive = _rowActive && i == 1,
HoverAmount = HoverState.Query(id, hovered),
SurfaceHoverAbgr = _palette.Abgr(Token.SurfaceHover, c),
SurfaceActiveAbgr = _palette.Abgr(Token.SurfaceActive, c),
AccentAbgr = _palette.Abgr(Token.AccentPrimary, c),
BorderAbgr = _palette.Abgr(Token.Border, c),
}
);
ImGui
.GetWindowDrawList()
.AddText(
origin + new Vector2(12f * Metrics.Scale, Metrics.CenterY(height)),
_palette.Abgr(Token.Text, c),
$"Row {i}"
);
}
ImGui.Spacing();
}
private void DrawSectionHeaderSection(ThemeColors c)
{
var colors = new SectionHeaderColors
{
TitleAbgr = _palette.Abgr(Token.Text, c),
DescriptionAbgr = _palette.Abgr(Token.TextMuted, c),
AccentAbgr = _palette.Abgr(Token.AccentPrimary, c),
BorderAbgr = _palette.Abgr(Token.Border, c),
HoverAbgr = _palette.Abgr(Token.SurfaceHover, c),
};
if (
SectionHeader.Draw(
ImGui.GetID("gallery.section.open"u8),
"Open by default",
null,
colors,
defaultOpen: true
)
)
ImGui.TextDisabled(" content of the open section");
if (
SectionHeader.Draw(
ImGui.GetID("gallery.section.described"u8),
"With a description",
"Explains what the section groups, wrapped to the available width.",
colors
)
)
ImGui.TextDisabled(" content of the described section");
// Disabled: BeginDisabled only dims ImGui's own widgets, so a draw-list
// header has to fade itself.
using (ImRaii.Disabled())
{
SectionHeader.Draw(
ImGui.GetID("gallery.section.disabled"u8),
"Disabled",
null,
colors,
disabled: true
);
}
ImGui.Spacing();
}
private void DrawToggleSection(ThemeColors c)
{
ImGui.TextUnformatted("Toggle");
var colors = new ToggleSwitchColors
{
TrackOffAbgr = _palette.Abgr(Token.SurfaceRaised, c),
TrackOnAbgr = _palette.Abgr(Token.AccentPrimary, c),
KnobAbgr = _palette.Abgr(Token.Text, c),
BorderAbgr = _palette.Abgr(Token.Border, c),
};
var size = ToggleSwitch.CalcSize();
var gap = 10f * Metrics.Scale;
for (var i = 0; i < 2; i++)
{
var origin = ImGui.GetCursorScreenPos();
var id = ImGui.GetID($"gallery.toggle.{i}");
if (ImGui.InvisibleButton($"##gallery-toggle-{i}", size))
{
if (i == 0)
_toggleA = !_toggleA;
else
_toggleB = !_toggleB;
}
ToggleSwitch.Draw(id, origin, i == 0 ? _toggleA : _toggleB, colors);
if (i == 0)
ImGui.SameLine(0f, gap);
}
ImGui.Spacing();
}
private void DrawSettingRowSection(ThemeColors c)
{
ImGui.TextUnformatted("SettingRow");
var colors = new SettingRowColors
{
LabelAbgr = _palette.Abgr(Token.Text, c),
DescriptionAbgr = _palette.Abgr(Token.TextMuted, c),
SurfaceHoverAbgr = _palette.Abgr(Token.SurfaceHover, c),
BorderAbgr = _palette.Abgr(Token.Border, c),
};
var switchColors = new ToggleSwitchColors
{
TrackOffAbgr = _palette.Abgr(Token.SurfaceRaised, c),
TrackOnAbgr = _palette.Abgr(Token.AccentPrimary, c),
KnobAbgr = _palette.Abgr(Token.Text, c),
};
// The pairing both widgets exist for: switch inside a row, whole label
// half clickable.
var idA = ImGui.GetID("gallery.settingrow.switch");
if (
SettingRow.Draw(
idA,
"A switch in a row",
null,
colors,
ctx =>
{
var size = ToggleSwitch.CalcSize();
var pos = ctx.AlignRight(size);
ImGui.SetCursorScreenPos(pos);
if (ImGui.InvisibleButton("##gallery-sr-switch", size))
_toggleA = !_toggleA;
ToggleSwitch.Draw(idA, pos, _toggleA, switchColors);
}
)
)
_toggleA = !_toggleA;
SettingRow.Draw(
ImGui.GetID("gallery.settingrow.described"),
"With a description",
"The second line wraps rather than being cut, so a real settings "
+ "explanation actually fits into the label column.",
colors,
_ => ImGui.Checkbox("##gallery-sr-b", ref _toggleB)
);
SettingRow.Draw(
ImGui.GetID("gallery.settingrow.long"),
"A deliberately very long label that has to be clipped somewhere",
null,
colors,
_ => ImGui.SliderInt("##gallery-sr-c", ref _badgeCount, 0, 150)
);
// No separator, narrow control column: the variants a settings tab will
// actually reach for.
SettingRow.Draw(
ImGui.GetID("gallery.settingrow.styled"),
"Style override",
null,
colors,
_ => ImGui.SliderInt("##gallery-sr-d", ref _badgeCount, 0, 150),
styleOverride: new SettingRowStyle
{
DrawSeparator = false,
PreferredControlWidth = 90f,
}
);
// Disabled: the label fades, the row stops lighting up, and the click
// does not reach the setting.
using (ImRaii.Disabled())
{
SettingRow.Draw(
ImGui.GetID("gallery.settingrow.disabled"),
"Disabled row",
"Neither the row nor its control may respond.",
colors,
_ => ImGui.SliderInt("##gallery-sr-e", ref _badgeCount, 0, 150),
disabled: true
);
}
ImGui.Spacing();
}
// Segment labels are held rather than built per frame: a collection
// expression in the draw call would allocate a fresh array every frame.
private static readonly string[] SegmentLabelsTwo = ["Sidebar", "Top tabs"];
private static readonly string[] SegmentLabelsThree = ["Off", "Compact", "Full"];
private int _segmentTwo;
private int _segmentThree = 1;
// Held, not rebuilt per frame: a collection expression inside Draw
// allocates a fresh array on every frame the window is open.
private readonly int[] _badgeSamples = [1, 9, 0, 120];
private static readonly FontAwesomeIcon[] GalleryGlyphs =
[
FontAwesomeIcon.ArrowUpRightFromSquare,
FontAwesomeIcon.Check,
FontAwesomeIcon.CheckCircle,
];
private void DrawSegmentedSection(ThemeColors c)
{
var colors = new SegmentedControlColors
{
TrackAbgr = _palette.Abgr(Token.SurfaceBase, c),
SelectedAbgr = _palette.Abgr(Token.AccentPrimary, c),
HoverAbgr = _palette.Abgr(Token.SurfaceHover, c),
LabelAbgr = _palette.Abgr(Token.TextMuted, c),
SelectedLabelAbgr = _palette.Abgr(Token.Text, c),
BorderAbgr = _palette.Abgr(Token.Border, c),
};
var width = MathF.Min(260f * Metrics.Scale, ImGui.GetContentRegionAvail().X);
_segmentTwo = SegmentedControl.Draw(
ImGui.GetID("gallery.segmented.two"),
width,
SegmentLabelsTwo,
_segmentTwo,
colors
);
_segmentThree = SegmentedControl.Draw(
ImGui.GetID("gallery.segmented.three"),
width,
SegmentLabelsThree,
_segmentThree,
colors
);
// Odd width over three segments: the edges must stay flush with no seam
// and no overhang on the right.
SegmentedControl.Draw(
ImGui.GetID("gallery.segmented.odd"),
201f,
SegmentLabelsThree,
0,
colors,
disabled: true
);
ImGui.Spacing();
}
private void DrawBadgeSection(ThemeColors c)
{
ImGui.TextUnformatted("Badge");
ImGui.SliderInt("count##badge", ref _badgeCount, 0, 150);
_badgeSamples[2] = _badgeCount;
var samples = _badgeSamples;
var origin = ImGui.GetCursorScreenPos();
var x = origin.X;
foreach (var n in samples)
{
Badge.Draw(
new Vector2(x, origin.Y),
n,
_palette.Abgr(Token.AccentEmber, c),
_palette.Abgr(Token.Text, c)
);
x += Badge.CalcSize(n).X + 8f * Metrics.Scale;
}
ImGui.Dummy(new Vector2(0, Badge.CalcSize(1).Y + 8f * Metrics.Scale));
ImGui.Spacing();
}
private void DrawPillSection(ThemeColors c)
{
ImGui.TextUnformatted("Pill");
var origin = ImGui.GetCursorScreenPos();
var x = origin.X;
Pill.Draw(
new Vector2(x, origin.Y),
"filled",
_palette.Abgr(Token.SurfaceRaised, c),
_palette.Abgr(Token.Text, c)
);
x += Pill.CalcSize("filled", withDot: false).X + 8f * Metrics.Scale;
Pill.Draw(
new Vector2(x, origin.Y),
"with dot",
_palette.Abgr(Token.SurfaceRaised, c),
_palette.Abgr(Token.Text, c),
_palette.Abgr(Token.AccentPrimary, c)
);
x += Pill.CalcSize("with dot", withDot: true).X + 8f * Metrics.Scale;
Pill.Draw(
new Vector2(x, origin.Y),
"outlined",
_palette.Abgr(Token.AccentPrimary, c),
_palette.Abgr(Token.TextMuted, c),
styleOverride: new PillStyle { Outlined = true }
);
ImGui.Dummy(new Vector2(0, Pill.CalcSize("x", false).Y + 8f * Metrics.Scale));
ImGui.Spacing();
}
private void DrawIconButtonSection(ThemeColors c)
{
ImGui.TextUnformatted("IconButton");
var size = new Vector2(Metrics.SidebarPopOutHitWidth, Metrics.SidebarRowHeight);
var glyphs = GalleryGlyphs;
for (var i = 0; i < glyphs.Length; i++)
{
if (i > 0)
ImGui.SameLine();
IconButton.Draw(
ImGui.GetID($"gallery.icon.{i}"),
size,
glyphs[i],
_palette.Abgr(Token.TextMuted, c),
_palette.Abgr(Token.SurfaceHover, c),
_plugin.FontManager.FontAwesome
);
}
ImGui.Spacing();
}
private void DrawDividerSection(ThemeColors c)
{
ImGui.TextUnformatted("LineDivider");
LineDivider.Draw(
"Section (2)",
_palette.Abgr(Token.Border, c),
_palette.Abgr(Token.TextMuted, c)
);
LineDivider.Draw(null, _palette.Abgr(Token.Border, c), _palette.Abgr(Token.TextMuted, c));
}
}
#endif