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.
This commit is contained in:
@@ -123,6 +123,12 @@ internal static class PluginHostFactory
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.Sidebar(
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>(),
|
||||
sp.GetRequiredService<FontManager>(),
|
||||
sp.GetRequiredService<ILogger<Ui.Components.Sidebar>>()
|
||||
));
|
||||
services.AddSingleton(sp => new Integrations.FailedTellNotifier(
|
||||
sp.GetRequiredService<ILogger<Integrations.FailedTellNotifier>>()
|
||||
));
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user