diff --git a/HellionChat/Ui/StyleEngine/Widgets/SectionHeader.cs b/HellionChat/Ui/StyleEngine/Widgets/SectionHeader.cs new file mode 100644 index 0000000..3aac2b6 --- /dev/null +++ b/HellionChat/Ui/StyleEngine/Widgets/SectionHeader.cs @@ -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 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 + ); + } +} diff --git a/HellionChat/Ui/Windows/WidgetGalleryWindow.cs b/HellionChat/Ui/Windows/WidgetGalleryWindow.cs index 3e3a143..227ed6a 100644 --- a/HellionChat/Ui/Windows/WidgetGalleryWindow.cs +++ b/HellionChat/Ui/Windows/WidgetGalleryWindow.cs @@ -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");