diff --git a/plugins/editor/CMakeLists.txt b/plugins/editor/CMakeLists.txt
index 62522b4e..c500e469 100644
--- a/plugins/editor/CMakeLists.txt
+++ b/plugins/editor/CMakeLists.txt
@@ -20,6 +20,7 @@ set(EDITOR_RESOURCES
Fonts/sfizz-fluentui-system-f20.ttf
Fonts/sfizz-misc-icons.ttf
Fonts/Roboto-Regular.ttf
+ Themes/Default/theme.xml
PARENT_SCOPE)
function(copy_editor_resources SOURCE_DIR DESTINATION_DIR)
@@ -50,6 +51,8 @@ add_library(sfizz_editor STATIC EXCLUDE_FROM_ALL
src/editor/GUIPiano.cpp
src/editor/DlgAbout.h
src/editor/DlgAbout.cpp
+ src/editor/Theme.h
+ src/editor/Theme.cpp
src/editor/ColorHelpers.h
src/editor/ColorHelpers.cpp
src/editor/ImageHelpers.h
@@ -104,7 +107,8 @@ else()
target_include_directories(sfizz_editor PRIVATE ${sfizz-gio_INCLUDE_DIRS})
target_link_libraries(sfizz_editor PRIVATE ${sfizz-gio_LIBRARIES})
endif()
-target_link_libraries(sfizz_editor PRIVATE sfizz::colorspaces sfizz::stb_image sfizz::bit_array sfizz::filesystem)
+target_link_libraries(sfizz_editor PRIVATE sfizz::colorspaces sfizz::stb_image
+ sfizz::bit_array sfizz::filesystem sfizz::pugixml)
# layout tool
if(NOT CMAKE_CROSSCOMPILING)
diff --git a/plugins/editor/layout/main.fl b/plugins/editor/layout/main.fl
index 7cbdde11..0429d773 100644
--- a/plugins/editor/layout/main.fl
+++ b/plugins/editor/layout/main.fl
@@ -356,6 +356,17 @@ widget_class mainView {open
class ValueMenu
}
}
+ Fl_Group {} {
+ label Theme open
+ xywh {40 270 105 100} box ROUNDED_BOX labelsize 12 align 17 hide
+ class TitleGroup
+ } {
+ Fl_Spinner themeMenu_ {
+ comment {tag=kTagThemeMenu}
+ xywh {60 330 65 25} labelsize 12 textsize 12
+ class OptionMenu
+ }
+ }
}
Fl_Box piano_ {
xywh {5 400 790 70} labelsize 12
diff --git a/plugins/editor/resources/Themes/Default/theme.xml b/plugins/editor/resources/Themes/Default/theme.xml
new file mode 100644
index 00000000..03bd2484
--- /dev/null
+++ b/plugins/editor/resources/Themes/Default/theme.xml
@@ -0,0 +1,34 @@
+
+
+ #d3d7cf
+
+ #babdb6
+ #000000
+ #b2b2b2
+ #fd9800
+ #ffffff
+ #2e3436
+ #000000
+ #fd9800
+ #ffffff
+ #2e3436
+ #00b62a
+ #303030
+ #000000
+
+
+ #2e3436
+ #ffffff
+ #b2b2b2
+ #fd9800
+ #000000
+ #babdb6
+ #b2b2b2
+ #fd9800
+ #000000
+ #9a9a9a
+ #00b62a
+ #606060
+ #ffffff
+
+
diff --git a/plugins/editor/src/editor/ColorHelpers.cpp b/plugins/editor/src/editor/ColorHelpers.cpp
index 0d92ce27..5ce3314d 100644
--- a/plugins/editor/src/editor/ColorHelpers.cpp
+++ b/plugins/editor/src/editor/ColorHelpers.cpp
@@ -44,3 +44,38 @@ SColorHCY::SColorHCY(const SColorRGB &rgb)
y = vhcy[2];
a = rgb.a;
}
+
+static int hexDigitFromChar(char c)
+{
+ return (c >= '0' && c <= '9') ? (c - '0') :
+ (c >= 'a' && c <= 'z') ? (c - 'a' + 10) :
+ (c >= 'A' && c <= 'Z') ? (c - 'A' + 10) : -1;
+}
+
+bool colorFromHex(absl::string_view hex, CColor& color)
+{
+ if (hex.empty() || hex[0] != '#')
+ return false;
+
+ hex = hex.substr(1, hex.size());
+ size_t length = hex.size();
+ uint32_t rgba = 0;
+ if (length == 6 || length == 8) {
+ for (size_t i = 0; i < length; ++i) {
+ int d = hexDigitFromChar(hex[i]);
+ if (d == -1)
+ return false;
+
+ rgba = (rgba << 4) | d;
+ }
+ }
+ if (length == 6)
+ rgba = (rgba << 8) | 0xff;
+
+ color.red = rgba >> 24;
+ color.green = (rgba >> 16) & 0xff;
+ color.blue = (rgba >> 8) & 0xff;
+ color.alpha = rgba & 0xff;
+
+ return true;
+}
diff --git a/plugins/editor/src/editor/ColorHelpers.h b/plugins/editor/src/editor/ColorHelpers.h
index ca1885f8..c79501cd 100644
--- a/plugins/editor/src/editor/ColorHelpers.h
+++ b/plugins/editor/src/editor/ColorHelpers.h
@@ -8,6 +8,7 @@
#include "utility/vstgui_before.h"
#include "vstgui/lib/ccolor.h"
#include "utility/vstgui_after.h"
+#include
using namespace VSTGUI;
@@ -33,3 +34,5 @@ struct SColorHCY {
float h {}, c {}, y {}, a { 1.0 };
};
+
+bool colorFromHex(absl::string_view hex, CColor& color);
diff --git a/plugins/editor/src/editor/Editor.cpp b/plugins/editor/src/editor/Editor.cpp
index 939b979e..eed83010 100644
--- a/plugins/editor/src/editor/Editor.cpp
+++ b/plugins/editor/src/editor/Editor.cpp
@@ -15,6 +15,7 @@
#include "NativeHelpers.h"
#include "VSTGUIHelpers.h"
#include "BitArray.h"
+#include "Theme.h"
#include "plugin/MessageUtils.h"
#include
#include
@@ -32,6 +33,7 @@
#include
#include
#include
+#include
#include "utility/vstgui_before.h"
#include "vstgui/vstgui.h"
@@ -50,6 +52,7 @@ struct Editor::Impl : EditorController::Receiver, IControlListener {
std::string currentSfzFile_;
std::string currentScalaFile_;
+ std::string currentThemeName_;
std::string userFilesDir_;
std::string fallbackFilesDir_;
@@ -91,6 +94,7 @@ struct Editor::Impl : EditorController::Receiver, IControlListener {
kTagSetCCPan,
kTagChooseUserFilesDir,
kTagAbout,
+ kTagThemeMenu,
kTagFirstChangePanel,
kTagLastChangePanel = kTagFirstChangePanel + kNumPanels - 1,
};
@@ -119,6 +123,8 @@ struct Editor::Impl : EditorController::Receiver, IControlListener {
CTextLabel* keyswitchLabel_ = nullptr;
CTextLabel* keyswitchInactiveLabel_ = nullptr;
CTextLabel* keyswitchBadge_ = nullptr;
+ COptionMenu* themeMenu_ = nullptr;
+ std::unique_ptr theme_;
STitleContainer* userFilesGroup_ = nullptr;
STextButton* userFilesDirButton_ = nullptr;
@@ -590,54 +596,14 @@ void Editor::Impl::createFrameContents()
backgroundBitmap_ = background;
{
- const CColor frameBackground = { 0xd3, 0xd7, 0xcf };
-
- struct Palette {
- CColor boxBackground;
- CColor text;
- CColor inactiveText;
- CColor highlightedText;
- CColor titleBoxText;
- CColor titleBoxBackground;
- CColor icon;
- CColor iconHighlight;
- CColor valueText;
- CColor valueBackground;
- CColor knobActiveTrackColor;
- CColor knobInactiveTrackColor;
- CColor knobLineIndicatorColor;
- };
-
- Palette normalPalette;
- normalPalette.boxBackground = { 0xba, 0xbd, 0xb6 };
- normalPalette.text = { 0x00, 0x00, 0x00 };
- normalPalette.inactiveText = { 0xb2, 0xb2, 0xb2 };
- normalPalette.highlightedText = { 0xfd, 0x98, 0x00 };
- normalPalette.titleBoxText = { 0xff, 0xff, 0xff };
- normalPalette.titleBoxBackground = { 0x2e, 0x34, 0x36 };
- normalPalette.icon = normalPalette.text;
- normalPalette.iconHighlight = { 0xfd, 0x98, 0x00 };
- normalPalette.valueText = { 0xff, 0xff, 0xff };
- normalPalette.valueBackground = { 0x2e, 0x34, 0x36 };
- normalPalette.knobActiveTrackColor = { 0x00, 0xb6, 0x2a };
- normalPalette.knobInactiveTrackColor = { 0x30, 0x30, 0x30 };
- normalPalette.knobLineIndicatorColor = { 0x00, 0x00, 0x00 };
- Palette invertedPalette;
- invertedPalette.boxBackground = { 0x2e, 0x34, 0x36 };
- invertedPalette.text = { 0xff, 0xff, 0xff };
- invertedPalette.inactiveText = { 0xb2, 0xb2, 0xb2 };
- invertedPalette.highlightedText = { 0xfd, 0x98, 0x00 };
- invertedPalette.titleBoxText = { 0x00, 0x00, 0x00 };
- invertedPalette.titleBoxBackground = { 0xba, 0xbd, 0xb6 };
- invertedPalette.icon = { 0xb2, 0xb2, 0xb2 };
- invertedPalette.iconHighlight = { 0xfd, 0x98, 0x00 };
- invertedPalette.valueText = { 0x00, 0x00, 0x00 };
- invertedPalette.valueBackground = { 0x9a, 0x9a, 0x9a };
- invertedPalette.knobActiveTrackColor = { 0x00, 0xb6, 0x2a };
- invertedPalette.knobInactiveTrackColor = { 0x60, 0x60, 0x60 };
- invertedPalette.knobLineIndicatorColor = { 0xff, 0xff, 0xff };
- Palette& defaultPalette = normalPalette;
+ // Try to load the Default theme from disk or hardcoded one as fallback
+ Theme* theme = new Theme;
+ theme_.reset(theme);
+ currentThemeName_ = theme->loadCurrentName();
+ theme->load(currentThemeName_);
+ Palette& invertedPalette = theme->invertedPalette;
+ Palette& defaultPalette = theme->normalPalette;
Palette* palette = &defaultPalette;
auto enterPalette = [&palette](Palette& p) { palette = &p; };
@@ -697,9 +663,9 @@ void Editor::Impl::createFrameContents()
};
auto createStyledKnob = [this, &palette](const CRect& bounds, int tag, const char*, CHoriTxtAlign, int) {
SStyledKnob* knob = new SStyledKnob(bounds, this, tag);
- knob->setActiveTrackColor(palette->knobActiveTrackColor);
- knob->setInactiveTrackColor(palette->knobInactiveTrackColor);
- knob->setLineIndicatorColor(palette->knobLineIndicatorColor);
+ knob->setActiveTrackColor(palette->knobActiveTrack);
+ knob->setInactiveTrackColor(palette->knobInactiveTrack);
+ knob->setLineIndicatorColor(palette->knobLineIndicator);
return knob;
};
auto createValueLabel = [&palette](const CRect& bounds, int, const char* label, CHoriTxtAlign align, int fontsize) {
@@ -781,6 +747,18 @@ void Editor::Impl::createFrameContents()
vm->setRoundRectRadius(5.0);
return vm;
};
+ auto createOptionMenu = [this, &palette](const CRect& bounds, int tag, const char*, CHoriTxtAlign align, int fontsize) {
+ auto* cb = new COptionMenu(bounds, this, tag);
+ cb->setHoriAlign(align);
+ auto font = makeOwned("Roboto", fontsize);
+ cb->setFont(font);
+ cb->setFontColor(palette->valueText);
+ cb->setBackColor(palette->valueBackground);
+ cb->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00));
+ cb->setStyle(CParamDisplay::kRoundRectStyle);
+ cb->setRoundRectRadius(5.0);
+ return cb;
+ };
auto createGlyphButton = [this, &palette](UTF8StringPtr glyph, const CRect& bounds, int tag, int fontsize) {
STextButton* btn = new STextButton(bounds, this, tag, glyph);
btn->setFont(makeOwned("Sfizz Fluent System F20", fontsize));
@@ -821,10 +799,12 @@ void Editor::Impl::createFrameContents()
btn->setFont(makeOwned("Sfizz Fluent System F20", fontsize));
return btn;
};
- auto createPiano = [](const CRect& bounds, int, const char*, CHoriTxtAlign, int fontsize) {
+ auto createPiano = [&palette](const CRect& bounds, int, const char*, CHoriTxtAlign, int fontsize) {
SPiano* piano = new SPiano(bounds);
auto font = makeOwned("Roboto", fontsize);
piano->setFont(font);
+ piano->setFontColor(palette->text);
+ piano->setBackColor(palette->boxBackground);
return piano;
};
auto createChevronDropDown = [this, &palette](const CRect& bounds, int, const char*, CHoriTxtAlign, int fontsize) {
@@ -858,7 +838,7 @@ void Editor::Impl::createFrameContents()
box->setNameLabelFontColor(palette->text);
box->setKnobFont(font);
box->setKnobFontColor(palette->text);
- box->setKnobLineIndicatorColor(palette->knobLineIndicatorColor);
+ box->setKnobLineIndicatorColor(palette->knobLineIndicator);
box->setValueToStringFunction([](float value, std::string& text) -> bool {
text = std::to_string(std::lround(value * 127));
return true;
@@ -877,7 +857,7 @@ void Editor::Impl::createFrameContents()
#include "layout/main.hpp"
- mainView->setBackgroundColor(frameBackground);
+ mainView->setBackgroundColor(theme->frameBackground);
#if LINUX
if (!isZenityAvailable()) {
@@ -1111,6 +1091,19 @@ void Editor::Impl::createFrameContents()
}
applyBackgroundForCurrentPanel();
+
+ if (COptionMenu* menu = themeMenu_) {
+ const std::vector& names = Theme::getAvailableNames();
+ size_t index = ~size_t(0);
+ for (size_t i = 0, n = names.size(); i < n; ++i) {
+ const std::string& name = names[i];
+ menu->addEntry(UTF8String(name));
+ if (name == currentThemeName_)
+ index = i;
+ }
+ if (index != ~size_t(0))
+ menu->setCurrent(index);
+ }
}
void Editor::Impl::chooseSfzFile()
@@ -1847,6 +1840,14 @@ void Editor::Impl::valueChanged(CControl* ctl)
Call::later([this]() { aboutDialog_->setVisible(true); });
break;
+ case kTagThemeMenu:
+ {
+ currentThemeName_ = Theme::getAvailableNames()[int(value)];
+ Theme::storeCurrentName(currentThemeName_);
+ // TODO: live reload theme
+ }
+ break;
+
default:
if (tag >= kTagFirstChangePanel && tag <= kTagLastChangePanel) {
int panelId = tag - kTagFirstChangePanel;
diff --git a/plugins/editor/src/editor/Theme.cpp b/plugins/editor/src/editor/Theme.cpp
new file mode 100644
index 00000000..44057862
--- /dev/null
+++ b/plugins/editor/src/editor/Theme.cpp
@@ -0,0 +1,164 @@
+// SPDX-License-Identifier: BSD-2-Clause
+
+// This code is part of the sfizz library and is licensed under a BSD 2-clause
+// license. You should have receive a LICENSE.md file along with the code.
+// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
+
+#include "Theme.h"
+#include "ColorHelpers.h"
+#include "VSTGUIHelpers.h"
+#include "plugin/SfizzSettings.h"
+#include "sfizz/utility/StringViewHelpers.h"
+#include
+#include
+#include
+#include
+
+void Theme::clear() {
+ frameBackground = {};
+ normalPalette = {};
+ invertedPalette = {};
+}
+
+void Theme::load(const std::string& name)
+{
+ fs::path resPath = getResourceBasePath();
+ fs::path themePath = resPath / "Themes" / fs::u8path(name) / "theme.xml";
+
+ pugi::xml_document doc;
+ pugi::xml_parse_result result = doc.load_file(themePath.c_str());
+ if (!result) {
+ std::cerr << "[sfizz] cannot load theme from " << resPath << '\n';
+ return;
+ }
+
+ loadDocument(doc);
+}
+
+void Theme::loadDocument(const pugi::xml_document& doc)
+{
+ pugi::xml_node rootNode(doc.child("sfizz-theme"));
+ if (!rootNode) {
+ std::cerr << "[sfizz] trying to load an invalid theme\n";
+ return;
+ }
+
+ ///
+ auto loadChildColorNodes = [this](pugi::xml_node topNode, bool inverted) {
+ for (pugi::xml_node colorNode : topNode.children("color")) {
+ absl::string_view name = colorNode.attribute("name").as_string();
+ CColor* slot = getColorFromName(name, inverted);
+ if (!slot) {
+ std::cerr << "[sfizz] color not recognized: " << name << "\n";
+ continue;
+ }
+
+ *slot = {};
+
+ absl::string_view colorText = colorNode.text().as_string();
+ if (!colorFromHex(colorText, *slot))
+ std::cerr << "[sfizz] invalid color value: " << colorText << "\n";
+ }
+ };
+
+ ///
+ loadChildColorNodes(rootNode, false);
+
+ for (pugi::xml_node paletteNode : rootNode.children("palette")) {
+ absl::string_view paletteName = paletteNode.attribute("name").as_string();
+
+ bool inverted;
+ if (paletteName == "normal")
+ inverted = false;
+ else if (paletteName == "inverted")
+ inverted = true;
+ else {
+ std::cerr << "[sfizz] palette not recognized: " << paletteName << "\n";
+ continue;
+ }
+
+ loadChildColorNodes(paletteNode, inverted);
+ }
+}
+
+void Theme::storeCurrentName(absl::string_view name)
+{
+ SfizzSettings settings;
+ settings.store("current_theme", name);
+}
+
+std::string Theme::loadCurrentName()
+{
+ SfizzSettings settings;
+ return settings.load_or("current_theme", "Default");
+}
+
+const std::vector& Theme::getAvailableNames()
+{
+ static const std::vector names = extractAvailableNames();
+ return names;
+}
+
+std::vector Theme::extractAvailableNames()
+{
+ fs::path themesPath = getResourceBasePath() / "Themes";
+
+ std::error_code ec;
+ fs::directory_iterator it(themesPath, ec);
+ if (ec) {
+ std::cerr << "[sfizz] error reading the theme directory: " << ec.message() << '\n';
+ return {};
+ }
+
+ std::vector names;
+ for (; !ec && it != fs::directory_iterator(); it.increment(ec)) {
+ const fs::directory_entry& entry = *it;
+ if (entry.is_directory())
+ names.emplace_back(entry.path().filename().u8string());
+ }
+
+ std::sort(
+ names.begin(), names.end(),
+ [](const std::string& a, const std::string& b) -> bool {
+ return (a == "Default") ? true : (b == "Default") ? false : a < b;
+ });
+
+ return names;
+}
+
+CColor* Theme::getColorFromName(absl::string_view name, bool fromInvertedPalette)
+{
+ CColor* c = nullptr;
+ switch (hash(name)) {
+ #define COLOR_CASE(X) \
+ case hash(#X): \
+ c = &X; \
+ break
+ #define PALETTE_COLOR_CASE(X) \
+ case hash(#X): \
+ c = fromInvertedPalette ? \
+ &invertedPalette.X : &normalPalette.X; \
+ break
+
+ COLOR_CASE(frameBackground);
+ PALETTE_COLOR_CASE(boxBackground);
+ PALETTE_COLOR_CASE(highlightedText);
+ PALETTE_COLOR_CASE(icon);
+ PALETTE_COLOR_CASE(iconHighlight);
+ PALETTE_COLOR_CASE(inactiveText);
+ PALETTE_COLOR_CASE(knobActiveTrack);
+ PALETTE_COLOR_CASE(knobInactiveTrack);
+ PALETTE_COLOR_CASE(knobLineIndicator);
+ PALETTE_COLOR_CASE(text);
+ PALETTE_COLOR_CASE(titleBoxBackground);
+ PALETTE_COLOR_CASE(titleBoxText);
+ PALETTE_COLOR_CASE(valueBackground);
+ PALETTE_COLOR_CASE(valueText);
+
+ #undef COLOR_CASE
+ #undef PALETTE_COLOR_CASE
+
+ default: break;
+ }
+ return c;
+}
diff --git a/plugins/editor/src/editor/Theme.h b/plugins/editor/src/editor/Theme.h
new file mode 100644
index 00000000..b4693833
--- /dev/null
+++ b/plugins/editor/src/editor/Theme.h
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: BSD-2-Clause
+
+// This code is part of the sfizz library and is licensed under a BSD 2-clause
+// license. You should have receive a LICENSE.md file along with the code.
+// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
+
+#pragma once
+#include "utility/vstgui_before.h"
+#include "vstgui/vstgui.h"
+#include "utility/vstgui_after.h"
+#include
+#include
+#include
+namespace pugi { class xml_document; }
+
+using namespace VSTGUI;
+
+struct Palette {
+ CColor boxBackground;
+ CColor text;
+ CColor inactiveText;
+ CColor highlightedText;
+ CColor titleBoxText;
+ CColor titleBoxBackground;
+ CColor icon;
+ CColor iconHighlight;
+ CColor valueText;
+ CColor valueBackground;
+ CColor knobActiveTrack;
+ CColor knobInactiveTrack;
+ CColor knobLineIndicator;
+};
+
+struct Theme {
+ CColor frameBackground;
+ Palette normalPalette {};
+ Palette invertedPalette {};
+
+ void clear();
+ void load(const std::string& name);
+ void loadDocument(const pugi::xml_document& doc);
+
+ static void storeCurrentName(absl::string_view name);
+ static std::string loadCurrentName();
+ static const std::vector& getAvailableNames();
+
+ CColor* getColorFromName(absl::string_view name, bool fromInvertedPalette);
+
+private:
+ static std::vector extractAvailableNames();
+};
diff --git a/plugins/editor/src/editor/layout/main.hpp b/plugins/editor/src/editor/layout/main.hpp
index e7234625..ea9dfa3f 100644
--- a/plugins/editor/src/editor/layout/main.hpp
+++ b/plugins/editor/src/editor/layout/main.hpp
@@ -181,6 +181,12 @@ view__66->addView(view__69);
auto* const view__70 = createValueMenu(CRect(100, 60, 180, 85), kTagSetOscillatorQuality, "", kCenterText, 12);
oscillatorQualitySlider_ = view__70;
view__66->addView(view__70);
-auto* const view__71 = createPiano(CRect(5, 400, 795, 470), -1, "", kCenterText, 12);
-piano_ = view__71;
-view__0->addView(view__71);
+auto* const view__71 = createTitleGroup(CRect(35, 161, 140, 261), -1, "Theme", kCenterText, 12);
+view__46->addView(view__71);
+view__71->setVisible(false);
+auto* const view__72 = createOptionMenu(CRect(20, 60, 85, 85), kTagThemeMenu, "", kCenterText, 12);
+themeMenu_ = view__72;
+view__71->addView(view__72);
+auto* const view__73 = createPiano(CRect(5, 400, 795, 470), -1, "", kCenterText, 12);
+piano_ = view__73;
+view__0->addView(view__73);