fix(settings): align the theme preview with the real chrome

The preview had been showing surfaces, an accent bar and an unread marker for
months while the real sidebar drew none of them. Now that the sidebar has
caught up, the preview is the one that is wrong -- in two specific ways.

It put the accent bar on row 0 and the raised surface on row 1, so it showed
two half-active rows instead of one active row. Both now sit on row 0, and the
rows get the separator the real ones have.

The unread marker was a 4x4 square. It is a rounded count badge now.

The status bar preview was a separate 20px reimplementation with three coloured
squares and a hard-coded label. It mirrors the pill layout instead, with the
status colours riding along as slot dots so a theme still shows what it does to
them.
This commit is contained in:
2026-08-18 00:04:00 +02:00
parent a7ed8e1146
commit 0d6877ddc6
@@ -181,18 +181,29 @@ internal sealed class LivePreviewPanel : IDisposable
var primaryAbgr = ColourUtil.RgbaToAbgr(theme.Colors.Primary);
var accentAbgr = ColourUtil.RgbaToAbgr(theme.Colors.Accent);
// Row 0 is the active one and carries BOTH the raised surface and the
// accent bar, the way the real sidebar draws it. Until v1.10.0 those two
// sat on different rows here, so the preview promised a layout the
// sidebar never delivered -- and then the sidebar caught up.
var borderAbgr = ColourUtil.RgbaToAbgr(theme.Colors.Border);
ReadOnlySpan<string> 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);
var isActive = i == 0;
if (i == 0)
{
draw.AddRectFilled(rowMin, rowMax, isActive ? surfaceHover : surface);
if (isActive)
draw.AddRectFilled(rowMin, new Vector2(rowMin.X + 2f, rowMax.Y), primaryAbgr);
}
draw.AddLine(
new Vector2(rowMin.X, rowMax.Y - 1f),
new Vector2(rowMax.X, rowMax.Y - 1f),
borderAbgr,
1f
);
var labelSize = ImGui.CalcTextSize(labels[i]);
var textPos = new Vector2(rowMin.X + 6f, rowMin.Y + (rowHeight - labelSize.Y) * 0.5f);
@@ -200,13 +211,21 @@ internal sealed class LivePreviewPanel : IDisposable
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
// Unread marker: a rounded count badge in Accent, not a square.
var badgeH = MathF.Min(rowHeight - 4f, 14f);
var badgeW = badgeH * 1.4f;
var badgeMin = new Vector2(
rowMax.X - badgeW - 4f,
rowMin.Y + (rowHeight - badgeH) * 0.5f
);
var badgeMax = badgeMin + new Vector2(badgeW, badgeH);
draw.AddRectFilled(
badgeMin,
badgeMax,
(accentAbgr & 0x00FFFFFFu) | 0x38000000u,
badgeH * 0.5f
);
draw.AddRect(badgeMin, badgeMax, accentAbgr, badgeH * 0.5f);
}
}
}
@@ -298,44 +317,79 @@ internal sealed class LivePreviewPanel : IDisposable
ImGui.Dummy(new Vector2(width, height));
}
// Mirrors the real status bar: a top rule, then pill-shaped slots, the last
// one right-aligned. The status colours ride along as slot dots so a theme
// still shows what it does to them.
private static void DrawStatusBar(Theme theme)
{
const float height = 20f;
const float iconSize = 8f;
const float iconGap = 6f;
const float height = 24f;
const float pillH = 18f;
const float padX = 6f;
const float gap = 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));
draw.AddLine(
origin,
new Vector2(max.X, origin.Y),
ColourUtil.RgbaToAbgr(theme.Colors.Border),
1f
);
ReadOnlySpan<uint> statusRgba =
var fill = ColourUtil.RgbaToAbgr(theme.Colors.SurfaceHover);
var textAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary);
var pillY = origin.Y + (height - pillH) * 0.5f;
ReadOnlySpan<string> slots = ["Say", "open", "3 tabs"];
ReadOnlySpan<uint> dots =
[
theme.Colors.StatusSuccess,
theme.Colors.StatusDanger,
theme.Colors.StatusWarning,
theme.Colors.StatusDanger,
];
var iconY = origin.Y + (height - iconSize) * 0.5f;
for (var i = 0; i < statusRgba.Length; i++)
var x = origin.X + padX;
for (var i = 0; i < slots.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 labelSize = ImGui.CalcTextSize(slots[i]);
var slotW = labelSize.X + padX * 2f + 10f;
var slotMin = new Vector2(x, pillY);
var slotMax = new Vector2(x + slotW, pillY + pillH);
draw.AddRectFilled(slotMin, slotMax, fill, pillH * 0.5f);
draw.AddCircleFilled(
new Vector2(x + padX + 2f, pillY + pillH * 0.5f),
2.5f,
ColourUtil.RgbaToAbgr(dots[i]),
10
);
draw.AddText(
new Vector2(x + padX + 10f, pillY + (pillH - labelSize.Y) * 0.5f),
textAbgr,
slots[i]
);
x += slotW + gap;
}
var label = "preview";
var labelSize = ImGui.CalcTextSize(label);
var labelPos = new Vector2(
max.X - labelSize.X - 6f,
origin.Y + (height - labelSize.Y) * 0.5f
var versionSize = ImGui.CalcTextSize(label);
var versionW = versionSize.X + padX * 2f;
var versionMin = new Vector2(max.X - versionW - padX, pillY);
draw.AddRectFilled(
versionMin,
versionMin + new Vector2(versionW, pillH),
fill,
pillH * 0.5f
);
draw.AddText(
new Vector2(versionMin.X + padX, pillY + (pillH - versionSize.Y) * 0.5f),
ColourUtil.RgbaToAbgr(theme.Colors.TextDim),
label
);
draw.AddText(labelPos, ColourUtil.RgbaToAbgr(theme.Colors.TextDim), label);
ImGui.Dummy(new Vector2(width, height));
}