diff --git a/HellionChat/Resources/HellionStrings.ca.resx b/HellionChat/Resources/HellionStrings.ca.resx
index 1cedf9c..bc4c079 100644
--- a/HellionChat/Resources/HellionStrings.ca.resx
+++ b/HellionChat/Resources/HellionStrings.ca.resx
@@ -1359,6 +1359,6 @@
obert
- Sense sessió
+ Sessió no iniciada
\ No newline at end of file
diff --git a/HellionChat/Resources/HellionStrings.es.resx b/HellionChat/Resources/HellionStrings.es.resx
index 1a867ff..e274935 100644
--- a/HellionChat/Resources/HellionStrings.es.resx
+++ b/HellionChat/Resources/HellionStrings.es.resx
@@ -1359,6 +1359,6 @@
abierto
- Sin sesión
+ Sesión no iniciada
\ No newline at end of file
diff --git a/HellionChat/Resources/HellionStrings.fi.resx b/HellionChat/Resources/HellionStrings.fi.resx
index fc6cbc9..ec8548c 100644
--- a/HellionChat/Resources/HellionStrings.fi.resx
+++ b/HellionChat/Resources/HellionStrings.fi.resx
@@ -1358,6 +1358,6 @@
auki
- Ei kirjautunut sisään
+ Ei kirjautuneena
\ No newline at end of file
diff --git a/HellionChat/Resources/HellionStrings.ko.resx b/HellionChat/Resources/HellionStrings.ko.resx
index 8be4681..8bf133b 100644
--- a/HellionChat/Resources/HellionStrings.ko.resx
+++ b/HellionChat/Resources/HellionStrings.ko.resx
@@ -1359,6 +1359,6 @@
열림
- 로그인하지 않음
+ 로그인되지 않음
\ No newline at end of file
diff --git a/HellionChat/Resources/HellionStrings.ru.resx b/HellionChat/Resources/HellionStrings.ru.resx
index 8cb69a7..d7da149 100644
--- a/HellionChat/Resources/HellionStrings.ru.resx
+++ b/HellionChat/Resources/HellionStrings.ru.resx
@@ -1359,6 +1359,6 @@
открыт
- Не выполнен вход
+ Вход не выполнен
\ No newline at end of file
diff --git a/HellionChat/SelfTests/TypeScaleStep.cs b/HellionChat/SelfTests/TypeScaleStep.cs
index 1456f97..87c8ed3 100644
--- a/HellionChat/SelfTests/TypeScaleStep.cs
+++ b/HellionChat/SelfTests/TypeScaleStep.cs
@@ -35,10 +35,14 @@ internal sealed class TypeScaleStep : ISelfTestStep
return SelfTestStepResult.Fail;
}
+ // Waiting, not Fail. The atlas rebuild this step's own weight buttons
+ // trigger is asynchronous, so a Fail here would make the step destroy
+ // itself the moment it is used as intended. Same guard as
+ // AboutIntegrationsStatusStep and HonorificHeaderRenderStep.
if (!fm.FontsReady)
{
- ImGui.Text("FontsReady is false - atlas still building, run again in a moment");
- return SelfTestStepResult.Fail;
+ ImGui.Text("Atlas still building - the step resumes on its own");
+ return SelfTestStepResult.Waiting;
}
if (fm.SenderFont is null || fm.MetaFont is null || fm.RegularFont is null)
@@ -70,11 +74,16 @@ internal sealed class TypeScaleStep : ISelfTestStep
+ "Pick one and watch the row above change."
);
- // Three builds would be the alternative, and nobody compares a face
- // across a restart. RebuildDelegateFonts runs synchronously on this
- // thread -- self-test steps are on the draw thread, same as every push
- // site -- so the sample row redraws in the next frame with the new
- // rasterisation.
+ // Three builds would be the alternative, and nobody compares a typeface
+ // across a plugin restart.
+ //
+ // The rebuild is asynchronous -- it goes through
+ // Framework.RunOnFrameworkThread and BuildFontsAsync -- so FontsReady
+ // drops to false for a moment after each click and the step reports
+ // Waiting until the atlas is back. It does NOT show the three weights
+ // side by side, which is what the plan asked for; one handle can only
+ // carry one weight, and three throwaway handles for a self-test is a
+ // worse trade than clicking through them.
DrawWeightPicker(fm, 1.2f);
ImGui.SameLine();
DrawWeightPicker(fm, 1.3f);
diff --git a/HellionChat/Ui/StyleEngine/Widgets/ChannelHeader.cs b/HellionChat/Ui/StyleEngine/Widgets/ChannelHeader.cs
index 935be04..c773066 100644
--- a/HellionChat/Ui/StyleEngine/Widgets/ChannelHeader.cs
+++ b/HellionChat/Ui/StyleEngine/Widgets/ChannelHeader.cs
@@ -1,6 +1,7 @@
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
+using Dalamud.Interface.ManagedFontAtlas;
using HellionChat.Util;
namespace HellionChat.Ui.StyleEngine.Widgets;
@@ -14,8 +15,10 @@ namespace HellionChat.Ui.StyleEngine.Widgets;
// range, because tab names are free user input and can be CJK. Tracking carries
// the same weight in every palette and costs nothing.
//
-// The tab name draws in the body face for the same reason. Only the world and the
-// clock use the meta face, whose glyph range is ASCII plus a middle dot.
+// 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;
@@ -29,7 +32,27 @@ internal static class ChannelHeader
internal static float Height =>
ImGui.GetTextLineHeight() + MathF.Round(PadYRaw * 2f * Metrics.Scale);
- internal static void Draw(Tab tab, ChannelHeaderMode mode, FontManager fonts, string detail)
+ // 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;
@@ -39,30 +62,52 @@ internal static class ChannelHeader
var theme = Plugin.Instance.ThemeRegistry.Active;
var surface = theme.Colors.Surface;
- var name = tab.Name.ToUpperInvariant();
+ var body = BodyFace(fonts);
+ var meta = MetaFace(fonts);
+
+ var icon = Components.Sidebar.ResolveTabIcon(tab);
var track = TrackRaw * scale;
var detailTrack = DetailTrackRaw * scale;
+ var showName = mode is ChannelHeaderMode.Full;
- // Measure before deciding, the way the status bar does. Icon and gap are
- // part of the name run because they are dropped together with it.
- float nameRun;
- using (fonts.RegularFont!.Push())
- nameRun = DrawListExtensions.MeasureTrackedText(name, track);
+ // ToUpperInvariant allocates, so only where the name is actually drawn.
+ var name = showName ? tab.Name.ToUpperInvariant() : string.Empty;
- var iconWidth = MeasureIcon(fonts, Components.Sidebar.ResolveTabIcon(tab));
- nameRun += iconWidth + IconGapRaw * scale;
+ Vector2 iconSize;
+ using (fonts.FontAwesome.Push())
+ iconSize = ImGui.CalcTextSize(icon.ToIconString());
- float detailRun;
- using (fonts.MetaFont!.Push())
- detailRun = DrawListExtensions.MeasureTrackedText(detail, detailTrack);
+ 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);
+
+ var rest = 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(
mode,
width - inset * 2f,
- ImGui.GetContentRegionAvail().Y - height,
+ ImGui.GetContentRegionAvail().Y - height - reservedBelow,
nameRun,
- detailRun
+ detailRun,
+ scale
);
if (!plan.ShowHeader)
@@ -87,14 +132,20 @@ internal static class ChannelHeader
);
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())
- {
- var glyph = Components.Sidebar.ResolveTabIcon(tab).ToIconString();
- dl.AddText(new Vector2(x, textY), accent, glyph);
- x += ImGui.CalcTextSize(glyph).X + IconGapRaw * scale;
- }
+ dl.AddText(
+ new Vector2(x, origin.Y + MetricsMath.CenterY(height, iconSize.Y)),
+ accent,
+ icon.ToIconString()
+ );
- using (fonts.RegularFont.Push())
+ x += iconSize.X + IconGapRaw * scale;
+
+ using (body.Push())
dl.DrawTrackedText(new Vector2(x, textY), name, accent, track);
}
@@ -104,22 +155,23 @@ internal static class ChannelHeader
ColourUtil.EnsureContrast(theme.Colors.TextMuted, surface, 4.5f)
);
- // The meta face is smaller, so it would hang from the top edge
- // without the drop -- ImGui aligns by top, not by baseline.
- float bodyAscent;
- using (fonts.RegularFont.Push())
- bodyAscent = ImGui.GetFont().Ascent;
+ var whereFace = detail.WhereIsTranslated ? body : meta;
+ var x = bottomRight.X - inset - detailRun;
- float metaAscent;
- using (fonts.MetaFont.Push())
- metaAscent = ImGui.GetFont().Ascent;
-
- var drop = BaselineMath.OffsetFor(bodyAscent, metaAscent, scale);
-
- using (fonts.MetaFont.Push())
+ using (whereFace.Push())
dl.DrawTrackedText(
- new Vector2(bottomRight.X - inset - detailRun, textY + drop),
- detail,
+ 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
);
@@ -131,12 +183,11 @@ internal static class ChannelHeader
ImGuiP.ItemSize(new Vector2(width, height - ImGui.GetStyle().ItemSpacing.Y));
}
- // Both windows need the same string, and only one of them should know how to
- // get at the world. IsValid is the guard the payload handler already uses --
- // HomeWorld is a row reference that stays unresolved until a character is
- // actually logged in.
- internal static string CurrentDetail()
+ 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;
@@ -149,9 +200,20 @@ internal static class ChannelHeader
);
}
- private static float MeasureIcon(FontManager fonts, FontAwesomeIcon icon)
+ // Zero whenever both runs use the same handle, which is the common case.
+ private static float DropFor(IFontHandle body, IFontHandle other, float scale)
{
- using (fonts.FontAwesome.Push())
- return ImGui.CalcTextSize(icon.ToIconString()).X;
+ 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);
}
}
diff --git a/HellionChat/Ui/StyleEngine/Widgets/ChannelHeaderDetail.cs b/HellionChat/Ui/StyleEngine/Widgets/ChannelHeaderDetail.cs
index 765f1b5..e2b6c3c 100644
--- a/HellionChat/Ui/StyleEngine/Widgets/ChannelHeaderDetail.cs
+++ b/HellionChat/Ui/StyleEngine/Widgets/ChannelHeaderDetail.cs
@@ -2,33 +2,43 @@ using System.Globalization;
namespace HellionChat.Ui.StyleEngine.Widgets;
-// TEST-MIRROR: Ui/ChannelHeaderDetailTests.cs
+// Where you are and what time it is, as two parts rather than one string.
//
-// The trailing half of the channel header: where you are and what time it is.
+// They are split because they cannot share a face. The clock and a world name are
+// Latin in every client, which is why the meta face gets away with a glyph range
+// of ASCII plus a middle dot. The stand-in for "no world known" is translated
+// into 25 languages, and fifteen of those reach outside that range -- drawn in the
+// meta face they would come out as rows of question marks. So the caller draws
+// WhereIsTranslated text in the body face and the rest in the meta face.
//
// The clock follows the same Use24HourClock setting the message timestamps do.
-// Two clock formats in one window would be a defect rather than a preference, and
-// the header sits directly above a column of timestamps.
-//
-// Culture is pinned for the same reason it is pinned in the message list: a
-// German machine renders "PM" as "nachm." under its own culture, and the two
-// would then disagree on the same screen.
-internal static class ChannelHeaderDetail
+// 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: a German machine
+// renders "PM" as "nachm." under its own culture.
+internal readonly record struct ChannelHeaderDetailParts(
+ string Where,
+ string Clock,
+ bool WhereIsTranslated
+)
{
internal const string Separator = " · ";
+}
- internal static string Format(
+internal static class ChannelHeaderDetail
+{
+ internal static ChannelHeaderDetailParts Format(
string? world,
DateTimeOffset now,
bool use24Hour,
string fallback
)
{
- var where = string.IsNullOrWhiteSpace(world) ? fallback : world;
+ var missing = string.IsNullOrWhiteSpace(world);
var clock = use24Hour
? now.ToString("HH:mm", CultureInfo.InvariantCulture)
: now.ToString("h:mm tt", CultureInfo.InvariantCulture);
- return string.Concat(where, Separator, clock);
+ return new ChannelHeaderDetailParts(missing ? fallback : world!, clock, missing);
}
}
diff --git a/HellionChat/Ui/StyleEngine/Widgets/ChannelHeaderLayout.cs b/HellionChat/Ui/StyleEngine/Widgets/ChannelHeaderLayout.cs
index 3e6a302..314235c 100644
--- a/HellionChat/Ui/StyleEngine/Widgets/ChannelHeaderLayout.cs
+++ b/HellionChat/Ui/StyleEngine/Widgets/ChannelHeaderLayout.cs
@@ -29,21 +29,31 @@ internal static class ChannelHeaderLayout
{
// Below this the message area stops being a conversation and starts being a
// peephole. Four body lines at a typical scale.
- internal const float MinMessageAreaHeight = 90f;
+ //
+ // Raw, like every other layout value in the project: the caller passes the
+ // display scale so this stays arithmetic. Comparing an unscaled 90 against
+ // scaled pixels would dissolve the threshold as the scale goes up -- at 200%
+ // it would be worth 45 logical pixels.
+ internal const float MinMessageAreaHeightRaw = 90f;
+
+ // Keeps the name and the trailing detail from touching at the exact pixel
+ // where they both still "fit".
+ internal const float MinGapRaw = 12f;
internal static ChannelHeaderPlan Plan(
ChannelHeaderMode mode,
float availableWidth,
float availableHeight,
float nameWidth,
- float detailWidth
+ float detailWidth,
+ float scale
)
{
- if (availableHeight < MinMessageAreaHeight)
+ if (availableHeight < MinMessageAreaHeightRaw * scale)
return new ChannelHeaderPlan(false, false, false);
var showName = mode is ChannelHeaderMode.Full;
- var used = showName ? nameWidth : 0f;
+ var used = showName ? nameWidth + MinGapRaw * scale : 0f;
var showDetail = used + detailWidth <= availableWidth;
return new ChannelHeaderPlan(true, showName, showDetail);
diff --git a/HellionChat/Ui/Windows/ChannelPopoutWindow.cs b/HellionChat/Ui/Windows/ChannelPopoutWindow.cs
index d0ffa97..b82dcf0 100644
--- a/HellionChat/Ui/Windows/ChannelPopoutWindow.cs
+++ b/HellionChat/Ui/Windows/ChannelPopoutWindow.cs
@@ -138,7 +138,8 @@ internal sealed class ChannelPopoutWindow : Window, IFocusableChatWindow
? StyleEngine.Widgets.ChannelHeaderMode.DetailOnly
: StyleEngine.Widgets.ChannelHeaderMode.Full,
Plugin.Instance.FontManager,
- StyleEngine.Widgets.ChannelHeader.CurrentDetail()
+ StyleEngine.Widgets.ChannelHeader.CurrentDetail(),
+ InputBar.Height
);
// The header close button can unbind us mid-frame (CloseRequested ->
diff --git a/HellionChat/Ui/Windows/MainWindow.cs b/HellionChat/Ui/Windows/MainWindow.cs
index 909579f..b496c3e 100644
--- a/HellionChat/Ui/Windows/MainWindow.cs
+++ b/HellionChat/Ui/Windows/MainWindow.cs
@@ -400,11 +400,15 @@ internal sealed class MainWindow : Window, IFocusableChatWindow
? StyleEngine.Widgets.ChannelHeaderMode.DetailOnly
: StyleEngine.Widgets.ChannelHeaderMode.Full;
+ // What follows the log in this column, so the header can tell how
+ // much room the conversation is actually left with. Without it the
+ // drop-out rule would measure the input row as readable chat.
StyleEngine.Widgets.ChannelHeader.Draw(
headerTab,
mode,
Plugin.Instance.FontManager,
- StyleEngine.Widgets.ChannelHeader.CurrentDetail()
+ StyleEngine.Widgets.ChannelHeader.CurrentDetail(),
+ inputHeight + previewHeight
);
}
diff --git a/HellionChat/Ui/Windows/WidgetGalleryWindow.cs b/HellionChat/Ui/Windows/WidgetGalleryWindow.cs
index a3ebd03..7704d31 100644
--- a/HellionChat/Ui/Windows/WidgetGalleryWindow.cs
+++ b/HellionChat/Ui/Windows/WidgetGalleryWindow.cs
@@ -25,6 +25,7 @@ internal sealed class WidgetGalleryWindow : Window
private int _badgeCount = 3;
private Tab? _headerSample;
private int _headerMode;
+ private bool _headerLoggedOut;
private bool _rowActive = true;
private bool _toggleA = true;
private bool _toggleB;
@@ -87,7 +88,18 @@ internal sealed class WidgetGalleryWindow : Window
fonts.RebuildDelegateFonts();
var mode = _headerMode == 0 ? ChannelHeaderMode.Full : ChannelHeaderMode.DetailOnly;
- ChannelHeader.Draw(_headerSample, mode, fonts, "Ravana · 14:32");
+
+ // 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
+ );
+
+ ChannelHeader.Draw(_headerSample, mode, fonts, detail, 0f);
ImGui.Spacing();
}