Step 160: add theme engine and JSON themes

This commit is contained in:
Bill
2026-02-09 19:49:03 -07:00
parent 3d919d39c5
commit 300cf070b2
15 changed files with 863 additions and 43 deletions

View File

@@ -507,3 +507,4 @@ Sprint 5 in progress. Step 141 (Emacs daemon with user config) done. Next: Step
| 2026-02-10 | Codex | Step 157: Multi-agent roles with permission policy, agent provenance in transform history, and UI role controls. 8/8 tests pass. |
| 2026-02-10 | Codex | Step 158: Agent workflow recorder with JSON export, replay support, and RPC wiring. 5/5 tests pass. |
| 2026-02-10 | Codex | Step 159: Agent marketplace registry with UI panel, install toggles, and registry serialization. 6/6 tests pass. |
| 2026-02-10 | Codex | Step 160: Theme engine with JSON themes, ImGui styling, and syntax/editor color mapping. 4/4 tests pass. |

View File

@@ -894,6 +894,10 @@ add_executable(step159_test tests/step159_test.cpp)
target_include_directories(step159_test PRIVATE src)
target_link_libraries(step159_test PRIVATE nlohmann_json::nlohmann_json)
add_executable(step160_test tests/step160_test.cpp)
target_include_directories(step160_test PRIVATE src)
target_link_libraries(step160_test PRIVATE nlohmann_json::nlohmann_json imgui::imgui)
find_package(SDL2 CONFIG REQUIRED)
find_package(OpenGL REQUIRED)
find_package(glad CONFIG REQUIRED)

View File

