feat(ui): migrate SymbolPicker into the components layer
Lifted out of Ui/ into Ui/Components and registered as a DI singleton so the new InputBar can consume it via constructor injection. PUA tab still sources from SeIconChar, BMP tab keeps the server-verified whitelist verbatim. The old Ui/SymbolPicker.cs stays in place until the cleanup block removes the v1.5.6 file.
This commit is contained in:
@@ -134,6 +134,7 @@ internal static class PluginHostFactory
|
|||||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>(),
|
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>(),
|
||||||
sp.GetRequiredService<FontManager>()
|
sp.GetRequiredService<FontManager>()
|
||||||
));
|
));
|
||||||
|
services.AddSingleton(_ => new Ui.Components.SymbolPicker());
|
||||||
services.AddSingleton(sp => new Integrations.FailedTellNotifier(
|
services.AddSingleton(sp => new Integrations.FailedTellNotifier(
|
||||||
sp.GetRequiredService<ILogger<Integrations.FailedTellNotifier>>()
|
sp.GetRequiredService<ILogger<Integrations.FailedTellNotifier>>()
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -0,0 +1,294 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Game.Text;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
|
||||||
|
namespace HellionChat.Ui.Components;
|
||||||
|
|
||||||
|
// Popup picker for chat-input symbol insertion. Two tabs:
|
||||||
|
// PUA — Dalamud's SeIconChar enum (server-safe FFXIV glyphs)
|
||||||
|
// BMP — server-verified Unicode symbols (whitelist probed via /echo + /say)
|
||||||
|
//
|
||||||
|
// Render-only — the visibility toggle for the trigger button lives on the
|
||||||
|
// caller side (InputBar). Recent-Used is session state by design.
|
||||||
|
internal sealed class SymbolPicker
|
||||||
|
{
|
||||||
|
private const string PopupId = "HellionSymbolPicker";
|
||||||
|
private const int RecentCapacity = 16;
|
||||||
|
|
||||||
|
private string _search = string.Empty;
|
||||||
|
private readonly List<uint> _recentUsed = new(capacity: RecentCapacity);
|
||||||
|
|
||||||
|
// FFXIV server-safe BMP symbols, verified via /echo + /say. Filtered
|
||||||
|
// ranges live in the v1.4.10 BMP-Whitelist Notes for the original probe;
|
||||||
|
// the list stays inline so the picker has no external lookup table.
|
||||||
|
private static readonly (uint Codepoint, string Name)[] BmpWhitelist = new[]
|
||||||
|
{
|
||||||
|
(0x00A1u, "Inverted Exclamation"),
|
||||||
|
(0x00A2u, "Cent Sign"),
|
||||||
|
(0x00A3u, "Pound Sign"),
|
||||||
|
(0x00A4u, "Currency Sign"),
|
||||||
|
(0x00A5u, "Yen Sign"),
|
||||||
|
(0x00A7u, "Section Sign"),
|
||||||
|
(0x00A9u, "Copyright Sign"),
|
||||||
|
(0x00ABu, "Left Angle Quote"),
|
||||||
|
(0x00AEu, "Registered Sign"),
|
||||||
|
(0x00B0u, "Degree Sign"),
|
||||||
|
(0x00B1u, "Plus-Minus Sign"),
|
||||||
|
(0x00B6u, "Pilcrow Sign"),
|
||||||
|
(0x00BBu, "Right Angle Quote"),
|
||||||
|
(0x00BCu, "One Quarter"),
|
||||||
|
(0x00BDu, "One Half"),
|
||||||
|
(0x00BEu, "Three Quarters"),
|
||||||
|
(0x00BFu, "Inverted Question"),
|
||||||
|
(0x00D7u, "Multiplication Sign"),
|
||||||
|
(0x00F7u, "Division Sign"),
|
||||||
|
(0x0393u, "Greek Capital Gamma"),
|
||||||
|
(0x0394u, "Greek Capital Delta"),
|
||||||
|
(0x0398u, "Greek Capital Theta"),
|
||||||
|
(0x039Bu, "Greek Capital Lambda"),
|
||||||
|
(0x039Eu, "Greek Capital Xi"),
|
||||||
|
(0x03A0u, "Greek Capital Pi"),
|
||||||
|
(0x03A3u, "Greek Capital Sigma"),
|
||||||
|
(0x03A6u, "Greek Capital Phi"),
|
||||||
|
(0x03A8u, "Greek Capital Psi"),
|
||||||
|
(0x03A9u, "Greek Capital Omega"),
|
||||||
|
(0x03B1u, "Greek Small Alpha"),
|
||||||
|
(0x03B2u, "Greek Small Beta"),
|
||||||
|
(0x03B3u, "Greek Small Gamma"),
|
||||||
|
(0x03B4u, "Greek Small Delta"),
|
||||||
|
(0x03B5u, "Greek Small Epsilon"),
|
||||||
|
(0x03B6u, "Greek Small Zeta"),
|
||||||
|
(0x03B7u, "Greek Small Eta"),
|
||||||
|
(0x03B8u, "Greek Small Theta"),
|
||||||
|
(0x03B9u, "Greek Small Iota"),
|
||||||
|
(0x03BAu, "Greek Small Kappa"),
|
||||||
|
(0x03BBu, "Greek Small Lambda"),
|
||||||
|
(0x03BCu, "Greek Small Mu"),
|
||||||
|
(0x03BDu, "Greek Small Nu"),
|
||||||
|
(0x03BEu, "Greek Small Xi"),
|
||||||
|
(0x03BFu, "Greek Small Omicron"),
|
||||||
|
(0x03C0u, "Greek Small Pi"),
|
||||||
|
(0x03C1u, "Greek Small Rho"),
|
||||||
|
(0x03C3u, "Greek Small Sigma"),
|
||||||
|
(0x03C4u, "Greek Small Tau"),
|
||||||
|
(0x03C5u, "Greek Small Upsilon"),
|
||||||
|
(0x03C6u, "Greek Small Phi"),
|
||||||
|
(0x03C7u, "Greek Small Chi"),
|
||||||
|
(0x03C8u, "Greek Small Psi"),
|
||||||
|
(0x03C9u, "Greek Small Omega"),
|
||||||
|
(0x2013u, "En Dash"),
|
||||||
|
(0x2014u, "Em Dash"),
|
||||||
|
(0x2020u, "Dagger"),
|
||||||
|
(0x2021u, "Double Dagger"),
|
||||||
|
(0x2026u, "Horizontal Ellipsis"),
|
||||||
|
(0x203Bu, "Reference Mark"),
|
||||||
|
(0x20ACu, "Euro Sign"),
|
||||||
|
(0x2122u, "Trade Mark Sign"),
|
||||||
|
(0x2190u, "Leftwards Arrow"),
|
||||||
|
(0x2191u, "Upwards Arrow"),
|
||||||
|
(0x2192u, "Rightwards Arrow"),
|
||||||
|
(0x2193u, "Downwards Arrow"),
|
||||||
|
(0x21D2u, "Rightwards Double Arrow"),
|
||||||
|
(0x21D4u, "Left Right Double Arrow"),
|
||||||
|
(0x2202u, "Partial Differential"),
|
||||||
|
(0x2207u, "Nabla"),
|
||||||
|
(0x2211u, "Summation"),
|
||||||
|
(0x221Au, "Square Root"),
|
||||||
|
(0x221Eu, "Infinity"),
|
||||||
|
(0x222Bu, "Integral"),
|
||||||
|
(0x2260u, "Not Equal To"),
|
||||||
|
(0x25A0u, "Black Square"),
|
||||||
|
(0x25A1u, "White Square"),
|
||||||
|
(0x25B2u, "Black Up Triangle"),
|
||||||
|
(0x25B3u, "White Up Triangle"),
|
||||||
|
(0x25BCu, "Black Down Triangle"),
|
||||||
|
(0x25C6u, "Black Diamond"),
|
||||||
|
(0x25C7u, "White Diamond"),
|
||||||
|
(0x25CBu, "White Circle"),
|
||||||
|
(0x25CFu, "Black Circle"),
|
||||||
|
(0x2600u, "Black Sun With Rays"),
|
||||||
|
(0x2601u, "Cloud"),
|
||||||
|
(0x2602u, "Umbrella"),
|
||||||
|
(0x2603u, "Snowman"),
|
||||||
|
(0x2605u, "Black Star"),
|
||||||
|
(0x2606u, "White Star"),
|
||||||
|
(0x2640u, "Female Sign"),
|
||||||
|
(0x2642u, "Male Sign"),
|
||||||
|
(0x2660u, "Black Spade Suit"),
|
||||||
|
(0x2661u, "White Heart Suit"),
|
||||||
|
(0x2663u, "Black Club Suit"),
|
||||||
|
(0x2665u, "Black Heart Suit"),
|
||||||
|
(0x266Au, "Eighth Note"),
|
||||||
|
(0x2713u, "Check Mark"),
|
||||||
|
};
|
||||||
|
|
||||||
|
public void OpenPopup() => ImGui.OpenPopup(PopupId);
|
||||||
|
|
||||||
|
// Returns the inserted codepoint as a string fragment if the user clicked
|
||||||
|
// one this frame, or null otherwise. Caller splices the fragment into the
|
||||||
|
// chat-input buffer at the current cursor position.
|
||||||
|
public string? DrawAndConsume()
|
||||||
|
{
|
||||||
|
using var popup = ImRaii.Popup(PopupId);
|
||||||
|
if (!popup)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
string? inserted = null;
|
||||||
|
|
||||||
|
if (_recentUsed.Count > 0)
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled("Recent");
|
||||||
|
ImGui.SameLine();
|
||||||
|
foreach (var codepoint in _recentUsed)
|
||||||
|
{
|
||||||
|
var glyph = char.ConvertFromUtf32((int)codepoint);
|
||||||
|
if (
|
||||||
|
ImGui.Selectable(
|
||||||
|
glyph,
|
||||||
|
false,
|
||||||
|
ImGuiSelectableFlags.DontClosePopups,
|
||||||
|
new Vector2(20, 20)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
inserted = glyph;
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
}
|
||||||
|
ImGui.NewLine();
|
||||||
|
ImGui.Separator();
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var tabs = ImRaii.TabBar("##symbolpicker-tabs"))
|
||||||
|
{
|
||||||
|
if (tabs)
|
||||||
|
{
|
||||||
|
inserted = DrawPuaTab() ?? inserted;
|
||||||
|
inserted = DrawBmpTab() ?? inserted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inserted is not null)
|
||||||
|
TrackRecent(inserted);
|
||||||
|
|
||||||
|
return inserted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? DrawPuaTab()
|
||||||
|
{
|
||||||
|
using var tab = ImRaii.TabItem("FFXIV Icons");
|
||||||
|
if (!tab)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
ImGui.InputTextWithHint(
|
||||||
|
"##pua-search",
|
||||||
|
"Search by name (e.g. HighQuality)",
|
||||||
|
ref _search,
|
||||||
|
64
|
||||||
|
);
|
||||||
|
|
||||||
|
string? inserted = null;
|
||||||
|
|
||||||
|
if (ImGui.BeginChild("##pua-grid", new Vector2(0, 280), false))
|
||||||
|
{
|
||||||
|
var query = _search;
|
||||||
|
foreach (var icon in Enum.GetValues<SeIconChar>())
|
||||||
|
{
|
||||||
|
var label = icon.ToString();
|
||||||
|
if (
|
||||||
|
query.Length > 0
|
||||||
|
&& label.IndexOf(query, StringComparison.OrdinalIgnoreCase) < 0
|
||||||
|
)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (
|
||||||
|
ImGui.Selectable(
|
||||||
|
icon.ToIconString(),
|
||||||
|
false,
|
||||||
|
ImGuiSelectableFlags.DontClosePopups,
|
||||||
|
new Vector2(24, 24)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
inserted = icon.ToIconString();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip(label);
|
||||||
|
|
||||||
|
// Manual wrap — GetWindowContentRegionMax was deprecated in
|
||||||
|
// ImGui 1.92, so we compute the right edge ourselves.
|
||||||
|
var style = ImGui.GetStyle();
|
||||||
|
var lastItemX2 = ImGui.GetItemRectMax().X;
|
||||||
|
var availableRightX =
|
||||||
|
ImGui.GetCursorScreenPos().X + ImGui.GetContentRegionAvail().X;
|
||||||
|
if (lastItemX2 + style.ItemSpacing.X + 24f < availableRightX)
|
||||||
|
ImGui.SameLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui.EndChild();
|
||||||
|
|
||||||
|
return inserted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? DrawBmpTab()
|
||||||
|
{
|
||||||
|
using var tab = ImRaii.TabItem("Symbols");
|
||||||
|
if (!tab)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
ImGui.InputTextWithHint("##bmp-search", "Search by name (e.g. Heart)", ref _search, 64);
|
||||||
|
|
||||||
|
string? inserted = null;
|
||||||
|
|
||||||
|
if (ImGui.BeginChild("##bmp-grid", new Vector2(0, 280), false))
|
||||||
|
{
|
||||||
|
var query = _search;
|
||||||
|
foreach (var (codepoint, name) in BmpWhitelist)
|
||||||
|
{
|
||||||
|
if (query.Length > 0 && name.IndexOf(query, StringComparison.OrdinalIgnoreCase) < 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var glyph = char.ConvertFromUtf32((int)codepoint);
|
||||||
|
if (
|
||||||
|
ImGui.Selectable(
|
||||||
|
glyph,
|
||||||
|
false,
|
||||||
|
ImGuiSelectableFlags.DontClosePopups,
|
||||||
|
new Vector2(24, 24)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
inserted = glyph;
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip(name);
|
||||||
|
|
||||||
|
var style = ImGui.GetStyle();
|
||||||
|
var lastItemX2 = ImGui.GetItemRectMax().X;
|
||||||
|
var availableRightX =
|
||||||
|
ImGui.GetCursorScreenPos().X + ImGui.GetContentRegionAvail().X;
|
||||||
|
if (lastItemX2 + style.ItemSpacing.X + 24f < availableRightX)
|
||||||
|
ImGui.SameLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui.EndChild();
|
||||||
|
|
||||||
|
return inserted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TrackRecent(string fragment)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(fragment) || fragment.Length > 4)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var codepoint = (uint)char.ConvertToUtf32(fragment, 0);
|
||||||
|
|
||||||
|
// Move-to-front so the head stays the freshest pick.
|
||||||
|
_recentUsed.RemoveAll(c => c == codepoint);
|
||||||
|
_recentUsed.Insert(0, codepoint);
|
||||||
|
|
||||||
|
if (_recentUsed.Count > RecentCapacity)
|
||||||
|
_recentUsed.RemoveAt(_recentUsed.Count - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user