fix(style): reserve the height a wrapped description actually needs

Both widgets drew their description with a wrap width and then sized the row as
if it were always one line. Same bug, two different symptoms.

A setting row clips to its label column, so the second line was simply cut off,
directly under a comment claiming the text wraps rather than being cut. A
section header has no clip rect at all, so the overflow was drawn over its own
border line and into whatever the caller rendered next.

Both now measure with CalcTextSize against the same wrap width they draw with.
The section header also gained the clip rect it never had, and its lead offset
is computed once instead of twice, so the measured width and the drawn width
cannot drift apart.

Third fix, same family: the segmented control relied on its last InvisibleButton
to leave the cursor in the right place. It happened to work, because the loop
re-pins the cursor before every segment, but a caller cannot see that from the
outside -- and the gallery duly reserved the row a second time with Dummy,
doubling the gap after every segmented control. The widget now owns its advance
explicitly, like the other two, and CalcSize is gone with its only caller.
This commit is contained in:
2026-08-18 13:53:51 +02:00
parent f999036e50
commit 1c78a84b9b
4 changed files with 36 additions and 10 deletions
@@ -60,7 +60,16 @@ internal static class SectionHeader
var origin = ImGui.GetCursorScreenPos();
var width = ImGui.GetContentRegionAvail().X;
var titleHeight = ImGui.GetTextLineHeight();
var descHeight = description is null ? 0f : titleHeight;
// The description starts after the chevron, so its wrap width is the
// remainder of the row. Measuring it matters more here than in a setting
// row: nothing clips this text, so an unreserved second line would draw
// straight over the border line and whatever the caller renders next.
var leadWidth = (style.AccentBarWidth + style.ChevronInset * 2f) * scale;
var descWrap = width - leadWidth;
var descHeight = description is null
? 0f
: ImGui.CalcTextSize(description, false, descWrap).Y;
var size = WidgetGeometry.SectionHeader(width, titleHeight, descHeight, padY, 1f * scale);
ImGui.SetCursorScreenPos(origin);
@@ -95,7 +104,7 @@ internal static class SectionHeader
ColourUtil.ApplyAlpha(colors.AccentAbgr, alpha)
);
var textX = origin.X + barWidth + style.ChevronInset * scale * 2f;
var textX = origin.X + leadWidth;
dl.AddText(
new Vector2(textX, origin.Y + padY),
ColourUtil.ApplyAlpha(colors.TitleAbgr, alpha),
@@ -114,14 +123,18 @@ internal static class SectionHeader
);
if (description is not null)
{
dl.PushClipRect(origin, max, true);
dl.AddText(
ImGui.GetFont(),
ImGui.GetFontSize(),
new Vector2(textX, origin.Y + padY + titleHeight),
ColourUtil.ApplyAlpha(colors.DescriptionAbgr, alpha),
description,
width - (textX - origin.X)
descWrap
);
dl.PopClipRect();
}
dl.AddLine(
new Vector2(origin.X, max.Y - 1f * scale),
@@ -127,8 +127,15 @@ internal static class SegmentedControl
rounding
);
// 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.
ImGui.SetCursorScreenPos(origin);
ImGuiP.ItemSize(new Vector2(width, height));
return picked;
}
internal static Vector2 CalcSize(float width) => new(width, ImGui.GetFrameHeight());
}
@@ -73,14 +73,23 @@ internal static class SettingRow
var origin = ImGui.GetCursorScreenPos();
var width = ImGui.GetContentRegionAvail().X;
var lineHeight = ImGui.GetFrameHeight();
var descHeight = description is null ? 0f : ImGui.GetTextLineHeight();
var size = WidgetGeometry.SettingRow(width, lineHeight, descHeight, padY);
// Split first: the description is drawn with the label column as its
// wrap width, so its height cannot be known before that width is.
var (labelWidth, controlX, controlWidth) = WidgetGeometry.SettingRowSplit(
width,
style.PreferredControlWidth * scale,
gap
);
// Measured, not assumed to be one line. AddText wraps at labelWidth, so
// a description long enough to need a second line used to be drawn into
// height the row never reserved, and the clip rect below cut it off.
var descHeight = description is null
? 0f
: ImGui.CalcTextSize(description, false, labelWidth).Y;
var size = WidgetGeometry.SettingRow(width, lineHeight, descHeight, padY);
// The label half is the hit area: the control column submits its own
// item and would fight with a button underneath it.
ImGui.SetCursorScreenPos(origin);
@@ -304,7 +304,6 @@ internal sealed class WidgetGalleryWindow : Window
_segmentTwo,
colors
);
ImGui.Dummy(SegmentedControl.CalcSize(width));
_segmentThree = SegmentedControl.Draw(
ImGui.GetID("gallery.segmented.three"),
@@ -314,7 +313,6 @@ internal sealed class WidgetGalleryWindow : Window
_segmentThree,
colors
);
ImGui.Dummy(SegmentedControl.CalcSize(width));
// Odd width over three segments: the edges must stay flush with no seam
// and no overhang on the right.
@@ -327,7 +325,6 @@ internal sealed class WidgetGalleryWindow : Window
colors,
disabled: true
);
ImGui.Dummy(SegmentedControl.CalcSize(201f));
ImGui.Spacing();
}