diff --git a/HellionChat/Ui/Components/Settings/ContentArea.cs b/HellionChat/Ui/Components/Settings/ContentArea.cs index 71e872a..160eb34 100644 --- a/HellionChat/Ui/Components/Settings/ContentArea.cs +++ b/HellionChat/Ui/Components/Settings/ContentArea.cs @@ -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); } diff --git a/HellionChat/Ui/Components/Settings/SettingsPalette.cs b/HellionChat/Ui/Components/Settings/SettingsPalette.cs index c7c24da..6d95cfc 100644 --- a/HellionChat/Ui/Components/Settings/SettingsPalette.cs +++ b/HellionChat/Ui/Components/Settings/SettingsPalette.cs @@ -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), diff --git a/HellionChat/Ui/Components/Settings/TabSidebar.cs b/HellionChat/Ui/Components/Settings/TabSidebar.cs index 4b08dce..335c252 100644 --- a/HellionChat/Ui/Components/Settings/TabSidebar.cs +++ b/HellionChat/Ui/Components/Settings/TabSidebar.cs @@ -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 diff --git a/HellionChat/Ui/StyleEngine/SurfaceBackdrop.cs b/HellionChat/Ui/StyleEngine/SurfaceBackdrop.cs index fb506b1..6990e1b 100644 --- a/HellionChat/Ui/StyleEngine/SurfaceBackdrop.cs +++ b/HellionChat/Ui/StyleEngine/SurfaceBackdrop.cs @@ -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 diff --git a/HellionChat/Ui/Windows/SettingsWindow.cs b/HellionChat/Ui/Windows/SettingsWindow.cs index 543b2e1..23b6455 100644 --- a/HellionChat/Ui/Windows/SettingsWindow.cs +++ b/HellionChat/Ui/Windows/SettingsWindow.cs @@ -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()