diff --git a/editor/src/ThemeData.h b/editor/src/ThemeData.h new file mode 100644 index 0000000..b77e1fd --- /dev/null +++ b/editor/src/ThemeData.h @@ -0,0 +1,185 @@ +#pragma once +// Step 345-347: Theme data model — headless semantic color definitions +// Does NOT depend on ImGui; provides color values that can be applied to ImGui or other renderers + +#include +#include +#include + +struct Color4 { + float r = 0, g = 0, b = 0, a = 1.0f; + + Color4() = default; + Color4(float r_, float g_, float b_, float a_ = 1.0f) : r(r_), g(g_), b(b_), a(a_) {} + + // From hex "#RRGGBB" or "#RRGGBBAA" + static Color4 fromHex(const std::string& hex) { + Color4 c; + if (hex.size() >= 7 && hex[0] == '#') { + c.r = std::stoi(hex.substr(1, 2), nullptr, 16) / 255.0f; + c.g = std::stoi(hex.substr(3, 2), nullptr, 16) / 255.0f; + c.b = std::stoi(hex.substr(5, 2), nullptr, 16) / 255.0f; + if (hex.size() >= 9) c.a = std::stoi(hex.substr(7, 2), nullptr, 16) / 255.0f; + } + return c; + } + + std::string toHex() const { + char buf[10]; + snprintf(buf, sizeof(buf), "#%02x%02x%02x", + (int)(r * 255), (int)(g * 255), (int)(b * 255)); + return buf; + } + + bool isPureBlack() const { return r == 0 && g == 0 && b == 0; } + bool isPureWhite() const { return r == 1 && g == 1 && b == 1; } +}; + +struct WhetstoneTheme { + std::string name; + + // Backgrounds + Color4 bg, bgAlt, bgPanel, bgPopup; + + // Text + Color4 text, textDim, textAccent, textError, textWarning, textSuccess; + + // Borders + Color4 border, borderFocused; + + // Accent (primary brand color) + Color4 accent, accentHover, accentActive; + + // Selection + Color4 selectionBg, selectionText; + + // Scrollbar + Color4 scrollbar, scrollbarHover; + + // Diagnostics + Color4 diagError, diagWarning, diagInfo, diagHint; + + // Syntax highlighting + Color4 syntaxKeyword, syntaxString, syntaxNumber, syntaxComment; + Color4 syntaxFunction, syntaxType, syntaxOperator, syntaxAnnotation; + + // Font sizes + float fontSizePrimary = 14.0f; // monospace code font + float fontSizeUI = 13.0f; // sans-serif UI font + float fontSizeSmall = 11.0f; // labels, badges + + nlohmann::json toJson() const { + nlohmann::json j; + j["name"] = name; + j["bg"] = bg.toHex(); j["bgAlt"] = bgAlt.toHex(); + j["bgPanel"] = bgPanel.toHex(); j["bgPopup"] = bgPopup.toHex(); + j["text"] = text.toHex(); j["textDim"] = textDim.toHex(); + j["accent"] = accent.toHex(); + j["diagError"] = diagError.toHex(); j["diagWarning"] = diagWarning.toHex(); + j["syntaxKeyword"] = syntaxKeyword.toHex(); j["syntaxString"] = syntaxString.toHex(); + j["syntaxNumber"] = syntaxNumber.toHex(); j["syntaxComment"] = syntaxComment.toHex(); + j["fontSizePrimary"] = fontSizePrimary; + j["fontSizeUI"] = fontSizeUI; + return j; + } +}; + +inline WhetstoneTheme getDefaultDarkTheme() { + WhetstoneTheme t; + t.name = "Whetstone Dark"; + + // Background: deep blue-grey + t.bg = Color4::fromHex("#1a1d23"); + t.bgAlt = Color4::fromHex("#22252b"); + t.bgPanel = Color4::fromHex("#2a2d35"); + t.bgPopup = Color4::fromHex("#30333b"); + + // Text + t.text = Color4::fromHex("#e0e0e0"); + t.textDim = Color4::fromHex("#808890"); + t.textAccent = Color4::fromHex("#7aa2f7"); + t.textError = Color4::fromHex("#f7768e"); + t.textWarning = Color4::fromHex("#e0af68"); + t.textSuccess = Color4::fromHex("#9ece6a"); + + // Borders + t.border = Color4::fromHex("#3a3d45"); + t.borderFocused = Color4::fromHex("#7aa2f7"); + + // Accent + t.accent = Color4::fromHex("#7aa2f7"); + t.accentHover = Color4::fromHex("#89b4fa"); + t.accentActive = Color4::fromHex("#5d8fea"); + + // Selection + t.selectionBg = Color4::fromHex("#3d4f6f"); + t.selectionText = Color4::fromHex("#e0e0e0"); + + // Scrollbar + t.scrollbar = Color4::fromHex("#3a3d45"); + t.scrollbarHover = Color4::fromHex("#4a4d55"); + + // Diagnostics + t.diagError = Color4::fromHex("#f7768e"); + t.diagWarning = Color4::fromHex("#e0af68"); + t.diagInfo = Color4::fromHex("#7dcfff"); + t.diagHint = Color4::fromHex("#bb9af7"); + + // Syntax + t.syntaxKeyword = Color4::fromHex("#7aa2f7"); + t.syntaxString = Color4::fromHex("#9ece6a"); + t.syntaxNumber = Color4::fromHex("#ff9e64"); + t.syntaxComment = Color4::fromHex("#565f89"); + t.syntaxFunction = Color4::fromHex("#7aa2f7"); + t.syntaxType = Color4::fromHex("#2ac3de"); + t.syntaxOperator = Color4::fromHex("#89ddff"); + t.syntaxAnnotation = Color4::fromHex("#bb9af7"); + + return t; +} + +inline WhetstoneTheme getDefaultLightTheme() { + WhetstoneTheme t; + t.name = "Whetstone Light"; + + t.bg = Color4::fromHex("#f0f0f0"); + t.bgAlt = Color4::fromHex("#e8e8e8"); + t.bgPanel = Color4::fromHex("#ffffff"); + t.bgPopup = Color4::fromHex("#ffffff"); + + t.text = Color4::fromHex("#1a1a1a"); + t.textDim = Color4::fromHex("#6a6a6a"); + t.textAccent = Color4::fromHex("#2563eb"); + t.textError = Color4::fromHex("#dc2626"); + t.textWarning = Color4::fromHex("#d97706"); + t.textSuccess = Color4::fromHex("#16a34a"); + + t.border = Color4::fromHex("#d0d0d0"); + t.borderFocused = Color4::fromHex("#2563eb"); + + t.accent = Color4::fromHex("#2563eb"); + t.accentHover = Color4::fromHex("#3b82f6"); + t.accentActive = Color4::fromHex("#1d4ed8"); + + t.selectionBg = Color4::fromHex("#bfdbfe"); + t.selectionText = Color4::fromHex("#1a1a1a"); + + t.scrollbar = Color4::fromHex("#c0c0c0"); + t.scrollbarHover = Color4::fromHex("#a0a0a0"); + + t.diagError = Color4::fromHex("#dc2626"); + t.diagWarning = Color4::fromHex("#d97706"); + t.diagInfo = Color4::fromHex("#2563eb"); + t.diagHint = Color4::fromHex("#7c3aed"); + + t.syntaxKeyword = Color4::fromHex("#2563eb"); + t.syntaxString = Color4::fromHex("#16a34a"); + t.syntaxNumber = Color4::fromHex("#d97706"); + t.syntaxComment = Color4::fromHex("#6b7280"); + t.syntaxFunction = Color4::fromHex("#7c3aed"); + t.syntaxType = Color4::fromHex("#0891b2"); + t.syntaxOperator = Color4::fromHex("#374151"); + t.syntaxAnnotation = Color4::fromHex("#7c3aed"); + + return t; +} diff --git a/editor/tests/step345_test.cpp b/editor/tests/step345_test.cpp new file mode 100644 index 0000000..be59e5d --- /dev/null +++ b/editor/tests/step345_test.cpp @@ -0,0 +1,163 @@ +// Step 345: Theme Data + Tabbed Panel Logic (12 tests) +// Tests WhetstoneTheme, Color4, dark/light themes, and tabbed panel state + +#include +#include +#include +#include "ThemeData.h" +#include "PanelManager.h" + +int main() { + int passed = 0; + + // Test 1: Dark theme applies — all semantic colors defined + { + auto theme = getDefaultDarkTheme(); + assert(theme.name == "Whetstone Dark"); + assert(!theme.bg.isPureBlack()); + assert(!theme.text.isPureWhite()); + assert(!theme.bgAlt.isPureBlack()); + std::cout << "Test 1 PASSED: Dark theme colors defined\n"; + passed++; + } + + // Test 2: Light theme alternative + { + auto theme = getDefaultLightTheme(); + assert(theme.name == "Whetstone Light"); + assert(!theme.bg.isPureWhite()); // off-white, not pure + assert(!theme.text.isPureBlack()); // near-black + std::cout << "Test 2 PASSED: Light theme alternative\n"; + passed++; + } + + // Test 3: Color4 hex roundtrip + { + Color4 c = Color4::fromHex("#7aa2f7"); + std::string hex = c.toHex(); + assert(hex == "#7aa2f7"); + + Color4 c2 = Color4::fromHex("#ff9e64"); + assert(c2.toHex() == "#ff9e64"); + std::cout << "Test 3 PASSED: Color4 hex roundtrip\n"; + passed++; + } + + // Test 4: Syntax colors distinct from each other + { + auto theme = getDefaultDarkTheme(); + // At least keyword, string, number, comment should be different + assert(theme.syntaxKeyword.toHex() != theme.syntaxString.toHex()); + assert(theme.syntaxString.toHex() != theme.syntaxNumber.toHex()); + assert(theme.syntaxNumber.toHex() != theme.syntaxComment.toHex()); + assert(theme.syntaxComment.toHex() != theme.syntaxKeyword.toHex()); + std::cout << "Test 4 PASSED: Syntax colors distinct\n"; + passed++; + } + + // Test 5: Diagnostic colors match severity (error = red-ish, warning = amber-ish) + { + auto theme = getDefaultDarkTheme(); + // Error should be red-ish (high r, low g) + assert(theme.diagError.r > 0.5f); + // Warning should be amber-ish (high r, medium g) + assert(theme.diagWarning.r > 0.5f); + assert(theme.diagWarning.g > 0.3f); + // Info should be blue-ish + assert(theme.diagInfo.b > 0.5f); + std::cout << "Test 5 PASSED: Diagnostic colors appropriate for severity\n"; + passed++; + } + + // Test 6: Font sizes reasonable + { + auto theme = getDefaultDarkTheme(); + assert(theme.fontSizePrimary >= 10.0f && theme.fontSizePrimary <= 24.0f); + assert(theme.fontSizeUI >= 10.0f && theme.fontSizeUI <= 20.0f); + assert(theme.fontSizeSmall >= 8.0f && theme.fontSizeSmall <= 14.0f); + std::cout << "Test 6 PASSED: Font sizes reasonable\n"; + passed++; + } + + // Test 7: Theme JSON serialization + { + auto theme = getDefaultDarkTheme(); + auto j = theme.toJson(); + assert(j["name"] == "Whetstone Dark"); + assert(j.contains("bg")); + assert(j.contains("accent")); + assert(j.contains("syntaxKeyword")); + assert(j.contains("fontSizePrimary")); + std::cout << "Test 7 PASSED: Theme JSON serialization\n"; + passed++; + } + + // Test 8: Accent color used for focused elements + { + auto theme = getDefaultDarkTheme(); + assert(theme.accent.toHex() == theme.borderFocused.toHex()); + assert(theme.accent.toHex() == theme.textAccent.toHex()); + std::cout << "Test 8 PASSED: Accent color consistent\n"; + passed++; + } + + // Test 9: Tabbed bottom panel — hide one tab, others remain + { + PanelManager pm; + pm.registerDefaults(); + pm.setVisible("workflow", false); + auto bottom = pm.getPanelsByDock(DockPosition::Bottom); + assert(bottom.size() == 2); // diagnostics + output + std::cout << "Test 9 PASSED: Hide tab removes from dock\n"; + passed++; + } + + // Test 10: Tab ordering in bottom panel + { + PanelManager pm; + pm.registerDefaults(); + auto bottom = pm.getPanelsByDock(DockPosition::Bottom); + // Should be ordered by order field: diagnostics(5), output(6), workflow(7) + assert(bottom[0].id == "diagnostics"); + assert(bottom[1].id == "output"); + assert(bottom[2].id == "workflow"); + std::cout << "Test 10 PASSED: Tab ordering in bottom dock\n"; + passed++; + } + + // Test 11: Dark vs light contrast — text vs background + { + auto dark = getDefaultDarkTheme(); + // Dark theme: light text on dark bg + assert(dark.text.r > dark.bg.r); // text lighter than bg + assert(dark.text.g > dark.bg.g); + + auto light = getDefaultLightTheme(); + // Light theme: dark text on light bg + assert(light.text.r < light.bg.r); + assert(light.text.g < light.bg.g); + std::cout << "Test 11 PASSED: Dark/light contrast correct\n"; + passed++; + } + + // Test 12: Color4 component access + { + Color4 c(0.5f, 0.6f, 0.7f, 0.8f); + assert(c.r == 0.5f); + assert(c.g == 0.6f); + assert(c.b == 0.7f); + assert(c.a == 0.8f); + assert(!c.isPureBlack()); + assert(!c.isPureWhite()); + Color4 black(0, 0, 0); + assert(black.isPureBlack()); + Color4 white(1, 1, 1); + assert(white.isPureWhite()); + std::cout << "Test 12 PASSED: Color4 component access\n"; + passed++; + } + + std::cout << "\nResults: " << passed << "/12\n"; + assert(passed == 12); + return 0; +}