fix(console): readable theme-colored log text on light themes
The console log text washed out on Marble/Light (~1.2:1 contrast): refreshColors chose text colors from the stored IsDarkTheme() flag (default true, and only re-checked on a dark/light toggle) while the terminal surface used IsLightTheme(), so dark-theme pale text landed on the light surface. Tie both to the same live background-luminance predicate and refresh on any theme (schema generation) change. Light themes now derive each channel from that theme's own palette (Primary, OnSurface, Error, Secondary, ...) nudged to a WCAG contrast floor — on-theme AND high-contrast (measured 4.6-17.8:1 across the five light skins). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -90,12 +90,78 @@ bool isResultBodyChannel(ConsoleChannel ch)
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// ── WCAG contrast helpers — keep console text high-contrast while following each theme's palette. ──
|
||||||
|
inline float srgbToLinear(float c8) {
|
||||||
|
float c = c8 / 255.0f;
|
||||||
|
return c <= 0.04045f ? c / 12.92f : std::pow((c + 0.055f) / 1.055f, 2.4f);
|
||||||
|
}
|
||||||
|
inline float relLuminance(float r, float g, float b) {
|
||||||
|
return 0.2126f * srgbToLinear(r) + 0.7152f * srgbToLinear(g) + 0.0722f * srgbToLinear(b);
|
||||||
|
}
|
||||||
|
inline float contrastRatio(float lumA, float lumB) {
|
||||||
|
float hi = std::max(lumA, lumB), lo = std::min(lumA, lumB);
|
||||||
|
return (hi + 0.05f) / (lo + 0.05f);
|
||||||
|
}
|
||||||
|
// Nudge `fg` toward black (light surface) or white (dark surface), preserving hue + alpha, until its
|
||||||
|
// WCAG contrast against `bg` reaches `minRatio`. Colors already passing come back essentially unchanged.
|
||||||
|
inline ImU32 EnsureContrast(ImU32 fg, ImU32 bg, float minRatio) {
|
||||||
|
const float bgL = relLuminance((float)((bg >> IM_COL32_R_SHIFT) & 0xFF),
|
||||||
|
(float)((bg >> IM_COL32_G_SHIFT) & 0xFF),
|
||||||
|
(float)((bg >> IM_COL32_B_SHIFT) & 0xFF));
|
||||||
|
float r = (float)((fg >> IM_COL32_R_SHIFT) & 0xFF);
|
||||||
|
float g = (float)((fg >> IM_COL32_G_SHIFT) & 0xFF);
|
||||||
|
float b = (float)((fg >> IM_COL32_B_SHIFT) & 0xFF);
|
||||||
|
const int a = (int)((fg >> IM_COL32_A_SHIFT) & 0xFF);
|
||||||
|
const bool darken = bgL > 0.5f; // light surface -> darken the text; dark surface -> lighten it
|
||||||
|
for (int i = 0; i < 40 && contrastRatio(relLuminance(r, g, b), bgL) < minRatio; ++i) {
|
||||||
|
if (darken) { r *= 0.90f; g *= 0.90f; b *= 0.90f; }
|
||||||
|
else { r += (255.0f - r) * 0.12f; g += (255.0f - g) * 0.12f; b += (255.0f - b) * 0.12f; }
|
||||||
|
}
|
||||||
|
return IM_COL32((int)(r + 0.5f), (int)(g + 0.5f), (int)(b + 0.5f), a);
|
||||||
|
}
|
||||||
|
// Floor a dynamically-derived channel color to a readable contrast on light terminals; no-op on dark.
|
||||||
|
inline ImU32 FloorLight(ImU32 c) {
|
||||||
|
return material::IsLightTheme() ? EnsureContrast(c, IM_COL32(255, 255, 255, 255), 4.5f) : c;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void ConsoleTab::refreshColors()
|
void ConsoleTab::refreshColors()
|
||||||
{
|
{
|
||||||
|
using namespace material;
|
||||||
auto& S = schema::UI();
|
auto& S = schema::UI();
|
||||||
bool dark = material::IsDarkTheme();
|
// Derive light/dark from the SAME live background-luminance predicate the terminal surface uses
|
||||||
|
// (the overlay at line ~274 is chosen by IsLightTheme()). Using the stored IsDarkTheme() flag here
|
||||||
|
// let the two disagree — baking dark-theme (pale) text onto the light near-white console surface.
|
||||||
|
bool dark = !IsLightTheme();
|
||||||
|
|
||||||
// Try schema overrides first, then use sensible per-theme defaults
|
// Per-theme channel defaults.
|
||||||
|
ImU32 defCmd, defRes, defErr, defDmn, defInf, defRpc;
|
||||||
|
if (dark) {
|
||||||
|
// Dark terminal: keep the authored light-on-dark palette (schema overrides honored below).
|
||||||
|
defCmd = IM_COL32(191, 209, 229, 255);
|
||||||
|
defRes = IM_COL32(200, 200, 200, 255);
|
||||||
|
defErr = IM_COL32(246, 71, 64, 255);
|
||||||
|
defDmn = IM_COL32(160, 160, 160, 180);
|
||||||
|
defInf = IM_COL32(191, 209, 229, 255);
|
||||||
|
defRpc = IM_COL32(120, 180, 255, 210);
|
||||||
|
} else {
|
||||||
|
// Light terminal: follow THIS theme's own palette (Marble slate/taupe, Dune sand, Light blue, …)
|
||||||
|
// so the console reads on-theme — each color nudged toward black until it clears a WCAG contrast
|
||||||
|
// floor on the near-white console surface. High-contrast AND theme-colored.
|
||||||
|
const ImU32 W = IM_COL32(255, 255, 255, 255); // overlay is white(205); target pure white = safe floor
|
||||||
|
defCmd = EnsureContrast(Primary(), W, 4.5f); // command echo — theme primary
|
||||||
|
defRes = EnsureContrast(OnSurface(), W, 7.0f); // result body — theme text (already dark)
|
||||||
|
defErr = EnsureContrast(Error(), W, 4.5f); // errors — theme error
|
||||||
|
defDmn = WithAlpha(EnsureContrast(OnSurfaceMedium(), W, 4.5f), 235); // node log — dimmer, still legible
|
||||||
|
defInf = EnsureContrast(Primary(), W, 4.5f); // info / app log — theme primary
|
||||||
|
defRpc = EnsureContrast(Secondary(), W, 4.5f); // rpc trace — theme secondary
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schema console colors are authored for the dark terminal (light-on-dark); honor them only in dark
|
||||||
|
// themes. Light themes always use the palette-derived, contrast-floored defaults above.
|
||||||
if (S.isLoaded()) {
|
if (S.isLoaded()) {
|
||||||
auto cmd = S.drawElement("console", "color-command");
|
auto cmd = S.drawElement("console", "color-command");
|
||||||
auto res = S.drawElement("console", "color-result");
|
auto res = S.drawElement("console", "color-result");
|
||||||
@@ -103,17 +169,6 @@ void ConsoleTab::refreshColors()
|
|||||||
auto dmn = S.drawElement("console", "color-daemon");
|
auto dmn = S.drawElement("console", "color-daemon");
|
||||||
auto inf = S.drawElement("console", "color-info");
|
auto inf = S.drawElement("console", "color-info");
|
||||||
auto rpc = S.drawElement("console", "color-rpc");
|
auto rpc = S.drawElement("console", "color-rpc");
|
||||||
|
|
||||||
ImU32 defCmd = dark ? IM_COL32(191, 209, 229, 255) : IM_COL32(21, 101, 192, 255);
|
|
||||||
ImU32 defRes = dark ? IM_COL32(200, 200, 200, 255) : IM_COL32(50, 50, 50, 255);
|
|
||||||
ImU32 defErr = dark ? IM_COL32(246, 71, 64, 255) : IM_COL32(198, 40, 40, 255);
|
|
||||||
ImU32 defDmn = dark ? IM_COL32(160, 160, 160, 180) : IM_COL32(90, 90, 90, 200);
|
|
||||||
ImU32 defInf = dark ? IM_COL32(191, 209, 229, 255) : IM_COL32(21, 101, 192, 255);
|
|
||||||
ImU32 defRpc = dark ? IM_COL32(120, 180, 255, 210) : IM_COL32(25, 118, 210, 220);
|
|
||||||
|
|
||||||
// The schema console colors are authored for the dark terminal (light-on-dark); honor them only
|
|
||||||
// in dark themes. Light themes always use the dark-text defaults so log text stays legible on the
|
|
||||||
// now near-white console surface (otherwise the schema's light-blue text sat pale-on-white ~1.2:1).
|
|
||||||
COLOR_COMMAND = (dark && !cmd.color.empty()) ? S.resolveColor(cmd.color, defCmd) : defCmd;
|
COLOR_COMMAND = (dark && !cmd.color.empty()) ? S.resolveColor(cmd.color, defCmd) : defCmd;
|
||||||
COLOR_RESULT = (dark && !res.color.empty()) ? S.resolveColor(res.color, defRes) : defRes;
|
COLOR_RESULT = (dark && !res.color.empty()) ? S.resolveColor(res.color, defRes) : defRes;
|
||||||
COLOR_ERROR = (dark && !err.color.empty()) ? S.resolveColor(err.color, defErr) : defErr;
|
COLOR_ERROR = (dark && !err.color.empty()) ? S.resolveColor(err.color, defErr) : defErr;
|
||||||
@@ -121,13 +176,12 @@ void ConsoleTab::refreshColors()
|
|||||||
COLOR_INFO = (dark && !inf.color.empty()) ? S.resolveColor(inf.color, defInf) : defInf;
|
COLOR_INFO = (dark && !inf.color.empty()) ? S.resolveColor(inf.color, defInf) : defInf;
|
||||||
COLOR_RPC = (dark && !rpc.color.empty()) ? S.resolveColor(rpc.color, defRpc) : defRpc;
|
COLOR_RPC = (dark && !rpc.color.empty()) ? S.resolveColor(rpc.color, defRpc) : defRpc;
|
||||||
} else {
|
} else {
|
||||||
// No schema — use hardcoded defaults per theme
|
COLOR_COMMAND = defCmd;
|
||||||
COLOR_COMMAND = dark ? IM_COL32(191, 209, 229, 255) : IM_COL32(21, 101, 192, 255);
|
COLOR_RESULT = defRes;
|
||||||
COLOR_RESULT = dark ? IM_COL32(200, 200, 200, 255) : IM_COL32(50, 50, 50, 255);
|
COLOR_ERROR = defErr;
|
||||||
COLOR_ERROR = dark ? IM_COL32(246, 71, 64, 255) : IM_COL32(198, 40, 40, 255);
|
COLOR_DAEMON = defDmn;
|
||||||
COLOR_DAEMON = dark ? IM_COL32(160, 160, 160, 180) : IM_COL32(90, 90, 90, 200);
|
COLOR_INFO = defInf;
|
||||||
COLOR_INFO = dark ? IM_COL32(191, 209, 229, 255) : IM_COL32(21, 101, 192, 255);
|
COLOR_RPC = defRpc;
|
||||||
COLOR_RPC = dark ? IM_COL32(120, 180, 255, 210) : IM_COL32(25, 118, 210, 220);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,19 +189,22 @@ ImU32 ConsoleTab::channelTextColor(ConsoleChannel channel) const
|
|||||||
{
|
{
|
||||||
using namespace material;
|
using namespace material;
|
||||||
switch (channel) {
|
switch (channel) {
|
||||||
|
// COLOR_* channels are already palette-derived + contrast-floored in refreshColors(). The
|
||||||
|
// roles below are computed live from the theme palette, so floor them to a readable contrast on
|
||||||
|
// light terminals here (FloorLight is a no-op on dark themes, preserving the authored look).
|
||||||
case ConsoleChannel::Command: return COLOR_COMMAND;
|
case ConsoleChannel::Command: return COLOR_COMMAND;
|
||||||
case ConsoleChannel::Info: return COLOR_INFO;
|
case ConsoleChannel::Info: return COLOR_INFO;
|
||||||
case ConsoleChannel::Success: return WithAlpha(Success(), 255);
|
case ConsoleChannel::Success: return FloorLight(WithAlpha(Success(), 255));
|
||||||
case ConsoleChannel::Warning: return Warning();
|
case ConsoleChannel::Warning: return FloorLight(Warning());
|
||||||
case ConsoleChannel::Error: return COLOR_ERROR;
|
case ConsoleChannel::Error: return COLOR_ERROR;
|
||||||
case ConsoleChannel::Rpc: return COLOR_RPC;
|
case ConsoleChannel::Rpc: return COLOR_RPC;
|
||||||
case ConsoleChannel::Daemon: return COLOR_DAEMON;
|
case ConsoleChannel::Daemon: return COLOR_DAEMON;
|
||||||
case ConsoleChannel::Xmrig: return COLOR_DAEMON;
|
case ConsoleChannel::Xmrig: return COLOR_DAEMON;
|
||||||
case ConsoleChannel::App: return COLOR_INFO;
|
case ConsoleChannel::App: return COLOR_INFO;
|
||||||
// JSON syntax roles — highlight against the plain-result body.
|
// JSON syntax roles — highlight against the plain-result body.
|
||||||
case ConsoleChannel::JsonKey: return WithAlpha(Secondary(), 255);
|
case ConsoleChannel::JsonKey: return FloorLight(WithAlpha(Secondary(), 255));
|
||||||
case ConsoleChannel::JsonString: return WithAlpha(Success(), 255);
|
case ConsoleChannel::JsonString: return FloorLight(WithAlpha(Success(), 255));
|
||||||
case ConsoleChannel::JsonNumber: return WithAlpha(Warning(), 255);
|
case ConsoleChannel::JsonNumber: return FloorLight(WithAlpha(Warning(), 255));
|
||||||
case ConsoleChannel::JsonBrace: return IsLightTheme() ? IM_COL32(90, 90, 90, 180) : IM_COL32(200, 200, 200, 150);
|
case ConsoleChannel::JsonBrace: return IsLightTheme() ? IM_COL32(90, 90, 90, 180) : IM_COL32(200, 200, 200, 150);
|
||||||
case ConsoleChannel::None:
|
case ConsoleChannel::None:
|
||||||
default: return COLOR_RESULT;
|
default: return COLOR_RESULT;
|
||||||
@@ -208,14 +265,16 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec)
|
|||||||
{
|
{
|
||||||
using namespace material;
|
using namespace material;
|
||||||
|
|
||||||
// Refresh the console theme colors on a dark/light switch. Line colors are derived
|
// Refresh the console theme colors whenever the theme changes. Keyed on the schema generation
|
||||||
// from each line's channel at draw time, so no per-line remap is needed.
|
// (bumped on every theme/skin load) rather than a dark/light toggle: the constructor's refreshColors()
|
||||||
|
// can run before the theme is applied (s_isDarkTheme still defaults true), and a toggle-only trigger
|
||||||
|
// would never correct that on startup. Line colors are derived from each line's channel at draw time.
|
||||||
{
|
{
|
||||||
static bool s_lastDark = IsDarkTheme();
|
static uint32_t s_lastGen = ~0u;
|
||||||
bool nowDark = IsDarkTheme();
|
uint32_t gen = schema::UI().generation();
|
||||||
if (nowDark != s_lastDark) {
|
if (gen != s_lastGen) {
|
||||||
refreshColors();
|
refreshColors();
|
||||||
s_lastDark = nowDark;
|
s_lastGen = gen;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user