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.
487 lines
16 KiB
C#
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
|