Files
HellionChat/HellionChat/Ui/Components/Sidebar.cs
T
JonKazama-Hellion 6ab2e9cece feat(ui): add Sidebar component with width auto-switch
Channel-list panel for the chat window's left side. Auto-switches between
icon-only (38px) and expanded (150px) based on
Config.SidebarAutoSwitchThresholdPx. Each row renders a FontAwesome tab
icon, an expanded-mode name label, a hover-sheen sweep keyed on the tab
identifier, and a pop-out affordance — both the trailing hover button and
the right-click context menu route through a log stub until the channel
popout pool comes online. Glyph table is inlined so the Ui layer carries
its own lookup after the standalone mapping file is removed.
2026-05-23 18:30:29 +02:00

181 lines
5.9 KiB
C#

using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using HellionChat.Themes;
using HellionChat.Ui.StyleEngine;
using HellionChat.Util;
using Microsoft.Extensions.Logging;
namespace HellionChat.Ui.Components;
// Channel-list panel pinned to the left of the chat window. Auto-switches
// between an icon-only column (38px) and an expanded column (150px) once
// the outer window crosses Config.SidebarAutoSwitchThresholdPx. The
// pop-out trigger is wired later (channel-popout cycle); the hover button
// and right-click menu route through a log stub for now so the discovery
// affordance is already in place.
internal sealed class Sidebar
{
public const float IconOnlyWidth = 38f;
public const float ExpandedWidth = 150f;
private const float RowHeight = 32f;
private const float PopOutHitWidth = 22f;
// Inline mirror of the old TabIconMapping table so the Ui layer carries
// its own glyph lookup once the standalone file is removed.
private static readonly Dictionary<string, FontAwesomeIcon> IconByName = new(
StringComparer.OrdinalIgnoreCase
)
{
["comment"] = FontAwesomeIcon.Comment,
["comments"] = FontAwesomeIcon.Comments,
["cog"] = FontAwesomeIcon.Cog,
["users"] = FontAwesomeIcon.Users,
["user-friends"] = FontAwesomeIcon.UserFriends,
["link"] = FontAwesomeIcon.Link,
["envelope"] = FontAwesomeIcon.Envelope,
["clock"] = FontAwesomeIcon.Clock,
["hashtag"] = FontAwesomeIcon.Hashtag,
["star"] = FontAwesomeIcon.Star,
["heart"] = FontAwesomeIcon.Heart,
["bell"] = FontAwesomeIcon.Bell,
["bookmark"] = FontAwesomeIcon.Bookmark,
["flag"] = FontAwesomeIcon.Flag,
["fire"] = FontAwesomeIcon.Fire,
};
private readonly ThemeRegistry _themes;
private readonly TokenResolver _resolver;
private readonly FontManager _fonts;
private readonly ILogger<Sidebar> _logger;
public Sidebar(
ThemeRegistry themes,
TokenResolver resolver,
FontManager fonts,
ILogger<Sidebar> logger
)
{
_themes = themes;
_resolver = resolver;
_fonts = fonts;
_logger = logger;
}
public bool IsExpanded(float windowWidth) =>
windowWidth >= Plugin.Config.SidebarAutoSwitchThresholdPx;
public float GetWidth(float windowWidth) =>
IsExpanded(windowWidth) ? ExpandedWidth : IconOnlyWidth;
public void Draw(float windowWidth, IList<Tab> tabs, ref Tab? activeTab)
{
if (!_fonts.FontsReady)
{
ImGui.Dummy(new Vector2(IconOnlyWidth, 0));
return;
}
var expanded = IsExpanded(windowWidth);
var width = expanded ? ExpandedWidth : IconOnlyWidth;
using var child = ImRaii.Child("##hellion-sidebar", new Vector2(width, 0));
if (!child.Success)
return;
var theme = _themes.Active;
var accentRgba = _resolver.Resolve(Token.AccentPrimary, theme.Colors);
var textAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary);
var mutedAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextMuted);
var dl = ImGui.GetWindowDrawList();
for (var i = 0; i < tabs.Count; i++)
DrawRow(tabs[i], i, expanded, accentRgba, textAbgr, mutedAbgr, dl, ref activeTab);
}
private void DrawRow(
Tab tab,
int index,
bool expanded,
uint accentRgba,
uint textAbgr,
uint mutedAbgr,
ImDrawListPtr dl,
ref Tab? activeTab
)
{
ImGui.PushID(index);
var origin = ImGui.GetCursorScreenPos();
var avail = ImGui.GetContentRegionAvail().X;
var tabHitWidth = MathF.Max(0f, avail - PopOutHitWidth);
// Tab hit area sits left of the pop-out button so the two never
// steal each other's clicks.
ImGui.InvisibleButton("row", new Vector2(tabHitWidth, RowHeight));
var rowHovered = ImGui.IsItemHovered();
if (ImGui.IsItemClicked())
activeTab = tab;
dl.DrawHoverSheen(
origin,
origin + new Vector2(avail, RowHeight),
accentRgba,
$"sidebar.tab.{tab.Identifier}",
rowHovered
);
var icon = ResolveTabIcon(tab);
using (_fonts.FontAwesome.Push())
dl.AddText(origin + new Vector2(10f, 8f), textAbgr, icon.ToIconString());
if (expanded)
dl.AddText(origin + new Vector2(32f, 8f), textAbgr, tab.Name);
if (ImGui.BeginPopupContextItem("ctx"))
{
if (ImGui.MenuItem("Pop Out"))
LogPopOutStub(tab);
ImGui.EndPopup();
}
ImGui.SameLine(0f, 0f);
ImGui.InvisibleButton("popout", new Vector2(PopOutHitWidth, RowHeight));
var popHovered = ImGui.IsItemHovered();
if (ImGui.IsItemClicked())
LogPopOutStub(tab);
if (rowHovered || popHovered)
{
using (_fonts.FontAwesome.Push())
{
var glyph = FontAwesomeIcon.ArrowUpRightFromSquare.ToIconString();
dl.AddText(origin + new Vector2(avail - PopOutHitWidth + 4f, 8f), mutedAbgr, glyph);
}
}
ImGui.PopID();
}
private static FontAwesomeIcon ResolveTabIcon(Tab tab)
{
if (
!string.IsNullOrWhiteSpace(tab.Icon) && IconByName.TryGetValue(tab.Icon, out var mapped)
)
return mapped;
return FontAwesomeIcon.Comment;
}
private void LogPopOutStub(Tab tab)
{
// The channel-popout pool is built in a later cycle; logging here
// keeps the trigger visible without faking the routing.
_logger.LogInformation(
"Pop-out requested for tab {Identifier} ({Name}); routing arrives later.",
tab.Identifier,
tab.Name
);
}
}