diff --git a/src/ui/windows/console_tab.cpp b/src/ui/windows/console_tab.cpp index c6f41ec..a43c5f3 100644 --- a/src/ui/windows/console_tab.cpp +++ b/src/ui/windows/console_tab.cpp @@ -90,12 +90,78 @@ bool isResultBodyChannel(ConsoleChannel ch) } // 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() { + using namespace material; 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()) { auto cmd = S.drawElement("console", "color-command"); auto res = S.drawElement("console", "color-result"); @@ -103,17 +169,6 @@ void ConsoleTab::refreshColors() auto dmn = S.drawElement("console", "color-daemon"); auto inf = S.drawElement("console", "color-info"); 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_RESULT = (dark && !res.color.empty()) ? S.resolveColor(res.color, defRes) : defRes; 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_RPC = (dark && !rpc.color.empty()) ? S.resolveColor(rpc.color, defRpc) : defRpc; } else { - // No schema — use hardcoded defaults per theme - COLOR_COMMAND = dark ? IM_COL32(191, 209, 229, 255) : IM_COL32(21, 101, 192, 255); - COLOR_RESULT = dark ? IM_COL32(200, 200, 200, 255) : IM_COL32(50, 50, 50, 255); - COLOR_ERROR = dark ? IM_COL32(246, 71, 64, 255) : IM_COL32(198, 40, 40, 255); - COLOR_DAEMON = dark ? IM_COL32(160, 160, 160, 180) : IM_COL32(90, 90, 90, 200); - COLOR_INFO = dark ? IM_COL32(191, 209, 229, 255) : IM_COL32(21, 101, 192, 255); - COLOR_RPC = dark ? IM_COL32(120, 180, 255, 210) : IM_COL32(25, 118, 210, 220); + COLOR_COMMAND = defCmd; + COLOR_RESULT = defRes; + COLOR_ERROR = defErr; + COLOR_DAEMON = defDmn; + COLOR_INFO = defInf; + COLOR_RPC = defRpc; } } @@ -135,19 +189,22 @@ ImU32 ConsoleTab::channelTextColor(ConsoleChannel channel) const { using namespace material; 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::Info: return COLOR_INFO; - case ConsoleChannel::Success: return WithAlpha(Success(), 255); - case ConsoleChannel::Warning: return Warning(); + case ConsoleChannel::Success: return FloorLight(WithAlpha(Success(), 255)); + case ConsoleChannel::Warning: return FloorLight(Warning()); case ConsoleChannel::Error: return COLOR_ERROR; case ConsoleChannel::Rpc: return COLOR_RPC; case ConsoleChannel::Daemon: return COLOR_DAEMON; case ConsoleChannel::Xmrig: return COLOR_DAEMON; case ConsoleChannel::App: return COLOR_INFO; // JSON syntax roles — highlight against the plain-result body. - case ConsoleChannel::JsonKey: return WithAlpha(Secondary(), 255); - case ConsoleChannel::JsonString: return WithAlpha(Success(), 255); - case ConsoleChannel::JsonNumber: return WithAlpha(Warning(), 255); + case ConsoleChannel::JsonKey: return FloorLight(WithAlpha(Secondary(), 255)); + case ConsoleChannel::JsonString: return FloorLight(WithAlpha(Success(), 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::None: default: return COLOR_RESULT; @@ -208,14 +265,16 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec) { using namespace material; - // Refresh the console theme colors on a dark/light switch. Line colors are derived - // from each line's channel at draw time, so no per-line remap is needed. + // Refresh the console theme colors whenever the theme changes. Keyed on the schema generation + // (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(); - bool nowDark = IsDarkTheme(); - if (nowDark != s_lastDark) { + static uint32_t s_lastGen = ~0u; + uint32_t gen = schema::UI().generation(); + if (gen != s_lastGen) { refreshColors(); - s_lastDark = nowDark; + s_lastGen = gen; } }