feat(style): add the collapsible section header widget
Replaces ImGui.CollapsingHeader, whose framed bar is the single most ImGui-looking element in the settings window -- 25 of them across nine files. State lives in the widget, not in ImGui's per-window storage. ImGui keys that off the label, so once the section titles are localised the open/closed state would reset on every language switch, and two titles that translate to the same string would share one state. Callers pass a stable key built from an ASCII literal instead. The disabled parameter is not optional decoration. BeginDisabled pushes an alpha that only reaches ImGui's own widgets, so a draw-list header would sit at full opacity while everything around it fades -- and ThemePicker wraps two of its headers in exactly that. Cursor advance goes through ImGuiP.ItemSize like SettingRow, so the scrollbar sees the full height. The gallery shows all three states, including the disabled one inside a real ImRaii.Disabled scope, which is where the alpha problem would otherwise only surface in the theme picker.
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using HellionChat.Util;
|
||||
|
||||
namespace HellionChat.Ui.StyleEngine.Widgets;
|
||||
|
||||
internal readonly record struct SectionHeaderColors
|
||||
{
|
||||
public SectionHeaderColors() { }
|
||||
|
||||
public uint TitleAbgr { get; init; }
|
||||
public uint DescriptionAbgr { get; init; }
|
||||
public uint AccentAbgr { get; init; }
|
||||
public uint BorderAbgr { get; init; }
|
||||
public uint HoverAbgr { get; init; }
|
||||
}
|
||||
|
||||
internal readonly record struct SectionHeaderStyle
|
||||
{
|
||||
public SectionHeaderStyle() { }
|
||||
|
||||
public float PadY { get; init; } = 6f;
|
||||
public float AccentBarWidth { get; init; } = 2f;
|
||||
public float ChevronInset { get; init; } = 6f;
|
||||
}
|
||||
|
||||
// Collapsible section heading. Replaces ImGui.CollapsingHeader, whose framed
|
||||
// bar is the single most ImGui-looking element in the settings window.
|
||||
//
|
||||
// State lives here rather than in ImGui's per-window storage: that keys off the
|
||||
// label, so once the titles are localised the open/closed state would reset on
|
||||
// every language switch and translated titles could collide. Callers pass a
|
||||
// stable key instead, built from an ASCII literal that never gets translated.
|
||||
internal static class SectionHeader
|
||||
{
|
||||
private static readonly Dictionary<uint, bool> Open = [];
|
||||
|
||||
internal static void Reset() => Open.Clear();
|
||||
|
||||
internal static bool Draw(
|
||||
uint key,
|
||||
string title,
|
||||
string? description,
|
||||
SectionHeaderColors colors,
|
||||
bool defaultOpen = false,
|
||||
bool disabled = false,
|
||||
SectionHeaderStyle? styleOverride = null
|
||||
)
|
||||
{
|
||||
var style = styleOverride ?? new SectionHeaderStyle();
|
||||
var scale = Metrics.Scale;
|
||||
var padY = style.PadY * scale;
|
||||
|
||||
if (!Open.TryGetValue(key, out var open))
|
||||
{
|
||||
open = defaultOpen;
|
||||
Open[key] = open;
|
||||
}
|
||||
|
||||
var origin = ImGui.GetCursorScreenPos();
|
||||
var width = ImGui.GetContentRegionAvail().X;
|
||||
var titleHeight = ImGui.GetTextLineHeight();
|
||||
var descHeight = description is null ? 0f : titleHeight;
|
||||
var size = WidgetGeometry.SectionHeader(width, titleHeight, descHeight, padY, 1f * scale);
|
||||
|
||||
ImGui.SetCursorScreenPos(origin);
|
||||
var clicked = ImGui.InvisibleButton($"##hellion-section-{key}", size) && !disabled;
|
||||
var hovered = ImGui.IsItemHovered() && !disabled;
|
||||
var hoverAmount = HoverState.Query(key, hovered);
|
||||
|
||||
if (clicked)
|
||||
{
|
||||
open = !open;
|
||||
Open[key] = open;
|
||||
}
|
||||
|
||||
// BeginDisabled only dims ImGui's own widgets, so a draw-list header
|
||||
// would stay at full opacity while everything around it fades. The
|
||||
// ThemePicker wraps two of its headers exactly like that.
|
||||
var alpha = disabled ? 0.5f : 1f;
|
||||
var dl = ImGui.GetWindowDrawList();
|
||||
var max = origin + size;
|
||||
|
||||
if (hoverAmount > 0f)
|
||||
dl.AddRectFilled(
|
||||
origin,
|
||||
max,
|
||||
ColourUtil.ApplyAlpha(colors.HoverAbgr, hoverAmount * alpha)
|
||||
);
|
||||
|
||||
var barWidth = style.AccentBarWidth * scale;
|
||||
dl.AddRectFilled(
|
||||
origin,
|
||||
new Vector2(origin.X + barWidth, origin.Y + titleHeight + padY * 2f),
|
||||
ColourUtil.ApplyAlpha(colors.AccentAbgr, alpha)
|
||||
);
|
||||
|
||||
var textX = origin.X + barWidth + style.ChevronInset * scale * 2f;
|
||||
dl.AddText(
|
||||
new Vector2(textX, origin.Y + padY),
|
||||
ColourUtil.ApplyAlpha(colors.TitleAbgr, alpha),
|
||||
title
|
||||
);
|
||||
|
||||
DrawChevron(
|
||||
dl,
|
||||
new Vector2(
|
||||
origin.X + barWidth + style.ChevronInset * scale,
|
||||
origin.Y + padY + titleHeight * 0.5f
|
||||
),
|
||||
titleHeight * 0.28f,
|
||||
open,
|
||||
ColourUtil.ApplyAlpha(colors.TitleAbgr, alpha)
|
||||
);
|
||||
|
||||
if (description is not null)
|
||||
dl.AddText(
|
||||
ImGui.GetFont(),
|
||||
ImGui.GetFontSize(),
|
||||
new Vector2(textX, origin.Y + padY + titleHeight),
|
||||
ColourUtil.ApplyAlpha(colors.DescriptionAbgr, alpha),
|
||||
description,
|
||||
width - (textX - origin.X)
|
||||
);
|
||||
|
||||
dl.AddLine(
|
||||
new Vector2(origin.X, max.Y - 1f * scale),
|
||||
new Vector2(max.X, max.Y - 1f * scale),
|
||||
ColourUtil.ApplyAlpha(colors.BorderAbgr, alpha),
|
||||
1f * scale
|
||||
);
|
||||
|
||||
ImGui.SetCursorScreenPos(origin);
|
||||
ImGuiP.ItemSize(new Vector2(width, size.Y - ImGui.GetStyle().ItemSpacing.Y));
|
||||
|
||||
return open;
|
||||
}
|
||||
|
||||
private static void DrawChevron(ImDrawListPtr dl, Vector2 centre, float r, bool open, uint abgr)
|
||||
{
|
||||
if (open)
|
||||
dl.AddTriangleFilled(
|
||||
centre + new Vector2(-r, -r * 0.5f),
|
||||
centre + new Vector2(r, -r * 0.5f),
|
||||
centre + new Vector2(0f, r * 0.7f),
|
||||
abgr
|
||||
);
|
||||
else
|
||||
dl.AddTriangleFilled(
|
||||
centre + new Vector2(-r * 0.5f, -r),
|
||||
centre + new Vector2(r * 0.7f, 0f),
|
||||
centre + new Vector2(-r * 0.5f, r),
|
||||
abgr
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@ internal sealed class WidgetGalleryWindow : Window
|
||||
);
|
||||
ImGui.Separator();
|
||||
|
||||
DrawSectionHeaderSection(c);
|
||||
DrawRowSection(c);
|
||||
DrawToggleSection(c);
|
||||
DrawSettingRowSection(c);
|
||||
@@ -101,6 +102,54 @@ internal sealed class WidgetGalleryWindow : Window
|
||||
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");
|
||||
|
||||
Reference in New Issue
Block a user