fix(style): close the remaining unreserved-space cases

Three more instances of the pattern the previous fix only half caught.

The section header clipped its description and not its title, so a translated
heading wider than the pane ran out over the scrollbar. Only the window clip
rect stopped it, not the content width.

Row's separator used an unscaled 1px offset with a scaled stroke. ImGui strokes
centred on the path, so at UI scale 2 half the line sits below the rectangle the
row reserved -- and rows stack flush, so it landed in the first pixel row of the
next one. Offset now derives from the thickness.

The segmented control claimed in its own comment to share the setting row's
contract. It did not. It took a position as a parameter and then re-pinned the
layout cursor to it, which moves a caller's layout whenever that position was
not the cursor, and it advanced by the full height where the other two subtract
ItemSpacing.Y. It now reads the cursor like they do, and the comment says why
the advance still differs: those two stack flush because they are list entries,
this is a single control and takes the normal gap.

Also: the label centres against the control band when it has no description
below it, since a slider draws its text at FramePadding.Y and a top-aligned
label sits visibly high next to it. Gallery arrays are held rather than rebuilt
per frame, which its own comment already demanded two sections earlier. And
SectionHeader.Reset is gone -- no caller, and section state keys off ids rather
than labels, so nothing needs to clear it on a language change.
This commit is contained in:
2026-08-18 14:38:42 +02:00
parent 0c3023c8cb
commit 64db8986e6
5 changed files with 46 additions and 22 deletions
+11 -3
View File
@@ -67,11 +67,19 @@ internal static class Row
}
if (style.DrawSeparator)
{
// Offset scales with the thickness. ImGui strokes centred on the
// path, so an unscaled 1px offset with a scaled stroke puts half the
// line below max.Y -- and rows stack flush, so that half lands in the
// first pixel row of the next one.
var thickness = Metrics.Scale;
var y = max.Y - thickness * 0.5f;
dl.AddLine(
new Vector2(origin.X, max.Y - 1f),
new Vector2(max.X, max.Y - 1f),
new Vector2(origin.X, y),
new Vector2(max.X, y),
state.BorderAbgr,
Metrics.Scale
thickness
);
}
}
}
@@ -35,8 +35,6 @@ internal static class SectionHeader
{
private static readonly Dictionary<uint, bool> Open = [];
internal static void Reset() => Open.Clear();
internal static bool Draw(
uint key,
string title,
@@ -105,11 +103,13 @@ internal static class SectionHeader
);
var textX = origin.X + leadWidth;
dl.PushClipRect(origin, max, true);
dl.AddText(
new Vector2(textX, origin.Y + padY),
ColourUtil.ApplyAlpha(colors.TitleAbgr, alpha),
title
);
dl.PopClipRect();
DrawChevron(
dl,
@@ -38,9 +38,13 @@ internal static class SegmentedControl
private static uint AnimKey(uint id, int index) =>
(id ^ (uint)(index + 1) * 0x85EBCA6Bu) * 2654435761u + 0x9E3779B9u;
// Reads the cursor rather than taking a position, like SettingRow and
// SectionHeader. It cannot avoid reserving space -- every segment submits an
// InvisibleButton, and those always advance -- so taking an origin would let
// a caller place it somewhere the reservation does not match.
// To right-align it inside a setting row, set the cursor before calling.
internal static int Draw(
uint id,
Vector2 origin,
float width,
ReadOnlySpan<string> labels,
int selected,
@@ -52,6 +56,7 @@ internal static class SegmentedControl
if (labels.Length == 0)
return selected;
var origin = ImGui.GetCursorScreenPos();
var style = styleOverride ?? new SegmentedControlStyle();
var scale = Metrics.Scale;
var rounding = style.Rounding * scale;
@@ -128,11 +133,12 @@ internal static class SegmentedControl
);
// Explicit, rather than inheriting whatever the last InvisibleButton left
// behind. The loop re-pins the cursor before every segment, so the run
// happens to end in the right place today -- but that is a side effect,
// and a caller cannot tell from here whether it still needs to reserve
// the row itself. Same contract as SettingRow and SectionHeader now:
// the widget owns its own advance.
// behind: the loop re-pins the cursor before every segment, so the run
// only ends in the right place as a side effect.
//
// The full height, unlike SettingRow and SectionHeader, which subtract
// ItemSpacing.Y. Those two stack flush on purpose because they are list
// entries. This is a single control and takes the normal gap.
ImGui.SetCursorScreenPos(origin);
ImGuiP.ItemSize(new Vector2(width, height));
@@ -124,8 +124,15 @@ internal static class SettingRow
// control. The description wraps instead of being cut.
var dl = ImGui.GetWindowDrawList();
dl.PushClipRect(origin, new Vector2(origin.X + labelWidth, origin.Y + size.Y), true);
// Without a description the label is alone next to the control, so it
// centres against the control band. A slider draws its text at
// FramePadding.Y, so a top-aligned label sits visibly high next to it.
// With a description the pair is top-aligned and this must not apply.
var labelY = description is null
? origin.Y + padY + MetricsMath.Center(lineHeight, ImGui.GetTextLineHeight())
: origin.Y + padY;
dl.AddText(
new Vector2(origin.X, origin.Y + padY),
new Vector2(origin.X, labelY),
ColourUtil.ApplyAlpha(colors.LabelAbgr, alpha),
label
);
+13 -10
View File
@@ -282,6 +282,16 @@ internal sealed class WidgetGalleryWindow : Window
private int _segmentTwo;
private int _segmentThree = 1;
// Held, not rebuilt per frame: a collection expression inside Draw
// allocates a fresh array on every frame the window is open.
private readonly int[] _badgeSamples = [1, 9, 0, 120];
private static readonly FontAwesomeIcon[] GalleryGlyphs =
[
FontAwesomeIcon.ArrowUpRightFromSquare,
FontAwesomeIcon.Check,
FontAwesomeIcon.CheckCircle,
];
private void DrawSegmentedSection(ThemeColors c)
{
var colors = new SegmentedControlColors
@@ -298,7 +308,6 @@ internal sealed class WidgetGalleryWindow : Window
_segmentTwo = SegmentedControl.Draw(
ImGui.GetID("gallery.segmented.two"),
ImGui.GetCursorScreenPos(),
width,
SegmentLabelsTwo,
_segmentTwo,
@@ -307,7 +316,6 @@ internal sealed class WidgetGalleryWindow : Window
_segmentThree = SegmentedControl.Draw(
ImGui.GetID("gallery.segmented.three"),
ImGui.GetCursorScreenPos(),
width,
SegmentLabelsThree,
_segmentThree,
@@ -318,7 +326,6 @@ internal sealed class WidgetGalleryWindow : Window
// and no overhang on the right.
SegmentedControl.Draw(
ImGui.GetID("gallery.segmented.odd"),
ImGui.GetCursorScreenPos(),
201f,
SegmentLabelsThree,
0,
@@ -334,7 +341,8 @@ internal sealed class WidgetGalleryWindow : Window
ImGui.TextUnformatted("Badge");
ImGui.SliderInt("count##badge", ref _badgeCount, 0, 150);
int[] samples = [1, 9, _badgeCount, 120];
_badgeSamples[2] = _badgeCount;
var samples = _badgeSamples;
var origin = ImGui.GetCursorScreenPos();
var x = origin.X;
foreach (var n in samples)
@@ -393,12 +401,7 @@ internal sealed class WidgetGalleryWindow : Window
ImGui.TextUnformatted("IconButton");
var size = new Vector2(Metrics.SidebarPopOutHitWidth, Metrics.SidebarRowHeight);
FontAwesomeIcon[] glyphs =
[
FontAwesomeIcon.ArrowUpRightFromSquare,
FontAwesomeIcon.Check,
FontAwesomeIcon.CheckCircle,
];
var glyphs = GalleryGlyphs;
for (var i = 0; i < glyphs.Length; i++)
{