feat(sidebar): soften the seam and let the active tab reach its content

Tester feedback, relayed by Flo: the transition from the tab list to the chat
field is too hard, and the tabs could present themselves better. Three causes,
three changes, no structural touch -- tabs stay tabs, per the standing decision.

The full-width border line under every row was a ladder of hard cuts. It is a
fading rule now, starting past the icon column and dissolving before the right
edge -- the same shape the section headers have used since v1.11.0, at about
half the opacity.

The gap between the sidebar group and the message area was a bare strip of
window background with a hard edge on both sides. A faint surface wash fades
across it toward the messages, turning the cut into a seam.

And the selected tab bridges that gap: its active fill extends across the
spacing so it touches the conversation it selects -- the classic tab metaphor,
attached instead of adjacent. The bridge is a RowStyle knob (default zero), so
sidebar rows opt in and nothing else inherits it. Hover fills picked up the
standard three-pixel rounding on the way.
This commit is contained in:
2026-08-19 19:20:22 +02:00
parent 8a9692a71d
commit 12a445e08f
3 changed files with 49 additions and 6 deletions
+7
View File
@@ -317,6 +317,13 @@ internal sealed class Sidebar
),
AccentAbgr = _palette.Abgr(Token.AccentPrimary, colors),
BorderAbgr = ColourUtil.ApplyAlpha(_palette.Abgr(Token.Border, colors), opacity),
},
new RowStyle
{
// Across the gap between the sidebar group and the message
// area, so the selected tab touches the conversation it
// selects instead of ending at the seam.
ActiveBridgeWidth = ImGui.GetStyle().ItemSpacing.X,
}
);
+23 -6
View File
@@ -25,6 +25,12 @@ internal readonly record struct RowStyle
public float AccentBarWidth { get; init; } = 2f;
public bool DrawSeparator { get; init; } = true;
// Extends the active fill past the right edge by this many pixels, so the
// selected tab reads as attached to the content it selects -- the classic
// tab metaphor -- instead of ending at a hard seam. Zero for rows that do
// not sit next to the thing they select.
public float ActiveBridgeWidth { get; init; } = 0f;
}
internal static class Row
@@ -51,13 +57,19 @@ internal static class Row
// default is 0.85 -- an opaque fill per row would make the sidebar a
// solid block inside a translucent window.
if (state.IsActive)
dl.AddRectFilled(origin, max, state.SurfaceActiveAbgr);
{
var fillMax = max;
if (style.ActiveBridgeWidth > 0f)
fillMax.X += style.ActiveBridgeWidth;
dl.AddRectFilled(origin, fillMax, state.SurfaceActiveAbgr);
}
if (state.HoverAmount > 0f)
dl.AddRectFilled(
origin,
max,
ColourUtil.ApplyAlpha(state.SurfaceHoverAbgr, state.HoverAmount)
ColourUtil.ApplyAlpha(state.SurfaceHoverAbgr, state.HoverAmount),
3f * Metrics.Scale
);
if (state.IsActive && style.AccentBarWidth > 0f)
@@ -72,12 +84,17 @@ internal static class Row
// 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.
// A full-width line under every row reads as a ladder of hard
// cuts -- the tester's words were that the list felt too hard.
// The rule now starts past the icon column and fades to nothing
// before the right edge, the same shape the section headers use.
var thickness = Metrics.Scale;
var y = max.Y - thickness * 0.5f;
dl.AddLine(
new Vector2(origin.X, y),
new Vector2(max.X, y),
state.BorderAbgr,
var inset = 10f * Metrics.Scale;
dl.DrawFadeRule(
new Vector2(origin.X + inset, y),
size.X - inset,
ColourUtil.ApplyAlpha(state.BorderAbgr, 0.55f),
thickness
);
}
+19
View File
@@ -367,6 +367,25 @@ internal sealed class MainWindow : Window, IFocusableChatWindow
_sidebar.Draw(bodyWidth, tabs, ref _activeTab);
}
// The gap between the tab list and the conversation used to be a bare
// strip of window background with a hard edge on both sides -- tester
// feedback called the transition too hard. A faint surface wash that
// fades toward the messages turns the cut into a seam. The active row
// bridges across it (RowStyle.ActiveBridgeWidth), so the selected tab
// stays attached to its content on top of the wash.
{
var seamMin = new Vector2(ImGui.GetItemRectMax().X, ImGui.GetItemRectMin().Y);
var seamMax = new Vector2(
seamMin.X + ImGui.GetStyle().ItemSpacing.X,
ImGui.GetItemRectMax().Y
);
var wash = ColourUtil.ApplyAlpha(
ColourUtil.RgbaToAbgr(Plugin.Instance.ThemeRegistry.Active.Colors.Surface),
0.35f
);
ImGui.GetWindowDrawList().AddRectFilledMultiColor(seamMin, seamMax, wash, 0u, 0u, wash);
}
ImGui.SameLine();
using (ImRaii.Group())