234 lines
8.1 KiB
C
234 lines
8.1 KiB
C
|
|
#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;
|
||
|
|
};
|