@@ -8,6 +8,7 @@
#include "imgui.h"
#include "SyntaxHighlighter.h"
#include "ThemeEngine.h"
#include "EditorMode.h"
#include <string>
#include <vector>
@@ -306,7 +307,10 @@ public:
// Gutter background
ImVec2 gutterA(origin.x, y);
ImVec2 gutterB(origin.x + gutterWidth, y + lineHeight);
drawList->AddRectFilled(gutterA, gutterB, IM_COL32(20, 20, 20, 255));
drawList->AddRectFilled(
gutterA, gutterB,
ThemeEngine::instance().editorColor("gutter_bg",
IM_COL32(20, 20, 20, 255)));
// Line number (right-aligned)
if (options.showLineNumbers) {
@@ -314,14 +318,19 @@ public:
snprintf(numBuf, sizeof(numBuf), "%d", ln + 1);
ImVec2 numSize = font->CalcTextSizeA(font->FontSize, FLT_MAX, -1.0f, numBuf);
ImVec2 numPos(origin.x + gutterWidth - 4.0f - numSize.x, y);
drawList->AddText(font, font->FontSize, numPos, IM_COL32(120, 120, 120, 255), numBuf);
drawList->AddText(font, font->FontSize, numPos,
ThemeEngine::instance().editorColor("gutter_text",
IM_COL32(120, 120, 120, 255)),
numBuf);
}
// Diagnostics marker
bool hasError = lineIn(options.errorLines, ln);
bool hasWarn = lineIn(options.warningLines, ln);
if (hasError || hasWarn) {
ImU32 color = hasError ? IM_COL32(220, 80, 80, 255) : IM_COL32(220, 160, 60, 255);
ImU32 color = hasError
? ThemeEngine::instance().editorColor("diag_error", IM_COL32(220, 80, 80, 255))
: ThemeEngine::instance().editorColor("diag_warning", IM_COL32(220, 160, 60, 255));
ImVec2 center(origin.x + 3.0f, y + lineHeight * 0.5f);
drawList->AddCircleFilled(center, 3.0f, color);
}
@@ -361,7 +370,8 @@ public:
SuggestionMarker suggestion;
if (suggestionAtLine(*options.suggestions, ln, suggestion)) {
ImVec2 center(origin.x + 20.0f, y + lineHeight * 0.5f);
ImU32 color = IM_COL32(240, 200, 40, 255);
ImU32 color = ThemeEngine::instance().editorColor("suggestion",
IM_COL32(240, 200, 40, 255));
drawList->AddCircleFilled(center, 3.0f, color);
ImVec2 a(center.x - 4.0f, center.y - 4.0f);
ImVec2 b(center.x + 4.0f, center.y + 4.0f);
@@ -384,14 +394,19 @@ public:
AnnotationConflictMarker conflict;
if (conflictAtLine(*options.conflicts, ln, conflict)) {
ImVec2 center(origin.x + 12.0f, y + lineHeight * 0.5f);
drawList->AddCircle(center, 5.0f, IM_COL32(220, 80, 80, 255), 12, 1.5f);
drawList->AddCircle(center, 5.0f,
ThemeEngine::instance().editorColor("conflict",
IM_COL32(220, 80, 80, 255)),
12, 1.5f);
if (ln == std::min(conflict.childLine, conflict.parentLine) &&
conflict.childLine >= 0 && conflict.parentLine >= 0) {
float y1 = textBase.y + conflict.childLine * lineHeight + lineHeight * 0.5f;
float y2 = textBase.y + conflict.parentLine * lineHeight + lineHeight * 0.5f;
drawList->AddLine(ImVec2(origin.x + 12.0f, y1),
ImVec2(origin.x + 12.0f, y2),
IM_COL32(220, 80, 80, 180), 1.0f);
ThemeEngine::instance().editorColor("conflict_line",
IM_COL32(220, 80, 80, 180)),
1.0f);
}
ImVec2 a(center.x - 5.0f, center.y - 5.0f);
ImVec2 b(center.x + 5.0f, center.y + 5.0f);
@@ -408,9 +423,15 @@ public:
if (fold) {
ImVec2 triCenter(origin.x + 6.0f, y + lineHeight * 0.5f);
if (fold->folded) {
drawTriangle(drawList, triCenter, IM_COL32(160, 160, 160, 255), true);
drawTriangle(drawList, triCenter,
ThemeEngine::instance().editorColor("fold_indicator",
IM_COL32(160, 160, 160, 255)),
true);
} else {
drawTriangle(drawList, triCenter, IM_COL32(160, 160, 160, 255), false);
drawTriangle(drawList, triCenter,
ThemeEngine::instance().editorColor("fold_indicator",
IM_COL32(160, 160, 160, 255)),
false);
}
}
@@ -418,7 +439,9 @@ public:
if (options.showCurrentLine && ln == currentLine) {
ImVec2 hlA(textBase.x, y);
ImVec2 hlB(textBase.x + textAreaWidth, y + lineHeight);
drawList->AddRectFilled(hlA, hlB, IM_COL32(40, 40, 40, 120));
drawList->AddRectFilled(hlA, hlB,
ThemeEngine::instance().editorColor("line_highlight",
IM_COL32(40, 40, 40, 120)));
}
if (options.highlightLine >= 0 && ln == options.highlightLine) {
ImVec2 hlA(textBase.x, y);
@@ -445,7 +468,9 @@ public:
int colB = lineSelEnd - start;
ImVec2 a(textBase.x + colA * charAdvance, y);
ImVec2 b(textBase.x + colB * charAdvance, y + lineHeight);
drawList->AddRectFilled(a, b, IM_COL32(60, 100, 160, 120));
drawList->AddRectFilled(a, b,
ThemeEngine::instance().editorColor("selection",
IM_COL32(60, 100, 160, 120)));
}
}
@@ -494,7 +519,10 @@ public:
ImVec2 textSize = font->CalcTextSizeA(font->FontSize, FLT_MAX, -1.0f, marker.message.c_str());
ImVec2 bgA(tagPos.x - 2.0f, tagPos.y);
ImVec2 bgB(tagPos.x + textSize.x + 6.0f, tagPos.y + lineHeight * 0.8f);
drawList->AddRectFilled(bgA, bgB, IM_COL32(30, 30, 30, 220), 2.0f);
drawList->AddRectFilled(bgA, bgB,
ThemeEngine::instance().editorColor("annotation_tag_bg",
IM_COL32(30, 30, 30, 220)),
2.0f);
drawList->AddText(font, font->FontSize, ImVec2(tagPos.x + 2.0f, tagPos.y),
marker.color, marker.message.c_str());
}
@@ -503,7 +531,10 @@ public:
// Folded placeholder
if (fold && fold->folded) {
ImVec2 p(textBase.x + len * charAdvance + 6.0f, y);
drawList->AddText(font, font->FontSize, p, IM_COL32(140, 140, 140, 255), "{...}");
drawList->AddText(font, font->FontSize, p,
ThemeEngine::instance().editorColor("fold_placeholder",
IM_COL32(140, 140, 140, 255)),
"{...}");
}
if (options.diagnostics) {
@@ -539,7 +570,10 @@ public:
int col = cursor_ - lineStart;
float x = textBase.x + col * charAdvance;
float y = textBase.y + curLine * lineHeight;
drawList->AddLine(ImVec2(x, y), ImVec2(x, y + lineHeight), IM_COL32(240, 240, 240, 255), 1.0f);
drawList->AddLine(ImVec2(x, y), ImVec2(x, y + lineHeight),
ThemeEngine::instance().editorColor("caret",
IM_COL32(240, 240, 240, 255)),
1.0f);
}
}
@@ -548,11 +582,17 @@ public:
float miniHeight = lineCount * lineHeight;
ImVec2 miniA(minimapBaseX, minimapBaseY);
ImVec2 miniB(minimapBaseX + minimapWidth, minimapBaseY + miniHeight);
drawList->AddRectFilled(miniA, miniB, IM_COL32(18, 18, 18, 255));
drawList->AddRectFilled(miniA, miniB,
ThemeEngine::instance().editorColor("minimap_bg",
IM_COL32(18, 18, 18, 255)));
for (int ln = 0; ln < lineCount; ++ln) {
float y = minimapBaseY + ln * lineHeight;
ImU32 color = IM_COL32(80, 80, 80, 255);
if (ln == currentLine) color = IM_COL32(120, 120, 160, 255);
ImU32 color = ThemeEngine::instance().editorColor("minimap_line",
IM_COL32(80, 80, 80, 255));
if (ln == currentLine) {
color = ThemeEngine::instance().editorColor("minimap_line_active",
IM_COL32(120, 120, 160, 255));
}
drawList->AddRectFilled(ImVec2(minimapBaseX + 2.0f, y),
ImVec2(minimapBaseX + minimapWidth - 2.0f, y + 2.0f),
color);
@@ -563,7 +603,9 @@ public:
viewEnd = std::max(0.0f, std::min(1.0f, viewEnd));
ImVec2 vA(minimapBaseX, minimapBaseY + viewStart * miniHeight);
ImVec2 vB(minimapBaseX + minimapWidth, minimapBaseY + viewEnd * miniHeight);
drawList->AddRect(vA, vB, IM_COL32(120, 160, 220, 180));
drawList->AddRect(vA, vB,
ThemeEngine::instance().editorColor("minimap_view",
IM_COL32(120, 160, 220, 180)));
}
ImGui::EndChild();
@@ -628,20 +670,22 @@ private:
}
static ImU32 colorFor(TokenCategory cat) {
ImU32 fallback = IM_COL32(220, 220, 220, 255);
switch (cat) {
case TokenCategory::Keyword: return IM_COL32(196, 126, 220, 255);
case TokenCategory::String: return IM_COL32(207, 138, 94, 255);
case TokenCategory::Comment: return IM_COL32(107, 133, 89, 255);
case TokenCategory::Number: return IM_COL32(181, 204, 140, 255);
case TokenCategory::Function: return IM_COL32(220, 220, 140, 255);
case TokenCategory::Parameter: return IM_COL32(153, 199, 229, 255);
case TokenCategory::Type: return IM_COL32(77, 179, 174, 255);
case TokenCategory::Operator: return IM_COL32(220, 220, 220, 255);
case TokenCategory::Punctuation: return IM_COL32(160, 160, 160, 255);
case TokenCategory::Builtin: return IM_COL32(77, 179, 229, 255);
case TokenCategory::Identifier: return IM_COL32(160, 200, 230, 255);
default: return IM_COL32(220, 220, 220, 255);
case TokenCategory::Keyword: fallback = IM_COL32(196, 126, 220, 255); break;
case TokenCategory::String: fallback = IM_COL32(207, 138, 94, 255); break;
case TokenCategory::Comment: fallback = IM_COL32(107, 133, 89, 255); break;
case TokenCategory::Number: fallback = IM_COL32(181, 204, 140, 255); break;
case TokenCategory::Function: fallback = IM_COL32(220, 220, 140, 255); break;
case TokenCategory::Parameter: fallback = IM_COL32(153, 199, 229, 255); break;
case TokenCategory::Type: fallback = IM_COL32(77, 179, 174, 255); break;
case TokenCategory::Operator: fallback = IM_COL32(220, 220, 220, 255); break;
case TokenCategory::Punctuation: fallback = IM_COL32(160, 160, 160, 255); break;
case TokenCategory::Builtin: fallback = IM_COL32(77, 179, 229, 255); break;
case TokenCategory::Identifier: fallback = IM_COL32(160, 200, 230, 255); break;
default: break;
}
return ThemeEngine::instance().syntaxColor(cat, fallback);
}
static int lineFromPos(int pos, const std::vector<int>& lineStarts) {
@@ -778,7 +822,9 @@ private:
endCol = std::max(startCol, endCol);
float xStart = textBaseX + startCol * charAdvance;
float xEnd = textBaseX + endCol * charAdvance;
ImU32 color = (d.severity == 1) ? IM_COL32(220, 80, 80, 255) : IM_COL32(220, 160, 60, 255);
ImU32 color = (d.severity == 1)
? ThemeEngine::instance().editorColor("diag_error", IM_COL32(220, 80, 80, 255))
: ThemeEngine::instance().editorColor("diag_warning", IM_COL32(220, 160, 60, 255));
float x = xStart;
float amp = 2.0f;
bool up = true;

View File

@@ -150,7 +150,7 @@ private:
std::string emacsConfigPath_;
int fontSize_ = 15;
int tabSize_ = 4;
std::string theme_ = "Dark";
std::string theme_ = "VSCode Dark";
int autoSaveSeconds_ = 0;
bool showMinimap_ = false;
bool showLineNumbers_ = true;

233
editor/src/ThemeEngine.h Normal file
View File

@@ -0,0 +1,233 @@
#pragma once
// Step 160: Theme engine
#include <string>
#include <vector>
#include <unordered_map>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <algorithm>
#include <nlohmann/json.hpp>
#include "imgui.h"
#include "SyntaxHighlighter.h"
using json = nlohmann::json;
struct ThemeDefinition {
std::string name;
std::unordered_map<std::string, ImU32> syntaxColors;
std::unordered_map<std::string, ImU32> editorColors;
std::unordered_map<int, ImVec4> imguiColors;
};
class ThemeEngine {
public:
static ThemeEngine& instance() {
static ThemeEngine inst;
return inst;
}
void clear() {
themes_.clear();
current_ = -1;
}
bool loadThemesFromDirectory(const std::string& dir) {
namespace fs = std::filesystem;
std::error_code ec;
if (!fs::exists(dir, ec)) return false;
bool loaded = false;
for (const auto& entry : fs::directory_iterator(dir)) {
if (!entry.is_regular_file()) continue;
auto path = entry.path();
if (path.extension() != ".json") continue;
if (loadThemeFile(path.string())) loaded = true;
}
return loaded;
}
bool loadThemeFile(const std::string& path) {
std::ifstream in(path);
if (!in.is_open()) return false;
std::ostringstream ss;
ss << in.rdbuf();
return loadThemeFromJson(ss.str());
}
bool loadThemeFromJson(const std::string& text) {
try {
json j = json::parse(text);
ThemeDefinition theme;
if (!parseTheme(j, theme)) return false;
registerTheme(theme);
return true;
} catch (...) {
return false;
}
}
void registerTheme(const ThemeDefinition& theme) {
for (auto& t : themes_) {
if (t.name == theme.name) {
t = theme;
return;
}
}
themes_.push_back(theme);
}
std::vector<std::string> listThemes() const {
std::vector<std::string> names;
for (const auto& t : themes_) names.push_back(t.name);
std::sort(names.begin(), names.end());
return names;
}
bool applyTheme(const std::string& name) {
for (size_t i = 0; i < themes_.size(); ++i) {
if (themes_[i].name == name) {
current_ = (int)i;
applyToImGui(themes_[i]);
return true;
}
}
return false;
}
std::string currentThemeName() const {
if (current_ < 0 || current_ >= (int)themes_.size()) return "";
return themes_[current_].name;
}
ImU32 syntaxColor(TokenCategory cat, ImU32 fallback) const {
const ThemeDefinition* theme = currentTheme();
if (!theme) return fallback;
std::string key = SyntaxHighlighter::categoryName(cat);
auto it = theme->syntaxColors.find(key);
return it != theme->syntaxColors.end() ? it->second : fallback;
}
ImU32 editorColor(const std::string& key, ImU32 fallback) const {
const ThemeDefinition* theme = currentTheme();
if (!theme) return fallback;
auto it = theme->editorColors.find(key);
return it != theme->editorColors.end() ? it->second : fallback;
}
private:
ThemeEngine() = default;
const ThemeDefinition* currentTheme() const {
if (current_ < 0 || current_ >= (int)themes_.size()) return nullptr;
return &themes_[current_];
}
static bool parseHexColor(const std::string& text,
ImU32& outU32,
ImVec4& outVec) {
std::string hex = text;
if (!hex.empty() && hex[0] == '#') hex = hex.substr(1);
if (hex.size() != 6 && hex.size() != 8) return false;
auto toByte = [](const std::string& s) {
return (int)std::strtol(s.c_str(), nullptr, 16);
};
int r = toByte(hex.substr(0, 2));
int g = toByte(hex.substr(2, 2));
int b = toByte(hex.substr(4, 2));
int a = 255;
if (hex.size() == 8) a = toByte(hex.substr(6, 2));
outU32 = IM_COL32(r, g, b, a);
outVec = ImVec4(r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f);
return true;
}
static int imguiColorId(const std::string& name) {
if (name == "Text") return ImGuiCol_Text;
if (name == "WindowBg") return ImGuiCol_WindowBg;
if (name == "ChildBg") return ImGuiCol_ChildBg;
if (name == "PopupBg") return ImGuiCol_PopupBg;
if (name == "Border") return ImGuiCol_Border;
if (name == "FrameBg") return ImGuiCol_FrameBg;
if (name == "FrameBgHovered") return ImGuiCol_FrameBgHovered;
if (name == "FrameBgActive") return ImGuiCol_FrameBgActive;
if (name == "TitleBg") return ImGuiCol_TitleBg;
if (name == "TitleBgActive") return ImGuiCol_TitleBgActive;
if (name == "Header") return ImGuiCol_Header;
if (name == "HeaderHovered") return ImGuiCol_HeaderHovered;
if (name == "HeaderActive") return ImGuiCol_HeaderActive;
if (name == "Tab") return ImGuiCol_Tab;
if (name == "TabHovered") return ImGuiCol_TabHovered;
if (name == "TabActive") return ImGuiCol_TabActive;
if (name == "TabUnfocused") return ImGuiCol_TabUnfocused;
if (name == "TabUnfocusedActive") return ImGuiCol_TabUnfocusedActive;
if (name == "Button") return ImGuiCol_Button;
if (name == "ButtonHovered") return ImGuiCol_ButtonHovered;
if (name == "ButtonActive") return ImGuiCol_ButtonActive;
if (name == "ScrollbarBg") return ImGuiCol_ScrollbarBg;
if (name == "ScrollbarGrab") return ImGuiCol_ScrollbarGrab;
if (name == "ScrollbarGrabHovered") return ImGuiCol_ScrollbarGrabHovered;
if (name == "ScrollbarGrabActive") return ImGuiCol_ScrollbarGrabActive;
if (name == "CheckMark") return ImGuiCol_CheckMark;
if (name == "SliderGrab") return ImGuiCol_SliderGrab;
if (name == "SliderGrabActive") return ImGuiCol_SliderGrabActive;
if (name == "Separator") return ImGuiCol_Separator;
if (name == "SeparatorHovered") return ImGuiCol_SeparatorHovered;
if (name == "SeparatorActive") return ImGuiCol_SeparatorActive;
return -1;
}
static bool parseTheme(const json& j, ThemeDefinition& out) {
if (!j.is_object()) return false;
out.name = j.value("name", "");
if (out.name.empty()) return false;
if (j.contains("syntax") && j["syntax"].is_object()) {
for (const auto& [key, value] : j["syntax"].items()) {
if (!value.is_string()) continue;
ImU32 u32;
ImVec4 v4;
if (parseHexColor(value.get<std::string>(), u32, v4)) {
out.syntaxColors[key] = u32;
}
}
}
if (j.contains("editor") && j["editor"].is_object()) {
for (const auto& [key, value] : j["editor"].items()) {
if (!value.is_string()) continue;
ImU32 u32;
ImVec4 v4;
if (parseHexColor(value.get<std::string>(), u32, v4)) {
out.editorColors[key] = u32;
}
}
}
if (j.contains("imgui") && j["imgui"].is_object()) {
for (const auto& [key, value] : j["imgui"].items()) {
if (!value.is_string()) continue;
ImU32 u32;
ImVec4 v4;
if (!parseHexColor(value.get<std::string>(), u32, v4)) continue;
int colId = imguiColorId(key);
if (colId >= 0) {
out.imguiColors[colId] = v4;
}
}
}
return true;
}
static void applyToImGui(const ThemeDefinition& theme) {
ImGuiStyle& style = ImGui::GetStyle();
for (const auto& [colId, color] : theme.imguiColors) {
if (colId >= 0 && colId < ImGuiCol_COUNT) {
style.Colors[colId] = color;
}
}
}
std::vector<ThemeDefinition> themes_;
int current_ = -1;
};

View File

@@ -16,6 +16,7 @@
#include "EditorState.h"
#include "EditorUtils.h"
#include "CompletionUtils.h"
#include "ThemeEngine.h"
static std::string emacsChordForEvent(SDL_Keycode sym, SDL_Keymod mods) {
std::string base;
@@ -105,17 +106,19 @@ int main(int, char**) {
uiFont = io.Fonts->AddFontDefault();
}
SetupVSCodeDarkTheme();
ImGui_ImplSDL2_InitForOpenGL(window, gl_context);
ImGui_ImplOpenGL3_Init(glsl_version);
// Editor state
EditorState state;
state.init();
if (state.settings.getTheme() == "Light") {
SetupVSCodeLightTheme();
} else {
ThemeEngine& themes = ThemeEngine::instance();
themes.loadThemesFromDirectory("themes");
themes.loadThemesFromDirectory("editor/themes");
std::string themeName = state.settings.getTheme();
if (themeName == "Dark") themeName = "VSCode Dark";
if (themeName == "Light") themeName = "VSCode Light";
if (!themes.applyTheme(themeName)) {
SetupVSCodeDarkTheme();
}
io.FontGlobalScale = state.settings.getFontSize() / baseFontSize;
@@ -835,12 +838,31 @@ int main(int, char**) {
settingsChanged = true;
}
const char* themeLabels[] = {"Dark", "Light"};
int themeIndex = state.settings.getTheme() == "Light" ? 1 : 0;
if (ImGui::Combo("Theme", &themeIndex, themeLabels, 2)) {
state.settings.setTheme(themeLabels[themeIndex]);
if (themeIndex == 1) SetupVSCodeLightTheme();
else SetupVSCodeDarkTheme();
std::vector<std::string> themeNames = ThemeEngine::instance().listThemes();
if (themeNames.empty()) {
themeNames = {"VSCode Dark", "VSCode Light"};
}
std::string currentTheme = state.settings.getTheme();
if (currentTheme == "Dark") currentTheme = "VSCode Dark";
if (currentTheme == "Light") currentTheme = "VSCode Light";
int themeIndex = 0;
for (size_t i = 0; i < themeNames.size(); ++i) {
if (themeNames[i] == currentTheme) {
themeIndex = (int)i;
break;
}
}
std::vector<const char*> themeLabels;
themeLabels.reserve(themeNames.size());
for (const auto& name : themeNames) themeLabels.push_back(name.c_str());
if (ImGui::Combo("Theme", &themeIndex, themeLabels.data(), (int)themeLabels.size())) {
state.settings.setTheme(themeNames[themeIndex]);
if (!ThemeEngine::instance().applyTheme(themeNames[themeIndex])) {
if (themeNames[themeIndex].find("Light") != std::string::npos)
SetupVSCodeLightTheme();
else
SetupVSCodeDarkTheme();
}
settingsChanged = true;
}

View File

@@ -0,0 +1,45 @@
// Step 160 TDD Test: Theme engine
#include "ThemeEngine.h"
#include "imgui.h"
#include <iostream>
static void expect(bool cond, const std::string& name, int& passed, int& failed) {
if (cond) {
std::cout << "Test " << (passed + failed + 1) << " PASS: " << name << "\n";
++passed;
} else {
std::cout << "Test " << (passed + failed + 1) << " FAIL: " << name << "\n";
++failed;
}
}
int main() {
int passed = 0;
int failed = 0;
ImGui::CreateContext();
ThemeEngine& themes = ThemeEngine::instance();
themes.clear();
std::string jsonText = R"({
"name": "TestTheme",
"syntax": { "keyword": "#FF0000" },
"editor": { "gutter_bg": "#001122" },
"imgui": { "WindowBg": "#112233" }
})";
expect(themes.loadThemeFromJson(jsonText), "load theme from json", passed, failed);
expect(themes.applyTheme("TestTheme"), "apply theme", passed, failed);
ImU32 keyword = themes.syntaxColor(TokenCategory::Keyword, IM_COL32(0,0,0,255));
ImU32 gutter = themes.editorColor("gutter_bg", IM_COL32(0,0,0,255));
expect(keyword == IM_COL32(255, 0, 0, 255), "syntax color parsed", passed, failed);
expect(gutter == IM_COL32(0, 17, 34, 255), "editor color parsed", passed, failed);
ImGui::DestroyContext();
std::cout << "\n=== Step 160 Results: " << passed << " passed, "
<< failed << " failed ===\n";
return failed == 0 ? 0 : 1;
}

View File

@@ -0,0 +1,67 @@
{
"name": "Dracula",
"syntax": {
"keyword": "#FF79C6",
"string": "#F1FA8C",
"comment": "#6272A4",
"number": "#BD93F9",
"function": "#50FA7B",
"parameter": "#FFB86C",
"type": "#8BE9FD",
"operator": "#F8F8F2",
"punctuation": "#F8F8F2",
"builtin": "#8BE9FD",
"identifier": "#F8F8F2",
"plain": "#F8F8F2"
},
"editor": {
"gutter_bg": "#282A36",
"gutter_text": "#6272A4",
"diag_error": "#FF5555",
"diag_warning": "#FFB86C",
"suggestion": "#F1FA8C",
"conflict": "#FF5555",
"conflict_line": "#FF5555B4",
"fold_indicator": "#6272A4",
"line_highlight": "#44475A",
"selection": "#44475A",
"annotation_tag_bg": "#44475A",
"fold_placeholder": "#6272A4",
"caret": "#F8F8F2",
"minimap_bg": "#2B2C36",
"minimap_line": "#3A3B45",
"minimap_line_active": "#525363",
"minimap_view": "#525363"
},
"imgui": {
"WindowBg": "#282A36",
"ChildBg": "#282A36",
"PopupBg": "#44475A",
"Border": "#44475A",
"Text": "#F8F8F2",
"FrameBg": "#44475A",
"FrameBgHovered": "#525363",
"FrameBgActive": "#525363",
"TitleBg": "#282A36",
"TitleBgActive": "#44475A",
"Header": "#44475A",
"HeaderHovered": "#525363",
"HeaderActive": "#525363",
"Tab": "#44475A",
"TabHovered": "#525363",
"TabActive": "#282A36",
"Button": "#44475A",
"ButtonHovered": "#525363",
"ButtonActive": "#525363",
"ScrollbarBg": "#282A36",
"ScrollbarGrab": "#44475A",
"ScrollbarGrabHovered": "#525363",
"ScrollbarGrabActive": "#525363",
"CheckMark": "#F8F8F2",
"SliderGrab": "#6272A4",
"SliderGrabActive": "#7080B0",
"Separator": "#44475A",
"SeparatorHovered": "#525363",
"SeparatorActive": "#525363"
}
}

View File

@@ -0,0 +1,67 @@
{
"name": "Monokai",
"syntax": {
"keyword": "#F92672",
"string": "#E6DB74",
"comment": "#75715E",
"number": "#AE81FF",
"function": "#A6E22E",
"parameter": "#FD971F",
"type": "#66D9EF",
"operator": "#F8F8F2",
"punctuation": "#F8F8F2",
"builtin": "#66D9EF",
"identifier": "#F8F8F2",
"plain": "#F8F8F2"
},
"editor": {
"gutter_bg": "#272822",
"gutter_text": "#75715E",
"diag_error": "#F92672",
"diag_warning": "#FD971F",
"suggestion": "#E6DB74",
"conflict": "#F92672",
"conflict_line": "#F92672B4",
"fold_indicator": "#A6A28C",
"line_highlight": "#3E3D32",
"selection": "#49483E",
"annotation_tag_bg": "#3B3A32",
"fold_placeholder": "#A6A28C",
"caret": "#F8F8F0",
"minimap_bg": "#2D2A2E",
"minimap_line": "#3F3F3F",
"minimap_line_active": "#5B5B5B",
"minimap_view": "#5E5E5E"
},
"imgui": {
"WindowBg": "#272822",
"ChildBg": "#272822",
"PopupBg": "#2D2A2E",
"Border": "#3B3A32",
"Text": "#F8F8F2",
"FrameBg": "#3B3A32",
"FrameBgHovered": "#45433A",
"FrameBgActive": "#45433A",
"TitleBg": "#272822",
"TitleBgActive": "#2D2A2E",
"Header": "#3B3A32",
"HeaderHovered": "#45433A",
"HeaderActive": "#45433A",
"Tab": "#3B3A32",
"TabHovered": "#45433A",
"TabActive": "#272822",
"Button": "#45433A",
"ButtonHovered": "#4E4B40",
"ButtonActive": "#4E4B40",
"ScrollbarBg": "#2D2A2E",
"ScrollbarGrab": "#45433A",
"ScrollbarGrabHovered": "#4E4B40",
"ScrollbarGrabActive": "#4E4B40",
"CheckMark": "#F8F8F2",
"SliderGrab": "#5B5B5B",
"SliderGrabActive": "#6A6A6A",
"Separator": "#3B3A32",
"SeparatorHovered": "#5B5B5B",
"SeparatorActive": "#6A6A6A"
}
}

67
editor/themes/nord.json Normal file
View File

@@ -0,0 +1,67 @@
{
"name": "Nord",
"syntax": {
"keyword": "#81A1C1",
"string": "#A3BE8C",
"comment": "#616E88",
"number": "#B48EAD",
"function": "#88C0D0",
"parameter": "#D08770",
"type": "#8FBCBB",
"operator": "#D8DEE9",
"punctuation": "#D8DEE9",
"builtin": "#81A1C1",
"identifier": "#D8DEE9",
"plain": "#D8DEE9"
},
"editor": {
"gutter_bg": "#2E3440",
"gutter_text": "#616E88",
"diag_error": "#BF616A",
"diag_warning": "#D08770",
"suggestion": "#EBCB8B",
"conflict": "#BF616A",
"conflict_line": "#BF616AB4",
"fold_indicator": "#616E88",
"line_highlight": "#3B4252",
"selection": "#3B4252",
"annotation_tag_bg": "#3B4252",
"fold_placeholder": "#616E88",
"caret": "#D8DEE9",
"minimap_bg": "#2E3440",
"minimap_line": "#3B4252",
"minimap_line_active": "#4C566A",
"minimap_view": "#4C566A"
},
"imgui": {
"WindowBg": "#2E3440",
"ChildBg": "#2E3440",
"PopupBg": "#3B4252",
"Border": "#3B4252",
"Text": "#D8DEE9",
"FrameBg": "#3B4252",
"FrameBgHovered": "#4C566A",
"FrameBgActive": "#4C566A",
"TitleBg": "#2E3440",
"TitleBgActive": "#3B4252",
"Header": "#3B4252",
"HeaderHovered": "#4C566A",
"HeaderActive": "#4C566A",
"Tab": "#3B4252",
"TabHovered": "#4C566A",
"TabActive": "#2E3440",
"Button": "#3B4252",
"ButtonHovered": "#4C566A",
"ButtonActive": "#4C566A",
"ScrollbarBg": "#2E3440",
"ScrollbarGrab": "#3B4252",
"ScrollbarGrabHovered": "#4C566A",
"ScrollbarGrabActive": "#4C566A",
"CheckMark": "#D8DEE9",
"SliderGrab": "#4C566A",
"SliderGrabActive": "#5E81AC",
"Separator": "#3B4252",
"SeparatorHovered": "#4C566A",
"SeparatorActive": "#4C566A"
}
}

View File

@@ -0,0 +1,67 @@
{
"name": "Solarized Dark",
"syntax": {
"keyword": "#859900",
"string": "#2AA198",
"comment": "#586E75",
"number": "#D33682",
"function": "#268BD2",
"parameter": "#B58900",
"type": "#B58900",
"operator": "#839496",
"punctuation": "#839496",
"builtin": "#6C71C4",
"identifier": "#839496",
"plain": "#839496"
},
"editor": {
"gutter_bg": "#002B36",
"gutter_text": "#586E75",
"diag_error": "#DC322F",
"diag_warning": "#CB4B16",
"suggestion": "#B58900",
"conflict": "#DC322F",
"conflict_line": "#DC322FB4",
"fold_indicator": "#586E75",
"line_highlight": "#073642",
"selection": "#073642",
"annotation_tag_bg": "#073642",
"fold_placeholder": "#657B83",
"caret": "#93A1A1",
"minimap_bg": "#002B36",
"minimap_line": "#073642",
"minimap_line_active": "#0B3F4A",
"minimap_view": "#0B3F4A"
},
"imgui": {
"WindowBg": "#002B36",
"ChildBg": "#002B36",
"PopupBg": "#073642",
"Border": "#073642",
"Text": "#839496",
"FrameBg": "#073642",
"FrameBgHovered": "#0B3F4A",
"FrameBgActive": "#0B3F4A",
"TitleBg": "#002B36",
"TitleBgActive": "#073642",
"Header": "#073642",
"HeaderHovered": "#0B3F4A",
"HeaderActive": "#0B3F4A",
"Tab": "#073642",
"TabHovered": "#0B3F4A",
"TabActive": "#002B36",
"Button": "#073642",
"ButtonHovered": "#0B3F4A",
"ButtonActive": "#0B3F4A",
"ScrollbarBg": "#002B36",
"ScrollbarGrab": "#073642",
"ScrollbarGrabHovered": "#0B3F4A",
"ScrollbarGrabActive": "#0B3F4A",
"CheckMark": "#839496",
"SliderGrab": "#586E75",
"SliderGrabActive": "#657B83",
"Separator": "#073642",
"SeparatorHovered": "#0B3F4A",
"SeparatorActive": "#0B3F4A"
}
}

View File

@@ -0,0 +1,67 @@
{
"name": "Solarized Light",
"syntax": {
"keyword": "#859900",
"string": "#2AA198",
"comment": "#93A1A1",
"number": "#D33682",
"function": "#268BD2",
"parameter": "#B58900",
"type": "#B58900",
"operator": "#657B83",
"punctuation": "#657B83",
"builtin": "#6C71C4",
"identifier": "#657B83",
"plain": "#657B83"
},
"editor": {
"gutter_bg": "#FDF6E3",
"gutter_text": "#93A1A1",
"diag_error": "#DC322F",
"diag_warning": "#CB4B16",
"suggestion": "#B58900",
"conflict": "#DC322F",
"conflict_line": "#DC322FB4",
"fold_indicator": "#93A1A1",
"line_highlight": "#EEE8D5",
"selection": "#EEE8D5",
"annotation_tag_bg": "#EEE8D5",
"fold_placeholder": "#93A1A1",
"caret": "#657B83",
"minimap_bg": "#EEE8D5",
"minimap_line": "#DDD6C1",
"minimap_line_active": "#C9C1AE",
"minimap_view": "#C9C1AE"
},
"imgui": {
"WindowBg": "#FDF6E3",
"ChildBg": "#FDF6E3",
"PopupBg": "#EEE8D5",
"Border": "#DDD6C1",
"Text": "#657B83",
"FrameBg": "#EEE8D5",
"FrameBgHovered": "#DDD6C1",
"FrameBgActive": "#DDD6C1",
"TitleBg": "#FDF6E3",
"TitleBgActive": "#EEE8D5",
"Header": "#EEE8D5",
"HeaderHovered": "#DDD6C1",
"HeaderActive": "#DDD6C1",
"Tab": "#EEE8D5",
"TabHovered": "#DDD6C1",
"TabActive": "#FDF6E3",
"Button": "#EEE8D5",
"ButtonHovered": "#DDD6C1",
"ButtonActive": "#DDD6C1",
"ScrollbarBg": "#FDF6E3",
"ScrollbarGrab": "#DDD6C1",
"ScrollbarGrabHovered": "#C9C1AE",
"ScrollbarGrabActive": "#C9C1AE",
"CheckMark": "#657B83",
"SliderGrab": "#C9C1AE",
"SliderGrabActive": "#B5AD9B",
"Separator": "#DDD6C1",
"SeparatorHovered": "#C9C1AE",
"SeparatorActive": "#B5AD9B"
}
}

View File

@@ -0,0 +1,67 @@
{
"name": "VSCode Dark",
"syntax": {
"keyword": "#C586C0",
"string": "#CE9178",
"comment": "#6A9955",
"number": "#B5CEA8",
"function": "#DCDCAA",
"parameter": "#9CDCFE",
"type": "#4EC9B0",
"operator": "#D4D4D4",
"punctuation": "#D4D4D4",
"builtin": "#4FC1FF",
"identifier": "#D4D4D4",
"plain": "#D4D4D4"
},
"editor": {
"gutter_bg": "#1E1E1E",
"gutter_text": "#858585",
"diag_error": "#F44747",
"diag_warning": "#FF8800",
"suggestion": "#FFCC00",
"conflict": "#F44747",
"conflict_line": "#F44747B4",
"fold_indicator": "#C5C5C5",
"line_highlight": "#2A2D2E",
"selection": "#264F78",
"annotation_tag_bg": "#333333",
"fold_placeholder": "#808080",
"caret": "#AEAFAD",
"minimap_bg": "#2D2D2D",
"minimap_line": "#3F3F3F",
"minimap_line_active": "#6A6A6A",
"minimap_view": "#3A6EA5"
},
"imgui": {
"WindowBg": "#1E1E1E",
"ChildBg": "#1E1E1E",
"PopupBg": "#252526",
"Border": "#3C3C3C",
"Text": "#D4D4D4",
"FrameBg": "#2D2D2D",
"FrameBgHovered": "#3A3D41",
"FrameBgActive": "#3A3D41",
"TitleBg": "#1E1E1E",
"TitleBgActive": "#252526",
"Header": "#2D2D2D",
"HeaderHovered": "#3A3D41",
"HeaderActive": "#3A3D41",
"Tab": "#2D2D2D",
"TabHovered": "#3A3D41",
"TabActive": "#1E1E1E",
"Button": "#3A3D41",
"ButtonHovered": "#4B4F55",
"ButtonActive": "#4B4F55",
"ScrollbarBg": "#252526",
"ScrollbarGrab": "#3A3D41",
"ScrollbarGrabHovered": "#4B4F55",
"ScrollbarGrabActive": "#4B4F55",
"CheckMark": "#D4D4D4",
"SliderGrab": "#5A5A5A",
"SliderGrabActive": "#6A6A6A",
"Separator": "#3C3C3C",
"SeparatorHovered": "#5A5A5A",
"SeparatorActive": "#6A6A6A"
}
}

View File

@@ -0,0 +1,67 @@
{
"name": "VSCode Light",
"syntax": {
"keyword": "#0000FF",
"string": "#A31515",
"comment": "#008000",
"number": "#098658",
"function": "#795E26",
"parameter": "#001080",
"type": "#267F99",
"operator": "#000000",
"punctuation": "#000000",
"builtin": "#267F99",
"identifier": "#000000",
"plain": "#000000"
},
"editor": {
"gutter_bg": "#FFFFFF",
"gutter_text": "#999999",
"diag_error": "#E51400",
"diag_warning": "#CA5010",
"suggestion": "#C19C00",
"conflict": "#E51400",
"conflict_line": "#E51400B4",
"fold_indicator": "#888888",
"line_highlight": "#F3F3F3",
"selection": "#ADD6FF",
"annotation_tag_bg": "#EFEFEF",
"fold_placeholder": "#666666",
"caret": "#000000",
"minimap_bg": "#F3F3F3",
"minimap_line": "#E0E0E0",
"minimap_line_active": "#C8C8C8",
"minimap_view": "#C8C8C8"
},
"imgui": {
"WindowBg": "#FFFFFF",
"ChildBg": "#FFFFFF",
"PopupBg": "#F3F3F3",
"Border": "#DDDDDD",
"Text": "#000000",
"FrameBg": "#F3F3F3",
"FrameBgHovered": "#E0E0E0",
"FrameBgActive": "#E0E0E0",
"TitleBg": "#FFFFFF",
"TitleBgActive": "#F3F3F3",
"Header": "#F3F3F3",
"HeaderHovered": "#E0E0E0",
"HeaderActive": "#E0E0E0",
"Tab": "#F3F3F3",
"TabHovered": "#E0E0E0",
"TabActive": "#FFFFFF",
"Button": "#E0E0E0",
"ButtonHovered": "#D0D0D0",
"ButtonActive": "#D0D0D0",
"ScrollbarBg": "#FFFFFF",
"ScrollbarGrab": "#D0D0D0",
"ScrollbarGrabHovered": "#B0B0B0",
"ScrollbarGrabActive": "#B0B0B0",
"CheckMark": "#000000",
"SliderGrab": "#B0B0B0",
"SliderGrabActive": "#909090",
"Separator": "#DDDDDD",
"SeparatorHovered": "#C0C0C0",
"SeparatorActive": "#C0C0C0"
}
}

View File

@@ -341,7 +341,7 @@ primitives, and assist with constructive coding.
Final polish, documentation, and ecosystem features.
- [ ] **Step 160: Theme engine**
- [x] **Step 160: Theme engine**
Full theme system beyond Dark/Light. Load themes from JSON files.
Theme affects: editor colors, syntax highlighting colors, gutter colors,
annotation marker colors, panel backgrounds. Ship with: VSCode Dark,