feat(style): headings by typography, surfaces by gradient
Two rounds of screenshots said the same thing twice: the section headings read against blue themes and vanished against violet ones. The cause was that their only distinction from a normal row was a fill colour, and no fill works in every palette. Headings are typography now. Small caps with wide tracking, no plate, no bar, followed by a rule that fades out along its length instead of stopping dead. The weight comes from letterforms and whitespace, which look the same in every theme. 18px of air above each one does the grouping that the bar used to fake. ImGui has no letter-spacing, so DrawTrackedText renders one glyph at a time through a stack buffer. That technique is documented in Character Select+, whose Boutique style layer solves exactly this problem the same way. Row separators are off by default. A rule under every row turns a settings page into a ledger, and the hover fill already marks where a row starts and ends. Surfaces get vertical gradients, derived from a single theme tone rather than a hardcoded pair, so they follow whatever the active theme sets. Lerp rather than a brightness multiplier: these surfaces sit near black, where scaling a channel of 12 by 1.14 lands back on 13. That is what LerpTowardBlack is for. The selected segment finally uses DrawSlipPolygon, which has been sitting in the plugin unused since it was written, and its highlight is a second, shorter chamfer rather than a clipped gradient -- PushClipRect is rectangular and would have squared the cut corner straight back off. Reference note: Lightless and Umbra were read for approach only. Both are AGPL-3.0 and none of their code is here; a vertical gradient is common knowledge, their implementation of it is theirs.
This commit is contained in:
@@ -29,17 +29,28 @@ internal sealed class ContentArea
|
|||||||
// fills it before the scrollbar and border, and the window's own opacity
|
// fills it before the scrollbar and border, and the window's own opacity
|
||||||
// still applies on top.
|
// still applies on top.
|
||||||
var colors = _themes.Active.Colors;
|
var colors = _themes.Active.Colors;
|
||||||
using var bg = ImRaii.PushColor(
|
|
||||||
ImGuiCol.ChildBg,
|
|
||||||
ColourUtil.RgbaToVector4(_resolver.Resolve(Token.SurfaceBase, colors))
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// Transparent child, gradient painted underneath. ChildBg only takes a
|
||||||
|
// flat colour, and a flat pane is what made the window read as a form
|
||||||
|
// dump: nothing told the eye where the surface started.
|
||||||
|
using var bg = ImRaii.PushColor(ImGuiCol.ChildBg, 0u);
|
||||||
using var child = ImRaii.Child("##settings-content", new Vector2(0, 0), true);
|
using var child = ImRaii.Child("##settings-content", new Vector2(0, 0), true);
|
||||||
if (!child.Success)
|
if (!child.Success)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetWindowDrawList inside the child is the child's own list, so this
|
||||||
|
// lands behind its content without a channel split.
|
||||||
|
var surface = ColourUtil.RgbaToAbgr(_resolver.Resolve(Token.SurfaceBase, colors));
|
||||||
|
ImGui
|
||||||
|
.GetWindowDrawList()
|
||||||
|
.DrawVerticalGradient(
|
||||||
|
ImGui.GetWindowPos(),
|
||||||
|
ImGui.GetWindowPos() + ImGui.GetWindowSize(),
|
||||||
|
surface
|
||||||
|
);
|
||||||
|
|
||||||
renderTab(activeTab);
|
renderTab(activeTab);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ internal sealed class SettingsPalette
|
|||||||
AccentAbgr = _palette.Abgr(Token.AccentPrimary, c),
|
AccentAbgr = _palette.Abgr(Token.AccentPrimary, c),
|
||||||
BorderAbgr = _palette.Abgr(Token.Border, c),
|
BorderAbgr = _palette.Abgr(Token.Border, c),
|
||||||
HoverAbgr = _palette.Abgr(Token.SurfaceHover, c),
|
HoverAbgr = _palette.Abgr(Token.SurfaceHover, c),
|
||||||
SurfaceAbgr = _palette.Abgr(Token.SurfaceRaised, c),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
internal SegmentedControlColors Segmented(ThemeColors c) =>
|
internal SegmentedControlColors Segmented(ThemeColors c) =>
|
||||||
|
|||||||
@@ -54,6 +54,94 @@ internal static class DrawListExtensions
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ImGui has no letter-spacing, so tracked text is drawn one glyph at a time
|
||||||
|
// with an extra gap between them. Small caps with wide tracking is what
|
||||||
|
// separates a heading from a label when both use the same font size -- and
|
||||||
|
// unlike colour, it reads the same in every theme.
|
||||||
|
//
|
||||||
|
// One char at a time through a stack buffer, so a heading costs no
|
||||||
|
// allocation per frame.
|
||||||
|
public static float DrawTrackedText(
|
||||||
|
this ImDrawListPtr dl,
|
||||||
|
Vector2 pos,
|
||||||
|
ReadOnlySpan<char> text,
|
||||||
|
uint abgr,
|
||||||
|
float trackPx
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Span<char> one = stackalloc char[1];
|
||||||
|
var x = pos.X;
|
||||||
|
for (var i = 0; i < text.Length; i++)
|
||||||
|
{
|
||||||
|
one[0] = text[i];
|
||||||
|
dl.AddText(new Vector2(x, pos.Y), abgr, one);
|
||||||
|
x += ImGui.CalcTextSize(one).X;
|
||||||
|
if (i < text.Length - 1)
|
||||||
|
x += trackPx;
|
||||||
|
}
|
||||||
|
|
||||||
|
return x - pos.X;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float MeasureTrackedText(ReadOnlySpan<char> text, float trackPx)
|
||||||
|
{
|
||||||
|
Span<char> one = stackalloc char[1];
|
||||||
|
var w = 0f;
|
||||||
|
for (var i = 0; i < text.Length; i++)
|
||||||
|
{
|
||||||
|
one[0] = text[i];
|
||||||
|
w += ImGui.CalcTextSize(one).X;
|
||||||
|
if (i < text.Length - 1)
|
||||||
|
w += trackPx;
|
||||||
|
}
|
||||||
|
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertical gradient from a single base colour, brightened at the top and
|
||||||
|
// darkened at the bottom. Deriving both ends from one tone keeps it working
|
||||||
|
// across every theme, where a hardcoded pair would only suit one of them.
|
||||||
|
//
|
||||||
|
// ImGui cannot put a gradient behind a child window -- ChildBg takes a flat
|
||||||
|
// colour -- so surfaces that want depth have to paint it themselves.
|
||||||
|
public static void DrawVerticalGradient(
|
||||||
|
this ImDrawListPtr dl,
|
||||||
|
Vector2 min,
|
||||||
|
Vector2 max,
|
||||||
|
uint baseAbgr,
|
||||||
|
float topLift = 0.14f,
|
||||||
|
float bottomDrop = 0.10f
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Lerp, not a multiplier: these surfaces sit near black, and scaling a
|
||||||
|
// channel of 12 by 1.14 lands back on 13.
|
||||||
|
var top = ColourUtil.LerpTowardWhite(baseAbgr, topLift);
|
||||||
|
var bottom = ColourUtil.LerpTowardBlack(baseAbgr, bottomDrop);
|
||||||
|
dl.AddRectFilledMultiColor(min, max, top, top, bottom, bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A rule that fades out along its length instead of stopping dead. A hard
|
||||||
|
// line boxes content in; a fading one suggests a boundary without drawing a
|
||||||
|
// wall, which is the whole difference between a heading and a header bar.
|
||||||
|
public static void DrawFadeRule(
|
||||||
|
this ImDrawListPtr dl,
|
||||||
|
Vector2 start,
|
||||||
|
float width,
|
||||||
|
uint abgr,
|
||||||
|
float thickness
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var transparent = abgr & 0x00FFFFFFu;
|
||||||
|
dl.AddRectFilledMultiColor(
|
||||||
|
start,
|
||||||
|
new Vector2(start.X + width, start.Y + thickness),
|
||||||
|
abgr,
|
||||||
|
transparent,
|
||||||
|
transparent,
|
||||||
|
abgr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public static void DrawGlowBorder(
|
public static void DrawGlowBorder(
|
||||||
this ImDrawListPtr dl,
|
this ImDrawListPtr dl,
|
||||||
Vector2 min,
|
Vector2 min,
|
||||||
|
|||||||
@@ -13,23 +13,27 @@ internal readonly record struct SectionHeaderColors
|
|||||||
public uint AccentAbgr { get; init; }
|
public uint AccentAbgr { get; init; }
|
||||||
public uint BorderAbgr { get; init; }
|
public uint BorderAbgr { get; init; }
|
||||||
public uint HoverAbgr { get; init; }
|
public uint HoverAbgr { get; init; }
|
||||||
|
|
||||||
// Own ground. Without it the heading floats between the rows at roughly
|
|
||||||
// their own weight and stops separating anything.
|
|
||||||
public uint SurfaceAbgr { get; init; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal readonly record struct SectionHeaderStyle
|
internal readonly record struct SectionHeaderStyle
|
||||||
{
|
{
|
||||||
public SectionHeaderStyle() { }
|
public SectionHeaderStyle() { }
|
||||||
|
|
||||||
public float PadY { get; init; } = 6f;
|
// Air above the heading, which is what actually groups the rows below it.
|
||||||
public float AccentBarWidth { get; init; } = 3f;
|
// A filled bar can be replaced by whitespace; whitespace cannot be replaced
|
||||||
public float ChevronInset { get; init; } = 6f;
|
// by a bar.
|
||||||
|
public float SpaceAbove { get; init; } = 18f;
|
||||||
|
public float SpaceBelow { get; init; } = 6f;
|
||||||
|
public float TrackPx { get; init; } = 1.6f;
|
||||||
|
public float ChevronGap { get; init; } = 7f;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collapsible section heading. Replaces ImGui.CollapsingHeader, whose framed
|
// Collapsible section heading, drawn as typography rather than as a bar.
|
||||||
// bar is the single most ImGui-looking element in the settings window.
|
//
|
||||||
|
// The bar version failed a real test: it read fine against blue themes and
|
||||||
|
// disappeared against violet ones, because its only distinction from a normal
|
||||||
|
// row was a fill colour. Small caps with wide tracking carries the same weight
|
||||||
|
// in every palette, since the difference is shape, not hue.
|
||||||
//
|
//
|
||||||
// State lives here rather than in ImGui's per-window storage: that keys off the
|
// State lives here rather than in ImGui's per-window storage: that keys off the
|
||||||
// label, so once the titles are localised the open/closed state would reset on
|
// label, so once the titles are localised the open/closed state would reset on
|
||||||
@@ -44,14 +48,16 @@ internal static class SectionHeader
|
|||||||
string title,
|
string title,
|
||||||
string? description,
|
string? description,
|
||||||
SectionHeaderColors colors,
|
SectionHeaderColors colors,
|
||||||
bool defaultOpen = false,
|
bool defaultOpen = true,
|
||||||
bool disabled = false,
|
bool disabled = false,
|
||||||
SectionHeaderStyle? styleOverride = null
|
SectionHeaderStyle? styleOverride = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var style = styleOverride ?? new SectionHeaderStyle();
|
var style = styleOverride ?? new SectionHeaderStyle();
|
||||||
var scale = Metrics.Scale;
|
var scale = Metrics.Scale;
|
||||||
var padY = style.PadY * scale;
|
var spaceAbove = style.SpaceAbove * scale;
|
||||||
|
var spaceBelow = style.SpaceBelow * scale;
|
||||||
|
var track = style.TrackPx * scale;
|
||||||
|
|
||||||
if (!Open.TryGetValue(key, out var open))
|
if (!Open.TryGetValue(key, out var open))
|
||||||
{
|
{
|
||||||
@@ -61,18 +67,19 @@ internal static class SectionHeader
|
|||||||
|
|
||||||
var origin = ImGui.GetCursorScreenPos();
|
var origin = ImGui.GetCursorScreenPos();
|
||||||
var width = ImGui.GetContentRegionAvail().X;
|
var width = ImGui.GetContentRegionAvail().X;
|
||||||
var titleHeight = ImGui.GetTextLineHeight();
|
var lineHeight = ImGui.GetTextLineHeight();
|
||||||
|
var chevronWidth = lineHeight * 0.45f + style.ChevronGap * scale;
|
||||||
|
|
||||||
// The description starts after the chevron, so its wrap width is the
|
var descWrap = width - chevronWidth;
|
||||||
// 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
|
var descHeight = description is null
|
||||||
? 0f
|
? 0f
|
||||||
: ImGui.CalcTextSize(description, false, descWrap).Y;
|
: ImGui.CalcTextSize(description, false, descWrap).Y;
|
||||||
var size = WidgetGeometry.SectionHeader(width, titleHeight, descHeight, padY, 1f * scale);
|
|
||||||
|
// The rule sits on the baseline gap, not on its own row.
|
||||||
|
var size = new Vector2(
|
||||||
|
width,
|
||||||
|
spaceAbove + lineHeight + spaceBelow + descHeight + (description is null ? 0f : 2f)
|
||||||
|
);
|
||||||
|
|
||||||
ImGui.SetCursorScreenPos(origin);
|
ImGui.SetCursorScreenPos(origin);
|
||||||
var clicked = ImGui.InvisibleButton($"##hellion-section-{key}", size) && !disabled;
|
var clicked = ImGui.InvisibleButton($"##hellion-section-{key}", size) && !disabled;
|
||||||
@@ -86,91 +93,88 @@ internal static class SectionHeader
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BeginDisabled only dims ImGui's own widgets, so a draw-list header
|
// BeginDisabled only dims ImGui's own widgets, so a draw-list header
|
||||||
// would stay at full opacity while everything around it fades. The
|
// would stay at full opacity while everything around it fades.
|
||||||
// ThemePicker wraps two of its headers exactly like that.
|
|
||||||
var alpha = disabled ? 0.5f : 1f;
|
var alpha = disabled ? 0.5f : 1f;
|
||||||
var dl = ImGui.GetWindowDrawList();
|
var dl = ImGui.GetWindowDrawList();
|
||||||
var max = origin + size;
|
var textY = origin.Y + spaceAbove;
|
||||||
|
|
||||||
dl.AddRectFilled(origin, max, ColourUtil.ApplyAlpha(colors.SurfaceAbgr, alpha));
|
// Hover brightens the heading itself. There is no plate to tint, and
|
||||||
|
// tinting the empty band above it would look like a stray selection.
|
||||||
if (hoverAmount > 0f)
|
var titleAbgr = ColourUtil.ApplyAlpha(
|
||||||
dl.AddRectFilled(
|
ColourUtil.Lerp(colors.TitleAbgr, colors.AccentAbgr, hoverAmount),
|
||||||
origin,
|
alpha
|
||||||
max,
|
|
||||||
ColourUtil.ApplyAlpha(colors.HoverAbgr, hoverAmount * alpha)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Full height, not just the title line. Stopping at the title left the
|
|
||||||
// bar looking like a stray tick next to a two-line heading.
|
|
||||||
var barWidth = style.AccentBarWidth * scale;
|
|
||||||
dl.AddRectFilled(
|
|
||||||
origin,
|
|
||||||
new Vector2(origin.X + barWidth, max.Y),
|
|
||||||
ColourUtil.ApplyAlpha(colors.AccentAbgr, alpha)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
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(
|
DrawChevron(
|
||||||
dl,
|
dl,
|
||||||
new Vector2(
|
new Vector2(origin.X + lineHeight * 0.18f, textY + lineHeight * 0.5f),
|
||||||
origin.X + barWidth + style.ChevronInset * scale,
|
lineHeight * 0.26f,
|
||||||
origin.Y + padY + titleHeight * 0.5f
|
|
||||||
),
|
|
||||||
titleHeight * 0.28f,
|
|
||||||
open,
|
open,
|
||||||
ColourUtil.ApplyAlpha(colors.TitleAbgr, alpha)
|
ColourUtil.ApplyAlpha(colors.AccentAbgr, alpha)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Upper-cased for the tracking to land: wide spacing between lowercase
|
||||||
|
// letters reads as a rendering fault, between caps as deliberate.
|
||||||
|
var textX = origin.X + chevronWidth;
|
||||||
|
dl.PushClipRect(origin, origin + size, true);
|
||||||
|
var titleWidth = dl.DrawTrackedText(
|
||||||
|
new Vector2(textX, textY),
|
||||||
|
title.ToUpperInvariant(),
|
||||||
|
titleAbgr,
|
||||||
|
track
|
||||||
|
);
|
||||||
|
|
||||||
|
// Starts where the title ends and fades into nothing, so it reads as a
|
||||||
|
// continuation of the heading rather than as a box lid.
|
||||||
|
var ruleX = textX + titleWidth + 10f * scale;
|
||||||
|
var ruleWidth = origin.X + width - ruleX;
|
||||||
|
if (ruleWidth > 0f)
|
||||||
|
dl.DrawFadeRule(
|
||||||
|
new Vector2(ruleX, textY + lineHeight * 0.5f),
|
||||||
|
ruleWidth,
|
||||||
|
ColourUtil.ApplyAlpha(colors.BorderAbgr, alpha),
|
||||||
|
MathF.Max(1f, scale)
|
||||||
|
);
|
||||||
|
|
||||||
if (description is not null)
|
if (description is not null)
|
||||||
{
|
|
||||||
dl.PushClipRect(origin, max, true);
|
|
||||||
dl.AddText(
|
dl.AddText(
|
||||||
ImGui.GetFont(),
|
ImGui.GetFont(),
|
||||||
ImGui.GetFontSize(),
|
ImGui.GetFontSize(),
|
||||||
new Vector2(textX, origin.Y + padY + titleHeight),
|
new Vector2(textX, textY + lineHeight + 2f * scale),
|
||||||
ColourUtil.ApplyAlpha(colors.DescriptionAbgr, alpha),
|
ColourUtil.ApplyAlpha(colors.DescriptionAbgr, alpha),
|
||||||
description,
|
description,
|
||||||
descWrap
|
descWrap
|
||||||
);
|
);
|
||||||
dl.PopClipRect();
|
dl.PopClipRect();
|
||||||
}
|
|
||||||
|
|
||||||
dl.AddLine(
|
|
||||||
new Vector2(origin.X, max.Y - 1f * scale),
|
|
||||||
new Vector2(max.X, max.Y - 1f * scale),
|
|
||||||
ColourUtil.ApplyAlpha(colors.BorderAbgr, alpha),
|
|
||||||
1f * scale
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// ItemSize, not SetCursorScreenPos: it advances the cursor AND extends
|
||||||
|
// CursorMaxPos, which is what the scrollbar measures.
|
||||||
ImGui.SetCursorScreenPos(origin);
|
ImGui.SetCursorScreenPos(origin);
|
||||||
ImGuiP.ItemSize(new Vector2(width, size.Y - ImGui.GetStyle().ItemSpacing.Y));
|
ImGuiP.ItemSize(new Vector2(width, size.Y - ImGui.GetStyle().ItemSpacing.Y));
|
||||||
|
|
||||||
return open;
|
return open;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void DrawChevron(ImDrawListPtr dl, Vector2 centre, float r, bool open, uint abgr)
|
private static void DrawChevron(
|
||||||
|
ImDrawListPtr dl,
|
||||||
|
Vector2 centre,
|
||||||
|
float radius,
|
||||||
|
bool open,
|
||||||
|
uint abgr
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (open)
|
if (open)
|
||||||
dl.AddTriangleFilled(
|
dl.AddTriangleFilled(
|
||||||
centre + new Vector2(-r, -r * 0.5f),
|
new Vector2(centre.X - radius, centre.Y - radius * 0.5f),
|
||||||
centre + new Vector2(r, -r * 0.5f),
|
new Vector2(centre.X + radius, centre.Y - radius * 0.5f),
|
||||||
centre + new Vector2(0f, r * 0.7f),
|
new Vector2(centre.X, centre.Y + radius * 0.75f),
|
||||||
abgr
|
abgr
|
||||||
);
|
);
|
||||||
else
|
else
|
||||||
dl.AddTriangleFilled(
|
dl.AddTriangleFilled(
|
||||||
centre + new Vector2(-r * 0.5f, -r),
|
new Vector2(centre.X - radius * 0.5f, centre.Y - radius),
|
||||||
centre + new Vector2(r * 0.7f, 0f),
|
new Vector2(centre.X - radius * 0.5f, centre.Y + radius),
|
||||||
centre + new Vector2(-r * 0.5f, r),
|
new Vector2(centre.X + radius * 0.75f, centre.Y),
|
||||||
abgr
|
abgr
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,12 @@ internal readonly record struct SegmentedControlStyle
|
|||||||
{
|
{
|
||||||
public SegmentedControlStyle() { }
|
public SegmentedControlStyle() { }
|
||||||
|
|
||||||
public float Rounding { get; init; } = 4f;
|
public float Rounding { get; init; } = 3f;
|
||||||
public float Inset { get; init; } = 2f;
|
public float Inset { get; init; } = 2f;
|
||||||
|
|
||||||
|
// Chamfer on the selected segment. The plugin already had DrawSlipPolygon
|
||||||
|
// for exactly this cut and had never called it once.
|
||||||
|
public float Chamfer { get; init; } = 5f;
|
||||||
}
|
}
|
||||||
|
|
||||||
// One setting, n mutually exclusive choices, one control. A radio group spends
|
// One setting, n mutually exclusive choices, one control. A radio group spends
|
||||||
@@ -65,11 +69,12 @@ internal static class SegmentedControl
|
|||||||
var alpha = disabled ? 0.5f : 1f;
|
var alpha = disabled ? 0.5f : 1f;
|
||||||
|
|
||||||
var dl = ImGui.GetWindowDrawList();
|
var dl = ImGui.GetWindowDrawList();
|
||||||
dl.AddRectFilled(
|
dl.DrawVerticalGradient(
|
||||||
origin,
|
origin,
|
||||||
origin + new Vector2(width, height),
|
origin + new Vector2(width, height),
|
||||||
ColourUtil.ApplyAlpha(colors.TrackAbgr, alpha),
|
ColourUtil.ApplyAlpha(colors.TrackAbgr, alpha),
|
||||||
rounding
|
topLift: 0.04f,
|
||||||
|
bottomDrop: 0.06f
|
||||||
);
|
);
|
||||||
|
|
||||||
var picked = selected;
|
var picked = selected;
|
||||||
@@ -91,12 +96,33 @@ internal static class SegmentedControl
|
|||||||
|
|
||||||
var isSelected = i == selected;
|
var isSelected = i == selected;
|
||||||
if (isSelected)
|
if (isSelected)
|
||||||
dl.AddRectFilled(
|
{
|
||||||
segOrigin + new Vector2(inset, inset),
|
// Chamfered rather than rounded: the cut corner is the shape the
|
||||||
segOrigin + segSize - new Vector2(inset, inset),
|
// rest of the plugin's HUD language uses, and it distinguishes
|
||||||
ColourUtil.ApplyAlpha(colors.SelectedAbgr, alpha),
|
// the active segment by silhouette instead of by fill alone.
|
||||||
rounding
|
var min = segOrigin + new Vector2(inset, inset);
|
||||||
|
var max = segOrigin + segSize - new Vector2(inset, inset);
|
||||||
|
var fill = ColourUtil.ApplyAlpha(colors.SelectedAbgr, alpha);
|
||||||
|
var cham = style.Chamfer * scale;
|
||||||
|
dl.DrawSlipPolygon(min, max, ColourUtil.RgbaToAbgr(fill), cham);
|
||||||
|
|
||||||
|
// Highlight as a second, shorter chamfered shape rather than a
|
||||||
|
// clipped gradient: PushClipRect is rectangular and would square
|
||||||
|
// the cut corner straight back off. Same silhouette, half the
|
||||||
|
// height, lifted toward white -- it reads as light from above
|
||||||
|
// without touching the outline.
|
||||||
|
dl.DrawSlipPolygon(
|
||||||
|
min,
|
||||||
|
new Vector2(max.X, min.Y + (max.Y - min.Y) * 0.5f),
|
||||||
|
ColourUtil.RgbaToAbgr(
|
||||||
|
ColourUtil.ApplyAlpha(
|
||||||
|
ColourUtil.LerpTowardWhite(fill, 0.16f),
|
||||||
|
0.55f * alpha
|
||||||
|
)
|
||||||
|
),
|
||||||
|
cham
|
||||||
);
|
);
|
||||||
|
}
|
||||||
else if (hoverAmount > 0f)
|
else if (hoverAmount > 0f)
|
||||||
dl.AddRectFilled(
|
dl.AddRectFilled(
|
||||||
segOrigin + new Vector2(inset, inset),
|
segOrigin + new Vector2(inset, inset),
|
||||||
|
|||||||
@@ -18,10 +18,14 @@ internal readonly record struct SettingRowStyle
|
|||||||
{
|
{
|
||||||
public SettingRowStyle() { }
|
public SettingRowStyle() { }
|
||||||
|
|
||||||
public float PadY { get; init; } = 4f;
|
public float PadY { get; init; } = 7f;
|
||||||
public float Gap { get; init; } = 12f;
|
public float Gap { get; init; } = 12f;
|
||||||
public float PreferredControlWidth { get; init; } = 200f;
|
public float PreferredControlWidth { get; init; } = 200f;
|
||||||
public bool DrawSeparator { get; init; } = true;
|
|
||||||
|
// Off by default. A rule under every row turns a settings page into a
|
||||||
|
// ledger; the hover fill already tells the reader where a row begins and
|
||||||
|
// ends, and it only appears where the pointer is.
|
||||||
|
public bool DrawSeparator { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handed to the control callback. Widgets that respect SetNextItemWidth can
|
// Handed to the control callback. Widgets that respect SetNextItemWidth can
|
||||||
|
|||||||
@@ -125,6 +125,20 @@ internal static class ColourUtil
|
|||||||
internal static uint OnColour(uint background, uint light, uint dark) =>
|
internal static uint OnColour(uint background, uint light, uint dark) =>
|
||||||
Luminance(background) > 0.5f ? dark : light;
|
Luminance(background) > 0.5f ? dark : light;
|
||||||
|
|
||||||
|
// Mixes an ABGR colour's RGB channels toward black by factor t, alpha
|
||||||
|
// untouched. The counterpart to LerpTowardWhite, and the reason both exist
|
||||||
|
// rather than AdjustBrightness: this plugin's surfaces sit near black, where
|
||||||
|
// a multiplier has almost nothing to scale. 12 * 1.15 is still 13.
|
||||||
|
internal static uint LerpTowardBlack(uint abgr, float t)
|
||||||
|
{
|
||||||
|
t = Math.Clamp(t, 0f, 1f);
|
||||||
|
var a = (byte)((abgr >> 24) & 0xFFu);
|
||||||
|
var b = (byte)Math.Round(((abgr >> 16) & 0xFFu) * (1f - t));
|
||||||
|
var g = (byte)Math.Round(((abgr >> 8) & 0xFFu) * (1f - t));
|
||||||
|
var r = (byte)Math.Round((abgr & 0xFFu) * (1f - t));
|
||||||
|
return ((uint)a << 24) | ((uint)b << 16) | ((uint)g << 8) | r;
|
||||||
|
}
|
||||||
|
|
||||||
internal static uint LerpTowardWhite(uint abgr, float t)
|
internal static uint LerpTowardWhite(uint abgr, float t)
|
||||||
{
|
{
|
||||||
t = Math.Clamp(t, 0f, 1f);
|
t = Math.Clamp(t, 0f, 1f);
|
||||||
|
|||||||
Reference in New Issue
Block a user