Step 345: Theme Data + Tabbed Panel Logic (12/12 tests)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
185
editor/src/ThemeData.h
Normal file
185
editor/src/ThemeData.h
Normal file
@@ -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 <string>
|
||||
#include <map>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
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;
|
||||
}
|
||||
163
editor/tests/step345_test.cpp
Normal file
163
editor/tests/step345_test.cpp
Normal file
@@ -0,0 +1,163 @@
|
||||
// Step 345: Theme Data + Tabbed Panel Logic (12 tests)
|
||||
// Tests WhetstoneTheme, Color4, dark/light themes, and tabbed panel state
|
||||
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#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;
|
||||
}
|
||||
Reference in New Issue
Block a user