fix(ui): respect window opacity and keep badges off the icons
Reviews of blocks C and D found five things a user would see immediately. Row fills ignored the window's own opacity. Theme surfaces are fully opaque, and GlobalStyleScope zeroes ChildBg below full opacity so WindowBg alone carries the coverage -- with the default of 0.85 that made the sidebar a solid block inside a translucent window. Idle rows now draw no fill at all, and the active and hover fills are scaled by the current window opacity. The unread badge landed on the tab icon at the default sidebar width of 44px. Right-aligning it needs roughly 70px for one digit and 90px for three, and the old placement also subtracted the popout column even when there was no popout button. It is only drawn where it clears the icon; below that a plain dot takes over, which is what the sidebar did before this cycle anyway. The same collision existed in the top-tab strip, worse: the badge sat in the trailing padding, which is 10px against a badge at least 14px wide, so it covered the label on every tab that had one. The badge is part of the tab width now, and vertically centred rather than top-aligned. The context menu's spacing guard read the pushed zero back out of GetStyle, so the max never did anything and X stayed at zero -- which is what HelpMarker's SameLine uses, so the "(?)" clung to its label. It sets both axes outright now. Section captions had all their padding above them and one pixel below, so with zero item spacing the next row started immediately under the text. Three smaller items: the tab icon was centred against the text font's line height although FontAwesome is a fixed-width handle that ignores Config.FontSizeV2; IconButton interpolated a label string per button per frame, now a PushID over a u8 literal; and the alpha scaling that had grown four copies now goes through ColourUtil.ApplyAlpha everywhere.
This commit is contained in:
@@ -32,8 +32,10 @@ internal sealed class Sidebar
|
||||
private static float PopOutHitWidth => Metrics.SidebarPopOutHitWidth;
|
||||
private static float GreetedHitWidth => Metrics.SidebarGreetedHitWidth;
|
||||
|
||||
// Counts rows that got the active surface this frame. At most one, but not
|
||||
// exactly one: PickMainActiveTab returns null when every tab is popped out.
|
||||
// Counts rows that got the active surface this frame. At most one, with two
|
||||
// exceptions: zero when every tab is popped out (PickMainActiveTab returns
|
||||
// null), and two in the frame a click lands on a row drawn after the
|
||||
// previously active one -- that row was still active when it was painted.
|
||||
internal int LastRenderedActiveSurfaceCount { get; private set; }
|
||||
|
||||
// B3-2 render observability: counts greeted glyphs actually drawn this frame.
|
||||
@@ -237,7 +239,9 @@ internal sealed class Sidebar
|
||||
// icon-only or min-drag mode it is skipped entirely.
|
||||
var greetedConfigured = tab.IsTempTab && Plugin.Config.AutoTellTabsShowGreetedToggle;
|
||||
var showGreeted =
|
||||
greetedConfigured && expanded && avail > GreetedHitWidth + PopOutHitWidth + 4f;
|
||||
greetedConfigured
|
||||
&& expanded
|
||||
&& avail > GreetedHitWidth + PopOutHitWidth + Metrics.SidebarHitSlack;
|
||||
|
||||
// Only split off a separate pop-out hit area when there's room for
|
||||
// both buttons. Below that, the whole row stays as a single
|
||||
@@ -246,7 +250,7 @@ internal sealed class Sidebar
|
||||
// icon-only mode avail still clears the width threshold, which used to
|
||||
// paint the pop-out glyph over the tab icon. The row stays a single
|
||||
// selectable strip when collapsed; right-click pop-out is unaffected.
|
||||
var hasPopOut = expanded && avail > PopOutHitWidth + 4f;
|
||||
var hasPopOut = expanded && avail > PopOutHitWidth + Metrics.SidebarHitSlack;
|
||||
var tabHitWidth = hasPopOut ? avail - PopOutHitWidth : avail;
|
||||
if (showGreeted)
|
||||
{
|
||||
@@ -284,6 +288,14 @@ internal sealed class Sidebar
|
||||
LastRenderedActiveSurfaceCount++;
|
||||
|
||||
var colors = _themes.Active.Colors;
|
||||
|
||||
// Row fills follow the window's own opacity. Theme surfaces are fully
|
||||
// opaque and the window is translucent by default (0.85 focused, 0.65
|
||||
// not), so unscaled fills would sit on top as solid blocks.
|
||||
var opacity = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootWindow)
|
||||
? Plugin.Config.WindowOpacity
|
||||
: Plugin.Config.WindowOpacityInactive;
|
||||
|
||||
Row.Draw(
|
||||
origin,
|
||||
new Vector2(avail, RowHeight),
|
||||
@@ -291,11 +303,16 @@ internal sealed class Sidebar
|
||||
{
|
||||
IsActive = isActiveRow,
|
||||
HoverAmount = hoverAmount,
|
||||
SurfaceAbgr = _palette.Abgr(Token.SurfaceBase, colors),
|
||||
SurfaceHoverAbgr = _palette.Abgr(Token.SurfaceHover, colors),
|
||||
SurfaceActiveAbgr = _palette.Abgr(Token.SurfaceActive, colors),
|
||||
SurfaceHoverAbgr = ColourUtil.ApplyAlpha(
|
||||
_palette.Abgr(Token.SurfaceHover, colors),
|
||||
opacity
|
||||
),
|
||||
SurfaceActiveAbgr = ColourUtil.ApplyAlpha(
|
||||
_palette.Abgr(Token.SurfaceActive, colors),
|
||||
opacity
|
||||
),
|
||||
AccentAbgr = _palette.Abgr(Token.AccentPrimary, colors),
|
||||
BorderAbgr = _palette.Abgr(Token.Border, colors),
|
||||
BorderAbgr = ColourUtil.ApplyAlpha(_palette.Abgr(Token.Border, colors), opacity),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -326,9 +343,18 @@ internal sealed class Sidebar
|
||||
float iconRight;
|
||||
using (_fonts.FontAwesome.Push())
|
||||
{
|
||||
// Measured inside the push: FontAwesome is a fixed-width icon handle
|
||||
// that does not follow Config.FontSizeV2, so the text font's line
|
||||
// height would misplace the glyph at any other body size.
|
||||
var iconStr = icon.ToIconString();
|
||||
dl.AddText(origin + new Vector2(iconInset + contentX, contentY), iconColor, iconStr);
|
||||
iconRight = iconInset + contentX + ImGui.CalcTextSize(iconStr).X;
|
||||
var iconSize = ImGui.CalcTextSize(iconStr);
|
||||
dl.AddText(
|
||||
origin
|
||||
+ new Vector2(iconInset + contentX, MetricsMath.CenterY(RowHeight, iconSize.Y)),
|
||||
iconColor,
|
||||
iconStr
|
||||
);
|
||||
iconRight = iconInset + contentX + iconSize.X;
|
||||
}
|
||||
|
||||
if (expanded)
|
||||
@@ -342,16 +368,34 @@ internal sealed class Sidebar
|
||||
{
|
||||
var unread = (int)Math.Min(tab.Unread, int.MaxValue);
|
||||
var badgeSize = Badge.CalcSize(unread);
|
||||
var badgeX = expanded
|
||||
? avail - PopOutHitWidth - badgeSize.X - 4f * scale
|
||||
: iconRight - badgeSize.X * 0.5f;
|
||||
var accentAbgr = _palette.Abgr(Token.AccentEmber, colors);
|
||||
var slack = Metrics.SidebarHitSlack;
|
||||
var reserved = hasPopOut ? PopOutHitWidth : 0f;
|
||||
var badgeX = avail - reserved - badgeSize.X - slack;
|
||||
|
||||
// The count only fits where it can sit clear of the icon. At the
|
||||
// default sidebar width of 44 it cannot, so a plain dot takes over
|
||||
// rather than the badge landing on the glyph.
|
||||
if (badgeX >= iconRight + slack)
|
||||
{
|
||||
Badge.Draw(
|
||||
origin + new Vector2(badgeX, MetricsMath.CenterY(RowHeight, badgeSize.Y)),
|
||||
unread,
|
||||
accentAbgr,
|
||||
textAbgr
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
var r = Metrics.SidebarUnreadRadius;
|
||||
dl.AddCircleFilled(
|
||||
origin + new Vector2(iconRight - r * 0.5f, RowHeight * 0.5f - r),
|
||||
r,
|
||||
accentAbgr,
|
||||
12
|
||||
);
|
||||
}
|
||||
|
||||
Badge.Draw(
|
||||
origin + new Vector2(badgeX, MetricsMath.CenterY(RowHeight, badgeSize.Y)),
|
||||
unread,
|
||||
_palette.Abgr(Token.AccentEmber, colors),
|
||||
textAbgr
|
||||
);
|
||||
LastRenderedUnreadDotCount++;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,14 +41,12 @@ internal static class TabContextMenu
|
||||
}
|
||||
|
||||
// The sidebar pushes ItemSpacing to zero so its rows sit flush, and style
|
||||
// vars are a global stack the popup inherits. Without restoring a normal
|
||||
// spacing here the menu entries would touch each other.
|
||||
// vars are a global stack the popup inherits. Reading GetStyle() here
|
||||
// would read that zero back, so the popup sets its own spacing outright
|
||||
// -- including X, which HelpMarker's SameLine depends on.
|
||||
using var spacing = ImRaii.PushStyle(
|
||||
ImGuiStyleVar.ItemSpacing,
|
||||
ImGui.GetStyle().ItemSpacing with
|
||||
{
|
||||
Y = MathF.Max(ImGui.GetStyle().ItemSpacing.Y, 4f * ImGuiHelpers.GlobalScale),
|
||||
}
|
||||
new System.Numerics.Vector2(8f, 4f) * Ui.StyleEngine.Metrics.Scale
|
||||
);
|
||||
|
||||
// Rename: focus the field the first frame the popup appears.
|
||||
|
||||
@@ -16,8 +16,13 @@ internal sealed class TopTabBar
|
||||
private readonly ThemeRegistry _themes;
|
||||
private readonly WidgetPalette _palette;
|
||||
|
||||
// Render observability, mirroring the sidebar counters: at most one tab
|
||||
// carries the active underline. Zero is valid -- every tab can be popped out.
|
||||
// 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)
|
||||
@@ -58,8 +63,22 @@ internal sealed class TopTabBar
|
||||
|
||||
var selected = ReferenceEquals(tab, activeTab);
|
||||
var origin = ImGui.GetCursorScreenPos();
|
||||
var width = ImGui.CalcTextSize(tab.Name).X + padX * 2f;
|
||||
var size = new Vector2(width, height);
|
||||
|
||||
// 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;
|
||||
var unread = showUnread ? (int)Math.Min(tab.Unread, int.MaxValue) : 0;
|
||||
var badgeSize = showUnread ? Badge.CalcSize(unread, TabBadge) : Vector2.Zero;
|
||||
|
||||
var width =
|
||||
ImGui.CalcTextSize(tab.Name).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
|
||||
@@ -96,24 +115,20 @@ internal sealed class TopTabBar
|
||||
if (selected)
|
||||
LastRenderedUnderlineCount++;
|
||||
|
||||
// 1.5.6-parity unread marker. Gated on the POST-click selection so
|
||||
// clicking a tab clears it the same frame, like the sidebar.
|
||||
if (
|
||||
!ReferenceEquals(tab, activeTab)
|
||||
&& tab.UnreadMode != UnreadMode.None
|
||||
&& tab.Unread > 0
|
||||
)
|
||||
{
|
||||
var unread = (int)Math.Min(tab.Unread, int.MaxValue);
|
||||
var badgeSize = Badge.CalcSize(unread);
|
||||
var inset = Metrics.TopTabUnreadInset;
|
||||
// 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 - inset, origin.Y + inset),
|
||||
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
|
||||
textAbgr,
|
||||
TabBadge
|
||||
);
|
||||
}
|
||||
|
||||
TabContextMenu.Draw(tab, $"toptab_ctx_{tab.Identifier}", _pool);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ internal static class Metrics
|
||||
internal const float HonorificBracketGapRaw = 6f;
|
||||
|
||||
// --- Top tab bar ---
|
||||
internal const float TopTabUnreadRadiusRaw = 3.5f;
|
||||
internal const float TopTabUnreadInsetRaw = 4f;
|
||||
internal const float TopTabPaddingXRaw = 10f;
|
||||
internal const float TopTabUnderlineRaw = 2f;
|
||||
@@ -96,7 +95,6 @@ internal static class Metrics
|
||||
internal static float HonorificInset => MetricsMath.Scale(HonorificInsetRaw, Scale);
|
||||
internal static float HonorificBracketGap => MetricsMath.Scale(HonorificBracketGapRaw, Scale);
|
||||
|
||||
internal static float TopTabUnreadRadius => MetricsMath.Scale(TopTabUnreadRadiusRaw, Scale);
|
||||
internal static float TopTabUnreadInset => MetricsMath.Scale(TopTabUnreadInsetRaw, Scale);
|
||||
internal static float TopTabPaddingX => MetricsMath.Scale(TopTabPaddingXRaw, Scale);
|
||||
internal static float TopTabUnderline => MetricsMath.Scale(TopTabUnderlineRaw, Scale);
|
||||
|
||||
@@ -51,9 +51,14 @@ internal static class IconButton
|
||||
var clamped = WidgetGeometry.IconButton(size.X, size.Y);
|
||||
var origin = ImGui.GetCursorScreenPos();
|
||||
|
||||
ImGui.InvisibleButton($"##hellion-iconbtn-{id}", clamped);
|
||||
// PushID over an interpolated label: the string version allocated once
|
||||
// per button per frame, which is the pattern the sidebar just removed
|
||||
// from its hover keys.
|
||||
ImGui.PushID((int)id);
|
||||
ImGui.InvisibleButton("##b"u8, clamped);
|
||||
var hovered = ImGui.IsItemHovered();
|
||||
var clicked = ImGui.IsItemClicked();
|
||||
ImGui.PopID();
|
||||
|
||||
var amount = HoverState.Query(id, hovered);
|
||||
var dl = ImGui.GetWindowDrawList();
|
||||
|
||||
@@ -23,7 +23,9 @@ internal static class LineDivider
|
||||
{
|
||||
var style = styleOverride ?? new LineDividerStyle();
|
||||
var scale = Metrics.Scale;
|
||||
var labelHeight = label is null ? 0f : ImGui.GetTextLineHeight();
|
||||
// Caption height plus its own bottom padding: without the second padY
|
||||
// the next row starts one pixel under the text.
|
||||
var labelHeight = label is null ? 0f : ImGui.GetTextLineHeight() + style.PadY * scale;
|
||||
return WidgetGeometry.LineDivider(
|
||||
ImGui.GetContentRegionAvail().X,
|
||||
style.Thickness * scale,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using HellionChat.Util;
|
||||
|
||||
namespace HellionChat.Ui.StyleEngine.Widgets;
|
||||
|
||||
@@ -12,7 +13,6 @@ internal readonly record struct RowVisualState
|
||||
|
||||
public bool IsActive { get; init; }
|
||||
public float HoverAmount { get; init; }
|
||||
public uint SurfaceAbgr { get; init; }
|
||||
public uint SurfaceHoverAbgr { get; init; }
|
||||
public uint SurfaceActiveAbgr { get; init; }
|
||||
public uint AccentAbgr { get; init; }
|
||||
@@ -46,11 +46,19 @@ internal static class Row
|
||||
var dl = ImGui.GetWindowDrawList();
|
||||
var max = origin + size;
|
||||
|
||||
var baseAbgr = state.IsActive ? state.SurfaceActiveAbgr : state.SurfaceAbgr;
|
||||
dl.AddRectFilled(origin, max, baseAbgr);
|
||||
// Idle rows draw no fill at all. GlobalStyleScope zeroes ChildBg below
|
||||
// full window opacity so WindowBg alone carries the coverage, and the
|
||||
// default is 0.85 -- an opaque fill per row would make the sidebar a
|
||||
// solid block inside a translucent window.
|
||||
if (state.IsActive)
|
||||
dl.AddRectFilled(origin, max, state.SurfaceActiveAbgr);
|
||||
|
||||
if (state.HoverAmount > 0f)
|
||||
dl.AddRectFilled(origin, max, ScaleAlpha(state.SurfaceHoverAbgr, state.HoverAmount));
|
||||
dl.AddRectFilled(
|
||||
origin,
|
||||
max,
|
||||
ColourUtil.ApplyAlpha(state.SurfaceHoverAbgr, state.HoverAmount)
|
||||
);
|
||||
|
||||
if (state.IsActive && style.AccentBarWidth > 0f)
|
||||
{
|
||||
@@ -66,10 +74,4 @@ internal static class Row
|
||||
Metrics.Scale
|
||||
);
|
||||
}
|
||||
|
||||
private static uint ScaleAlpha(uint abgr, float factor)
|
||||
{
|
||||
var a = (uint)Math.Clamp(MathF.Round(((abgr >> 24) & 0xFF) * factor), 0f, 255f);
|
||||
return (abgr & 0x00FFFFFFu) | (a << 24);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,6 @@ internal sealed class WidgetGalleryWindow : Window
|
||||
{
|
||||
IsActive = _rowActive && i == 1,
|
||||
HoverAmount = HoverState.Query(id, hovered),
|
||||
SurfaceAbgr = _palette.Abgr(Token.SurfaceBase, c),
|
||||
SurfaceHoverAbgr = _palette.Abgr(Token.SurfaceHover, c),
|
||||
SurfaceActiveAbgr = _palette.Abgr(Token.SurfaceActive, c),
|
||||
AccentAbgr = _palette.Abgr(Token.AccentPrimary, c),
|
||||
|
||||
Reference in New Issue
Block a user