From 1c78a84b9b9fc351b74af39a23f18f0ad4e53c8d Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 18 Aug 2026 13:53:51 +0200 Subject: [PATCH] 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. --- .../Ui/StyleEngine/Widgets/SectionHeader.cs | 19 ++++++++++++++++--- .../StyleEngine/Widgets/SegmentedControl.cs | 11 +++++++++-- .../Ui/StyleEngine/Widgets/SettingRow.cs | 13 +++++++++++-- HellionChat/Ui/Windows/WidgetGalleryWindow.cs | 3 --- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/HellionChat/Ui/StyleEngine/Widgets/SectionHeader.cs b/HellionChat/Ui/StyleEngine/Widgets/SectionHeader.cs index 3aac2b6..f456543 100644 --- a/HellionChat/Ui/StyleEngine/Widgets/SectionHeader.cs +++ b/HellionChat/Ui/StyleEngine/Widgets/SectionHeader.cs @@ -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), diff --git a/HellionChat/Ui/StyleEngine/Widgets/SegmentedControl.cs b/HellionChat/Ui/StyleEngine/Widgets/SegmentedControl.cs index f04049a..1735b62 100644 --- a/HellionChat/Ui/StyleEngine/Widgets/SegmentedControl.cs +++ b/HellionChat/Ui/StyleEngine/Widgets/SegmentedControl.cs @@ -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()); } diff --git a/HellionChat/Ui/StyleEngine/Widgets/SettingRow.cs b/HellionChat/Ui/StyleEngine/Widgets/SettingRow.cs index 32385ba..71cdf75 100644 --- a/HellionChat/Ui/StyleEngine/Widgets/SettingRow.cs +++ b/HellionChat/Ui/StyleEngine/Widgets/SettingRow.cs @@ -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); diff --git a/HellionChat/Ui/Windows/WidgetGalleryWindow.cs b/HellionChat/Ui/Windows/WidgetGalleryWindow.cs index cb9c9fa..7695600 100644 --- a/HellionChat/Ui/Windows/WidgetGalleryWindow.cs +++ b/HellionChat/Ui/Windows/WidgetGalleryWindow.cs @@ -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(); }