feat(settings): opaque settings window, contrast applied throughout
The settings window no longer inherits the chat window's transparency. GlobalStyleScope pushes one opacity for every window in the plugin, so a value chosen to keep the chat log out of the way was also deciding how readable a settings dialog is. The stronger reason is that contrast cannot be computed against a background that is not there. Behind a translucent window the real background is the game: a black cave one minute, a snowfield the next. Every foreground measured last commit was measured against a colour it does not actually land on. Opaque makes that measurement true. BgAlpha only reaches the window fill, never the draw list, so the backdrop cannot observe it -- it takes an explicit override instead of guessing from the pushed WindowBg. Contrast now runs through SettingsPalette, so every row and heading gets it without each call site asking: 4.5:1 for labels and titles, 3:1 for descriptions and accents. The lower floor on descriptions is deliberate. It preserves the rank between the two lines of a row, which is why the description is dimmer at all, while still guaranteeing it stays legible. The toggle's border is in there too. It is the only thing marking an off switch, and a border tone that blends into the surface leaves the control invisible.
This commit is contained in:
@@ -27,7 +27,9 @@ internal sealed class ContentArea
|
||||
return;
|
||||
}
|
||||
|
||||
_backdrop.Draw();
|
||||
// Fully opaque: SettingsWindow sets BgAlpha = 1, which the backdrop
|
||||
// cannot see for itself.
|
||||
_backdrop.Draw(opacityOverride: 1f);
|
||||
|
||||
renderTab(activeTab);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using HellionChat.Themes;
|
||||
using HellionChat.Ui.StyleEngine;
|
||||
using HellionChat.Ui.StyleEngine.Widgets;
|
||||
using HellionChat.Util;
|
||||
|
||||
namespace HellionChat.Ui.Components.Settings;
|
||||
|
||||
@@ -19,35 +20,66 @@ internal sealed class SettingsPalette
|
||||
_palette = new WidgetPalette(resolver);
|
||||
}
|
||||
|
||||
internal SettingRowColors Row(ThemeColors c) =>
|
||||
new()
|
||||
// Everything a caller reads is measured against the surface it lands on
|
||||
// rather than taken at face value. A theme sets one text colour; the pane
|
||||
// under it is tinted by the backdrop, and a muted tone that reads on the
|
||||
// base surface can disappear on the lit edge.
|
||||
//
|
||||
// 4.5:1 for the label, 3:1 for the description -- the lower floor keeps the
|
||||
// rank between the two lines, which is the whole reason the description is
|
||||
// dimmer, while still guaranteeing it stays readable.
|
||||
internal SettingRowColors Row(ThemeColors c)
|
||||
{
|
||||
var surface = _palette.Abgr(Token.SurfaceBase, c);
|
||||
return new SettingRowColors
|
||||
{
|
||||
LabelAbgr = _palette.Abgr(Token.Text, c),
|
||||
// A step below the label, not level with it: at TextMuted the two
|
||||
// lines carried the same weight and the row read as two settings.
|
||||
DescriptionAbgr = _palette.Abgr(Token.TextFaint, c),
|
||||
LabelAbgr = ColourUtil.EnsureContrast(_palette.Abgr(Token.Text, c), surface, 4.5f),
|
||||
DescriptionAbgr = ColourUtil.EnsureContrast(
|
||||
_palette.Abgr(Token.TextFaint, c),
|
||||
surface,
|
||||
3f
|
||||
),
|
||||
SurfaceHoverAbgr = _palette.Abgr(Token.SurfaceHover, c),
|
||||
BorderAbgr = _palette.Abgr(Token.Border, c),
|
||||
};
|
||||
}
|
||||
|
||||
internal ToggleSwitchColors Toggle(ThemeColors c) =>
|
||||
new()
|
||||
internal ToggleSwitchColors Toggle(ThemeColors c)
|
||||
{
|
||||
var surface = _palette.Abgr(Token.SurfaceBase, c);
|
||||
return new ToggleSwitchColors
|
||||
{
|
||||
TrackOffAbgr = _palette.Abgr(Token.SurfaceBase, c),
|
||||
TrackOffAbgr = surface,
|
||||
TrackOnAbgr = _palette.Abgr(Token.AccentPrimary, c),
|
||||
KnobAbgr = _palette.Abgr(Token.Text, c),
|
||||
BorderAbgr = _palette.Abgr(Token.Border, c),
|
||||
// The outline is the only thing that marks an off switch, so it has
|
||||
// to clear the surface it sits on rather than blend into it.
|
||||
BorderAbgr = ColourUtil.EnsureContrast(_palette.Abgr(Token.Border, c), surface, 3f),
|
||||
};
|
||||
}
|
||||
|
||||
internal SectionHeaderColors Section(ThemeColors c) =>
|
||||
new()
|
||||
internal SectionHeaderColors Section(ThemeColors c)
|
||||
{
|
||||
var surface = _palette.Abgr(Token.SurfaceBase, c);
|
||||
return new SectionHeaderColors
|
||||
{
|
||||
TitleAbgr = _palette.Abgr(Token.Text, c),
|
||||
DescriptionAbgr = _palette.Abgr(Token.TextMuted, c),
|
||||
AccentAbgr = _palette.Abgr(Token.AccentPrimary, c),
|
||||
TitleAbgr = ColourUtil.EnsureContrast(_palette.Abgr(Token.Text, c), surface, 4.5f),
|
||||
DescriptionAbgr = ColourUtil.EnsureContrast(
|
||||
_palette.Abgr(Token.TextMuted, c),
|
||||
surface,
|
||||
3f
|
||||
),
|
||||
// The chevron and the hover tint of the heading both use this, so it
|
||||
// has to clear the icon floor even when a theme picks a dim accent.
|
||||
AccentAbgr = ColourUtil.EnsureContrast(
|
||||
_palette.Abgr(Token.AccentPrimary, c),
|
||||
surface,
|
||||
3f
|
||||
),
|
||||
BorderAbgr = _palette.Abgr(Token.Border, c),
|
||||
HoverAbgr = _palette.Abgr(Token.SurfaceHover, c),
|
||||
};
|
||||
}
|
||||
|
||||
internal SegmentedControlColors Segmented(ThemeColors c) =>
|
||||
new()
|
||||
@@ -55,6 +87,8 @@ internal sealed class SettingsPalette
|
||||
TrackAbgr = _palette.Abgr(Token.SurfaceBase, c),
|
||||
SelectedAbgr = _palette.Abgr(Token.AccentPrimary, c),
|
||||
HoverAbgr = _palette.Abgr(Token.SurfaceHover, c),
|
||||
// Both are re-measured inside the widget against the fill each one
|
||||
// actually sits on, which differs per segment.
|
||||
LabelAbgr = _palette.Abgr(Token.TextMuted, c),
|
||||
SelectedLabelAbgr = _palette.Abgr(Token.Text, c),
|
||||
BorderAbgr = _palette.Abgr(Token.Border, c),
|
||||
|
||||
@@ -44,8 +44,9 @@ internal sealed class TabSidebar
|
||||
// its own background already; filling over it stacks a second layer and
|
||||
// turns a translucent window solid. This only needs to read as the
|
||||
// darker of two planes, and a tint does that.
|
||||
var opacity = ((ImGui.GetColorU32(ImGuiCol.WindowBg) >> 24) & 0xFFu) / 255f;
|
||||
dl.AddRectFilled(min, max, (uint)(0x38 * opacity) << 24);
|
||||
// Opaque, matching SettingsWindow.BgAlpha; the pushed WindowBg still
|
||||
// carries the chat window's transparency and would wash this out.
|
||||
dl.AddRectFilled(min, max, 0x38u << 24);
|
||||
|
||||
// The icon font has no ASCII glyphs, so anything textual drawn inside a
|
||||
// FontAwesome scope comes out blank -- twice bitten in this plugin. The
|
||||
|
||||
@@ -32,7 +32,8 @@ internal sealed class SurfaceBackdrop
|
||||
float accentWashHeight = 0.38f,
|
||||
float darken = 0f,
|
||||
float moteIntensity = 1f,
|
||||
float strength = 1f
|
||||
float strength = 1f,
|
||||
float? opacityOverride = null
|
||||
)
|
||||
{
|
||||
var dl = ImGui.GetWindowDrawList();
|
||||
@@ -50,7 +51,13 @@ internal sealed class SurfaceBackdrop
|
||||
// Near-transparent white over black instead: the window colour stays
|
||||
// visible underneath, the ramp only tints it, and the game showing
|
||||
// through breaks up any step that is left.
|
||||
var opacity = ((ImGui.GetColorU32(ImGuiCol.WindowBg) >> 24) & 0xFFu) / 255f;
|
||||
// Read from the pushed WindowBg by default, because BgAlpha only reaches
|
||||
// the window fill and not the draw list. A window that overrides its own
|
||||
// opacity has to say so -- SetNextWindowBgAlpha is invisible from here,
|
||||
// so the settings pane would otherwise tint itself for the chat window's
|
||||
// transparency while being fully opaque.
|
||||
var opacity =
|
||||
opacityOverride ?? ((ImGui.GetColorU32(ImGuiCol.WindowBg) >> 24) & 0xFFu) / 255f;
|
||||
|
||||
// Two short fades rather than one tall ramp: light from the top edge,
|
||||
// shadow gathering at the bottom, flat in between. A ramp stretched over
|
||||
|
||||
@@ -83,6 +83,18 @@ internal sealed class SettingsWindow : Window
|
||||
var wanted = $"{Language.Settings_Title.Format(Plugin.PluginName)}###chat2-settings";
|
||||
if (!string.Equals(WindowName, wanted, StringComparison.Ordinal))
|
||||
WindowName = wanted;
|
||||
|
||||
// Opaque, unlike the chat window. GlobalStyleScope pushes the chat's
|
||||
// opacity for every window in the plugin, and this one inherited it.
|
||||
//
|
||||
// Two reasons it should not. Contrast can only be computed against a
|
||||
// known background, and behind a translucent window the real background
|
||||
// is the game -- a black cave one minute, a snowfield the next, so the
|
||||
// colour every foreground was just measured against is not the colour it
|
||||
// lands on. And nobody adjusting a plugin needs to watch what is
|
||||
// happening behind the dialog; the chat window is the one that has to
|
||||
// stay out of the way.
|
||||
BgAlpha = 1f;
|
||||
}
|
||||
|
||||
public override void Draw()
|
||||
|
||||
Reference in New Issue
Block a user