diff --git a/HellionChat/Ui/Components/Settings/LivePreviewPanel.cs b/HellionChat/Ui/Components/Settings/LivePreviewPanel.cs index 383b095..ea99560 100644 --- a/HellionChat/Ui/Components/Settings/LivePreviewPanel.cs +++ b/HellionChat/Ui/Components/Settings/LivePreviewPanel.cs @@ -4,6 +4,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility.Raii; using HellionChat.Themes; using HellionChat.Ui.StyleEngine; +using HellionChat.Util; namespace HellionChat.Ui.Components.Settings; @@ -14,6 +15,20 @@ internal sealed class LivePreviewPanel : IDisposable // higher signals a Dispose skip and a subscriber leak against ThemeRegistry. internal static int InstanceCount; + // Plan-mandated mock strings — international tester-ready, do not localise. + private const string MockSystem = "System: Connection established"; + private const string MockSay = "Say: Hello, world!"; + private const string MockTell = "Tell → Player: Hey, want to party?"; + private const string MockFc = "FC: Welcome aboard."; + + // FontAwesome is intentionally not pulled in — crown/cog render as Unicode + // glyphs in the default font so this panel stays DI-light (Step 2 scope). + private const string CrownGlyph = "♛"; + private const string CogGlyph = "⚙"; + + private const float MiddleBandHeight = 220f; + private const float SidebarWidth = 70f; + private readonly ThemeRegistry _themes; private readonly TokenResolver _resolver; @@ -49,40 +64,256 @@ internal sealed class LivePreviewPanel : IDisposable DrawBrandBar(theme); DrawHonorificHeader(theme); + // Sidebar paints the left strip without advancing the cursor; the + // MessageList paints the right strip and reserves the full band. DrawSidebar(theme); DrawMessageList(theme); DrawInputBar(theme); DrawStatusBar(theme); } - private void DrawBrandBar(Theme theme) + private static void DrawBrandBar(Theme theme) { - // Mini gradient: PrimaryDark → Primary → PrimaryLight → PrimaryGlow. - // Implementation lands in next sub-step. + const float height = 24f; + var draw = ImGui.GetWindowDrawList(); + var origin = ImGui.GetCursorScreenPos(); + var width = ImGui.GetContentRegionAvail().X; + var max = new Vector2(origin.X + width, origin.Y + height); + + // Horizontal gradient split into three rects so all four primary + // slots (PrimaryDark/Primary/PrimaryLight/PrimaryGlow) drive the bar. + var pdAbgr = ColourUtil.RgbaToAbgr(theme.Colors.PrimaryDark); + var pAbgr = ColourUtil.RgbaToAbgr(theme.Colors.Primary); + var plAbgr = ColourUtil.RgbaToAbgr(theme.Colors.PrimaryLight); + var pgAbgr = ColourUtil.RgbaToAbgr(theme.Colors.PrimaryGlow); + + var third = width / 3f; + draw.AddRectFilledMultiColor( + origin, + new Vector2(origin.X + third, max.Y), + pdAbgr, + pAbgr, + pAbgr, + pdAbgr + ); + draw.AddRectFilledMultiColor( + new Vector2(origin.X + third, origin.Y), + new Vector2(origin.X + 2 * third, max.Y), + pAbgr, + plAbgr, + plAbgr, + pAbgr + ); + draw.AddRectFilledMultiColor( + new Vector2(origin.X + 2 * third, origin.Y), + max, + plAbgr, + pgAbgr, + pgAbgr, + plAbgr + ); + + var label = "HellionChat"; + var textSize = ImGui.CalcTextSize(label); + var textPos = new Vector2( + origin.X + (width - textSize.X) * 0.5f, + origin.Y + (height - textSize.Y) * 0.5f + ); + draw.AddText(textPos, ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary), label); + + ImGui.Dummy(new Vector2(width, height)); } - private void DrawHonorificHeader(Theme theme) + private static void DrawHonorificHeader(Theme theme) { - // Crown + Brackets-Title in Identity. Implementation in next sub-step. + const float height = 32f; + var draw = ImGui.GetWindowDrawList(); + var origin = ImGui.GetCursorScreenPos(); + var width = ImGui.GetContentRegionAvail().X; + + draw.AddLine( + origin, + new Vector2(origin.X + width, origin.Y), + ColourUtil.RgbaToAbgr(theme.Colors.Border), + 1f + ); + + var crownAbgr = ColourUtil.RgbaToAbgr(theme.Colors.Identity); + var textAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary); + var title = "«Champion» Preview"; + var crownSize = ImGui.CalcTextSize(CrownGlyph); + var titleSize = ImGui.CalcTextSize(title); + var totalWidth = crownSize.X + 4f + titleSize.X; + var startX = origin.X + (width - totalWidth) * 0.5f; + var y = origin.Y + (height - titleSize.Y) * 0.5f; + draw.AddText(new Vector2(startX, y), crownAbgr, CrownGlyph); + draw.AddText(new Vector2(startX + crownSize.X + 4f, y), textAbgr, title); + + ImGui.Dummy(new Vector2(width, height)); } - private void DrawSidebar(Theme theme) + private static void DrawSidebar(Theme theme) { - // 3 channel rows. Implementation in next sub-step. + // Paints the left strip of the horizontal middle band. MessageList + // consumes the cursor reservation for the full band height. + var draw = ImGui.GetWindowDrawList(); + var origin = ImGui.GetCursorScreenPos(); + var rowHeight = MiddleBandHeight / 3f; + var surface = ColourUtil.RgbaToAbgr(theme.Colors.Surface); + var surfaceHover = ColourUtil.RgbaToAbgr(theme.Colors.SurfaceHover); + var textAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary); + var primaryAbgr = ColourUtil.RgbaToAbgr(theme.Colors.Primary); + var accentAbgr = ColourUtil.RgbaToAbgr(theme.Colors.Accent); + + ReadOnlySpan labels = ["Linkshell", "Tell", "FC"]; + for (var i = 0; i < 3; i++) + { + var rowMin = new Vector2(origin.X, origin.Y + i * rowHeight); + var rowMax = new Vector2(origin.X + SidebarWidth, rowMin.Y + rowHeight); + var bg = i == 1 ? surfaceHover : surface; + draw.AddRectFilled(rowMin, rowMax, bg); + + if (i == 0) + { + draw.AddRectFilled(rowMin, new Vector2(rowMin.X + 2f, rowMax.Y), primaryAbgr); + } + + var labelSize = ImGui.CalcTextSize(labels[i]); + var textPos = new Vector2(rowMin.X + 6f, rowMin.Y + (rowHeight - labelSize.Y) * 0.5f); + draw.AddText(textPos, textAbgr, labels[i]); + + if (i == 1) + { + // Tell row carries an unread-dot in Accent on the right. + var dotCenter = new Vector2(rowMax.X - 8f, rowMin.Y + rowHeight * 0.5f); + draw.AddRectFilled( + new Vector2(dotCenter.X - 2f, dotCenter.Y - 2f), + new Vector2(dotCenter.X + 2f, dotCenter.Y + 2f), + accentAbgr + ); + } + } } - private void DrawMessageList(Theme theme) + private static void DrawMessageList(Theme theme) { - // 4 mock messages. Implementation in next sub-step. + var draw = ImGui.GetWindowDrawList(); + var origin = ImGui.GetCursorScreenPos(); + var totalWidth = ImGui.GetContentRegionAvail().X; + var listOrigin = new Vector2(origin.X + SidebarWidth, origin.Y); + var listWidth = totalWidth - SidebarWidth; + var max = new Vector2(listOrigin.X + listWidth, listOrigin.Y + MiddleBandHeight); + + draw.AddRectFilled(listOrigin, max, ColourUtil.RgbaToAbgr(theme.Colors.WindowBg)); + + var padMin = new Vector2(listOrigin.X + 2f, max.Y - 6f); + draw.AddRectFilled( + padMin, + new Vector2(max.X - 2f, max.Y - 2f), + ColourUtil.RgbaToAbgr(theme.Colors.FrameBg) + ); + + ReadOnlySpan<(string Text, uint Rgba)> rows = + [ + (MockSystem, theme.Colors.TextMuted), + (MockSay, theme.Colors.TextPrimary), + (MockTell, theme.Colors.StatusInfo), + (MockFc, theme.Colors.StatusSuccess), + ]; + + var lineHeight = ImGui.GetTextLineHeightWithSpacing(); + for (var i = 0; i < rows.Length; i++) + { + var pos = new Vector2(listOrigin.X + 6f, listOrigin.Y + 6f + i * lineHeight); + draw.AddText(pos, ColourUtil.RgbaToAbgr(rows[i].Rgba), rows[i].Text); + } + + draw.AddLine( + new Vector2(origin.X, max.Y - 1f), + new Vector2(origin.X + totalWidth, max.Y - 1f), + ColourUtil.RgbaToAbgr(theme.Colors.Border), + 1f + ); + + // Reserve the full middle-band height (sidebar overlays into the + // same vertical span and does not advance the cursor itself). + ImGui.Dummy(new Vector2(totalWidth, MiddleBandHeight)); } - private void DrawInputBar(Theme theme) + private static void DrawInputBar(Theme theme) { - // Pill + text + cog. Implementation in next sub-step. + const float height = 24f; + const float pillWidth = 50f; + var draw = ImGui.GetWindowDrawList(); + var origin = ImGui.GetCursorScreenPos(); + var width = ImGui.GetContentRegionAvail().X; + var max = new Vector2(origin.X + width, origin.Y + height); + + draw.AddRectFilled(origin, max, ColourUtil.RgbaToAbgr(theme.Colors.FrameBg)); + + var pillMin = new Vector2(origin.X + 4f, origin.Y + 4f); + var pillMax = new Vector2(origin.X + pillWidth, max.Y - 4f); + draw.AddRectFilled(pillMin, pillMax, ColourUtil.RgbaToAbgr(theme.Colors.Primary), 6f); + + var pillLabel = "Say"; + var pillLabelSize = ImGui.CalcTextSize(pillLabel); + var pillTextPos = new Vector2( + pillMin.X + ((pillMax.X - pillMin.X) - pillLabelSize.X) * 0.5f, + pillMin.Y + ((pillMax.Y - pillMin.Y) - pillLabelSize.Y) * 0.5f + ); + draw.AddText(pillTextPos, ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary), pillLabel); + + var placeholder = "Type a message..."; + var phSize = ImGui.CalcTextSize(placeholder); + var phPos = new Vector2(pillMax.X + 6f, origin.Y + (height - phSize.Y) * 0.5f); + draw.AddText(phPos, ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary), placeholder); + + var cogSize = ImGui.CalcTextSize(CogGlyph); + var cogPos = new Vector2(max.X - cogSize.X - 6f, origin.Y + (height - cogSize.Y) * 0.5f); + draw.AddText(cogPos, ColourUtil.RgbaToAbgr(theme.Colors.TextMuted), CogGlyph); + + ImGui.Dummy(new Vector2(width, height)); } - private void DrawStatusBar(Theme theme) + private static void DrawStatusBar(Theme theme) { - // Status icons. Implementation in next sub-step. + const float height = 20f; + const float iconSize = 8f; + const float iconGap = 6f; + var draw = ImGui.GetWindowDrawList(); + var origin = ImGui.GetCursorScreenPos(); + var width = ImGui.GetContentRegionAvail().X; + var max = new Vector2(origin.X + width, origin.Y + height); + + draw.AddRectFilled(origin, max, ColourUtil.RgbaToAbgr(theme.Colors.ChildBg)); + + ReadOnlySpan statusRgba = + [ + theme.Colors.StatusSuccess, + theme.Colors.StatusDanger, + theme.Colors.StatusWarning, + ]; + + var iconY = origin.Y + (height - iconSize) * 0.5f; + for (var i = 0; i < statusRgba.Length; i++) + { + var iconX = origin.X + 6f + i * (iconSize + iconGap); + draw.AddRectFilled( + new Vector2(iconX, iconY), + new Vector2(iconX + iconSize, iconY + iconSize), + ColourUtil.RgbaToAbgr(statusRgba[i]), + 2f + ); + } + + var label = "preview"; + var labelSize = ImGui.CalcTextSize(label); + var labelPos = new Vector2( + max.X - labelSize.X - 6f, + origin.Y + (height - labelSize.Y) * 0.5f + ); + draw.AddText(labelPos, ColourUtil.RgbaToAbgr(theme.Colors.TextDim), label); + + ImGui.Dummy(new Vector2(width, height)); } }