From 94407330d9af52691296e41010e082da6a07dd33 Mon Sep 17 00:00:00 2001 From: Kai Blaschke Date: Sun, 7 Sep 2025 03:12:20 +0200 Subject: [PATCH 01/14] Implement ImGui-based preset editor --- CMakeLists.txt | 1 + src/ProjectMWrapper.cpp | 73 + src/ProjectMWrapper.h | 46 +- src/SDLRenderingWindow.cpp | 10 +- src/SDLRenderingWindow.h | 2 +- src/gui/CMakeLists.txt | 2 + src/gui/MainMenu.cpp | 252 +- src/gui/MainMenu.h | 9 +- src/gui/ProjectMGUI.cpp | 16 +- src/gui/ProjectMGUI.h | 8 + src/gui/preset_editor/CMakeLists.txt | 28 + src/gui/preset_editor/EditorMenu.cpp | 107 + src/gui/preset_editor/EditorMenu.h | 33 + src/gui/preset_editor/EditorPreset.cpp | 352 ++ src/gui/preset_editor/EditorPreset.h | 166 + src/gui/preset_editor/PresetEditorGUI.cpp | 702 ++++ src/gui/preset_editor/PresetEditorGUI.h | 80 + src/gui/preset_editor/PresetFile.cpp | 590 +++ src/gui/preset_editor/PresetFile.h | 246 ++ .../imgui_color_text_editor/CMakeLists.txt | 14 + .../imgui_color_text_editor/CONTRIBUTING | 11 + .../imgui_color_text_editor/LICENSE | 21 + .../imgui_color_text_editor/README.md | 33 + .../imgui_color_text_editor/TextEditor.cpp | 3535 +++++++++++++++++ .../imgui_color_text_editor/TextEditor.h | 467 +++ .../UpdateWindowTitleNotification.h | 10 + vcpkg.json | 3 +- 27 files changed, 6709 insertions(+), 108 deletions(-) create mode 100644 src/gui/preset_editor/CMakeLists.txt create mode 100644 src/gui/preset_editor/EditorMenu.cpp create mode 100644 src/gui/preset_editor/EditorMenu.h create mode 100644 src/gui/preset_editor/EditorPreset.cpp create mode 100644 src/gui/preset_editor/EditorPreset.h create mode 100644 src/gui/preset_editor/PresetEditorGUI.cpp create mode 100644 src/gui/preset_editor/PresetEditorGUI.h create mode 100644 src/gui/preset_editor/PresetFile.cpp create mode 100644 src/gui/preset_editor/PresetFile.h create mode 100644 src/gui/preset_editor/imgui_color_text_editor/CMakeLists.txt create mode 100644 src/gui/preset_editor/imgui_color_text_editor/CONTRIBUTING create mode 100644 src/gui/preset_editor/imgui_color_text_editor/LICENSE create mode 100644 src/gui/preset_editor/imgui_color_text_editor/README.md create mode 100644 src/gui/preset_editor/imgui_color_text_editor/TextEditor.cpp create mode 100644 src/gui/preset_editor/imgui_color_text_editor/TextEditor.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 1d1f4ed..681eb6a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -96,6 +96,7 @@ if(NOT SDL2_LINKAGE STREQUAL "shared" AND NOT SDL2_LINKAGE STREQUAL "static") endif() find_package(projectM4 REQUIRED COMPONENTS Playlist) +find_package(projectM-Eval REQUIRED) find_package(SDL2 REQUIRED) find_package(Poco REQUIRED COMPONENTS JSON XML Util Foundation) diff --git a/src/ProjectMWrapper.cpp b/src/ProjectMWrapper.cpp index 75894f1..0f38a7b 100644 --- a/src/ProjectMWrapper.cpp +++ b/src/ProjectMWrapper.cpp @@ -143,6 +143,31 @@ projectm_playlist_handle ProjectMWrapper::Playlist() const return _playlist; } +bool ProjectMWrapper::LoadPresetData(const std::string& presetData, std::string& errorMessage) +{ + projectm_load_preset_data(_projectM, presetData.c_str(), false); + + if (_presetLoadFailed) + { + errorMessage = _presetLoadFailedMessage; + _presetLoadFailed = false; + return false; + } + + return true; +} + +void ProjectMWrapper::UnbindPlaylist() +{ + projectm_playlist_connect(_playlist, nullptr); +} + +void ProjectMWrapper::BindPlaylist() +{ + projectm_playlist_connect(_playlist, _projectM); + projectm_playlist_set_position(_playlist, projectm_playlist_get_position(_playlist), false); +} + int ProjectMWrapper::TargetFPS() { return _projectMConfigView->getInt("fps", 60); @@ -206,6 +231,42 @@ std::string ProjectMWrapper::ProjectMRuntimeVersion() return projectMRuntimeVersion; } +std::string ProjectMWrapper::CurrentPresetFileName() const +{ + if (projectm_playlist_size(_playlist) == 0) + { + return {}; + } + + auto presetName = projectm_playlist_item(_playlist, projectm_playlist_get_position(_playlist)); + std::string presetNameString(presetName); + projectm_playlist_free_string(presetName); + + if (presetNameString.substr(0, 5) == "idle:") + { + return {}; + } + + return presetNameString; +} + +void ProjectMWrapper::EnablePlaybackControl(bool enable) +{ + _playbackControlEnabled = enable; +} + +void ProjectMWrapper::HardLockPreset(bool lock) +{ + if (lock) + { + projectm_set_preset_locked(_projectM, true); + } + else + { + projectm_set_preset_locked(_projectM, _userConfig->getBool("projectM.presetLocked", false)); + } +} + void ProjectMWrapper::PresetFileNameToClipboard() const { auto presetName = projectm_playlist_item(_playlist, projectm_playlist_get_position(_playlist)); @@ -223,8 +284,20 @@ void ProjectMWrapper::PresetSwitchedEvent(bool isHardCut, unsigned int index, vo Poco::NotificationCenter::defaultCenter().postNotification(new UpdateWindowTitleNotification); } +void ProjectMWrapper::PresetSwitchFailedEvent(const char* presetFilename, const char* message, void* context) +{ + auto that = reinterpret_cast(context); + that->_presetLoadFailedMessage = message; + that->_presetLoadFailed = true; +} + void ProjectMWrapper::PlaybackControlNotificationHandler(const Poco::AutoPtr& notification) { + if (!_playbackControlEnabled) + { + return; + } + bool shuffleEnabled = projectm_playlist_get_shuffle(_playlist); switch (notification->ControlAction()) diff --git a/src/ProjectMWrapper.h b/src/ProjectMWrapper.h index 3493dc4..f50c99d 100644 --- a/src/ProjectMWrapper.h +++ b/src/ProjectMWrapper.h @@ -2,8 +2,8 @@ #include "notifications/PlaybackControlNotification.h" -#include #include +#include #include #include @@ -34,6 +34,24 @@ class ProjectMWrapper : public Poco::Util::Subsystem */ projectm_playlist_handle Playlist() const; + /** + * @brief Detaches the current playlist and loads a single preset. + * @param presetData The preset data to load. + * @param errorMessage The error message from projectM if loading failed. + * @return true if the preset was loaded successfully, false if an error occurred. + */ + bool LoadPresetData(const std::string& presetData, std::string& errorMessage); + + /** + * @brief Detaches the internal playlist, so it no longer controls the preset playback. + */ + void UnbindPlaylist(); + + /** + * @brief Binds the internal playlist and resets the preset lock to the user setting. + */ + void BindPlaylist(); + /** * Renders a single projectM frame. */ @@ -70,11 +88,29 @@ class ProjectMWrapper : public Poco::Util::Subsystem std::string ProjectMBuildVersion(); /** - * @brief Returns the libprojectM version this applications currently runs with. + * @brief Returns the libprojectM version this application currently runs with. * @return A string with the libprojectM runtime library version. */ std::string ProjectMRuntimeVersion(); + /** + * @brief Returns the full path of the currently displayed preset. + * @return The full path of the currently displayed preset, or an empty string if the idle preset is loaded. + */ + std::string CurrentPresetFileName() const; + + /** + * @brief Toggles handling of playback control notifications. + * @param enable true to enable handling of playback control notifications, false to disable. + */ + void EnablePlaybackControl(bool enable); + + /** + * @brief Locks or unlocks the current preset without changing the user setting. + * @param lock true to lock the current preset, false to enable auto-switching. + */ + void HardLockPreset(bool lock); + /** * Copies the full path of the current preset into the OS clipboard. */ @@ -89,6 +125,8 @@ class ProjectMWrapper : public Poco::Util::Subsystem */ static void PresetSwitchedEvent(bool isHardCut, unsigned int index, void* context); + static void PresetSwitchFailedEvent(const char* presetFilename, const char* message, void* context); + void PlaybackControlNotificationHandler(const Poco::AutoPtr& notification); std::vector GetPathListWithDefault(const std::string& baseKey, const std::string& defaultPath); @@ -110,6 +148,10 @@ class ProjectMWrapper : public Poco::Util::Subsystem projectm_handle _projectM{nullptr}; //!< Pointer to the projectM instance used by the application. projectm_playlist_handle _playlist{nullptr}; //!< Pointer to the projectM playlist manager instance. + bool _playbackControlEnabled{true}; //!< If false, any playback control notifications are ignored. + + bool _presetLoadFailed{false}; + std::string _presetLoadFailedMessage; Poco::NObserver _playbackControlNotificationObserver{*this, &ProjectMWrapper::PlaybackControlNotificationHandler}; diff --git a/src/SDLRenderingWindow.cpp b/src/SDLRenderingWindow.cpp index efac35e..2186766 100644 --- a/src/SDLRenderingWindow.cpp +++ b/src/SDLRenderingWindow.cpp @@ -384,11 +384,17 @@ SDL_GLContext SDLRenderingWindow::GetGlContext() const void SDLRenderingWindow::UpdateWindowTitleNotificationHandler(POCO_UNUSED const Poco::AutoPtr& notification) { - UpdateWindowTitle(); + UpdateWindowTitle(notification->_customTitle); } -void SDLRenderingWindow::UpdateWindowTitle() +void SDLRenderingWindow::UpdateWindowTitle(const std::string& customTitle) { + if (!customTitle.empty()) + { + SDL_SetWindowTitle(_renderingWindow, customTitle.c_str()); + return; + } + std::string newTitle = "projectM"; if (_config->getBool("displayPresetNameInTitle", true)) diff --git a/src/SDLRenderingWindow.h b/src/SDLRenderingWindow.h index 06c3bbe..b036f8e 100644 --- a/src/SDLRenderingWindow.h +++ b/src/SDLRenderingWindow.h @@ -122,7 +122,7 @@ class SDLRenderingWindow : public Poco::Util::Subsystem /** * @brief Updates the window title. */ - void UpdateWindowTitle(); + void UpdateWindowTitle(const std::string& customTitle = ""); /** * @brief Updates the swap interval from the user settings. diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index a2c72ee..3a08546 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -54,9 +54,11 @@ target_include_directories(ProjectMSDL-GUI target_link_libraries(ProjectMSDL-GUI PUBLIC + PresetEditor Poco::Util ImGui libprojectM::projectM "$<$:-framework ApplicationServices>" ) +add_subdirectory(preset_editor) diff --git a/src/gui/MainMenu.cpp b/src/gui/MainMenu.cpp index 02afba6..9fe7881 100644 --- a/src/gui/MainMenu.cpp +++ b/src/gui/MainMenu.cpp @@ -22,145 +22,203 @@ MainMenu::MainMenu(ProjectMGUI& gui) , _projectMWrapper(Poco::Util::Application::instance().getSubsystem()) , _audioCapture(Poco::Util::Application::instance().getSubsystem()) { + _presetChooser.AllowedExtensions({".milk"}); + _presetChooser.MultiSelect(false); } void MainMenu::Draw() { if (ImGui::BeginMainMenuBar()) { - if (ImGui::BeginMenu("File")) - { - if (ImGui::MenuItem("Settings...", "Ctrl+s")) - { - _gui.ShowSettingsWindow(); - } - - ImGui::Separator(); + DrawFileMenu(); + DrawPlaybackMenu(); + DrawOptionsMenu(); + DrawHelpMenu(); - if (ImGui::MenuItem("Quit projectM", "Ctrl+q")) - { - _notificationCenter.postNotification(new QuitNotification); - } + ImGui::EndMainMenuBar(); + } - ImGui::EndMenu(); + if (_presetChooser.Draw()) + { + auto selectedFile = _presetChooser.SelectedFiles(); + if (!selectedFile.empty()) + { + // Open preset editor + _gui.ShowPresetEditor(selectedFile.at(0).path()); } + } +} - if (ImGui::BeginMenu("Playback")) +void MainMenu::DrawFileMenu() +{ + if (ImGui::BeginMenu("File")) + { + if (ImGui::BeginMenu("Preset Editor")) { - auto& app = ProjectMSDLApplication::instance(); - - if (ImGui::MenuItem("Play Next Preset", "n")) - { - _notificationCenter.postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::LastPreset)); - } - if (ImGui::MenuItem("Play Previous Preset", "p")) + if (ImGui::MenuItem("Edit Current Preset", "Ctrl+e")) { - _notificationCenter.postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::PreviousPreset)); + auto currentPreset = _projectMWrapper.CurrentPresetFileName(); + if (currentPreset.empty()) + { + Poco::NotificationCenter::defaultCenter().postNotification(new DisplayToastNotification("No preset is currently loaded which can be edited.")); + } + else + { + _gui.ShowPresetEditor(currentPreset); + } } - if (ImGui::MenuItem("Go Back One Preset", "Backspace")) + if (ImGui::MenuItem("Select Preset From Disk...", "Ctrl+l")) { - _notificationCenter.postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::LastPreset)); + _presetChooser.Title("Select a Preset for Editing"); + _presetChooser.Show(); } - if (ImGui::MenuItem("Random Preset", "r")) + if (ImGui::MenuItem("Create New Preset", "Ctrl+Shift+n")) { - _notificationCenter.postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::RandomPreset)); + _gui.ShowPresetEditor({}); } - ImGui::Separator(); + ImGui::EndMenu(); + } - if (ImGui::MenuItem("Lock Preset", "Spacebar", app.config().getBool("projectM.presetLocked", false))) - { - _notificationCenter.postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::TogglePresetLocked)); - } - if (ImGui::MenuItem("Enable Shuffle", "y", app.config().getBool("projectM.shuffleEnabled", true))) - { - _notificationCenter.postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::ToggleShuffle)); - } + ImGui::Separator(); - ImGui::Separator(); + if (ImGui::MenuItem("Settings...", "Ctrl+s")) + { + _gui.ShowSettingsWindow(); + } - if (ImGui::MenuItem("Copy Current Preset Filename", "Ctrl+c")) - { - _projectMWrapper.PresetFileNameToClipboard(); - } + ImGui::Separator(); - ImGui::EndMenu(); + if (ImGui::MenuItem("Quit projectM", "Ctrl+q")) + { + _notificationCenter.postNotification(new QuitNotification); } - if (ImGui::BeginMenu("Options")) + ImGui::EndMenu(); + } +} + +void MainMenu::DrawPlaybackMenu() +{ + if (ImGui::BeginMenu("Playback")) + { + auto& app = ProjectMSDLApplication::instance(); + + if (ImGui::MenuItem("Play Next Preset", "n")) { - auto& app = ProjectMSDLApplication::instance(); + _notificationCenter.postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::LastPreset)); + } + if (ImGui::MenuItem("Play Previous Preset", "p")) + { + _notificationCenter.postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::PreviousPreset)); + } + if (ImGui::MenuItem("Go Back One Preset", "Backspace")) + { + _notificationCenter.postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::LastPreset)); + } + if (ImGui::MenuItem("Random Preset", "r")) + { + _notificationCenter.postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::RandomPreset)); + } - if (ImGui::BeginMenu("Audio Capture Device")) - { - auto devices = _audioCapture.AudioDeviceList(); - auto currentIndex = _audioCapture.AudioDeviceIndex(); + ImGui::Separator(); - for (const auto& device : devices) - { - if (ImGui::MenuItem(device.second.c_str(), "", device.first == currentIndex)) - { - _audioCapture.AudioDeviceIndex(device.first); - } - } - ImGui::EndMenu(); - } + if (ImGui::MenuItem("Lock Preset", "Spacebar", app.config().getBool("projectM.presetLocked", false))) + { + _notificationCenter.postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::TogglePresetLocked)); + } + if (ImGui::MenuItem("Enable Shuffle", "y", app.config().getBool("projectM.shuffleEnabled", true))) + { + _notificationCenter.postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::ToggleShuffle)); + } - ImGui::Separator(); + ImGui::Separator(); - if (ImGui::MenuItem("Display Toast Messages", "", app.config().getBool("projectM.displayToasts", true))) - { - app.UserConfiguration()->setBool("projectM.displayToasts", !app.config().getBool("projectM.displayToasts", true)); - } - if (ImGui::MenuItem("Display Preset Name in Window Title", "", app.config().getBool("window.displayPresetNameInTitle", true))) - { - app.UserConfiguration()->setBool("window.displayPresetNameInTitle", !app.config().getBool("window.displayPresetNameInTitle", true)); - _notificationCenter.postNotification(new UpdateWindowTitleNotification); - } + if (ImGui::MenuItem("Copy Current Preset Filename", "Ctrl+c")) + { + _projectMWrapper.PresetFileNameToClipboard(); + } + + ImGui::EndMenu(); + } +} + +void MainMenu::DrawOptionsMenu() +{ + if (ImGui::BeginMenu("Options")) + { + auto& app = ProjectMSDLApplication::instance(); - ImGui::Separator(); + if (ImGui::BeginMenu("Audio Capture Device")) + { + auto devices = _audioCapture.AudioDeviceList(); + auto currentIndex = _audioCapture.AudioDeviceIndex(); - float beatSensitivity = projectm_get_beat_sensitivity(_projectMWrapper.ProjectM()); - if (ImGui::SliderFloat("Beat Sensitivity", &beatSensitivity, 0.0f, 2.0f)) + for (const auto& device : devices) { - projectm_set_beat_sensitivity(_projectMWrapper.ProjectM(), beatSensitivity); - app.UserConfiguration()->setDouble("projectM.beatSensitivity", beatSensitivity); + if (ImGui::MenuItem(device.second.c_str(), "", device.first == currentIndex)) + { + _audioCapture.AudioDeviceIndex(device.first); + } } - ImGui::EndMenu(); } - if (ImGui::BeginMenu("Help")) + ImGui::Separator(); + + if (ImGui::MenuItem("Display Toast Messages", "", app.config().getBool("projectM.displayToasts", true))) { - if (ImGui::MenuItem("Quick Help...")) - { - _gui.ShowHelpWindow(); - } + app.UserConfiguration()->setBool("projectM.displayToasts", !app.config().getBool("projectM.displayToasts", true)); + } + if (ImGui::MenuItem("Display Preset Name in Window Title", "", app.config().getBool("window.displayPresetNameInTitle", true))) + { + app.UserConfiguration()->setBool("window.displayPresetNameInTitle", !app.config().getBool("window.displayPresetNameInTitle", true)); + _notificationCenter.postNotification(new UpdateWindowTitleNotification); + } - ImGui::Separator(); + ImGui::Separator(); - if (ImGui::MenuItem("About projectM...")) - { - _gui.ShowAboutWindow(); - } + float beatSensitivity = projectm_get_beat_sensitivity(_projectMWrapper.ProjectM()); + if (ImGui::SliderFloat("Beat Sensitivity", &beatSensitivity, 0.0f, 2.0f)) + { + projectm_set_beat_sensitivity(_projectMWrapper.ProjectM(), beatSensitivity); + app.UserConfiguration()->setDouble("projectM.beatSensitivity", beatSensitivity); + } - ImGui::Separator(); + ImGui::EndMenu(); + } +} - if (ImGui::MenuItem("Visit the projectM Wiki on GitHub")) - { - SystemBrowser::OpenURL("https://github.com/projectM-visualizer/projectm/wiki"); - } - if (ImGui::MenuItem("Report a Bug or Request a Feature")) - { - SystemBrowser::OpenURL("https://github.com/projectM-visualizer/projectm/issues/new/choose"); - } - if (ImGui::MenuItem("Sponsor projectM on OpenCollective")) - { - SystemBrowser::OpenURL("https://opencollective.com/projectm"); - } - ImGui::EndMenu(); +void MainMenu::DrawHelpMenu() +{ + if (ImGui::BeginMenu("Help")) + { + if (ImGui::MenuItem("Quick Help...")) + { + _gui.ShowHelpWindow(); } - ImGui::EndMainMenuBar(); + ImGui::Separator(); + + if (ImGui::MenuItem("About projectM...")) + { + _gui.ShowAboutWindow(); + } + + ImGui::Separator(); + + if (ImGui::MenuItem("Visit the projectM Wiki on GitHub")) + { + SystemBrowser::OpenURL("https://github.com/projectM-visualizer/projectm/wiki"); + } + if (ImGui::MenuItem("Report a Bug or Request a Feature")) + { + SystemBrowser::OpenURL("https://github.com/projectM-visualizer/projectm/issues/new/choose"); + } + if (ImGui::MenuItem("Sponsor projectM on OpenCollective")) + { + SystemBrowser::OpenURL("https://opencollective.com/projectm"); + } + ImGui::EndMenu(); } } diff --git a/src/gui/MainMenu.h b/src/gui/MainMenu.h index b3e4fc3..133e779 100644 --- a/src/gui/MainMenu.h +++ b/src/gui/MainMenu.h @@ -1,6 +1,6 @@ #pragma once -#include +#include "FileChooser.h" class ProjectMGUI; class ProjectMWrapper; @@ -23,8 +23,15 @@ class MainMenu void Draw(); private: + void DrawFileMenu(); + void DrawPlaybackMenu(); + void DrawOptionsMenu(); + void DrawHelpMenu(); + Poco::NotificationCenter& _notificationCenter; //!< Notification center instance. ProjectMGUI& _gui; //!< Reference to the GUI subsystem. ProjectMWrapper& _projectMWrapper; //!< Reference to the projectM wrapper subsystem. AudioCapture& _audioCapture; //!< Reference to the audio capture subsystem. + + FileChooser _presetChooser{FileChooser::Mode::File}; //!< The file chooser dialog to select presets for editing. }; diff --git a/src/gui/ProjectMGUI.cpp b/src/gui/ProjectMGUI.cpp index 925c843..498cbef 100644 --- a/src/gui/ProjectMGUI.cpp +++ b/src/gui/ProjectMGUI.cpp @@ -160,10 +160,13 @@ void ProjectMGUI::Draw() if (_visible) { - _mainMenu.Draw(); - _settingsWindow.Draw(); - _aboutWindow.Draw(); - _helpWindow.Draw(); + if (!_presetEditorGUI.Draw()) + { + _mainMenu.Draw(); + _settingsWindow.Draw(); + _aboutWindow.Draw(); + _helpWindow.Draw(); + } } ImGui::Render(); @@ -197,6 +200,11 @@ void ProjectMGUI::PopFont() ImGui::PopFont(); } +void ProjectMGUI::ShowPresetEditor(const std::string& presetFileName) +{ + _presetEditorGUI.Show(presetFileName); +} + void ProjectMGUI::ShowSettingsWindow() { _settingsWindow.Show(); diff --git a/src/gui/ProjectMGUI.h b/src/gui/ProjectMGUI.h index aca9d88..ac7e35c 100644 --- a/src/gui/ProjectMGUI.h +++ b/src/gui/ProjectMGUI.h @@ -3,6 +3,7 @@ #include "AboutWindow.h" #include "HelpWindow.h" #include "MainMenu.h" +#include "PresetEditorGUI.h" #include "SettingsWindow.h" #include "ToastMessage.h" @@ -89,6 +90,12 @@ class ProjectMGUI : public Poco::Util::Subsystem */ void PopFont(); + /** + * @brief Opens the preset editor UI. + * @param presetFileName The file name of the preset to edit, or empty to use the currently loaded preset. + */ + void ShowPresetEditor(const std::string& presetFileName); + /** * @brief Displays the settings window. */ @@ -128,6 +135,7 @@ class ProjectMGUI : public Poco::Util::Subsystem float _textScalingFactor{0.0f}; //!< The text scaling factor. MainMenu _mainMenu{*this}; + Editor::PresetEditorGUI _presetEditorGUI{*this}; //!< The preset editor GUI. SettingsWindow _settingsWindow{*this}; //!< The settings window. AboutWindow _aboutWindow{*this}; //!< The about window. HelpWindow _helpWindow; //!< Help window with shortcuts and tips. diff --git a/src/gui/preset_editor/CMakeLists.txt b/src/gui/preset_editor/CMakeLists.txt new file mode 100644 index 0000000..f5dae8a --- /dev/null +++ b/src/gui/preset_editor/CMakeLists.txt @@ -0,0 +1,28 @@ +add_subdirectory(imgui_color_text_editor) + +add_library(PresetEditor STATIC + EditorPreset.cpp + EditorPreset.h + PresetEditorGUI.cpp + PresetEditorGUI.h + PresetFile.cpp + PresetFile.h + EditorMenu.cpp + EditorMenu.h + ) + +target_link_libraries(PresetEditor + PUBLIC + ImGUIColorTextEditor + ImGui + Poco::Util + libprojectM::projectM + ) + +target_include_directories(PresetEditor + PRIVATE + "${CMAKE_SOURCE_DIR}/src" + "${CMAKE_CURRENT_SOURCE_DIR}/.." + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ) diff --git a/src/gui/preset_editor/EditorMenu.cpp b/src/gui/preset_editor/EditorMenu.cpp new file mode 100644 index 0000000..3ea9950 --- /dev/null +++ b/src/gui/preset_editor/EditorMenu.cpp @@ -0,0 +1,107 @@ +#include "EditorMenu.h" + +#include "PresetEditorGUI.h" + +#include "gui/SystemBrowser.h" + +#include "notifications/QuitNotification.h" + +#include + +namespace Editor { + +EditorMenu::EditorMenu(PresetEditorGUI& gui) + : _notificationCenter(Poco::NotificationCenter::defaultCenter()) + , _presetEditorGUI(gui) +{ +} + +void EditorMenu::Draw() +{ + if (ImGui::BeginMainMenuBar()) + { + DrawFileMenu(); + + ImGui::Separator(); + if (ImGui::Button("Update Preset Preview")) + { + _presetEditorGUI.UpdatePresetPreview(); + } + ImGui::Separator(); + + DrawHelpMenu(); + + ImGui::EndMainMenuBar(); + } +} + +void EditorMenu::DrawFileMenu() +{ + if (ImGui::BeginMenu("File")) + { + if (ImGui::MenuItem("New Preset")) + { + _presetEditorGUI.Show(""); + } + + if (ImGui::MenuItem("Open Preset...")) + { + } + + if (ImGui::MenuItem("Save Preset")) + { + } + + if (ImGui::MenuItem("Save Preset As...")) + { + } + + ImGui::Separator(); + + if (ImGui::MenuItem("Exit Preset Editor")) + { + _presetEditorGUI.Close(); + } + + ImGui::Separator(); + + if (ImGui::MenuItem("Quit projectM", "Ctrl+q")) + { + _notificationCenter.postNotification(new QuitNotification); + } + + ImGui::EndMenu(); + } +} + +void EditorMenu::DrawHelpMenu() +{ + + if (ImGui::BeginMenu("Help")) + { + + if (ImGui::BeginMenu("Online Documentation")) + { + if (ImGui::MenuItem("Milkdrop Preset Authoring Guide")) + { + SystemBrowser::OpenURL("https://www.geisswerks.com/milkdrop/milkdrop_preset_authoring.html"); + } + + if (ImGui::MenuItem("Milkdrop Expression Syntax")) + { + SystemBrowser::OpenURL("https://github.com/projectM-visualizer/projectm-eval/blob/master/docs/Expression-Syntax.md"); + } + + if (ImGui::MenuItem("DirectX HLSL Reference")) + { + SystemBrowser::OpenURL("https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-reference"); + } + + ImGui::EndMenu(); + } + + ImGui::EndMenu(); + } +} + +} // namespace Editor diff --git a/src/gui/preset_editor/EditorMenu.h b/src/gui/preset_editor/EditorMenu.h new file mode 100644 index 0000000..6c8613f --- /dev/null +++ b/src/gui/preset_editor/EditorMenu.h @@ -0,0 +1,33 @@ +#pragma once + +namespace Poco { +class NotificationCenter; +} + +namespace Editor { + +class PresetEditorGUI; + +class EditorMenu +{ +public: + EditorMenu() = delete; + + explicit EditorMenu(PresetEditorGUI& gui); + + /** + * @brief Draws the editor menu bar. + */ + void Draw(); + +private: + void DrawFileMenu(); + void DrawHelpMenu(); + + Poco::NotificationCenter& _notificationCenter; //!< Notification center instance. + + PresetEditorGUI& _presetEditorGUI; +}; + + +} // namespace Editor diff --git a/src/gui/preset_editor/EditorPreset.cpp b/src/gui/preset_editor/EditorPreset.cpp new file mode 100644 index 0000000..7421278 --- /dev/null +++ b/src/gui/preset_editor/EditorPreset.cpp @@ -0,0 +1,352 @@ +#include "EditorPreset.h" + +namespace Editor { + +void EditorPreset::FromParsedFile(const PresetFile& parsedFile) +{ + // General: + decay = parsedFile.GetFloat("fDecay", decay); + gammaAdj = parsedFile.GetFloat("fGammaAdj", gammaAdj); + videoEchoZoom = parsedFile.GetFloat("fVideoEchoZoom", videoEchoZoom); + videoEchoAlpha = parsedFile.GetFloat("fVideoEchoAlpha", videoEchoAlpha); + videoEchoOrientation = parsedFile.GetInt("nVideoEchoOrientation", videoEchoOrientation); + redBlueStereo = parsedFile.GetBool("bRedBlueStereo", redBlueStereo); + brighten = parsedFile.GetBool("bBrighten", brighten); + darken = parsedFile.GetBool("bDarken", darken); + solarize = parsedFile.GetBool("bSolarize", solarize); + invert = parsedFile.GetBool("bInvert", invert); + shader = parsedFile.GetFloat("fShader", shader); + blur1Min = parsedFile.GetFloat("b1n", blur1Min); + blur2Min = parsedFile.GetFloat("b2n", blur2Min); + blur3Min = parsedFile.GetFloat("b3n", blur3Min); + blur1Max = parsedFile.GetFloat("b1x", blur1Max); + blur2Max = parsedFile.GetFloat("b2x", blur2Max); + blur3Max = parsedFile.GetFloat("b3x", blur3Max); + blur1EdgeDarken = parsedFile.GetFloat("b1ed", blur1EdgeDarken); + + // Wave: + waveMode = parsedFile.GetInt("nWaveMode", waveMode); + additiveWaves = parsedFile.GetBool("bAdditiveWaves", additiveWaves); + waveDots = parsedFile.GetBool("bWaveDots", waveDots); + waveThick = parsedFile.GetBool("bWaveThick", waveThick); + modWaveAlphaByVolume = parsedFile.GetBool("bModWaveAlphaByVolume", modWaveAlphaByVolume); + maximizeWaveColor = parsedFile.GetBool("bMaximizeWaveColor", maximizeWaveColor); + waveScale = parsedFile.GetFloat("fWaveScale", waveScale); + waveSmoothing = parsedFile.GetFloat("fWaveSmoothing", waveSmoothing); + waveParam = parsedFile.GetFloat("fWaveParam", waveParam); + modWaveAlphaStart = parsedFile.GetFloat("fModWaveAlphaStart", modWaveAlphaStart); + modWaveAlphaEnd = parsedFile.GetFloat("fModWaveAlphaEnd", modWaveAlphaEnd); + waveColor.red = parsedFile.GetFloat("wave_r", waveColor.red); + waveColor.green = parsedFile.GetFloat("wave_g", waveColor.green); + waveColor.blue = parsedFile.GetFloat("wave_b", waveColor.blue); + waveColor.alpha = parsedFile.GetFloat("fWaveAlpha", waveColor.alpha); + waveX = parsedFile.GetFloat("wave_x", waveX); + waveY = parsedFile.GetFloat("wave_y", waveY); + mvX = parsedFile.GetFloat("nMotionVectorsX", mvX); + mvY = parsedFile.GetFloat("nMotionVectorsY", mvY); + mvDX = parsedFile.GetFloat("mv_dx", mvDX); + mvDY = parsedFile.GetFloat("mv_dy", mvDY); + mvL = parsedFile.GetFloat("mv_l", mvL); + mvColor.red = parsedFile.GetFloat("mv_r", mvColor.red); + mvColor.green = parsedFile.GetFloat("mv_g", mvColor.green); + mvColor.blue = parsedFile.GetFloat("mv_b", mvColor.blue); + mvColor.alpha = parsedFile.GetBool("bMotionVectorsOn", false) ? 1.0f : 0.0f; // for backwards compatibility + mvColor.alpha = parsedFile.GetFloat("mv_a", mvColor.alpha); + + // Motion: + zoom = parsedFile.GetFloat("zoom", zoom); + rot = parsedFile.GetFloat("rot", rot); + rotCX = parsedFile.GetFloat("cx", rotCX); + rotCY = parsedFile.GetFloat("cy", rotCY); + xPush = parsedFile.GetFloat("dx", xPush); + yPush = parsedFile.GetFloat("dy", yPush); + warpAmount = parsedFile.GetFloat("warp", warpAmount); + stretchX = parsedFile.GetFloat("sx", stretchX); + stretchY = parsedFile.GetFloat("sy", stretchY); + texWrap = parsedFile.GetBool("bTexWrap", texWrap); + darkenCenter = parsedFile.GetBool("bDarkenCenter", darkenCenter); + warpAnimSpeed = parsedFile.GetFloat("fWarpAnimSpeed", warpAnimSpeed); + warpScale = parsedFile.GetFloat("fWarpScale", warpScale); + zoomExponent = parsedFile.GetFloat("fZoomExponent", zoomExponent); + + // Borders: + outerBorderSize = parsedFile.GetFloat("ob_size", outerBorderSize); + outerBorderColor.red = parsedFile.GetFloat("ob_r", outerBorderColor.red); + outerBorderColor.green = parsedFile.GetFloat("ob_g", outerBorderColor.green); + outerBorderColor.blue = parsedFile.GetFloat("ob_b", outerBorderColor.blue); + outerBorderColor.alpha = parsedFile.GetFloat("ob_a", outerBorderColor.alpha); + innerBorderSize = parsedFile.GetFloat("ib_size", innerBorderSize); + innerBorderColor.red = parsedFile.GetFloat("ib_r", innerBorderColor.red); + innerBorderColor.green = parsedFile.GetFloat("ib_g", innerBorderColor.green); + innerBorderColor.blue = parsedFile.GetFloat("ib_b", innerBorderColor.blue); + innerBorderColor.alpha = parsedFile.GetFloat("ib_a", innerBorderColor.alpha); + + // Versions: + presetVersion = parsedFile.GetInt("MILKDROP_PRESET_VERSION", presetVersion); + if (presetVersion < 200) + { + // Milkdrop 1.x did not use shaders. + warpShaderVersion = 0; + compositeShaderVersion = 0; + } + else if (presetVersion == 200) + { + // Milkdrop 2.0 only supported a single shader language level variable. + warpShaderVersion = parsedFile.GetInt("PSVERSION", warpShaderVersion); + compositeShaderVersion = parsedFile.GetInt("PSVERSION", compositeShaderVersion); + } + else + { + warpShaderVersion = parsedFile.GetInt("PSVERSION_WARP", warpShaderVersion); + compositeShaderVersion = parsedFile.GetInt("PSVERSION_COMP", compositeShaderVersion); + } + + // Code: + perFrameInitCode = parsedFile.GetCode("per_frame_init_"); + perFrameCode = parsedFile.GetCode("per_frame_"); + perPixelCode = parsedFile.GetCode("per_pixel_"); + + // Custom waveform code: + for (int index = 0; index < 4; index++) + { + Wave& wave = waves[index]; + + std::string const wavecodePrefix = "wavecode_" + std::to_string(index) + "_"; + + wave.index = index; + wave.enabled = parsedFile.GetInt(wavecodePrefix + "enabled", wave.enabled); + wave.samples = parsedFile.GetInt(wavecodePrefix + "samples", wave.samples); + wave.sep = parsedFile.GetInt(wavecodePrefix + "sep", wave.sep); + wave.spectrum = parsedFile.GetBool(wavecodePrefix + "bSpectrum", wave.spectrum); + wave.useDots = parsedFile.GetBool(wavecodePrefix + "bUseDots", wave.useDots); + wave.drawThick = parsedFile.GetBool(wavecodePrefix + "bDrawThick", wave.drawThick); + wave.additive = parsedFile.GetBool(wavecodePrefix + "bAdditive", wave.additive); + wave.scaling = parsedFile.GetFloat(wavecodePrefix + "scaling", wave.scaling); + wave.smoothing = parsedFile.GetFloat(wavecodePrefix + "smoothing", wave.smoothing); + wave.color.red = parsedFile.GetFloat(wavecodePrefix + "r", wave.color.red); + wave.color.green = parsedFile.GetFloat(wavecodePrefix + "g", wave.color.green); + wave.color.blue = parsedFile.GetFloat(wavecodePrefix + "b", wave.color.blue); + wave.color.alpha = parsedFile.GetFloat(wavecodePrefix + "a", wave.color.alpha); + + std::string const wavePrefix = "wave_" + std::to_string(index) + "_"; + + wave.initCode = parsedFile.GetCode(wavePrefix + "init"); + wave.perFrameCode = parsedFile.GetCode(wavePrefix + "per_frame"); + wave.perPointCode = parsedFile.GetCode(wavePrefix + "per_point"); + } + + // Custom shapes + for (int index = 0; index < 4; index++) + { + Shape& shape = shapes[index]; + + std::string const shapecodePrefix = "shapecode_" + std::to_string(index) + "_"; + + shape.index = index; + shape.enabled = parsedFile.GetBool(shapecodePrefix + "enabled", shape.enabled); + shape.sides = parsedFile.GetInt(shapecodePrefix + "sides", shape.sides); + shape.additive = parsedFile.GetBool(shapecodePrefix + "additive", shape.additive); + shape.thickOutline = parsedFile.GetBool(shapecodePrefix + "thickOutline", shape.thickOutline); + shape.textured = parsedFile.GetBool(shapecodePrefix + "textured", shape.textured); + shape.instances = parsedFile.GetInt(shapecodePrefix + "nushape.inst", shape.instances); + shape.x = parsedFile.GetFloat(shapecodePrefix + "x", shape.x); + shape.y = parsedFile.GetFloat(shapecodePrefix + "y", shape.y); + shape.radius = parsedFile.GetFloat(shapecodePrefix + "rad", shape.radius); + shape.angle = parsedFile.GetFloat(shapecodePrefix + "ang", shape.angle); + shape.tex_ang = parsedFile.GetFloat(shapecodePrefix + "tex_ang", shape.tex_ang); + shape.tex_zoom = parsedFile.GetFloat(shapecodePrefix + "tex_zoom", shape.tex_zoom); + shape.color.red = parsedFile.GetFloat(shapecodePrefix + "r", shape.color.red); + shape.color.green = parsedFile.GetFloat(shapecodePrefix + "g", shape.color.green); + shape.color.blue = parsedFile.GetFloat(shapecodePrefix + "b", shape.color.blue); + shape.color.alpha = parsedFile.GetFloat(shapecodePrefix + "a", shape.color.alpha); + shape.color2.red = parsedFile.GetFloat(shapecodePrefix + "r2", shape.color2.red); + shape.color2.green = parsedFile.GetFloat(shapecodePrefix + "g2", shape.color2.green); + shape.color2.blue = parsedFile.GetFloat(shapecodePrefix + "b2", shape.color2.blue); + shape.color2.alpha = parsedFile.GetFloat(shapecodePrefix + "a2", shape.color2.alpha); + shape.borderColor.red = parsedFile.GetFloat(shapecodePrefix + "border_r", shape.borderColor.red); + shape.borderColor.green = parsedFile.GetFloat(shapecodePrefix + "border_g", shape.borderColor.green); + shape.borderColor.blue = parsedFile.GetFloat(shapecodePrefix + "border_b", shape.borderColor.blue); + shape.borderColor.alpha = parsedFile.GetFloat(shapecodePrefix + "border_a", shape.borderColor.alpha); + + std::string const shapePrefix = "shape_" + std::to_string(index) + "_"; + + shape.initCode = parsedFile.GetCode(shapePrefix + "init"); + shape.perFrameCode = parsedFile.GetCode(shapePrefix + "per_frame"); + } + + // Shader code: + warpShader = parsedFile.GetCode("warp_"); + compositeShader = parsedFile.GetCode("comp_"); +} + +void EditorPreset::ToParsedFile(PresetFile& parsedFile) +{ + // General: + parsedFile.SetFloat("fDecay", decay); + parsedFile.SetFloat("fGammaAdj", gammaAdj); + parsedFile.SetFloat("fVideoEchoZoom", videoEchoZoom); + parsedFile.SetFloat("fVideoEchoAlpha", videoEchoAlpha); + parsedFile.SetInt("nVideoEchoOrientation", videoEchoOrientation); + parsedFile.SetBool("bRedBlueStereo", redBlueStereo); + parsedFile.SetBool("bBrighten", brighten); + parsedFile.SetBool("bDarken", darken); + parsedFile.SetBool("bSolarize", solarize); + parsedFile.SetBool("bInvert", invert); + parsedFile.SetFloat("fShader", shader); + parsedFile.SetFloat("b1n", blur1Min); + parsedFile.SetFloat("b2n", blur2Min); + parsedFile.SetFloat("b3n", blur3Min); + parsedFile.SetFloat("b1x", blur1Max); + parsedFile.SetFloat("b2x", blur2Max); + parsedFile.SetFloat("b3x", blur3Max); + parsedFile.SetFloat("b1ed", blur1EdgeDarken); + + // Wave: + parsedFile.SetInt("nWaveMode", waveMode); + parsedFile.SetBool("bAdditiveWaves", additiveWaves); + parsedFile.SetBool("bWaveDots", waveDots); + parsedFile.SetBool("bWaveThick", waveThick); + parsedFile.SetBool("bModWaveAlphaByVolume", modWaveAlphaByVolume); + parsedFile.SetBool("bMaximizeWaveColor", maximizeWaveColor); + parsedFile.SetFloat("fWaveAlpha", waveColor.alpha); + parsedFile.SetFloat("fWaveScale", waveScale); + parsedFile.SetFloat("fWaveSmoothing", waveSmoothing); + parsedFile.SetFloat("fWaveParam", waveParam); + parsedFile.SetFloat("fModWaveAlphaStart", modWaveAlphaStart); + parsedFile.SetFloat("fModWaveAlphaEnd", modWaveAlphaEnd); + parsedFile.SetFloat("wave_r", waveColor.red); + parsedFile.SetFloat("wave_g", waveColor.green); + parsedFile.SetFloat("wave_b", waveColor.blue); + parsedFile.SetFloat("wave_x", waveX); + parsedFile.SetFloat("wave_y", waveY); + parsedFile.SetFloat("nMotionVectorsX", mvX); + parsedFile.SetFloat("nMotionVectorsY", mvY); + parsedFile.SetFloat("mv_dx", mvDX); + parsedFile.SetFloat("mv_dy", mvDY); + parsedFile.SetFloat("mv_l", mvL); + parsedFile.SetFloat("mv_r", mvColor.red); + parsedFile.SetFloat("mv_g", mvColor.green); + parsedFile.SetFloat("mv_b", mvColor.blue); + parsedFile.SetFloat("mv_a", mvColor.alpha); + + // Motion: + parsedFile.SetFloat("zoom", zoom); + parsedFile.SetFloat("rot", rot); + parsedFile.SetFloat("cx", rotCX); + parsedFile.SetFloat("cy", rotCY); + parsedFile.SetFloat("dx", xPush); + parsedFile.SetFloat("dy", yPush); + parsedFile.SetFloat("warp", warpAmount); + parsedFile.SetFloat("sx", stretchX); + parsedFile.SetFloat("sy", stretchY); + parsedFile.SetBool("bTexWrap", texWrap); + parsedFile.SetBool("bDarkenCenter", darkenCenter); + parsedFile.SetFloat("fWarpAnimSpeed", warpAnimSpeed); + parsedFile.SetFloat("fWarpScale", warpScale); + parsedFile.SetFloat("fZoomExponent", zoomExponent); + + // Borders: + parsedFile.SetFloat("ob_size", outerBorderSize); + parsedFile.SetFloat("ob_r", outerBorderColor.red); + parsedFile.SetFloat("ob_g", outerBorderColor.green); + parsedFile.SetFloat("ob_b", outerBorderColor.blue); + parsedFile.SetFloat("ob_a", outerBorderColor.alpha); + parsedFile.SetFloat("ib_size", innerBorderSize); + parsedFile.SetFloat("ib_r", innerBorderColor.red); + parsedFile.SetFloat("ib_g", innerBorderColor.green); + parsedFile.SetFloat("ib_b", innerBorderColor.blue); + parsedFile.SetFloat("ib_a", innerBorderColor.alpha); + + // Versions: + parsedFile.SetInt("MILKDROP_PRESET_VERSION", presetVersion); + if (presetVersion < 200) + { + // Milkdrop 1.x did not use shaders. + parsedFile.SetInt("PSVERSION", 0); + parsedFile.SetInt("PSVERSION_WARP", 0); + parsedFile.SetInt("PSVERSION_COMP", 0); + } + else + { + parsedFile.SetInt("PSVERSION", warpShaderVersion); + parsedFile.SetInt("PSVERSION_WARP", warpShaderVersion); + parsedFile.SetInt("PSVERSION_COMP", compositeShaderVersion); + } + + // Code: + parsedFile.SetCode("per_frame_init_", perFrameInitCode); + parsedFile.SetCode("per_frame_", perFrameCode); + parsedFile.SetCode("per_pixel_", perPixelCode); + + // Custom waveform code: + for (int index = 0; index < 4; index++) + { + Wave& wave = waves[index]; + + std::string const wavecodePrefix = "wavecode_" + std::to_string(index) + "_"; + + parsedFile.SetInt(wavecodePrefix + "enabled", wave.enabled); + parsedFile.SetInt(wavecodePrefix + "samples", wave.samples); + parsedFile.SetInt(wavecodePrefix + "sep", wave.sep); + parsedFile.SetBool(wavecodePrefix + "bSpectrum", wave.spectrum); + parsedFile.SetBool(wavecodePrefix + "bUseDots", wave.useDots); + parsedFile.SetBool(wavecodePrefix + "bDrawThick", wave.drawThick); + parsedFile.SetBool(wavecodePrefix + "bAdditive", wave.additive); + parsedFile.SetFloat(wavecodePrefix + "scaling", wave.scaling); + parsedFile.SetFloat(wavecodePrefix + "smoothing", wave.smoothing); + parsedFile.SetFloat(wavecodePrefix + "r", wave.color.red); + parsedFile.SetFloat(wavecodePrefix + "g", wave.color.green); + parsedFile.SetFloat(wavecodePrefix + "b", wave.color.blue); + parsedFile.SetFloat(wavecodePrefix + "a", wave.color.alpha); + + std::string const wavePrefix = "wave_" + std::to_string(index) + "_"; + + parsedFile.SetCode(wavePrefix + "init", wave.initCode); + parsedFile.SetCode(wavePrefix + "per_frame", wave.perFrameCode); + parsedFile.SetCode(wavePrefix + "per_point", wave.perPointCode); + } + + // Custom shapes + for (int index = 0; index < 4; index++) + { + Shape& shape = shapes[index]; + + std::string const shapecodePrefix = "shapecode_" + std::to_string(index) + "_"; + + parsedFile.SetBool(shapecodePrefix + "enabled", shape.enabled); + parsedFile.SetInt(shapecodePrefix + "sides", shape.sides); + parsedFile.SetBool(shapecodePrefix + "additive", shape.additive); + parsedFile.SetBool(shapecodePrefix + "thickOutline", shape.thickOutline); + parsedFile.SetBool(shapecodePrefix + "textured", shape.textured); + parsedFile.SetInt(shapecodePrefix + "nushape.inst", shape.instances); + parsedFile.SetFloat(shapecodePrefix + "x", shape.x); + parsedFile.SetFloat(shapecodePrefix + "y", shape.y); + parsedFile.SetFloat(shapecodePrefix + "rad", shape.radius); + parsedFile.SetFloat(shapecodePrefix + "ang", shape.angle); + parsedFile.SetFloat(shapecodePrefix + "tex_ang", shape.tex_ang); + parsedFile.SetFloat(shapecodePrefix + "tex_zoom", shape.tex_zoom); + parsedFile.SetFloat(shapecodePrefix + "r", shape.color.red); + parsedFile.SetFloat(shapecodePrefix + "g", shape.color.green); + parsedFile.SetFloat(shapecodePrefix + "b", shape.color.blue); + parsedFile.SetFloat(shapecodePrefix + "a", shape.color.alpha); + parsedFile.SetFloat(shapecodePrefix + "r2", shape.color2.red); + parsedFile.SetFloat(shapecodePrefix + "g2", shape.color2.green); + parsedFile.SetFloat(shapecodePrefix + "b2", shape.color2.blue); + parsedFile.SetFloat(shapecodePrefix + "a2", shape.color2.alpha); + parsedFile.SetFloat(shapecodePrefix + "border_r", shape.borderColor.red); + parsedFile.SetFloat(shapecodePrefix + "border_g", shape.borderColor.green); + parsedFile.SetFloat(shapecodePrefix + "border_b", shape.borderColor.blue); + parsedFile.SetFloat(shapecodePrefix + "border_a", shape.borderColor.alpha); + + std::string const shapePrefix = "shape_" + std::to_string(index) + "_"; + + parsedFile.SetCode(shapePrefix + "init", shape.initCode); + parsedFile.SetCode(shapePrefix + "per_frame", shape.perFrameCode); + } + + // Shader code: + parsedFile.SetCode("warp_", warpShader); + parsedFile.SetCode("comp_", compositeShader); +} + +} // namespace Editor diff --git a/src/gui/preset_editor/EditorPreset.h b/src/gui/preset_editor/EditorPreset.h new file mode 100644 index 0000000..58ed86e --- /dev/null +++ b/src/gui/preset_editor/EditorPreset.h @@ -0,0 +1,166 @@ +#pragma once + +#include "PresetFile.h" + +#include +#include + +namespace Editor { + +/** + * @class EditorPreset + * @brief Holds all parsed preset values and code and syncs them with the file. + * + * Since ImGUI needs to retrieve data on every rendered frame, using the PresetFile getter/setter methods + * is too slow. For this reason, we keep the values easily accessible in this class as public members. + * + * The class provides methods to sync the data in both directions with a PresetFile. + */ +class EditorPreset +{ +public: + /** + * Used like an array of floats in ImGui::ColorEditor4 + */ + struct ColorRGBA { + float red{}; + float green{}; + float blue{}; + float alpha{}; + }; + + class Wave + { + public: + int index{0}; //!< Custom waveform index in the preset. + int enabled{0}; //!< Render waveform if non-zero. + int samples{512}; //!< Number of samples/vertices in the waveform. + int sep{0}; //!< Separation distance of dual waveforms. + + float scaling{1.0f}; //!< Scale factor of waveform. + float smoothing{0.5f}; //!< Smooth factor of waveform. + float x{0.5f}; + float y{0.5f}; + + ColorRGBA color{1.0f, 1.0f, 1.0f, 1.0f}; //!< Wave color + + bool spectrum{false}; //!< Spectrum data or PCM data. + bool useDots{false}; //!< If non-zero, draw wave as dots instead of lines. + bool drawThick{false}; //!< Draw thicker lines. + bool additive{false}; //!< Add color values together. + + std::string initCode; //!< Custom wave init code, run once on load. + std::string perFrameCode; //!< Custom wave per-frame code, run once after the per-frame code. + std::string perPointCode; //!< Custom wave per-point code, run once per waveform vertex. + }; + + class Shape + { + public: + int index{0}; //!< The custom shape index in the preset. + bool enabled{false}; //!< If false, the shape isn't drawn. + int sides{4}; //!< Number of sides (vertices) + bool additive{false}; //!< Flag that specifies whether the shape should be drawn additive. + bool thickOutline{false}; //!< If true, the shape is rendered with a thick line, otherwise a single-pixel line. + bool textured{false}; //!< If true, the shape will be rendered with the given texture. + int instances{1}; //!< Number of shape instances to render + + float x{0.5f}; //!< The shape x position. + float y{0.5f}; //!< The shape y position. + float radius{0.1f}; //!< The shape radius (1.0 fills the whole screen). + float angle{0.0f}; //!< The shape rotation. + + ColorRGBA color{1.0f, 0.0f, 0.0f, 1.0f}; //!< First color + ColorRGBA color2{0.0f, 1.0f, 0.0f, 0.0f}; //!< Second color + ColorRGBA borderColor{0.0f, 1.0f, 0.0f, 0.0f}; //!< Border color + + float tex_ang{0.0f}; //!< Texture rotation angle. + float tex_zoom{1.0f}; //!< Texture zoom value. + + std::string initCode; //!< Custom shape init code, run once on load. + std::string perFrameCode; //!< Custom shape per-frame code, run once per shape instance. + }; + + void FromParsedFile(const PresetFile& parsedFile); + + void ToParsedFile(PresetFile& parsedFile); + + float gammaAdj{2.0f}; + float videoEchoZoom{2.0f}; + float videoEchoAlpha{0.0f}; + float videoEchoAlphaOld{0.0f}; + int videoEchoOrientation{0}; + int videoEchoOrientationOld{0}; + + float decay{0.98f}; + + int waveMode{0}; + int oldWaveMode{-1}; + bool additiveWaves{false}; + float waveScale{1.0f}; + float waveSmoothing{0.75f}; + bool waveDots{false}; + bool waveThick{false}; + float waveParam{0.0f}; + bool modWaveAlphaByVolume{false}; + float modWaveAlphaStart{0.75f}; + float modWaveAlphaEnd{0.95f}; + float warpAnimSpeed{1.0f}; + float warpScale{1.0f}; + float zoomExponent{1.0f}; + float shader{0.0f}; + bool maximizeWaveColor{true}; + bool texWrap{true}; + bool darkenCenter{false}; + bool redBlueStereo{false}; + bool brighten{false}; + bool darken{false}; + bool solarize{false}; + bool invert{false}; + + float zoom{1.0f}; + float rot{0.0f}; + float rotCX{0.5f}; + float rotCY{0.5f}; + float xPush{0.0f}; + float yPush{0.0f}; + float warpAmount{1.0f}; + float stretchX{1.0f}; + float stretchY{1.0f}; + ColorRGBA waveColor{1.0f, 1.0f, 1.0f, 0.8f}; + float waveX{0.5f}; + float waveY{0.5f}; + float outerBorderSize{0.01f}; + ColorRGBA outerBorderColor{0.0f, 0.0f, 0.0f, 0.0f}; + float innerBorderSize{0.01f}; + ColorRGBA innerBorderColor{0.25f, 0.25f, 0.25f, 0.0f}; + float mvX{12.0f}; + float mvY{9.0f}; + float mvDX{0.0f}; + float mvDY{0.0f}; + float mvL{0.9f}; + ColorRGBA mvColor{1.0f, 1.0f, 1.0f, 1.0f}; + float blur1Min{0.0f}; + float blur2Min{0.0f}; + float blur3Min{0.0f}; + float blur1Max{1.0f}; + float blur2Max{1.0f}; + float blur3Max{1.0f}; + float blur1EdgeDarken{0.25f}; + + int presetVersion{100}; //!< Value of MILKDROP_PRESET_VERSION in preset files. + int warpShaderVersion{2}; //!< PSVERSION or PSVERSION_WARP. + int compositeShaderVersion{2}; //!< PSVERSION or PSVERSION_COMP. + + std::string perFrameInitCode; //!< Preset init code, run once on load. + std::string perFrameCode; //!< Preset per-frame code, run once at the start of each frame. + std::string perPixelCode; //!< Preset per-pixel/per-vertex code, run once per warp mesh vertex. + + std::array waves; + std::array shapes; + + std::string warpShader; //!< Warp shader code. + std::string compositeShader; //!< Composite shader code. +}; + +} // namespace Editor diff --git a/src/gui/preset_editor/PresetEditorGUI.cpp b/src/gui/preset_editor/PresetEditorGUI.cpp new file mode 100644 index 0000000..5c6e368 --- /dev/null +++ b/src/gui/preset_editor/PresetEditorGUI.cpp @@ -0,0 +1,702 @@ +#include "PresetEditorGUI.h" + +#include "ProjectMGUI.h" + +#include "ProjectMSDLApplication.h" +#include "ProjectMWrapper.h" + +#include "notifications/DisplayToastNotification.h" +#include "notifications/UpdateWindowTitleNotification.h" + +#include + +#include +#include + +namespace Editor { + +PresetEditorGUI::PresetEditorGUI(ProjectMGUI& gui) + : _gui(gui) + , _application(ProjectMSDLApplication::instance()) + , _projectMWrapper(Poco::Util::Application::instance().getSubsystem()) + , _menu(*this) +{ +} + +void PresetEditorGUI::Show(const std::string& presetFile) +{ + if (presetFile.empty()) + { + _presetFile = PresetFile::EmptyPreset(); + } + else + { + _presetFile = PresetFile(); + if (!_presetFile.ReadFile(presetFile)) + { + Poco::NotificationCenter::defaultCenter().postNotification(new DisplayToastNotification("The selected preset could not be loaded.")); + return; + } + } + + TakeProjectMControl(); + + // Load the parsed preset as data + std::string errorMessage; + if (!_projectMWrapper.LoadPresetData(_presetFile.AsString(), errorMessage)) + { + Poco::NotificationCenter::defaultCenter().postNotification(new DisplayToastNotification(errorMessage)); + ReleaseProjectMControl(); + return; + } + + _editorPreset.FromParsedFile(_presetFile); + + _visible = true; +} + +void PresetEditorGUI::Close() +{ + _wantClose = true; +} + +bool PresetEditorGUI::Draw() +{ + if (!_visible) + { + return false; + } + + _menu.Draw(); + + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(viewport->WorkPos); + ImGui::SetNextWindowSize(viewport->WorkSize); + ImGui::PushStyleColor(ImGuiCol_TitleBgActive, IM_COL32(0xd9, 0x1e, 0x18, 0xff)); + + if (ImGui::Begin("Preset Editor", &_visible, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoBackground)) + { + DrawLeftSideBar(); + } + ImGui::End(); + + ImGui::PopStyleColor(); + + if (!_visible || _wantClose) + { + // Check for unsaved data + + ReleaseProjectMControl(); + + _visible = false; + _wantClose = false; + } + + return true; +} + +void PresetEditorGUI::UpdatePresetPreview() +{ + std::string errorMessage; + + _editorPreset.ToParsedFile(_presetFile); + if (!_projectMWrapper.LoadPresetData(_presetFile.AsString(), errorMessage)) + { + Poco::NotificationCenter::defaultCenter().postNotification(new DisplayToastNotification("Preset reload failed:\n" + errorMessage)); + } +} + +void PresetEditorGUI::TakeProjectMControl() +{ + // Detach playlist, seize control over projectM + _projectMWrapper.UnbindPlaylist(); + _projectMWrapper.EnablePlaybackControl(false); + _projectMWrapper.HardLockPreset(true); + + Poco::NotificationCenter::defaultCenter().postNotification(new UpdateWindowTitleNotification("projectM Preset Editor")); +} + +void PresetEditorGUI::ReleaseProjectMControl() +{ + // Reattach playlist and restore original settings + _projectMWrapper.BindPlaylist(); + _projectMWrapper.EnablePlaybackControl(true); + _projectMWrapper.HardLockPreset(false); + + Poco::NotificationCenter::defaultCenter().postNotification(new UpdateWindowTitleNotification()); +} + +void PresetEditorGUI::DrawLeftSideBar() +{ + ImGuiWindowFlags window_flags = ImGuiWindowFlags_None; + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 5.0f); + ImGui::SetNextWindowBgAlpha(0.5f); + ImGui::BeginChild("LeftSideBar", ImVec2(400, 0), ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeX, window_flags); + + DrawPresetCompatibilitySettings(); + DrawGeneralParameters(); + DrawDefaultWaveformSettings(); + DrawMotionVectorSettings(); + + if (ImGui::CollapsingHeader("Warping and Motion")) + { + ImGui::TextUnformatted("Translation"); + ImGui::Indent(16.0f); + + ImGui::SliderFloat("Horizontal Motion", &_editorPreset.xPush, -1.00f, 1.0f); + DrawHelpTooltip("Controls amount of constant horizontal motion- -0.01 = move left 1% per frame, 0=none, 0.01 = move right 1%"); + + ImGui::SliderFloat("Vertical Motion", &_editorPreset.yPush, -1.00f, 1.0f); + DrawHelpTooltip("Controls amount of constant vertical motion. -0.01 = move up 1% per frame, 0=none, 0.01 = move down 1%"); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + ImGui::TextUnformatted("Rotation"); + ImGui::Indent(16.0f); + + ImGui::SliderFloat("Rotation##WarpRotation", &_editorPreset.rot, -1.00f, 1.0f); + DrawHelpTooltip("Controls the amount of rotation. 0=none, 0.1=slightly right, -0.1=slightly clockwise, 0.1=CCW"); + + ImGui::SliderFloat("Center X##WarpCenterX", &_editorPreset.rotCX, 0.00f, 1.0f); + DrawHelpTooltip("Controls where the center of rotation and stretching is, horizontally. 0=left, 0.5=center, 1=right"); + + ImGui::SliderFloat("Center Y##WarpCenterY", &_editorPreset.rotCY, 0.00f, 1.0f); + DrawHelpTooltip("Controls where the center of rotation and stretching is, vertically. 0=top, 0.5=center, 1=bottom"); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + ImGui::TextUnformatted("Scaling"); + ImGui::Indent(16.0f); + + ImGui::SliderFloat("Stretch X##WarpStretchX", &_editorPreset.stretchX, 0.00f, 2.0f); + DrawHelpTooltip("Controls amount of constant horizontal stretching. 0.99=shrink 1%, 1=normal, 1.01=stretch 1%"); + + ImGui::SliderFloat("Stretch Y##WarpStretchY", &_editorPreset.stretchY, 0.00f, 2.0f); + DrawHelpTooltip("Controls amount of constant vertical stretching. 0.99=shrink 1%, 1=normal, 1.01=stretch 1%"); + + ImGui::SliderFloat("Zoom##WarpZoom", &_editorPreset.zoom, 0.00f, 2.0f); + DrawHelpTooltip("Controls inward/outward motion. 0.9=zoom out 10% per frame, 1.0=no zoom, 1.1=zoom in 10%"); + + ImGui::SliderFloat("Zoom Exponent##Warp", &_editorPreset.zoomExponent, 0.00f, 5.0f); + DrawHelpTooltip("Controls the curvature of the zoom; 1=normal"); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + ImGui::TextUnformatted("Warping"); + ImGui::Indent(16.0f); + + ImGui::SliderFloat("Warp Amount##WarpAmount", &_editorPreset.warpAmount, 0.00f, 10.0f); + DrawHelpTooltip("Controls the magnitude of the warping. 0=none, 1=normal, 2=major warping..."); + + ImGui::SliderFloat("Warp Scale##WarpScale", &_editorPreset.warpScale, 0.00f, 10.0f); + DrawHelpTooltip("Controls the scale of the warp effect."); + + ImGui::SliderFloat("Warp Animation Speed##WarpAnimSpeed", &_editorPreset.warpAnimSpeed, 0.00f, 5.0f); + DrawHelpTooltip("Controls the speed of the warp effect."); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + ImGui::TextUnformatted("Options"); + ImGui::Indent(16.0f); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + } + + if (ImGui::CollapsingHeader("Motion Code")) + { + + if (ImGui::Button("Per-Frame Init")) + { + EditCode(_editorPreset.perFrameInitCode, false); + } + if (ImGui::Button("Per-Frame Code")) + { + EditCode(_editorPreset.perFrameCode, false); + } + if (ImGui::Button("Per-Vertex Code")) + { + EditCode(_editorPreset.perPixelCode, false); + } + } + + DrawBorderSettings(); + + if (ImGui::CollapsingHeader("Custom Waveforms")) + { + } + + if (ImGui::CollapsingHeader("Custom Shapes")) + { + } + + bool shaderTabDisabled = _editorPreset.presetVersion < 200 || (_editorPreset.warpShaderVersion < 2 && _editorPreset.compositeShaderVersion < 2); + + ImGui::BeginDisabled(shaderTabDisabled); + if (ImGui::CollapsingHeader("Warp / Composite Shaders")) + { + if (!_editorPreset.warpShader.empty()) + { + if (ImGui::Button("Warp Shader")) + { + EditCode(_editorPreset.warpShader, true); + } + } + if (!_editorPreset.compositeShader.empty()) + { + if (ImGui::Button("Composite Shader")) + { + EditCode(_editorPreset.compositeShader, true); + } + } + } + if (shaderTabDisabled) + { + DrawHelpTooltip("To enable HLSL shaders, open the compatibility settings and set the " + "preset version to 200 or higher and the PS version to 2 or higher."); + } + ImGui::EndDisabled(); + + DrawShaderLossWarning(); + + ImGui::EndChild(); + + ImGui::SameLine(); + + ImGui::SetNextWindowBgAlpha(0.5f); + ImGui::BeginChild("CodeEditor", ImVec2(0, 500), ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeY, window_flags); + + _textEditor.Render("Code Editor"); + + ImGui::EndChild(); + + + ImGui::PopStyleVar(); +} + +void PresetEditorGUI::DrawPresetCompatibilitySettings() +{ + if (ImGui::CollapsingHeader("Preset Compatibility")) + { + { + static const char* milkdropVersions[] = { + "140 - Milkdrop 1.4, No Shaders", + "200 - Milkdrop 2.0, Same Warp/Comp PS Versions", + "201 - Milkdrop 2.1+ Separate Warp/Comp PS Versions"}; + int selectionIndex = _editorPreset.presetVersion < 200 ? 0 + : _editorPreset.presetVersion == 200 ? 1 + : 2; + const char* comboboxPreviewValue = milkdropVersions[selectionIndex]; + if (ImGui::BeginCombo("Preset Version", comboboxPreviewValue)) + { + for (int index = 0; index < 3; index++) + { + const bool isSelected = (selectionIndex == index); + if (ImGui::Selectable(milkdropVersions[index], isSelected)) + { + switch (index) + { + case 0: + default: + _editorPreset.presetVersion = 140; + break; + case 1: + _editorPreset.presetVersion = 200; + break; + case 2: + _editorPreset.presetVersion = 201; + break; + } + } + + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + + ImGui::EndCombo(); + } + DrawHelpTooltip("Determines the feature set of a preset regarding use of shaders."); + } + + if (_editorPreset.presetVersion == 200) + { + ImGui::TextUnformatted("Pixel Shader Version"); + ImGui::Indent(16.0f); + + if (ImGui::SliderInt("PS Version", &_editorPreset.warpShaderVersion, 0, 4)) + { + _editorPreset.compositeShaderVersion = _editorPreset.warpShaderVersion; + } + DrawHelpTooltip("Minimum required DirectX Pixel Shader version. 0/1=No PS, 2=PS 2.0, 3=PS 2.x, 4=PS 3.0 (Ctrl-click to set higher value)"); + + ImGui::Unindent(16.0f); + } + else if (_editorPreset.presetVersion > 200) + { + ImGui::TextUnformatted("Pixel Shader Versions"); + ImGui::Indent(16.0f); + + ImGui::SliderInt("Warp PS Version", &_editorPreset.warpShaderVersion, 0, 4); + DrawHelpTooltip("Minimum required DirectX Pixel Shader version for the warp shader. 0/1=No PS, 2=PS 2.0, 3=PS 2.x, 4=PS 3.0 (Ctrl-click to set higher value)"); + ImGui::SliderInt("Composite PS Version", &_editorPreset.compositeShaderVersion, 0, 4); + DrawHelpTooltip("Minimum required DirectX Pixel Shader version for the composite shader. 0/1=No PS, 2=PS 2.0, 3=PS 2.x, 4=PS 3.0 (Ctrl-click to set higher value)"); + + ImGui::Unindent(16.0f); + } + } +} + +void PresetEditorGUI::DrawGeneralParameters() +{ + if (ImGui::CollapsingHeader("General Parameters")) + { + ImGui::TextUnformatted("Post-Processing Filters"); + ImGui::Indent(16.0f); + + ImGui::SliderFloat("Decay##PerFrameDecay", &_editorPreset.decay, 0.00f, 1.0f); + DrawHelpTooltip("Controls the eventual fade to black. 1=no fade, 0.9=strong fade, 0.98=recommended"); + + ImGui::SliderFloat("Gamma Adjustment##GammaAdjustment", &_editorPreset.gammaAdj, 0.00f, 10.0f); + DrawHelpTooltip("Controls display brightness. 1=normal, 2=double, 3=triple, etc."); + + ImGui::Checkbox("Brighten", &_editorPreset.brighten); + DrawHelpTooltip("Brightens the darker parts of the image (nonlinear; square root filter)"); + + ImGui::Checkbox("Darken", &_editorPreset.darken); + DrawHelpTooltip("Darkens the brighter parts of the image (nonlinear; squaring filter)"); + + ImGui::Checkbox("Solarize", &_editorPreset.solarize); + DrawHelpTooltip("Emphasizes mid-range colors"); + + ImGui::Checkbox("Invert", &_editorPreset.invert); + DrawHelpTooltip("Inverts the colors in the image"); + + ImGui::Checkbox("Darken Center", &_editorPreset.darkenCenter); + DrawHelpTooltip("Darkens a diamond-shaped area in the center of the image.\nApplied after drawing shapes/waveforms, but before drawing borders."); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + static const char* videoEchoOrientations[] = { + "Normal", + "Flip X", + "Flip Y", + "Flip X + Y"}; + + ImGui::TextUnformatted("Video Echo"); + ImGui::Indent(16.0f); + ImGui::SliderFloat("Zoom##VideoEchoZoom", &_editorPreset.videoEchoZoom, 0.01f, 10.0f); + DrawHelpTooltip("Controls the size of the second graphics layer"); + + ImGui::SliderFloat("Alpha##VideoEchoAlpha", &_editorPreset.videoEchoAlpha, 0.00f, 1.0f); + DrawHelpTooltip("Controls the opacity of the second graphics layer. 0=transparent (off), 0.5=half-mix, 1=opaque"); + + { + int selectionIndex = _editorPreset.videoEchoOrientation % 4; + const char* comboboxPreviewValue = videoEchoOrientations[_editorPreset.videoEchoOrientation % 4]; + if (ImGui::BeginCombo("Orientation##VideoEchoOrientation", comboboxPreviewValue)) + { + for (int index = 0; index < 4; index++) + { + const bool isSelected = (selectionIndex == index); + if (ImGui::Selectable(videoEchoOrientations[index], isSelected)) + { + _editorPreset.videoEchoOrientation = index; + } + + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + + ImGui::EndCombo(); + } + DrawHelpTooltip("Selects an orientation for the second graphics layer."); + } + ImGui::Unindent(16.0f); + + ImGui::Spacing(); + + ImGui::TextUnformatted("Blur Texture Value Range"); + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + DrawHelpTooltip("Normally these are set to 0 (min) and 1 (max) each.\n" + "You can clamp the values in the blur texture to a tighter range,though.\n" + "This will increase the precision in the blur textures, but you run the risk of clamping values to your min/max.\n" + "If you use the GetBlur1() .. GetBlur3() functions to sample the blur texture, they will automatically \"unpack\" the values for you in the end!"); + + ImGui::Indent(16.0f); + + ImGui::DragFloatRange2("Blur 1 Range", &_editorPreset.blur1Min, &_editorPreset.blur1Max, 0.01, 0.0, 1.0); + ImGui::DragFloatRange2("Blur 2 Range", &_editorPreset.blur2Min, &_editorPreset.blur2Max, 0.01, 0.0, 1.0); + ImGui::DragFloatRange2("Blur 3 Range", &_editorPreset.blur3Min, &_editorPreset.blur3Max, 0.01, 0.0, 1.0); + ImGui::Spacing(); + ImGui::SliderFloat("Blur 1 Edge Darken", &_editorPreset.blur1EdgeDarken, 0.00f, 1.0f); + + ImGui::Unindent(16.0f); + + ImGui::Spacing(); + } +} + +void PresetEditorGUI::DrawDefaultWaveformSettings() +{ + if (ImGui::CollapsingHeader("Default Waveform Effect")) + { + DrawWaveformModeSelection(); + + ImGui::TextUnformatted("Drawing Settings"); + + ImGui::Indent(16.0f); + + ImGui::Checkbox("Additive Waves##WaveformAdditive", &_editorPreset.additiveWaves); + DrawHelpTooltip("The wave is drawn additively, saturating the image at white"); + + ImGui::Checkbox("Dots##WaveformDrawDots", &_editorPreset.waveDots); + DrawHelpTooltip("The waveform is drawn as dots (instead of lines)"); + + ImGui::Checkbox("Thick##WaveformDrawThick", &_editorPreset.waveThick); + DrawHelpTooltip("The waveform's lines (or dots) are drawn with double thickness"); + + ImGui::SliderFloat("Scale##DefaultWaveformScale", &_editorPreset.waveScale, 0.00f, 5.0f); + DrawHelpTooltip("Scaling factor of the waveform. 1 = original size, 2 = twice the size, 0.5 = half the size"); + + ImGui::SliderFloat("Smoothing##DefaultWaveformSmoothing", &_editorPreset.waveSmoothing, 0.00f, 1.0f); + DrawHelpTooltip("Smoothing of the waveform. 0 = no smoothing, 0.75 = heavy smoothing"); + + ImGui::SliderFloat("Mystery Param##DefaultWaveformParam", &_editorPreset.waveParam, -1.00f, 1.0f); + DrawHelpTooltip("This value does different things depending on the mode. For example, it could control angle at which the waveform was drawn."); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + ImGui::TextUnformatted("Position"); + + ImGui::Indent(16.0f); + + ImGui::SliderFloat("X##DefaultWaveformX", &_editorPreset.waveX, 0.00f, 1.0f); + DrawHelpTooltip("Position of the waveform. 0 = far left edge of screen, 0.5 = center, 1 = far right"); + + ImGui::SliderFloat("Y##DefaultWaveformY", &_editorPreset.waveY, 0.00f, 1.0f); + DrawHelpTooltip("Position of the waveform. 0 = very bottom of screen, 0.5 = center, 1 = top"); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + ImGui::TextUnformatted("Color"); + + ImGui::Indent(16.0f); + + ImGui::ColorEdit4("Color##DefaultWaveformColor", &_editorPreset.waveColor.red, ImGuiColorEditFlags_Float); + DrawHelpTooltip("The color of the waveform"); + + ImGui::Checkbox("Maximize Wave Color", &_editorPreset.maximizeWaveColor); + DrawHelpTooltip("All 3 R/G/B colors will be scaled up until at least one reaches 1.0"); + + ImGui::Checkbox("Modulate Alpha by Volume", &_editorPreset.modWaveAlphaByVolume); + DrawHelpTooltip("Modulate waveform alpha value by audio volume"); + + ImGui::DragFloatRange2("Modulation Range", &_editorPreset.modWaveAlphaStart, &_editorPreset.modWaveAlphaEnd, 0.01, 0.0, 1.0); + DrawHelpTooltip("Clamps alpha modulation 0->1 within this relative volume range."); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + } +} + +void PresetEditorGUI::DrawWaveformModeSelection() +{ + static const char* waveformModes[] = { + "Circle", + "X/Y Oscillation Spiral", + "Centered Spiro", + "Centered Spiro (Volume)", + "Derivative Line", + "Explosive Hash", + "Line", + "Double Line", + "Spectrum Line", + "Wave 9", + "Wave X", + "Wave 11", + "Wave Skewed", + "Wave Star", + "Wave Flower", + "Wave Lasso"}; + + ImGui::TextUnformatted("Mode"); + + ImGui::Indent(16.0f); + + int selectionIndex = _editorPreset.waveMode % 16; + const char* comboboxPreviewValue = waveformModes[selectionIndex]; + if (ImGui::BeginCombo("##DefaultWaveformMode", comboboxPreviewValue)) + { + for (int index = 0; index < 16; index++) + { + if (index == 0) + { + ImGui::TextDisabled("%s", "-- Original Milkdrop Modes --"); + } + if (index == 9) + { + ImGui::TextDisabled("%s", "-- projectM / Milkdrop 3 Extras --"); + } + const bool isSelected = (selectionIndex == index); + if (ImGui::Selectable(waveformModes[index], isSelected)) + { + _editorPreset.waveMode = index; + } + + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + + ImGui::EndCombo(); + } + DrawHelpTooltip("Determines the drawing style of the classic default waveform.\n" + "Selecting any of the \"extra\" waveform modes will wrap around to " + "the original modes in classic Milkdrop versions."); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); +} + +void PresetEditorGUI::DrawMotionVectorSettings() +{ + if (ImGui::CollapsingHeader("Motion Vector Grid")) + { + ImGui::TextUnformatted("Layout And Position"); + + ImGui::Indent(16.0f); + + if (ImGui::SliderFloat("Size X##MotionVectorX", &_editorPreset.mvX, 0.00f, 64.0f)) + { + _editorPreset.waveX = std::min(0.0f, std::max(64.0f, _editorPreset.mvX)); + } + DrawHelpTooltip("The number of motion vectors in the X direction"); + + if (ImGui::SliderFloat("size Y##MotionVectorY", &_editorPreset.mvY, 0.00f, 48.0f)) + { + _editorPreset.waveY = std::min(0.0f, std::max(48.0f, _editorPreset.mvY)); + } + DrawHelpTooltip("The number of motion vectors in the Y direction"); + + ImGui::SliderFloat("Length##MotionVectorLength", &_editorPreset.mvL, 0.00f, 5.0f); + DrawHelpTooltip("The length of the motion vectors (0=no trail, 1=normal, 2=double...)"); + + ImGui::SliderFloat("X Offset##MotionVectorOffsetX", &_editorPreset.mvDX, -1.00f, 1.0f); + DrawHelpTooltip("Horizontal placement offset of the motion vectors"); + + ImGui::SliderFloat("Y Offset##MotionVectorOffsetY", &_editorPreset.mvDY, -1.00f, 1.0f); + DrawHelpTooltip("Vertical placement offset of the motion vectors"); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + ImGui::TextUnformatted("Color"); + + ImGui::Indent(16.0f); + + ImGui::ColorEdit4("Color##MotionVectorColor", &_editorPreset.mvColor.red, ImGuiColorEditFlags_Float); + DrawHelpTooltip("The color of the motion vector grid"); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + } +} + +void PresetEditorGUI::DrawBorderSettings() +{ + if (ImGui::CollapsingHeader("Border Effect")) + { + ImGui::TextUnformatted("Outer Border"); + ImGui::Indent(16.0f); + + ImGui::SliderFloat("Thickness##OuterBorderThickness", &_editorPreset.outerBorderSize, 0.0f, 1.0f); + DrawHelpTooltip("Thickness of the outer border drawn at the edges of the screen every frame"); + ImGui::ColorEdit4("Color##OuterBorderColor", &_editorPreset.outerBorderColor.red, ImGuiColorEditFlags_Float); + DrawHelpTooltip("Color of the outer border drawn at the edges of the screen every frame"); + + ImGui::Unindent(16.0f); + + ImGui::Spacing(); + + ImGui::TextUnformatted("Inner Border"); + ImGui::Indent(16.0f); + + ImGui::SliderFloat("Thickness##InnerBorderThickness", &_editorPreset.innerBorderSize, 0.0f, 1.0f); + DrawHelpTooltip("Thickness of the inner border drawn at the edges of the screen every frame"); + + ImGui::ColorEdit4("Color##InnerBorderColor", &_editorPreset.innerBorderColor.red, ImGuiColorEditFlags_Float); + DrawHelpTooltip("Color of the inner border drawn at the edges of the screen every frame"); + + ImGui::Unindent(16.0f); + + ImGui::Spacing(); + } +} + +void PresetEditorGUI::DrawShaderLossWarning() const +{ + if (_editorPreset.presetVersion < 200) + { + if (!_editorPreset.warpShader.empty() || !_editorPreset.compositeShader.empty()) + { + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 0, 0, 255)); + ImGui::TextWrapped("%s", "WARNING: You have disabled shader support, but the preset still contains shader code.\n" + "The shader code WILL NOT BE SAVED and gets lost once you close this preset in the editor."); + ImGui::PopStyleColor(); + } + } + else + { + if (_editorPreset.warpShaderVersion < 2 && !_editorPreset.warpShader.empty()) + { + + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 0, 0, 255)); + ImGui::TextWrapped("%s", "WARNING: You have selected a WARP shader PS Version below 2, but the preset still contains warp shader code.\n" + "The warp shader code WILL NOT BE SAVED and gets lost once you close this preset in the editor."); + ImGui::PopStyleColor(); + } + if (_editorPreset.compositeShaderVersion < 2 && !_editorPreset.compositeShader.empty()) + { + + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 0, 0, 255)); + ImGui::TextWrapped("%s", "WARNING: You have selected a COMPOSITE shader PS Version below 2, but the preset still contains composite shader code.\n" + "The composite shader WILL NOT BE SAVED and gets lost once you close this preset in the editor."); + ImGui::PopStyleColor(); + } + } +} + +void PresetEditorGUI::DrawHelpTooltip(const std::string& helpText) +{ + if (ImGui::BeginItemTooltip()) + { + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + ImGui::TextUnformatted(helpText.c_str()); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } +} + +void PresetEditorGUI::EditCode(const std::string& code, bool isShaderCode) +{ + if (isShaderCode) + { + _textEditor.SetLanguageDefinition(TextEditor::LanguageDefinition::HLSL()); + } + else + { + _textEditor.SetLanguageDefinition(TextEditor::LanguageDefinition::MilkdropExpression()); + } + + _textEditor.SetText(code); +} + +} // namespace Editor \ No newline at end of file diff --git a/src/gui/preset_editor/PresetEditorGUI.h b/src/gui/preset_editor/PresetEditorGUI.h new file mode 100644 index 0000000..f6a24f5 --- /dev/null +++ b/src/gui/preset_editor/PresetEditorGUI.h @@ -0,0 +1,80 @@ +#pragma once + +#include "EditorPreset.h" +#include "PresetFile.h" +#include "TextEditor.h" +#include "EditorMenu.h" + +#include + +class ProjectMGUI; +class ProjectMSDLApplication; +class ProjectMWrapper; + +namespace Editor { + +class PresetEditorGUI +{ +public: + explicit PresetEditorGUI(ProjectMGUI& gui); + + ~PresetEditorGUI() = default; + + /** + * @brief Displays the preset editor screen and associated windows. + * @param presetFile The preset file name to load and edit. + */ + void Show(const std::string& presetFile); + + /** + * @brief Tells the editor UI to close. + * If there are unsaved changes, the user will be asked to save or abort the close. + */ + void Close(); + + /** + * @brief Draws the preset editor. + * @return true if the preset editor is visible and has been drawn, false otherwise. + */ + bool Draw(); + + /** + * @brief Reloads the rendered preview with the current changes. + */ + void UpdatePresetPreview(); + +private: + void TakeProjectMControl(); + void ReleaseProjectMControl(); + + void DrawLeftSideBar(); + + void DrawPresetCompatibilitySettings(); + void DrawGeneralParameters(); + void DrawDefaultWaveformSettings(); + void DrawWaveformModeSelection(); + void DrawMotionVectorSettings(); + void DrawBorderSettings(); + void DrawShaderLossWarning() const; + + static void DrawHelpTooltip(const std::string& helpText); + + void EditCode(const std::string& code, bool isShaderCode); + + ProjectMGUI& _gui; //!< Reference to the projectM GUI instance + ProjectMSDLApplication& _application; + ProjectMWrapper& _projectMWrapper; + + bool _wantClose{false}; + bool _visible{false}; //!< true if the editor is visible, false if not. + + EditorMenu _menu; //!< The editor-specific main menu bar. + + std::string _loadedPresetPath; //!< The full path of the currently loaded preset. Can be empty. + PresetFile _presetFile; //!< The raw preset data. + EditorPreset _editorPreset; //!< The preset data in a parsed, strongly-typed container. + + TextEditor _textEditor; //!< The expression/shader code editor. +}; + +} // namespace Editor diff --git a/src/gui/preset_editor/PresetFile.cpp b/src/gui/preset_editor/PresetFile.cpp new file mode 100644 index 0000000..7d586e7 --- /dev/null +++ b/src/gui/preset_editor/PresetFile.cpp @@ -0,0 +1,590 @@ +#include "PresetFile.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace Editor { + +PresetFile PresetFile::EmptyPreset() +{ + PresetFile emptyPreset; + + if (emptyPreset.ReadData(emptyPreset.ExportPreset().str())) + { + return emptyPreset; + } + + return {}; +} + +auto PresetFile::ReadFile(const std::string& presetFile) -> bool +{ + std::ifstream presetStream(presetFile.c_str(), std::ios_base::in | std::ios_base::binary); + return ReadStream(presetStream); +} + +auto PresetFile::ReadData(const std::string& presetData) -> bool +{ + std::istringstream presetStream(presetData); + return ReadStream(presetStream); +} + +auto PresetFile::ReadStream(std::istream& presetStream) -> bool +{ + if (!presetStream.good()) + { + return false; + } + + presetStream.seekg(0, std::istream::end); + auto fileSize = presetStream.tellg(); + presetStream.seekg(0, std::istream::beg); + + if (static_cast(fileSize) > maxFileSize) + { + return false; + } + + std::vector presetFileContents(fileSize); + presetStream.read(presetFileContents.data(), fileSize); + + if (presetStream.fail() || presetStream.bad()) + { + return false; + } + + _presetValues.clear(); + + size_t startPos{0}; // Starting position of current line + size_t pos{0}; // Current read position + + auto parseLineIfDataAvailable = [this, &pos, &startPos, &presetFileContents]() { + if (pos > startPos) + { + auto beg = presetFileContents.begin(); + std::string line(beg + startPos, beg + pos); + ParseLine(line); + } + }; + + while (pos < presetFileContents.size()) + { + switch (presetFileContents[pos]) + { + case '\r': + case '\n': + // EOL, skip over CR/LF + parseLineIfDataAvailable(); + startPos = pos + 1; + break; + + case '\0': + // Null char is not expected. Could be a random binary file. + return false; + } + + ++pos; + } + + parseLineIfDataAvailable(); + + return !_presetValues.empty(); +} + +auto PresetFile::Write(const std::string& presetFile) const -> bool +{ + std::ofstream presetStream(presetFile.c_str(), std::ios_base::out | std::ios_base::binary); + return Write(presetStream); +} + +auto PresetFile::Write(std::ostream& presetStream) const -> bool +{ + if (!presetStream.good()) + { + return false; + } + + auto presetData = ExportPreset(); + presetStream << presetData.rdbuf(); + presetStream.flush(); + + return true; +} + +auto PresetFile::AsString() const -> std::string +{ + return ExportPreset().str(); +} + +auto PresetFile::GetCode(const std::string& keyPrefix) const -> std::string +{ + auto lowerKey = ToLower(keyPrefix); + + std::stringstream code; //!< The parsed code + std::string key(lowerKey.length() + 5, '\0'); // Allocate a string that can hold up to 5 digits. + + key.replace(0, lowerKey.length(), lowerKey); + + for (int index{1}; index <= 99999; ++index) + { + key.replace(lowerKey.length(), 5, std::to_string(index)); + if (_presetValues.find(key) == _presetValues.end()) + { + break; + } + + auto line = _presetValues.at(key); + + // Remove backtick char in shader code + if (!line.empty() && line.at(0) == '`') + { + line.erase(0, 1); + } + code << line << std::endl; + } + + auto codeStr = code.str(); + + return codeStr; +} + +void PresetFile::SetCode(const std::string& keyPrefix, const std::string& code) +{ + auto lowerKeyPrefix = ToLower(keyPrefix); + + bool shaderPrefix = false; + + if (lowerKeyPrefix == "warp_" || lowerKeyPrefix == "comp_") + { + shaderPrefix = true; + } + + // Remove all previous lines for this code block prefix + for (auto it = begin(_presetValues); it != end(_presetValues);) + { + if (it->first.substr(0, lowerKeyPrefix.length()) == lowerKeyPrefix) + { + // Check if line prefix is only followed by numbers + std::string remainder = it->first.substr(lowerKeyPrefix.length()); + bool onlyDigits = true; + for (const auto ch : remainder) + { + if (ch < '0' || ch > '9') + { + onlyDigits = false; + break; + } + } + if (onlyDigits) + { + it = _presetValues.erase(it); + continue; + } + } + ++it; + } + + if (code.empty()) + { + return; + } + + uint32_t lineNumber = 1; + + std::string key(lowerKeyPrefix.length() + 5, '\0'); // Allocate a string that can hold up to 5 digits. + key.replace(0, lowerKeyPrefix.length(), lowerKeyPrefix); + + std::stringstream codeStream(code); + std::string codeLine; + while (std::getline(codeStream, codeLine, '\n')) + { + if (shaderPrefix) + { + codeLine = std::string("`").append(codeLine); + } + + key.replace(lowerKeyPrefix.length(), 5, std::to_string(lineNumber)); + _presetValues[key] = codeLine; + + lineNumber++; + + // Milkdrop doesn't enforce this on export, but stops reading at 99999 lines. + // So just stop writing more lines than necessary if someone tries. + if (lineNumber > 99999) + { + break; + } + } +} + +auto PresetFile::GetInt(const std::string& key, int defaultValue) const -> int +{ + auto lowerKey = ToLower(key); + if (_presetValues.find(lowerKey) != _presetValues.end()) + { + try + { + return std::stoi(_presetValues.at(lowerKey)); + } + catch (std::logic_error&) + { + } + } + + return defaultValue; +} + +void PresetFile::SetInt(const std::string& key, int value) +{ + auto lowerKey = ToLower(key); + _presetValues[lowerKey] = std::to_string(value); +} + +auto PresetFile::GetFloat(const std::string& key, float defaultValue) const -> float +{ + auto lowerKey = ToLower(key); + if (_presetValues.find(lowerKey) != _presetValues.end()) + { + try + { + return std::stof(_presetValues.at(lowerKey)); + } + catch (std::logic_error&) + { + } + } + + return defaultValue; +} + +void PresetFile::SetFloat(const std::string& key, float value) +{ + auto lowerKey = ToLower(key); + _presetValues[lowerKey] = std::to_string(value); +} + +auto PresetFile::GetBool(const std::string& key, bool defaultValue) const -> bool +{ + return GetInt(key, static_cast(defaultValue)) > 0; +} + +void PresetFile::SetBool(const std::string& key, bool value) +{ + auto lowerKey = ToLower(key); + _presetValues[lowerKey] = value ? "1" : "0"; +} + +auto PresetFile::GetString(const std::string& key, const std::string& defaultValue) const -> std::string +{ + auto lowerKey = ToLower(key); + if (_presetValues.find(lowerKey) != _presetValues.end()) + { + return _presetValues.at(lowerKey); + } + + return defaultValue; +} + +void PresetFile::SetString(const std::string& key, const std::string& value) +{ + auto lowerKey = ToLower(key); + _presetValues[lowerKey] = value; +} + +const std::map& PresetFile::PresetValues() const +{ + return _presetValues; +} + +void PresetFile::ParseLine(const std::string& line) +{ + // Search for first delimiter, either space or equal + auto varNameDelimiterPos = line.find_first_of(" ="); + + if (varNameDelimiterPos == std::string::npos || varNameDelimiterPos == 0) + { + // Empty line, delimiter at start of line or no delimiter found, skip. + return; + } + + // Convert key to lower case, as INI functions are not case-sensitive. + std::string varName(ToLower(std::string(line.begin(), line.begin() + varNameDelimiterPos))); + std::string value(line.begin() + varNameDelimiterPos + 1, line.end()); + + // Only add first occurrence to mimic Milkdrop behaviour + if (!varName.empty() && _presetValues.find(varName) == _presetValues.end()) + { + _presetValues.emplace(std::move(varName), std::move(value)); + } +} + +auto PresetFile::ToLower(std::string str) -> std::string +{ + std::transform(str.begin(), str.end(), str.begin(), + [](unsigned char c) { return std::tolower(c); }); + + return str; +} + +auto PresetFile::ExportPreset() const -> std::stringstream +{ + std::stringstream presetFileContents; + + int milkdropPresetVersion = GetInt("MILKDROP_PRESET_VERSION", 100); + + // Introduced in Milkdrop 2.0, but can be set to <200. + presetFileContents << "MILKDROP_PRESET_VERSION=" << milkdropPresetVersion << std::endl; + + // Milkdrop 2 Pixel Shader version information. + if (milkdropPresetVersion == 200) + { + presetFileContents << "PSVERSION=" << GetInt("psversion", 0) << std::endl; + } + else if (milkdropPresetVersion > 200) + { + presetFileContents << "PSVERSION_WARP=" << GetInt("psversion_warp", 0) << std::endl; + presetFileContents << "PSVERSION_COMP=" << GetInt("psversion_comp", 0) << std::endl; + } + + presetFileContents << "[preset00]" << std::endl; + + // (Unused) rating value and classic post-processing filter parameters + presetFileContents << std::setprecision(3) << std::fixed; + presetFileContents << "fRating=" << GetFloat("fRating", 3.0f) << std::endl; + presetFileContents << "fGammaAdj=" << GetFloat("fGammaAdj", 2.0f) << std::endl; + presetFileContents << "fDecay=" << GetFloat("fDecay", 0.98f) << std::endl; + presetFileContents << "fVideoEchoZoom=" << GetFloat("fVideoEchoZoom", 2.0f) << std::endl; + presetFileContents << "fVideoEchoAlpha=" << GetFloat("fVideoEchoAlpha", 0.0f) << std::endl; + presetFileContents << "nVideoEchoOrientation=" << GetInt("nVideoEchoOrientation", 0) << std::endl; + + // Flags for classic filters and default waveform parameters + presetFileContents << "nWaveMode=" << GetInt("nWaveMode", 0) << std::endl; + presetFileContents << "bAdditiveWaves=" << GetInt("bAdditiveWaves", 0) << std::endl; + presetFileContents << "bWaveDots=" << GetInt("bWaveDots", 0) << std::endl; + presetFileContents << "bWaveThick=" << GetInt("bWaveThick", 0) << std::endl; + presetFileContents << "bModWaveAlphaByVolume=" << GetInt("bModWaveAlphaByVolume", 0) << std::endl; + presetFileContents << "bMaximizeWaveColor=" << GetInt("bMaximizeWaveColor", 1) << std::endl; + presetFileContents << "bTexWrap=" << GetInt("bTexWrap", 1) << std::endl; + presetFileContents << "bDarkenCenter=" << GetInt("bDarkenCenter", 0) << std::endl; + presetFileContents << "bRedBlueStereo=" << GetInt("bRedBlueStereo", 0) << std::endl; + presetFileContents << "bBrighten=" << GetInt("bBrighten", 0) << std::endl; + presetFileContents << "bDarken=" << GetInt("bDarken", 0) << std::endl; + presetFileContents << "bSolarize=" << GetInt("bSolarize", 0) << std::endl; + presetFileContents << "bInvert=" << GetInt("bInvert", 0) << std::endl; + + // Default waveform and warp animation + presetFileContents << "fWaveAlpha=" << GetFloat("fWaveAlpha", 0.8f) << std::endl; + presetFileContents << "fWaveScale=" << GetFloat("fWaveScale", 1.0f) << std::endl; + presetFileContents << "fWaveSmoothing=" << GetFloat("fWaveSmoothing", 0.75f) << std::endl; + presetFileContents << "fWaveParam=" << GetFloat("fWaveParam", 0.0f) << std::endl; + presetFileContents << "fModWaveAlphaStart=" << GetFloat("fModWaveAlphaStart", 0.75f) << std::endl; + presetFileContents << "fModWaveAlphaEnd=" << GetFloat("fModWaveAlphaEnd", 0.95f) << std::endl; + presetFileContents << "fWarpAnimSpeed=" << GetFloat("fWarpAnimSpeed", 1.0f) << std::endl; + presetFileContents << "fWarpScale=" << GetFloat("fWarpScale", 1.0f) << std::endl; + presetFileContents << std::setprecision(5); + presetFileContents << "fZoomExponent=" << GetFloat("fZoomExponent", 1.0f) << std::endl; + presetFileContents << std::setprecision(3); + presetFileContents << "fShader=" << GetFloat("fShader", 0.0f) << std::endl; + + // Warp parameters and default waveform color/location + presetFileContents << std::setprecision(5); + presetFileContents << "zoom=" << GetFloat("zoom", 1.0f) << std::endl; + presetFileContents << "rot=" << GetFloat("rot", 0.0f) << std::endl; + presetFileContents << std::setprecision(3); + presetFileContents << "cx=" << GetFloat("cx", 0.5f) << std::endl; + presetFileContents << "cy=" << GetFloat("cy", 0.5f) << std::endl; + presetFileContents << std::setprecision(5); + presetFileContents << "dx=" << GetFloat("dx", 0.0f) << std::endl; + presetFileContents << "dy=" << GetFloat("dy", 0.0f) << std::endl; + presetFileContents << "warp=" << GetFloat("warp", 1.0f) << std::endl; + presetFileContents << "sx=" << GetFloat("sx", 1.0f) << std::endl; + presetFileContents << "sy=" << GetFloat("sy", 1.0f) << std::endl; + presetFileContents << std::setprecision(3); + presetFileContents << "wave_r=" << GetFloat("wave_r", 1.0f) << std::endl; + presetFileContents << "wave_g=" << GetFloat("wave_g", 1.0f) << std::endl; + presetFileContents << "wave_b=" << GetFloat("wave_b", 1.0f) << std::endl; + presetFileContents << "wave_x=" << GetFloat("wave_x", 0.5f) << std::endl; + presetFileContents << "wave_y=" << GetFloat("wave_y", 0.5f) << std::endl; + + // Borders and motion vectors + presetFileContents << "ob_size=" << GetFloat("ob_size", 0.01f) << std::endl; + presetFileContents << "ob_r=" << GetFloat("ob_r", 0.0f) << std::endl; + presetFileContents << "ob_g=" << GetFloat("ob_g", 0.0f) << std::endl; + presetFileContents << "ob_b=" << GetFloat("ob_b", 0.0f) << std::endl; + presetFileContents << "ob_a=" << GetFloat("ob_a", 0.0f) << std::endl; + presetFileContents << "ib_size=" << GetFloat("ib_size", 0.01f) << std::endl; + presetFileContents << "ib_r=" << GetFloat("ib_r", 0.25f) << std::endl; + presetFileContents << "ib_g=" << GetFloat("ib_g", 0.25f) << std::endl; + presetFileContents << "ib_b=" << GetFloat("ib_b", 0.25f) << std::endl; + presetFileContents << "ib_a=" << GetFloat("ib_a", 0.0f) << std::endl; + presetFileContents << "nMotionVectorsX=" << GetFloat("nMotionVectorsX", 12.0f) << std::endl; + presetFileContents << "nMotionVectorsY=" << GetFloat("nMotionVectorsY", 9.0f) << std::endl; + presetFileContents << "mv_dx=" << GetFloat("mv_dx", 0.0f) << std::endl; + presetFileContents << "mv_dy=" << GetFloat("mv_dy", 0.0f) << std::endl; + presetFileContents << "mv_l=" << GetFloat("mv_l", 0.9f) << std::endl; + presetFileContents << "mv_r=" << GetFloat("mv_r", 1.0f) << std::endl; + presetFileContents << "mv_g=" << GetFloat("mv_g", 1.0f) << std::endl; + presetFileContents << "mv_b=" << GetFloat("mv_b", 1.0f) << std::endl; + presetFileContents << "mv_a=" << GetFloat("mv_a", 1.0f) << std::endl; + presetFileContents << "b1n=" << GetFloat("b1n", 0.0f) << std::endl; + presetFileContents << "b2n=" << GetFloat("b2n", 0.0f) << std::endl; + presetFileContents << "b3n=" << GetFloat("b3n", 0.0f) << std::endl; + presetFileContents << "b1x=" << GetFloat("b1x", 1.0f) << std::endl; + presetFileContents << "b2x=" << GetFloat("b2x", 1.0f) << std::endl; + presetFileContents << "b3x=" << GetFloat("b3x", 1.0f) << std::endl; + presetFileContents << "b1ed=" << GetFloat("b1ed", 0.25f) << std::endl; + + // Custom waves + for (int index = 0; index < 4; index++) + { + ExportWave(index, presetFileContents); + } + + // Custom shapes + for (int index = 0; index < 4; index++) + { + ExportShape(index, presetFileContents); + } + + // Per-frame code + ExportCodeBlock("per_frame_init_", presetFileContents); + ExportCodeBlock("per_frame_", presetFileContents); + ExportCodeBlock("per_pixel_", presetFileContents); + + // Shaders + if (GetInt("psversion_warp", 0) >= 2) + { + ExportCodeBlock("warp_", presetFileContents); + } + if (GetInt("psversion_comp", 0) >= 2) + { + ExportCodeBlock("comp_", presetFileContents); + } + + return presetFileContents; +} + +void PresetFile::ExportWave(int index, std::stringstream& outputStream) const +{ + auto exportIntLine = [&](const std::string& key, int defaultValue) { + std::string linePrefix = std::string("wavecode_") + .append(std::to_string(index)) + .append("_") + .append(key); + + outputStream << linePrefix << "=" << GetInt(linePrefix, defaultValue) << std::endl; + }; + + auto exportFloatLine = [&](const std::string& key, int precision, float defaultValue) { + std::string linePrefix = std::string("wavecode_") + .append(std::to_string(index)) + .append("_") + .append(key); + + outputStream << std::setprecision(precision) << std::fixed + << linePrefix << "=" << GetFloat(linePrefix, defaultValue) << std::endl; + }; + + exportIntLine("enabled", 0); + exportIntLine("samples", 512); + exportIntLine("sep", 0); + exportIntLine("bSpectrum", 0); + exportIntLine("bUseDots", 0); + exportIntLine("bDrawThick", 0); + exportIntLine("bAdditive", 0); + exportFloatLine("scaling", 5, 1.0f); + exportFloatLine("smoothing", 5, 0.5f); + exportFloatLine("r", 3, 1.0f); + exportFloatLine("g", 3, 1.0f); + exportFloatLine("b", 3, 1.0f); + exportFloatLine("a", 3, 1.0f); + + std::string codeBlockPrefix = std::string("wave_") + .append(std::to_string(index)) + .append("_"); + + ExportCodeBlock(codeBlockPrefix + "init", outputStream); + ExportCodeBlock(codeBlockPrefix + "per_frame", outputStream); + ExportCodeBlock(codeBlockPrefix + "per_point", outputStream); +} + +void PresetFile::ExportShape(int index, std::stringstream& outputStream) const +{ + auto exportIntLine = [&](const std::string& key, int defaultValue) { + std::string linePrefix = std::string("shapecode_") + .append(std::to_string(index)) + .append("_") + .append(key); + + outputStream << linePrefix << "=" << GetInt(linePrefix, defaultValue) << std::endl; + }; + + auto exportFloatLine = [&](const std::string& key, int precision, float defaultValue) { + std::string linePrefix = std::string("shapecode_") + .append(std::to_string(index)) + .append("_") + .append(key); + + outputStream << std::setprecision(precision) << std::fixed + << linePrefix << "=" << GetFloat(linePrefix, defaultValue) << std::endl; + }; + + exportIntLine("enabled", 0); + exportIntLine("sides", 4); + exportIntLine("additive", 0); + exportIntLine("thickOutline", 0); + exportIntLine("textured", 0); + exportIntLine("num_inst", 1); + exportFloatLine("x", 3, 0.5f); + exportFloatLine("y", 3, 0.5f); + exportFloatLine("rad", 5, 0.1f); + exportFloatLine("ang", 5, 0.0f); + exportFloatLine("tex_ang", 5, 0.0f); + exportFloatLine("tex_zoom", 5, 1.0f); + exportFloatLine("r", 3, 1.0f); + exportFloatLine("g", 3, 0.0f); + exportFloatLine("b", 3, 0.0f); + exportFloatLine("a", 3, 1.0f); + exportFloatLine("r2", 3, 1.0f); + exportFloatLine("g2", 3, 0.0f); + exportFloatLine("b2", 3, 0.0f); + exportFloatLine("a2", 3, 0.0f); + exportFloatLine("border_r", 3, 1.0f); + exportFloatLine("border_g", 3, 1.0f); + exportFloatLine("border_b", 3, 1.0f); + exportFloatLine("border_a", 3, 0.1f); + + std::string codeBlockPrefix = std::string("shape_") + .append(std::to_string(index)) + .append("_"); + + ExportCodeBlock(codeBlockPrefix + "init", outputStream); + ExportCodeBlock(codeBlockPrefix + "per_frame", outputStream); +} + +void PresetFile::ExportCodeBlock(const std::string& keyPrefix, std::stringstream& outputStream) const +{ + std::string key(keyPrefix.length() + 5, '\0'); //!< Allocate a string that can hold up to 5 digits. + key.replace(0, keyPrefix.length(), keyPrefix); + + for (int index{1}; index <= 99999; ++index) + { + key.replace(keyPrefix.length(), 5, std::to_string(index)); + if (_presetValues.find(key) == _presetValues.end()) + { + break; + } + + auto line = _presetValues.at(key); + + outputStream << key << "=" << line << std::endl; + } +} + +} // namespace Editor + diff --git a/src/gui/preset_editor/PresetFile.h b/src/gui/preset_editor/PresetFile.h new file mode 100644 index 0000000..21725c7 --- /dev/null +++ b/src/gui/preset_editor/PresetFile.h @@ -0,0 +1,246 @@ +#pragma once + +#include +#include + +namespace Editor { + +/** + * @brief Milkdrop preset file parser/writer + * + * Extended version of the parser class in libprojectM, with added support for exporting presets. + * + * Reads in the file as key/value pairs, where the key is either separated from the value by an equal sign or a space. + * Lines not matching this pattern are simply ignored, e.g. the [preset00] INI section. + * + * Values and code blocks can easily be accessed via the helper functions. It is also possible to access the parsed + * map contents directly if required. + * + * When exporting, missing keys are added using Milkdrop's default values. Exporting an empty/unparsed preset + * will return a default file, which is identical to passing an empty preset to Milkdrop/projectM. + */ +class PresetFile +{ +public: + using ValueMap = std::map; //!< A map with key/value pairs, each representing one line in the preset file. + + static constexpr size_t maxFileSize = 0x100000; //!< Maximum size of a preset file. Used for sanity checks. + + /** + * @brief Returns a PresetFile containing a parsed, empty preset with proper default values already set. + * @return An empty preset with Milkdrop's default value. + */ + static PresetFile EmptyPreset(); + + /** + * @brief Reads the preset file into an internal map to prepare for parsing. + * @param presetFile The file name to read from. + * @return True if the file was parsed successfully, false if an error occurred or no line could be parsed. + */ + [[nodiscard]] auto ReadFile(const std::string& presetFile) -> bool; + + /** + * @brief Reads the preset data into an internal map to prepare for parsing. + * @param presetData The data to read from. + * @return True if the data was parsed successfully, false if an error occurred or no line could be parsed. + */ + [[nodiscard]] auto ReadData(const std::string& presetData) -> bool; + + /** + * @brief Reads the data stream into an internal map to prepare for parsing. + * @param presetStream The stream to read preset data form. + * @return True if the stream was parsed successfully, false if an error occurred or no line could be parsed. + */ + [[nodiscard]] auto ReadStream(std::istream& presetStream) -> bool; + + /** + * @brief Saves the preset file in Milkdrop's write order. + * @param presetFile The preset file name to write into. + */ + [[nodiscard]] auto Write(const std::string& presetFile) const -> bool; + + /** + * @brief Saves the preset file in Milkdrop's write order. + * @param presetStream The preset data stream to write into. + */ + [[nodiscard]] auto Write(std::ostream& presetStream) const -> bool; + + /** + * @brief Returns the preset file as a string in Milkdrop's write order. + * @return A string with the same data which would be saved to a preset file. + */ + [[nodiscard]] auto AsString() const -> std::string; + + /** + * @brief Returns a block of code, ready for parsing or use in shader compilation. + * + * Shaders have a "`" prepended on each line. If a line starts with this character, it's stripped and a newline + * character is added at the end of each line. Equations are returned as a single, long line. + * + * The function appends numbers to the prefix, starting with 1, and stops when a key is missing. This is following + * Milkdrop's behaviour, so any gap in numbers will essentially cut off all code after the gap. + * + * Comments starting with // or \\\\ will be stripped until the end of each line in both equations and shader code. + * + * @param keyPrefix The key prefix for the code block to be returned. + * @return The code that was parsed from the given prefix. Empty if no code was found. + */ + [[nodiscard]] auto GetCode(const std::string& keyPrefix) const -> std::string; + + /** + * @brief Replaces/sets a block of code with the given prefix to the new string. + * + * Calling this method will remove all existing map keys starting with the prefix and generate + * new entries, numbered from 1 to the number of lines in "code". + * + * If keyPrefix is either "comp_" or "warp_", each line will automatically be prefixed with the + * "`" character. + * + * @param keyPrefix The key prefix for the code block to be set. + * @param code The new code block to save. + */ + void SetCode(const std::string& keyPrefix, const std::string& code); + + /** + * @brief Returns the given key value as an integer. + * + * Returns the default value if no value can be parsed or the key doesn't exist. + * + * Any additional text after the number, e.g. a comment, is ignored. + * + * @param key The key to retrieve the value from. + * @param defaultValue The default value to return if key is not found. + * @return The converted value or the default value. + */ + [[nodiscard]] auto GetInt(const std::string& key, int defaultValue) const -> int; + + /** + * @brief Sets the given key to a new integer value. + * @param key The key to set the value for. + * @param value The new value to set. + */ + void SetInt(const std::string& key, int value); + + /** + * @brief Returns the given key value as a floating-point value. + * + * Returns the default value if no value can be parsed or the key doesn't exist. + * + * Any additional text after the number, e.g. a comment, is ignored. + * + * @param key The key to retrieve the value from. + * @param defaultValue The default value to return if key is not found. + * @return The converted value or the default value. + */ + [[nodiscard]] auto GetFloat(const std::string& key, float defaultValue) const -> float; + + /** + * @brief Sets the given key to a new float value. + * @param key The key to set the value for. + * @param value The new value to set. + */ + void SetFloat(const std::string& key, float value); + + /** + * @brief Returns the given key value as a boolean. + * + * Returns the default value if no value can be parsed or the key doesn't exist. + * + * Any additional text after the number, e.g. a comment, is ignored. + * + * @param key The key to retrieve the value from. + * @param defaultValue The default value to return if key is not found. + * @return True if the value is non-zero, false otherwise. + */ + [[nodiscard]] auto GetBool(const std::string& key, bool defaultValue) const -> bool; + + /** + * @brief Sets the given key to a new boolean (0/1) value. + * @param key The key to set the value for. + * @param value The new value to set. + */ + void SetBool(const std::string& key, bool value); + + /** + * @brief Returns the given key value as a string. + * + * Returns the default value if no value can be parsed or the key doesn't exist. + * + * @param key The key to retrieve the value from. + * @param defaultValue The default value to return if key is not found. + * @return the string content of the key, or the default value. + */ + [[nodiscard]] auto GetString(const std::string& key, const std::string& defaultValue) const -> std::string; + + /** + * @brief Sets the given key to a new string value. + * @param key The key to set the value for. + * @param value The new value to set. + */ + void SetString(const std::string& key, const std::string& value); + + /** + * @brief Returns a reference to the internal value map. + * @return A reference to the internal value map. + */ + [[nodiscard]] auto PresetValues() const -> const ValueMap&; + +protected: + /** + * @brief Parses a single line and stores the result in the value map. + * + * The function doesn't really care about invalid lines with random text or comments. The first "word" + * is added as key to the map, but will not be used afterward. + * + * @param line The line to parse. + */ + void ParseLine(const std::string& line); + +private: + /** + * @brief Converts the string to lower-case. + * Only letters A-Z are converted to a-z by default. + * @param str The original string. + * @return The lower-case string. + */ + static auto ToLower(std::string str) -> std::string; + + /** + * @brief Exports the currently stored preset data as a Milkdrop-compatible INI file. + * + * All contents are written in the exact same order and formatting as Milkdrop does. + * This means parsing and exporting an existing preset file will return identical contents. + * + * @return A stringstream containing the preset file data. + */ + [[nodiscard]] auto ExportPreset() const -> std::stringstream; + + /** + * @brief Exports a block for a single custom waveform. + * @param index The index of the custom waveform (0-3). + * @param outputStream The stream to write the exported data into. + */ + void ExportWave(int index, std::stringstream& outputStream) const; + + /** + * @brief Exports a block for a single custom shape. + * @param index The index of the custom shape (0-3). + * @param outputStream The stream to write the exported data into. + */ + void ExportShape(int index, std::stringstream& outputStream) const; + + /** + * @brief Exports a block of code, with one 1-based numbered key per line. + * + * This method will not add the "`" character for shader code. Each code line + * is written as-is. + * + * @param keyPrefix The code block prefix, without line numbers. + * @param outputStream The stream to write the exported data into. + */ + void ExportCodeBlock(const std::string& keyPrefix, std::stringstream& outputStream) const; + + ValueMap _presetValues; //!< Map with preset keys and their value. +}; + +} // namespace Editor diff --git a/src/gui/preset_editor/imgui_color_text_editor/CMakeLists.txt b/src/gui/preset_editor/imgui_color_text_editor/CMakeLists.txt new file mode 100644 index 0000000..05af63d --- /dev/null +++ b/src/gui/preset_editor/imgui_color_text_editor/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(ImGUIColorTextEditor STATIC + TextEditor.cpp + TextEditor.h + ) + +target_link_libraries(ImGUIColorTextEditor + PUBLIC + ImGui + ) + +target_include_directories(ImGUIColorTextEditor + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ) diff --git a/src/gui/preset_editor/imgui_color_text_editor/CONTRIBUTING b/src/gui/preset_editor/imgui_color_text_editor/CONTRIBUTING new file mode 100644 index 0000000..73ab161 --- /dev/null +++ b/src/gui/preset_editor/imgui_color_text_editor/CONTRIBUTING @@ -0,0 +1,11 @@ +# Contributing +Pull requests are welcome, feel free to contribute if you have implemented something which might be useful for the general audience of this little piece of software. Apparently, it became kind of a community project now. :) + +Whem contributing, please follow the following guidelines. I will keep it updated as we bump into something which worth doing better. +- Try to follow the same coding and naming conventions you find in the source already. I know that everyone has its own preference/taste in coding, but please keep the source consistent in style. +- Please submit to the 'dev' branch first for testing, and it will be merged to 'main' if it seems to work fine. I would like try keep 'master' in a good working condition, as more and more people are using it. +- Please send your submissions in small, well defined requests, i. e. do not accumulate many unrelated changes in one large pull request. Keep your submissions as small as possible, it will make everyone's life easier. +- Avoid using ImGui internal since it would make the source fragile against internal changes in ImGui. +- Try to keep the perormance high within the render function. Try to avoid doing anything which leads to memory allocations (like using temporary std::string, std::vector variables), or complex algorithm. If you really have to, try to amortise it between frames. + +Thank you. :) diff --git a/src/gui/preset_editor/imgui_color_text_editor/LICENSE b/src/gui/preset_editor/imgui_color_text_editor/LICENSE new file mode 100644 index 0000000..d0e3543 --- /dev/null +++ b/src/gui/preset_editor/imgui_color_text_editor/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 BalazsJako + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/gui/preset_editor/imgui_color_text_editor/README.md b/src/gui/preset_editor/imgui_color_text_editor/README.md new file mode 100644 index 0000000..fae0046 --- /dev/null +++ b/src/gui/preset_editor/imgui_color_text_editor/README.md @@ -0,0 +1,33 @@ +# ImGuiColorTextEdit +Syntax highlighting text editor for ImGui + +![Screenshot](https://github.com/BalazsJako/ImGuiColorTextEdit/wiki/ImGuiTextEdit.png "Screenshot") + +Demo project: https://github.com/BalazsJako/ColorTextEditorDemo + +This started as my attempt to write a relatively simple widget which provides text editing functionality with syntax highlighting. Now there are other contributors who provide valuable additions. + +While it relies on Omar Cornut's https://github.com/ocornut/imgui, it does not follow the "pure" one widget - one function approach. Since the editor has to maintain a relatively complex and large internal state, it did not seem to be practical to try and enforce fully immediate mode. It stores its internal state in an object instance which is reused across frames. + +The code is (still) work in progress, please report if you find any issues. + +# Main features + - approximates typical code editor look and feel (essential mouse/keyboard commands work - I mean, the commands _I_ normally use :)) + - undo/redo + - UTF-8 support + - works with both fixed and variable-width fonts + - extensible syntax highlighting for multiple languages + - identifier declarations: a small piece of description can be associated with an identifier. The editor displays it in a tooltip when the mouse cursor is hovered over the identifier + - error markers: the user can specify a list of error messages together the line of occurence, the editor will highligh the lines with red backround and display error message in a tooltip when the mouse cursor is hovered over the line + - large files: there is no explicit limit set on file size or number of lines (below 2GB, performance is not affected when large files are loaded (except syntax coloring, see below) + - color palette support: you can switch between different color palettes, or even define your own + - whitespace indicators (TAB, space) + +# Known issues + - syntax highligthing of most languages - except C/C++ - is based on std::regex, which is diasppointingly slow. Because of that, the highlighting process is amortized between multiple frames. C/C++ has a hand-written tokenizer which is much faster. + +Please post your screenshots if you find this little piece of software useful. :) + +# Contribute + +If you want to contribute, please refer to CONTRIBUTE file. diff --git a/src/gui/preset_editor/imgui_color_text_editor/TextEditor.cpp b/src/gui/preset_editor/imgui_color_text_editor/TextEditor.cpp new file mode 100644 index 0000000..dcb3fd0 --- /dev/null +++ b/src/gui/preset_editor/imgui_color_text_editor/TextEditor.cpp @@ -0,0 +1,3535 @@ +#include +#include +#include +#include +#include +#include + +#include "TextEditor.h" + +#define IMGUI_DEFINE_MATH_OPERATORS +#include "imgui.h" // for imGui::GetCurrentWindow() + +// TODO +// - multiline comments vs single-line: latter is blocking start of a ML + +template +bool equals(InputIt1 first1, InputIt1 last1, + InputIt2 first2, InputIt2 last2, BinaryPredicate p) +{ + for (; first1 != last1 && first2 != last2; ++first1, ++first2) + { + if (!p(*first1, *first2)) + return false; + } + return first1 == last1 && first2 == last2; +} + +TextEditor::TextEditor() + : mStartTime(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) +{ + SetPalette(GetDarkPalette()); + SetLanguageDefinition(LanguageDefinition::HLSL()); + mLines.emplace_back(); +} + + +void TextEditor::SetLanguageDefinition(const LanguageDefinition& aLanguageDef) +{ + mLanguageDefinition = aLanguageDef; + mRegexList.clear(); + + for (auto& r : mLanguageDefinition.mTokenRegexStrings) + mRegexList.emplace_back(std::make_pair(std::regex(r.first, std::regex_constants::optimize), r.second)); + + Colorize(); +} + +void TextEditor::SetPalette(const Palette& aValue) +{ + mPaletteBase = aValue; +} + +std::string TextEditor::GetText(const Coordinates& aStart, const Coordinates& aEnd) const +{ + std::string result; + + auto lstart = aStart.mLine; + auto lend = aEnd.mLine; + auto istart = GetCharacterIndex(aStart); + auto iend = GetCharacterIndex(aEnd); + size_t s = 0; + + for (size_t i = lstart; i < lend; i++) + s += mLines[i].size(); + + result.reserve(s + s / 8); + + while (istart < iend || lstart < lend) + { + if (lstart >= static_cast(mLines.size())) + break; + + auto& line = mLines[lstart]; + if (istart < static_cast(line.size())) + { + result += static_cast(line[istart].mChar); + istart++; + } + else + { + istart = 0; + ++lstart; + result += '\n'; + } + } + + return result; +} + +TextEditor::Coordinates TextEditor::GetActualCursorCoordinates() const +{ + return SanitizeCoordinates(mState.mCursorPosition); +} + +TextEditor::Coordinates TextEditor::SanitizeCoordinates(const Coordinates& aValue) const +{ + auto line = aValue.mLine; + auto column = aValue.mColumn; + if (line >= static_cast(mLines.size())) + { + if (mLines.empty()) + { + line = 0; + column = 0; + } + else + { + line = (int) mLines.size() - 1; + column = GetLineMaxColumn(line); + } + return {line, column}; + } + else + { + column = mLines.empty() ? 0 : std::min(column, GetLineMaxColumn(line)); + return {line, column}; + } +} + +// https://en.wikipedia.org/wiki/UTF-8 +// We assume that the char is a standalone character (<128) or a leading byte of an UTF-8 code sequence (non-10xxxxxx code) +static int UTF8CharLength(TextEditor::Char c) +{ + if ((c & 0xFE) == 0xFC) + return 6; + if ((c & 0xFC) == 0xF8) + return 5; + if ((c & 0xF8) == 0xF0) + return 4; + else if ((c & 0xF0) == 0xE0) + return 3; + else if ((c & 0xE0) == 0xC0) + return 2; + return 1; +} + +// "Borrowed" from ImGui source +static inline int ImTextCharToUtf8(char* buf, int buf_size, unsigned int c) +{ + if (c < 0x80) + { + buf[0] = static_cast(c); + return 1; + } + if (c < 0x800) + { + if (buf_size < 2) + return 0; + buf[0] = static_cast(0xc0 + (c >> 6)); + buf[1] = static_cast(0x80 + (c & 0x3f)); + return 2; + } + if (c >= 0xdc00 && c < 0xe000) + { + return 0; + } + if (c >= 0xd800 && c < 0xdc00) + { + if (buf_size < 4) + return 0; + buf[0] = static_cast(0xf0 + (c >> 18)); + buf[1] = static_cast(0x80 + ((c >> 12) & 0x3f)); + buf[2] = static_cast(0x80 + ((c >> 6) & 0x3f)); + buf[3] = static_cast(0x80 + ((c) & 0x3f)); + return 4; + } + //else if (c < 0x10000) + + if (buf_size < 3) + return 0; + buf[0] = static_cast(0xe0 + (c >> 12)); + buf[1] = static_cast(0x80 + ((c >> 6) & 0x3f)); + buf[2] = (char) (0x80 + ((c) & 0x3f)); + return 3; +} + +void TextEditor::Advance(Coordinates& aCoordinates) const +{ + if (aCoordinates.mLine < (int) mLines.size()) + { + auto& line = mLines[aCoordinates.mLine]; + auto cindex = GetCharacterIndex(aCoordinates); + + if (cindex + 1 < (int) line.size()) + { + auto delta = UTF8CharLength(line[cindex].mChar); + cindex = std::min(cindex + delta, (int) line.size() - 1); + } + else + { + ++aCoordinates.mLine; + cindex = 0; + } + aCoordinates.mColumn = GetCharacterColumn(aCoordinates.mLine, cindex); + } +} + +void TextEditor::DeleteRange(const Coordinates& aStart, const Coordinates& aEnd) +{ + assert(aEnd >= aStart); + assert(!mReadOnly); + + //printf("D(%d.%d)-(%d.%d)\n", aStart.mLine, aStart.mColumn, aEnd.mLine, aEnd.mColumn); + + if (aEnd == aStart) + return; + + auto start = GetCharacterIndex(aStart); + auto end = GetCharacterIndex(aEnd); + + if (aStart.mLine == aEnd.mLine) + { + auto& line = mLines[aStart.mLine]; + auto n = GetLineMaxColumn(aStart.mLine); + if (aEnd.mColumn >= n) + line.erase(line.begin() + start, line.end()); + else + line.erase(line.begin() + start, line.begin() + end); + } + else + { + auto& firstLine = mLines[aStart.mLine]; + auto& lastLine = mLines[aEnd.mLine]; + + firstLine.erase(firstLine.begin() + start, firstLine.end()); + lastLine.erase(lastLine.begin(), lastLine.begin() + end); + + if (aStart.mLine < aEnd.mLine) + firstLine.insert(firstLine.end(), lastLine.begin(), lastLine.end()); + + if (aStart.mLine < aEnd.mLine) + RemoveLine(aStart.mLine + 1, aEnd.mLine + 1); + } + + mTextChanged = true; +} + +int TextEditor::InsertTextAt(Coordinates& /* inout */ aWhere, const char* aValue) +{ + assert(!mReadOnly); + + int cindex = GetCharacterIndex(aWhere); + int totalLines = 0; + while (*aValue != '\0') + { + assert(!mLines.empty()); + + if (*aValue == '\r') + { + // skip + ++aValue; + } + else if (*aValue == '\n') + { + if (cindex < (int) mLines[aWhere.mLine].size()) + { + auto& newLine = InsertLine(aWhere.mLine + 1); + auto& line = mLines[aWhere.mLine]; + newLine.insert(newLine.begin(), line.begin() + cindex, line.end()); + line.erase(line.begin() + cindex, line.end()); + } + else + { + InsertLine(aWhere.mLine + 1); + } + ++aWhere.mLine; + aWhere.mColumn = 0; + cindex = 0; + ++totalLines; + ++aValue; + } + else + { + auto& line = mLines[aWhere.mLine]; + auto d = UTF8CharLength(*aValue); + while (d-- > 0 && *aValue != '\0') + line.insert(line.begin() + cindex++, Glyph(*aValue++, PaletteIndex::Default)); + ++aWhere.mColumn; + } + + mTextChanged = true; + } + + return totalLines; +} + +void TextEditor::AddUndo(const UndoRecord& aValue) +{ + assert(!mReadOnly); + //printf("AddUndo: (@%d.%d) +\'%s' [%d.%d .. %d.%d], -\'%s', [%d.%d .. %d.%d] (@%d.%d)\n", + // aValue.mBefore.mCursorPosition.mLine, aValue.mBefore.mCursorPosition.mColumn, + // aValue.mAdded.c_str(), aValue.mAddedStart.mLine, aValue.mAddedStart.mColumn, aValue.mAddedEnd.mLine, aValue.mAddedEnd.mColumn, + // aValue.mRemoved.c_str(), aValue.mRemovedStart.mLine, aValue.mRemovedStart.mColumn, aValue.mRemovedEnd.mLine, aValue.mRemovedEnd.mColumn, + // aValue.mAfter.mCursorPosition.mLine, aValue.mAfter.mCursorPosition.mColumn + // ); + + mUndoBuffer.resize(mUndoIndex + 1); + mUndoBuffer.back() = aValue; + ++mUndoIndex; +} + +TextEditor::Coordinates TextEditor::ScreenPosToCoordinates(const ImVec2& aPosition) const +{ + ImVec2 origin = ImGui::GetCursorScreenPos(); + ImVec2 local(aPosition.x - origin.x, aPosition.y - origin.y); + + int lineNo = std::max(0, static_cast(std::floor(local.y / mCharAdvance.y))); + + int columnCoord = 0; + + if (lineNo >= 0 && lineNo < static_cast(mLines.size())) + { + auto& line = mLines.at(lineNo); + + int columnIndex = 0; + float columnX = 0.0f; + + while ((size_t) columnIndex < line.size()) + { + float columnWidth = 0.0f; + + if (line[columnIndex].mChar == '\t') + { + float spaceSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, " ").x; + float oldX = columnX; + float newColumnX = (1.0f + std::floor((1.0f + columnX) / (float(mTabSize) * spaceSize))) * (float(mTabSize) * spaceSize); + columnWidth = newColumnX - oldX; + if (mTextStart + columnX + columnWidth * 0.5f > local.x) + break; + columnX = newColumnX; + columnCoord = (columnCoord / mTabSize) * mTabSize + mTabSize; + columnIndex++; + } + else + { + char buf[7]; + auto d = UTF8CharLength(line[columnIndex].mChar); + int i = 0; + while (i < 6 && d-- > 0) + buf[i++] = static_cast(line[columnIndex++].mChar); + buf[i] = '\0'; + columnWidth = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf).x; + if (mTextStart + columnX + columnWidth * 0.5f > local.x) + break; + columnX += columnWidth; + columnCoord++; + } + } + } + + return SanitizeCoordinates(Coordinates(lineNo, columnCoord)); +} + +TextEditor::Coordinates TextEditor::FindWordStart(const Coordinates& aFrom) const +{ + Coordinates at = aFrom; + if (at.mLine >= (int) mLines.size()) + return at; + + auto& line = mLines[at.mLine]; + auto cindex = GetCharacterIndex(at); + + if (cindex >= (int) line.size()) + return at; + + while (cindex > 0 && isspace(line[cindex].mChar)) + --cindex; + + auto cstart = (PaletteIndex) line[cindex].mColorIndex; + while (cindex > 0) + { + auto c = line[cindex].mChar; + if ((c & 0xC0) != 0x80) // not UTF code sequence 10xxxxxx + { + if (c <= 32 && isspace(c)) + { + cindex++; + break; + } + if (cstart != (PaletteIndex) line[size_t(cindex - 1)].mColorIndex) + break; + } + --cindex; + } + return {at.mLine, GetCharacterColumn(at.mLine, cindex)}; +} + +TextEditor::Coordinates TextEditor::FindWordEnd(const Coordinates& aFrom) const +{ + Coordinates at = aFrom; + if (at.mLine >= (int) mLines.size()) + return at; + + auto& line = mLines[at.mLine]; + auto cindex = GetCharacterIndex(at); + + if (cindex >= (int) line.size()) + return at; + + bool prevspace = (bool) isspace(line[cindex].mChar); + auto cstart = (PaletteIndex) line[cindex].mColorIndex; + while (cindex < (int) line.size()) + { + auto c = line[cindex].mChar; + auto d = UTF8CharLength(c); + if (cstart != (PaletteIndex) line[cindex].mColorIndex) + break; + + if (prevspace != !!isspace(c)) + { + if (isspace(c)) + while (cindex < (int) line.size() && isspace(line[cindex].mChar)) + ++cindex; + break; + } + cindex += d; + } + return {aFrom.mLine, GetCharacterColumn(aFrom.mLine, cindex)}; +} + +TextEditor::Coordinates TextEditor::FindNextWord(const Coordinates& aFrom) const +{ + Coordinates at = aFrom; + if (at.mLine >= (int) mLines.size()) + return at; + + // skip to the next non-word character + auto cindex = GetCharacterIndex(aFrom); + bool isword = false; + bool skip = false; + if (cindex < (int) mLines[at.mLine].size()) + { + auto& line = mLines[at.mLine]; + isword = isalnum(line[cindex].mChar); + skip = isword; + } + + while (!isword || skip) + { + if (at.mLine >= mLines.size()) + { + auto l = std::max(0, (int) mLines.size() - 1); + return {l, GetLineMaxColumn(l)}; + } + + auto& line = mLines[at.mLine]; + if (cindex < (int) line.size()) + { + isword = isalnum(line[cindex].mChar); + + if (isword && !skip) + return {at.mLine, GetCharacterColumn(at.mLine, cindex)}; + + if (!isword) + skip = false; + + cindex++; + } + else + { + cindex = 0; + ++at.mLine; + skip = false; + isword = false; + } + } + + return at; +} + +int TextEditor::GetCharacterIndex(const Coordinates& aCoordinates) const +{ + if (aCoordinates.mLine >= mLines.size()) + return -1; + auto& line = mLines[aCoordinates.mLine]; + int c = 0; + int i = 0; + for (; i < line.size() && c < aCoordinates.mColumn;) + { + if (line[i].mChar == '\t') + c = (c / mTabSize) * mTabSize + mTabSize; + else + ++c; + i += UTF8CharLength(line[i].mChar); + } + return i; +} + +int TextEditor::GetCharacterColumn(int aLine, int aIndex) const +{ + if (aLine >= mLines.size()) + return 0; + auto& line = mLines[aLine]; + int col = 0; + int i = 0; + while (i < aIndex && i < (int) line.size()) + { + auto c = line[i].mChar; + i += UTF8CharLength(c); + if (c == '\t') + col = (col / mTabSize) * mTabSize + mTabSize; + else + col++; + } + return col; +} + +int TextEditor::GetLineCharacterCount(int aLine) const +{ + if (aLine >= mLines.size()) + return 0; + auto& line = mLines[aLine]; + int c = 0; + for (unsigned i = 0; i < line.size(); c++) + i += UTF8CharLength(line[i].mChar); + return c; +} + +int TextEditor::GetLineMaxColumn(int aLine) const +{ + if (aLine >= mLines.size()) + return 0; + auto& line = mLines[aLine]; + int col = 0; + for (unsigned i = 0; i < line.size();) + { + auto c = line[i].mChar; + if (c == '\t') + col = (col / mTabSize) * mTabSize + mTabSize; + else + col++; + i += UTF8CharLength(c); + } + return col; +} + +bool TextEditor::IsOnWordBoundary(const Coordinates& aAt) const +{ + if (aAt.mLine >= (int) mLines.size() || aAt.mColumn == 0) + return true; + + auto& line = mLines[aAt.mLine]; + auto cindex = GetCharacterIndex(aAt); + if (cindex >= (int) line.size()) + return true; + + if (mColorizerEnabled) + return line[cindex].mColorIndex != line[size_t(cindex - 1)].mColorIndex; + + return isspace(line[cindex].mChar) != isspace(line[cindex - 1].mChar); +} + +void TextEditor::RemoveLine(int aStart, int aEnd) +{ + assert(!mReadOnly); + assert(aEnd >= aStart); + assert(mLines.size() > (size_t) (aEnd - aStart)); + + ErrorMarkers etmp; + for (auto& i : mErrorMarkers) + { + ErrorMarkers::value_type e(i.first >= aStart ? i.first - 1 : i.first, i.second); + if (e.first >= aStart && e.first <= aEnd) + continue; + etmp.insert(e); + } + mErrorMarkers = std::move(etmp); + + Breakpoints btmp; + for (auto i : mBreakpoints) + { + if (i >= aStart && i <= aEnd) + continue; + btmp.insert(i >= aStart ? i - 1 : i); + } + mBreakpoints = std::move(btmp); + + mLines.erase(mLines.begin() + aStart, mLines.begin() + aEnd); + assert(!mLines.empty()); + + mTextChanged = true; +} + +void TextEditor::RemoveLine(int aIndex) +{ + assert(!mReadOnly); + assert(mLines.size() > 1); + + ErrorMarkers etmp; + for (auto& i : mErrorMarkers) + { + ErrorMarkers::value_type e(i.first > aIndex ? i.first - 1 : i.first, i.second); + if (e.first - 1 == aIndex) + continue; + etmp.insert(e); + } + mErrorMarkers = std::move(etmp); + + Breakpoints btmp; + for (auto i : mBreakpoints) + { + if (i == aIndex) + continue; + btmp.insert(i >= aIndex ? i - 1 : i); + } + mBreakpoints = std::move(btmp); + + mLines.erase(mLines.begin() + aIndex); + assert(!mLines.empty()); + + mTextChanged = true; +} + +TextEditor::Line& TextEditor::InsertLine(int aIndex) +{ + assert(!mReadOnly); + + auto& result = *mLines.insert(mLines.begin() + aIndex, Line()); + + ErrorMarkers etmp; + for (auto& i : mErrorMarkers) + etmp.insert(ErrorMarkers::value_type(i.first >= aIndex ? i.first + 1 : i.first, i.second)); + mErrorMarkers = std::move(etmp); + + Breakpoints btmp; + for (auto i : mBreakpoints) + btmp.insert(i >= aIndex ? i + 1 : i); + mBreakpoints = std::move(btmp); + + return result; +} + +std::string TextEditor::GetWordUnderCursor() const +{ + auto c = GetCursorPosition(); + return GetWordAt(c); +} + +std::string TextEditor::GetWordAt(const Coordinates& aCoords) const +{ + auto start = FindWordStart(aCoords); + auto end = FindWordEnd(aCoords); + + std::string r; + + auto istart = GetCharacterIndex(start); + auto iend = GetCharacterIndex(end); + + for (auto it = istart; it < iend; ++it) + r.push_back(static_cast(mLines[aCoords.mLine][it].mChar)); + + return r; +} + +ImU32 TextEditor::GetGlyphColor(const Glyph& aGlyph) const +{ + if (!mColorizerEnabled) + return mPalette[static_cast(PaletteIndex::Default)]; + if (aGlyph.mComment) + return mPalette[(int) PaletteIndex::Comment]; + if (aGlyph.mMultiLineComment) + return mPalette[(int) PaletteIndex::MultiLineComment]; + auto const color = mPalette[(int) aGlyph.mColorIndex]; + if (aGlyph.mPreprocessor) + { + const auto ppcolor = mPalette[static_cast(PaletteIndex::Preprocessor)]; + const unsigned int c0 = ((ppcolor & 0xff) + (color & 0xff)) / 2; + const unsigned int c1 = (((ppcolor >> 8) & 0xff) + ((color >> 8) & 0xff)) / 2; + const unsigned int c2 = (((ppcolor >> 16) & 0xff) + ((color >> 16) & 0xff)) / 2; + const unsigned int c3 = (((ppcolor >> 24) & 0xff) + ((color >> 24) & 0xff)) / 2; + return c0 | c1 << 8 | c2 << 16 | c3 << 24; + } + return color; +} + +void TextEditor::HandleKeyboardInputs() +{ + ImGuiIO& io = ImGui::GetIO(); + auto shift = io.KeyShift; + auto ctrl = io.ConfigMacOSXBehaviors ? io.KeySuper : io.KeyCtrl; + auto alt = io.ConfigMacOSXBehaviors ? io.KeyCtrl : io.KeyAlt; + + if (ImGui::IsWindowFocused()) + { + if (ImGui::IsWindowHovered()) + ImGui::SetMouseCursor(ImGuiMouseCursor_TextInput); + //ImGui::CaptureKeyboardFromApp(true); + + io.WantCaptureKeyboard = true; + io.WantTextInput = true; + + if (!IsReadOnly() && ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Z)) + Undo(); + else if (!IsReadOnly() && !ctrl && !shift && alt && ImGui::IsKeyPressed(ImGuiKey_Backspace)) + Undo(); + else if (!IsReadOnly() && ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Y)) + Redo(); + else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_UpArrow)) + MoveUp(1, shift); + else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_DownArrow)) + MoveDown(1, shift); + else if (!alt && ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) + MoveLeft(1, shift, ctrl); + else if (!alt && ImGui::IsKeyPressed(ImGuiKey_RightArrow)) + MoveRight(1, shift, ctrl); + else if (!alt && ImGui::IsKeyPressed(ImGuiKey_PageUp)) + MoveUp(GetPageSize() - 4, shift); + else if (!alt && ImGui::IsKeyPressed(ImGuiKey_PageDown)) + MoveDown(GetPageSize() - 4, shift); + else if (!alt && ctrl && ImGui::IsKeyPressed(ImGuiKey_Home)) + MoveTop(shift); + else if (ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_End)) + MoveBottom(shift); + else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_Home)) + MoveHome(shift); + else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_End)) + MoveEnd(shift); + else if (!IsReadOnly() && !ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Delete)) + Delete(); + else if (!IsReadOnly() && !ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Backspace)) + Backspace(); + else if (!ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Insert)) + mOverwrite ^= true; + else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Insert)) + Copy(); + else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_C)) + Copy(); + else if (!IsReadOnly() && !ctrl && shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Insert)) + Paste(); + else if (!IsReadOnly() && ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_V)) + Paste(); + else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_X)) + Cut(); + else if (!ctrl && shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Delete)) + Cut(); + else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_A)) + SelectAll(); + else if (!IsReadOnly() && !ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Enter)) + EnterCharacter('\n', false); + else if (!IsReadOnly() && !ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_Tab)) + EnterCharacter('\t', shift); + + if (!IsReadOnly() && !io.InputQueueCharacters.empty()) + { + for (int i = 0; i < io.InputQueueCharacters.Size; i++) + { + auto c = io.InputQueueCharacters[i]; + if (c != 0 && (c == '\n' || c >= 32)) + EnterCharacter(c, shift); + } + io.InputQueueCharacters.resize(0); + } + } +} + +void TextEditor::HandleMouseInputs() +{ + ImGuiIO& io = ImGui::GetIO(); + auto shift = io.KeyShift; + auto ctrl = io.ConfigMacOSXBehaviors ? io.KeySuper : io.KeyCtrl; + auto alt = io.ConfigMacOSXBehaviors ? io.KeyCtrl : io.KeyAlt; + + if (ImGui::IsWindowHovered()) + { + if (!shift && !alt) + { + auto click = ImGui::IsMouseClicked(0); + auto doubleClick = ImGui::IsMouseDoubleClicked(0); + auto t = ImGui::GetTime(); + auto tripleClick = click && !doubleClick && (mLastClick != -1.0f && (t - mLastClick) < io.MouseDoubleClickTime); + + /* + Left mouse button triple click + */ + + if (tripleClick) + { + if (!ctrl) + { + mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos()); + mSelectionMode = SelectionMode::Line; + SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); + } + + mLastClick = -1.0f; + } + + /* + Left mouse button double click + */ + + else if (doubleClick) + { + if (!ctrl) + { + mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos()); + if (mSelectionMode == SelectionMode::Line) + mSelectionMode = SelectionMode::Normal; + else + mSelectionMode = SelectionMode::Word; + SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); + } + + mLastClick = (float) ImGui::GetTime(); + } + + /* + Left mouse button click + */ + else if (click) + { + mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos()); + if (ctrl) + mSelectionMode = SelectionMode::Word; + else + mSelectionMode = SelectionMode::Normal; + SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); + + mLastClick = (float) ImGui::GetTime(); + } + // Mouse left button dragging (=> update selection) + else if (ImGui::IsMouseDragging(0) && ImGui::IsMouseDown(0)) + { + io.WantCaptureMouse = true; + mState.mCursorPosition = mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos()); + SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); + } + } + } +} + +void TextEditor::Render() +{ + /* Compute mCharAdvance regarding to scaled font size (Ctrl + mouse wheel)*/ + const float fontSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, "#", nullptr, nullptr).x; + mCharAdvance = ImVec2(fontSize, ImGui::GetTextLineHeightWithSpacing() * mLineSpacing); + + /* Update palette with the current alpha from style */ + for (int i = 0; i < (int) PaletteIndex::Max; ++i) + { + auto color = ImGui::ColorConvertU32ToFloat4(mPaletteBase[i]); + color.w *= ImGui::GetStyle().Alpha; + mPalette[i] = ImGui::ColorConvertFloat4ToU32(color); + } + + assert(mLineBuffer.empty()); + + auto contentSize = ImGui::GetWindowContentRegionMax(); + auto drawList = ImGui::GetWindowDrawList(); + float longest(mTextStart); + + if (mScrollToTop) + { + mScrollToTop = false; + ImGui::SetScrollY(0.f); + } + + ImVec2 cursorScreenPos = ImGui::GetCursorScreenPos(); + auto scrollX = ImGui::GetScrollX(); + auto scrollY = ImGui::GetScrollY(); + + auto lineNo = static_cast(std::floor(scrollY / mCharAdvance.y)); + auto globalLineMax = static_cast(mLines.size()); + auto lineMax = std::max(0, std::min(static_cast(mLines.size()) - 1, lineNo + static_cast(std::floor((scrollY + contentSize.y) / mCharAdvance.y)))); + + // Deduce mTextStart by evaluating mLines size (global lineMax) plus two spaces as text width + char buf[16]; + snprintf(buf, 16, " %d ", globalLineMax); + mTextStart = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf, nullptr, nullptr).x + static_cast(mLeftMargin); + + if (!mLines.empty()) + { + float spaceSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, " ", nullptr, nullptr).x; + + while (lineNo <= lineMax) + { + ImVec2 lineStartScreenPos = ImVec2(cursorScreenPos.x, cursorScreenPos.y + static_cast(lineNo) * mCharAdvance.y); + ImVec2 textScreenPos = ImVec2(lineStartScreenPos.x + mTextStart, lineStartScreenPos.y); + + auto& line = mLines[lineNo]; + longest = std::max(mTextStart + TextDistanceToLineStart(Coordinates(lineNo, GetLineMaxColumn(lineNo))), longest); + auto columnNo = 0; + Coordinates lineStartCoord(lineNo, 0); + Coordinates lineEndCoord(lineNo, GetLineMaxColumn(lineNo)); + + // Draw selection for the current line + float sstart = -1.0f; + float ssend = -1.0f; + + assert(mState.mSelectionStart <= mState.mSelectionEnd); + if (mState.mSelectionStart <= lineEndCoord) + sstart = mState.mSelectionStart > lineStartCoord ? TextDistanceToLineStart(mState.mSelectionStart) : 0.0f; + if (mState.mSelectionEnd > lineStartCoord) + ssend = TextDistanceToLineStart(mState.mSelectionEnd < lineEndCoord ? mState.mSelectionEnd : lineEndCoord); + + if (mState.mSelectionEnd.mLine > lineNo) + ssend += mCharAdvance.x; + + if (sstart != -1 && ssend != -1 && sstart < ssend) + { + ImVec2 vstart(lineStartScreenPos.x + mTextStart + sstart, lineStartScreenPos.y); + ImVec2 vend(lineStartScreenPos.x + mTextStart + ssend, lineStartScreenPos.y + mCharAdvance.y); + drawList->AddRectFilled(vstart, vend, mPalette[(int) PaletteIndex::Selection]); + } + + // Draw breakpoints + auto start = ImVec2(lineStartScreenPos.x + scrollX, lineStartScreenPos.y); + + if (mBreakpoints.count(lineNo + 1) != 0) + { + auto end = ImVec2(lineStartScreenPos.x + contentSize.x + 2.0f * scrollX, lineStartScreenPos.y + mCharAdvance.y); + drawList->AddRectFilled(start, end, mPalette[(int) PaletteIndex::Breakpoint]); + } + + // Draw error markers + auto errorIt = mErrorMarkers.find(lineNo + 1); + if (errorIt != mErrorMarkers.end()) + { + auto end = ImVec2(lineStartScreenPos.x + contentSize.x + 2.0f * scrollX, lineStartScreenPos.y + mCharAdvance.y); + drawList->AddRectFilled(start, end, mPalette[(int) PaletteIndex::ErrorMarker]); + + if (ImGui::IsMouseHoveringRect(lineStartScreenPos, end)) + { + ImGui::BeginTooltip(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.2f, 0.2f, 1.0f)); + ImGui::Text("Error at line %d:", errorIt->first); + ImGui::PopStyleColor(); + ImGui::Separator(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 0.2f, 1.0f)); + ImGui::Text("%s", errorIt->second.c_str()); + ImGui::PopStyleColor(); + ImGui::EndTooltip(); + } + } + + // Draw line number (right aligned) + snprintf(buf, 16, "%d ", lineNo + 1); + + auto lineNoWidth = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf, nullptr, nullptr).x; + drawList->AddText(ImVec2(lineStartScreenPos.x + mTextStart - lineNoWidth, lineStartScreenPos.y), mPalette[(int) PaletteIndex::LineNumber], buf); + + if (mState.mCursorPosition.mLine == lineNo) + { + auto focused = ImGui::IsWindowFocused(); + + // Highlight the current line (where the cursor is) + if (!HasSelection()) + { + auto end = ImVec2(start.x + contentSize.x + scrollX, start.y + mCharAdvance.y); + drawList->AddRectFilled(start, end, mPalette[(int) (focused ? PaletteIndex::CurrentLineFill : PaletteIndex::CurrentLineFillInactive)]); + drawList->AddRect(start, end, mPalette[(int) PaletteIndex::CurrentLineEdge], 1.0f); + } + + // Render the cursor + if (focused) + { + auto timeEnd = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + auto elapsed = timeEnd - mStartTime; + if (elapsed > 400) + { + float width = 1.0f; + auto cindex = GetCharacterIndex(mState.mCursorPosition); + float cx = TextDistanceToLineStart(mState.mCursorPosition); + + if (mOverwrite && cindex < (int) line.size()) + { + auto c = line[cindex].mChar; + if (c == '\t') + { + auto x = (1.0f + std::floor((1.0f + cx) / (float(mTabSize) * spaceSize))) * (float(mTabSize) * spaceSize); + width = x - cx; + } + else + { + char buf2[2]; + buf2[0] = static_cast(line[cindex].mChar); + buf2[1] = '\0'; + width = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf2).x; + } + } + ImVec2 cstart(textScreenPos.x + cx, lineStartScreenPos.y); + ImVec2 cend(textScreenPos.x + cx + width, lineStartScreenPos.y + mCharAdvance.y); + drawList->AddRectFilled(cstart, cend, mPalette[(int) PaletteIndex::Cursor]); + if (elapsed > 800) + mStartTime = timeEnd; + } + } + } + + // Render colorized text + auto prevColor = line.empty() ? mPalette[(int) PaletteIndex::Default] : GetGlyphColor(line[0]); + ImVec2 bufferOffset; + + for (int i = 0; i < line.size();) + { + auto& glyph = line[i]; + auto color = GetGlyphColor(glyph); + + if ((color != prevColor || glyph.mChar == '\t' || glyph.mChar == ' ') && !mLineBuffer.empty()) + { + const ImVec2 newOffset(textScreenPos.x + bufferOffset.x, textScreenPos.y + bufferOffset.y); + drawList->AddText(newOffset, prevColor, mLineBuffer.c_str()); + auto textSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, mLineBuffer.c_str(), nullptr, nullptr); + bufferOffset.x += textSize.x; + mLineBuffer.clear(); + } + prevColor = color; + + if (glyph.mChar == '\t') + { + auto oldX = bufferOffset.x; + bufferOffset.x = (1.0f + std::floor((1.0f + bufferOffset.x) / (float(mTabSize) * spaceSize))) * (float(mTabSize) * spaceSize); + ++i; + + if (mShowWhitespaces) + { + const auto s = ImGui::GetFontSize(); + const auto x1 = textScreenPos.x + oldX + 1.0f; + const auto x2 = textScreenPos.x + bufferOffset.x - 1.0f; + const auto y = textScreenPos.y + bufferOffset.y + s * 0.5f; + const ImVec2 p1(x1, y); + const ImVec2 p2(x2, y); + const ImVec2 p3(x2 - s * 0.2f, y - s * 0.2f); + const ImVec2 p4(x2 - s * 0.2f, y + s * 0.2f); + drawList->AddLine(p1, p2, 0x90909090); + drawList->AddLine(p2, p3, 0x90909090); + drawList->AddLine(p2, p4, 0x90909090); + } + } + else if (glyph.mChar == ' ') + { + if (mShowWhitespaces) + { + const auto s = ImGui::GetFontSize(); + const auto x = textScreenPos.x + bufferOffset.x + spaceSize * 0.5f; + const auto y = textScreenPos.y + bufferOffset.y + s * 0.5f; + drawList->AddCircleFilled(ImVec2(x, y), 1.5f, 0x80808080, 4); + } + bufferOffset.x += spaceSize; + i++; + } + else + { + auto l = UTF8CharLength(glyph.mChar); + while (l-- > 0) + mLineBuffer.push_back(static_cast(line[i++].mChar)); + } + ++columnNo; + } + + if (!mLineBuffer.empty()) + { + const ImVec2 newOffset(textScreenPos.x + bufferOffset.x, textScreenPos.y + bufferOffset.y); + drawList->AddText(newOffset, prevColor, mLineBuffer.c_str()); + mLineBuffer.clear(); + } + + ++lineNo; + } + + // Draw a tooltip on known identifiers/preprocessor symbols + if (ImGui::IsMousePosValid()) + { + auto id = GetWordAt(ScreenPosToCoordinates(ImGui::GetMousePos())); + if (!id.empty()) + { + auto it = mLanguageDefinition.mIdentifiers.find(id); + if (it != mLanguageDefinition.mIdentifiers.end()) + { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(it->second.mDeclaration.c_str()); + ImGui::EndTooltip(); + } + else + { + auto pi = mLanguageDefinition.mPreprocIdentifiers.find(id); + if (pi != mLanguageDefinition.mPreprocIdentifiers.end()) + { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(pi->second.mDeclaration.c_str()); + ImGui::EndTooltip(); + } + } + } + } + } + + + ImGui::Dummy(ImVec2((longest + 2), static_cast(mLines.size()) * mCharAdvance.y)); + + if (mScrollToCursor) + { + EnsureCursorVisible(); + ImGui::SetWindowFocus(); + mScrollToCursor = false; + } +} + +void TextEditor::Render(const char* aTitle, const ImVec2& aSize, bool aBorder) +{ + mWithinRender = true; + mTextChanged = false; + mCursorPositionChanged = false; + + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(mPalette[(int) PaletteIndex::Background])); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f)); + if (!mIgnoreImGuiChild) + ImGui::BeginChild(aTitle, aSize, aBorder, ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar | ImGuiWindowFlags_NoMove); + + if (mHandleKeyboardInputs) + { + HandleKeyboardInputs(); + ImGui::PushTabStop(true); + } + + if (mHandleMouseInputs) + HandleMouseInputs(); + + ColorizeInternal(); + Render(); + + if (mHandleKeyboardInputs) + ImGui::PopTabStop(); + + if (!mIgnoreImGuiChild) + ImGui::EndChild(); + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + + mWithinRender = false; +} + +void TextEditor::SetText(const std::string& aText) +{ + mLines.clear(); + mLines.emplace_back(); + for (auto chr : aText) + { + if (chr == '\r') + { + // ignore the carriage return character + } + else if (chr == '\n') + mLines.emplace_back(); + else + { + mLines.back().emplace_back(chr, PaletteIndex::Default); + } + } + + mTextChanged = true; + mScrollToTop = true; + + mUndoBuffer.clear(); + mUndoIndex = 0; + + Colorize(); +} + +void TextEditor::SetTextLines(const std::vector& aLines) +{ + mLines.clear(); + + if (aLines.empty()) + { + mLines.emplace_back(); + } + else + { + mLines.resize(aLines.size()); + + for (size_t i = 0; i < aLines.size(); ++i) + { + const std::string& aLine = aLines[i]; + + mLines[i].reserve(aLine.size()); + for (char j : aLine) + mLines[i].emplace_back(j, PaletteIndex::Default); + } + } + + mTextChanged = true; + mScrollToTop = true; + + mUndoBuffer.clear(); + mUndoIndex = 0; + + Colorize(); +} + +void TextEditor::EnterCharacter(ImWchar aChar, bool aShift) +{ + assert(!mReadOnly); + + UndoRecord u; + + u.mBefore = mState; + + if (HasSelection()) + { + if (aChar == '\t' && mState.mSelectionStart.mLine != mState.mSelectionEnd.mLine) + { + + auto start = mState.mSelectionStart; + auto end = mState.mSelectionEnd; + auto originalEnd = end; + + if (start > end) + std::swap(start, end); + start.mColumn = 0; + // end.mColumn = end.mLine < mLines.size() ? mLines[end.mLine].size() : 0; + if (end.mColumn == 0 && end.mLine > 0) + --end.mLine; + if (end.mLine >= (int) mLines.size()) + end.mLine = mLines.empty() ? 0 : (int) mLines.size() - 1; + end.mColumn = GetLineMaxColumn(end.mLine); + + //if (end.mColumn >= GetLineMaxColumn(end.mLine)) + // end.mColumn = GetLineMaxColumn(end.mLine) - 1; + + u.mRemovedStart = start; + u.mRemovedEnd = end; + u.mRemoved = GetText(start, end); + + bool modified = false; + + for (int i = start.mLine; i <= end.mLine; i++) + { + auto& line = mLines[i]; + if (aShift) + { + if (!line.empty()) + { + if (line.front().mChar == '\t') + { + line.erase(line.begin()); + modified = true; + } + else + { + for (int j = 0; j < mTabSize && !line.empty() && line.front().mChar == ' '; j++) + { + line.erase(line.begin()); + modified = true; + } + } + } + } + else + { + line.insert(line.begin(), Glyph('\t', TextEditor::PaletteIndex::Background)); + modified = true; + } + } + + if (modified) + { + start = Coordinates(start.mLine, GetCharacterColumn(start.mLine, 0)); + Coordinates rangeEnd; + if (originalEnd.mColumn != 0) + { + end = Coordinates(end.mLine, GetLineMaxColumn(end.mLine)); + rangeEnd = end; + u.mAdded = GetText(start, end); + } + else + { + end = Coordinates(originalEnd.mLine, 0); + rangeEnd = Coordinates(end.mLine - 1, GetLineMaxColumn(end.mLine - 1)); + u.mAdded = GetText(start, rangeEnd); + } + + u.mAddedStart = start; + u.mAddedEnd = rangeEnd; + u.mAfter = mState; + + mState.mSelectionStart = start; + mState.mSelectionEnd = end; + AddUndo(u); + + mTextChanged = true; + + EnsureCursorVisible(); + } + + return; + } // c == '\t' + else + { + u.mRemoved = GetSelectedText(); + u.mRemovedStart = mState.mSelectionStart; + u.mRemovedEnd = mState.mSelectionEnd; + DeleteSelection(); + } + } // HasSelection + + auto coord = GetActualCursorCoordinates(); + u.mAddedStart = coord; + + assert(!mLines.empty()); + + if (aChar == '\n') + { + InsertLine(coord.mLine + 1); + auto& line = mLines[coord.mLine]; + auto& newLine = mLines[coord.mLine + 1]; + + if (mLanguageDefinition.mAutoIndentation) + for (size_t it = 0; it < line.size() && isascii(line[it].mChar) && isblank(line[it].mChar); ++it) + newLine.push_back(line[it]); + + const size_t whitespaceSize = newLine.size(); + auto cindex = GetCharacterIndex(coord); + newLine.insert(newLine.end(), line.begin() + cindex, line.end()); + line.erase(line.begin() + cindex, line.begin() + static_cast(line.size())); + SetCursorPosition(Coordinates(coord.mLine + 1, GetCharacterColumn(coord.mLine + 1, static_cast(whitespaceSize)))); + u.mAdded = static_cast(aChar); + } + else + { + char buf[7]; + int e = ImTextCharToUtf8(buf, 7, aChar); + if (e > 0) + { + buf[e] = '\0'; + auto& line = mLines[coord.mLine]; + auto cindex = GetCharacterIndex(coord); + + if (mOverwrite && cindex < static_cast(line.size())) + { + auto d = UTF8CharLength(line[cindex].mChar); + + u.mRemovedStart = mState.mCursorPosition; + u.mRemovedEnd = Coordinates(coord.mLine, GetCharacterColumn(coord.mLine, cindex + d)); + + while (d-- > 0 && cindex < static_cast(line.size())) + { + u.mRemoved += static_cast(line[cindex].mChar); + line.erase(line.begin() + cindex); + } + } + + for (auto p = buf; *p != '\0'; p++, ++cindex) + line.insert(line.begin() + cindex, Glyph(*p, PaletteIndex::Default)); + u.mAdded = buf; + + SetCursorPosition(Coordinates(coord.mLine, GetCharacterColumn(coord.mLine, cindex))); + } + else + return; + } + + mTextChanged = true; + + u.mAddedEnd = GetActualCursorCoordinates(); + u.mAfter = mState; + + AddUndo(u); + + Colorize(coord.mLine - 1, 3); + EnsureCursorVisible(); +} + +void TextEditor::SetReadOnly(bool aValue) +{ + mReadOnly = aValue; +} + +void TextEditor::SetColorizerEnable(bool aValue) +{ + mColorizerEnabled = aValue; +} + +void TextEditor::SetCursorPosition(const Coordinates& aPosition) +{ + if (mState.mCursorPosition != aPosition) + { + mState.mCursorPosition = aPosition; + mCursorPositionChanged = true; + EnsureCursorVisible(); + } +} + +void TextEditor::SetSelectionStart(const Coordinates& aPosition) +{ + mState.mSelectionStart = SanitizeCoordinates(aPosition); + if (mState.mSelectionStart > mState.mSelectionEnd) + std::swap(mState.mSelectionStart, mState.mSelectionEnd); +} + +void TextEditor::SetSelectionEnd(const Coordinates& aPosition) +{ + mState.mSelectionEnd = SanitizeCoordinates(aPosition); + if (mState.mSelectionStart > mState.mSelectionEnd) + std::swap(mState.mSelectionStart, mState.mSelectionEnd); +} + +void TextEditor::SetSelection(const Coordinates& aStart, const Coordinates& aEnd, SelectionMode aMode) +{ + auto oldSelStart = mState.mSelectionStart; + auto oldSelEnd = mState.mSelectionEnd; + + mState.mSelectionStart = SanitizeCoordinates(aStart); + mState.mSelectionEnd = SanitizeCoordinates(aEnd); + if (mState.mSelectionStart > mState.mSelectionEnd) + std::swap(mState.mSelectionStart, mState.mSelectionEnd); + + switch (aMode) + { + case SelectionMode::Normal: + break; + case SelectionMode::Word: { + mState.mSelectionStart = FindWordStart(mState.mSelectionStart); + if (!IsOnWordBoundary(mState.mSelectionEnd)) + mState.mSelectionEnd = FindWordEnd(FindWordStart(mState.mSelectionEnd)); + break; + } + case SelectionMode::Line: { + const auto lineNo = mState.mSelectionEnd.mLine; + mState.mSelectionStart = Coordinates(mState.mSelectionStart.mLine, 0); + mState.mSelectionEnd = Coordinates(lineNo, GetLineMaxColumn(lineNo)); + break; + } + default: + break; + } + + if (mState.mSelectionStart != oldSelStart || + mState.mSelectionEnd != oldSelEnd) + mCursorPositionChanged = true; +} + +void TextEditor::SetTabSize(int aValue) +{ + mTabSize = std::max(0, std::min(32, aValue)); +} + +void TextEditor::InsertText(const std::string& aValue) +{ + InsertText(aValue.c_str()); +} + +void TextEditor::InsertText(const char* aValue) +{ + if (aValue == nullptr) + return; + + auto pos = GetActualCursorCoordinates(); + auto start = std::min(pos, mState.mSelectionStart); + int totalLines = pos.mLine - start.mLine; + + totalLines += InsertTextAt(pos, aValue); + + SetSelection(pos, pos); + SetCursorPosition(pos); + Colorize(start.mLine - 1, totalLines + 2); +} + +void TextEditor::DeleteSelection() +{ + assert(mState.mSelectionEnd >= mState.mSelectionStart); + + if (mState.mSelectionEnd == mState.mSelectionStart) + return; + + DeleteRange(mState.mSelectionStart, mState.mSelectionEnd); + + SetSelection(mState.mSelectionStart, mState.mSelectionStart); + SetCursorPosition(mState.mSelectionStart); + Colorize(mState.mSelectionStart.mLine, 1); +} + +void TextEditor::MoveUp(int aAmount, bool aSelect) +{ + auto oldPos = mState.mCursorPosition; + mState.mCursorPosition.mLine = std::max(0, mState.mCursorPosition.mLine - aAmount); + if (oldPos != mState.mCursorPosition) + { + if (aSelect) + { + if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else if (oldPos == mInteractiveEnd) + mInteractiveEnd = mState.mCursorPosition; + else + { + mInteractiveStart = mState.mCursorPosition; + mInteractiveEnd = oldPos; + } + } + else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd); + + EnsureCursorVisible(); + } +} + +void TextEditor::MoveDown(int aAmount, bool aSelect) +{ + assert(mState.mCursorPosition.mColumn >= 0); + auto oldPos = mState.mCursorPosition; + mState.mCursorPosition.mLine = std::max(0, std::min((int) mLines.size() - 1, mState.mCursorPosition.mLine + aAmount)); + + if (mState.mCursorPosition != oldPos) + { + if (aSelect) + { + if (oldPos == mInteractiveEnd) + mInteractiveEnd = mState.mCursorPosition; + else if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else + { + mInteractiveStart = oldPos; + mInteractiveEnd = mState.mCursorPosition; + } + } + else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd); + + EnsureCursorVisible(); + } +} + +static bool IsUTFSequence(char c) +{ + return (c & 0xC0) == 0x80; +} + +void TextEditor::MoveLeft(int aAmount, bool aSelect, bool aWordMode) +{ + if (mLines.empty()) + return; + + auto oldPos = mState.mCursorPosition; + mState.mCursorPosition = GetActualCursorCoordinates(); + auto line = mState.mCursorPosition.mLine; + auto cindex = GetCharacterIndex(mState.mCursorPosition); + + while (aAmount-- > 0) + { + if (cindex == 0) + { + if (line > 0) + { + --line; + if ((int) mLines.size() > line) + cindex = (int) mLines[line].size(); + else + cindex = 0; + } + } + else + { + --cindex; + if (cindex > 0) + { + if (static_cast(mLines.size()) > line) + { + while (cindex > 0 && IsUTFSequence(static_cast(mLines[line][cindex].mChar))) + --cindex; + } + } + } + + mState.mCursorPosition = Coordinates(line, GetCharacterColumn(line, cindex)); + if (aWordMode) + { + mState.mCursorPosition = FindWordStart(mState.mCursorPosition); + cindex = GetCharacterIndex(mState.mCursorPosition); + } + } + + mState.mCursorPosition = Coordinates(line, GetCharacterColumn(line, cindex)); + + assert(mState.mCursorPosition.mColumn >= 0); + if (aSelect) + { + if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else if (oldPos == mInteractiveEnd) + mInteractiveEnd = mState.mCursorPosition; + else + { + mInteractiveStart = mState.mCursorPosition; + mInteractiveEnd = oldPos; + } + } + else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd, aSelect && aWordMode ? SelectionMode::Word : SelectionMode::Normal); + + EnsureCursorVisible(); +} + +void TextEditor::MoveRight(int aAmount, bool aSelect, bool aWordMode) +{ + auto oldPos = mState.mCursorPosition; + + if (mLines.empty() || oldPos.mLine >= mLines.size()) + return; + + auto cindex = GetCharacterIndex(mState.mCursorPosition); + while (aAmount-- > 0) + { + auto lindex = mState.mCursorPosition.mLine; + auto& line = mLines[lindex]; + + if (cindex >= line.size()) + { + if (mState.mCursorPosition.mLine < mLines.size() - 1) + { + mState.mCursorPosition.mLine = std::max(0, std::min((int) mLines.size() - 1, mState.mCursorPosition.mLine + 1)); + mState.mCursorPosition.mColumn = 0; + } + else + return; + } + else + { + cindex += UTF8CharLength(line[cindex].mChar); + mState.mCursorPosition = Coordinates(lindex, GetCharacterColumn(lindex, cindex)); + if (aWordMode) + mState.mCursorPosition = FindNextWord(mState.mCursorPosition); + } + } + + if (aSelect) + { + if (oldPos == mInteractiveEnd) + mInteractiveEnd = SanitizeCoordinates(mState.mCursorPosition); + else if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else + { + mInteractiveStart = oldPos; + mInteractiveEnd = mState.mCursorPosition; + } + } + else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd, aSelect && aWordMode ? SelectionMode::Word : SelectionMode::Normal); + + EnsureCursorVisible(); +} + +void TextEditor::MoveTop(bool aSelect) +{ + auto oldPos = mState.mCursorPosition; + SetCursorPosition(Coordinates(0, 0)); + + if (mState.mCursorPosition != oldPos) + { + if (aSelect) + { + mInteractiveEnd = oldPos; + mInteractiveStart = mState.mCursorPosition; + } + else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd); + } +} + +void TextEditor::TextEditor::MoveBottom(bool aSelect) +{ + auto oldPos = GetCursorPosition(); + auto newPos = Coordinates((int) mLines.size() - 1, 0); + SetCursorPosition(newPos); + if (aSelect) + { + mInteractiveStart = oldPos; + mInteractiveEnd = newPos; + } + else + mInteractiveStart = mInteractiveEnd = newPos; + SetSelection(mInteractiveStart, mInteractiveEnd); +} + +void TextEditor::MoveHome(bool aSelect) +{ + auto oldPos = mState.mCursorPosition; + SetCursorPosition(Coordinates(mState.mCursorPosition.mLine, 0)); + + if (mState.mCursorPosition != oldPos) + { + if (aSelect) + { + if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else if (oldPos == mInteractiveEnd) + mInteractiveEnd = mState.mCursorPosition; + else + { + mInteractiveStart = mState.mCursorPosition; + mInteractiveEnd = oldPos; + } + } + else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd); + } +} + +void TextEditor::MoveEnd(bool aSelect) +{ + auto oldPos = mState.mCursorPosition; + SetCursorPosition(Coordinates(mState.mCursorPosition.mLine, GetLineMaxColumn(oldPos.mLine))); + + if (mState.mCursorPosition != oldPos) + { + if (aSelect) + { + if (oldPos == mInteractiveEnd) + mInteractiveEnd = mState.mCursorPosition; + else if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else + { + mInteractiveStart = oldPos; + mInteractiveEnd = mState.mCursorPosition; + } + } + else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd); + } +} + +void TextEditor::Delete() +{ + assert(!mReadOnly); + + if (mLines.empty()) + return; + + UndoRecord u; + u.mBefore = mState; + + if (HasSelection()) + { + u.mRemoved = GetSelectedText(); + u.mRemovedStart = mState.mSelectionStart; + u.mRemovedEnd = mState.mSelectionEnd; + + DeleteSelection(); + } + else + { + auto pos = GetActualCursorCoordinates(); + SetCursorPosition(pos); + auto& line = mLines[pos.mLine]; + + if (pos.mColumn == GetLineMaxColumn(pos.mLine)) + { + if (pos.mLine == (int) mLines.size() - 1) + return; + + u.mRemoved = '\n'; + u.mRemovedStart = u.mRemovedEnd = GetActualCursorCoordinates(); + Advance(u.mRemovedEnd); + + auto& nextLine = mLines[pos.mLine + 1]; + line.insert(line.end(), nextLine.begin(), nextLine.end()); + RemoveLine(pos.mLine + 1); + } + else + { + auto cindex = GetCharacterIndex(pos); + u.mRemovedStart = u.mRemovedEnd = GetActualCursorCoordinates(); + u.mRemovedEnd.mColumn++; + u.mRemoved = GetText(u.mRemovedStart, u.mRemovedEnd); + + auto d = UTF8CharLength(line[cindex].mChar); + while (d-- > 0 && cindex < (int) line.size()) + line.erase(line.begin() + cindex); + } + + mTextChanged = true; + + Colorize(pos.mLine, 1); + } + + u.mAfter = mState; + AddUndo(u); +} + +void TextEditor::Backspace() +{ + assert(!mReadOnly); + + if (mLines.empty()) + return; + + UndoRecord u; + u.mBefore = mState; + + if (HasSelection()) + { + u.mRemoved = GetSelectedText(); + u.mRemovedStart = mState.mSelectionStart; + u.mRemovedEnd = mState.mSelectionEnd; + + DeleteSelection(); + } + else + { + auto pos = GetActualCursorCoordinates(); + SetCursorPosition(pos); + + if (mState.mCursorPosition.mColumn == 0) + { + if (mState.mCursorPosition.mLine == 0) + return; + + u.mRemoved = '\n'; + u.mRemovedStart = u.mRemovedEnd = Coordinates(pos.mLine - 1, GetLineMaxColumn(pos.mLine - 1)); + Advance(u.mRemovedEnd); + + auto& line = mLines[mState.mCursorPosition.mLine]; + auto& prevLine = mLines[mState.mCursorPosition.mLine - 1]; + auto prevSize = GetLineMaxColumn(mState.mCursorPosition.mLine - 1); + prevLine.insert(prevLine.end(), line.begin(), line.end()); + + ErrorMarkers etmp; + for (auto& i : mErrorMarkers) + etmp.insert(ErrorMarkers::value_type(i.first - 1 == mState.mCursorPosition.mLine ? i.first - 1 : i.first, i.second)); + mErrorMarkers = std::move(etmp); + + RemoveLine(mState.mCursorPosition.mLine); + --mState.mCursorPosition.mLine; + mState.mCursorPosition.mColumn = prevSize; + } + else + { + auto& line = mLines[mState.mCursorPosition.mLine]; + auto cindex = GetCharacterIndex(pos) - 1; + auto cend = cindex + 1; + while (cindex > 0 && IsUTFSequence(static_cast(line[cindex].mChar))) + --cindex; + + //if (cindex > 0 && UTF8CharLength(line[cindex].mChar) > 1) + // --cindex; + + u.mRemovedStart = u.mRemovedEnd = GetActualCursorCoordinates(); + --u.mRemovedStart.mColumn; + --mState.mCursorPosition.mColumn; + + while (cindex < line.size() && cend-- > cindex) + { + u.mRemoved += static_cast(line[cindex].mChar); + line.erase(line.begin() + cindex); + } + } + + mTextChanged = true; + + EnsureCursorVisible(); + Colorize(mState.mCursorPosition.mLine, 1); + } + + u.mAfter = mState; + AddUndo(u); +} + +void TextEditor::SelectWordUnderCursor() +{ + auto c = GetCursorPosition(); + SetSelection(FindWordStart(c), FindWordEnd(c)); +} + +void TextEditor::SelectAll() +{ + SetSelection(Coordinates(0, 0), Coordinates((int) mLines.size(), 0)); +} + +bool TextEditor::HasSelection() const +{ + return mState.mSelectionEnd > mState.mSelectionStart; +} + +void TextEditor::Copy() +{ + if (HasSelection()) + { + ImGui::SetClipboardText(GetSelectedText().c_str()); + } + else + { + if (!mLines.empty()) + { + std::string str; + auto& line = mLines[GetActualCursorCoordinates().mLine]; + for (auto& g : line) + str.push_back(static_cast(g.mChar)); + ImGui::SetClipboardText(str.c_str()); + } + } +} + +void TextEditor::Cut() +{ + if (IsReadOnly()) + { + Copy(); + } + else + { + if (HasSelection()) + { + UndoRecord u; + u.mBefore = mState; + u.mRemoved = GetSelectedText(); + u.mRemovedStart = mState.mSelectionStart; + u.mRemovedEnd = mState.mSelectionEnd; + + Copy(); + DeleteSelection(); + + u.mAfter = mState; + AddUndo(u); + } + } +} + +void TextEditor::Paste() +{ + if (IsReadOnly()) + return; + + auto clipText = ImGui::GetClipboardText(); + if (clipText != nullptr && strlen(clipText) > 0) + { + UndoRecord u; + u.mBefore = mState; + + if (HasSelection()) + { + u.mRemoved = GetSelectedText(); + u.mRemovedStart = mState.mSelectionStart; + u.mRemovedEnd = mState.mSelectionEnd; + DeleteSelection(); + } + + u.mAdded = clipText; + u.mAddedStart = GetActualCursorCoordinates(); + + InsertText(clipText); + + u.mAddedEnd = GetActualCursorCoordinates(); + u.mAfter = mState; + AddUndo(u); + } +} + +bool TextEditor::CanUndo() const +{ + return !mReadOnly && mUndoIndex > 0; +} + +bool TextEditor::CanRedo() const +{ + return !mReadOnly && mUndoIndex < (int) mUndoBuffer.size(); +} + +void TextEditor::Undo(int aSteps) +{ + while (CanUndo() && aSteps-- > 0) + mUndoBuffer[--mUndoIndex].Undo(this); +} + +void TextEditor::Redo(int aSteps) +{ + while (CanRedo() && aSteps-- > 0) + mUndoBuffer[mUndoIndex++].Redo(this); +} + +const TextEditor::Palette& TextEditor::GetDarkPalette() +{ + const static Palette p = {{ + 0xff7f7f7f, // Default + 0xffd69c56, // Keyword + 0xff00ff00, // Number + 0xff7070e0, // String + 0xff70a0e0, // Char literal + 0xffffffff, // Punctuation + 0xff408080, // Preprocessor + 0xffaaaaaa, // Identifier + 0xff9bc64d, // Known identifier + 0xffc040a0, // Preproc identifier + 0xff206020, // Comment (single line) + 0xff406020, // Comment (multi line) + 0xff101010, // Background + 0xffe0e0e0, // Cursor + 0x80a06020, // Selection + 0x800020ff, // ErrorMarker + 0x40f08000, // Breakpoint + 0xff707000, // Line number + 0x40000000, // Current line fill + 0x40808080, // Current line fill (inactive) + 0x40a0a0a0, // Current line edge + }}; + return p; +} + +const TextEditor::Palette& TextEditor::GetLightPalette() +{ + const static Palette p = {{ + 0xff7f7f7f, // None + 0xffff0c06, // Keyword + 0xff008000, // Number + 0xff2020a0, // String + 0xff304070, // Char literal + 0xff000000, // Punctuation + 0xff406060, // Preprocessor + 0xff404040, // Identifier + 0xff606010, // Known identifier + 0xffc040a0, // Preproc identifier + 0xff205020, // Comment (single line) + 0xff405020, // Comment (multi line) + 0xffffffff, // Background + 0xff000000, // Cursor + 0x80600000, // Selection + 0xa00010ff, // ErrorMarker + 0x80f08000, // Breakpoint + 0xff505000, // Line number + 0x40000000, // Current line fill + 0x40808080, // Current line fill (inactive) + 0x40000000, // Current line edge + }}; + return p; +} + +const TextEditor::Palette& TextEditor::GetRetroBluePalette() +{ + const static Palette p = {{ + 0xff00ffff, // None + 0xffffff00, // Keyword + 0xff00ff00, // Number + 0xff808000, // String + 0xff808000, // Char literal + 0xffffffff, // Punctuation + 0xff008000, // Preprocessor + 0xff00ffff, // Identifier + 0xffffffff, // Known identifier + 0xffff00ff, // Preproc identifier + 0xff808080, // Comment (single line) + 0xff404040, // Comment (multi line) + 0xff800000, // Background + 0xff0080ff, // Cursor + 0x80ffff00, // Selection + 0xa00000ff, // ErrorMarker + 0x80ff8000, // Breakpoint + 0xff808000, // Line number + 0x40000000, // Current line fill + 0x40808080, // Current line fill (inactive) + 0x40000000, // Current line edge + }}; + return p; +} + + +std::string TextEditor::GetText() const +{ + return GetText(Coordinates(), Coordinates((int) mLines.size(), 0)); +} + +std::vector TextEditor::GetTextLines() const +{ + std::vector result; + + result.reserve(mLines.size()); + + for (auto& line : mLines) + { + std::string text; + + text.resize(line.size()); + + for (size_t i = 0; i < line.size(); ++i) + text[i] = static_cast(line[i].mChar); + + result.emplace_back(std::move(text)); + } + + return result; +} + +std::string TextEditor::GetSelectedText() const +{ + return GetText(mState.mSelectionStart, mState.mSelectionEnd); +} + +std::string TextEditor::GetCurrentLineText() const +{ + auto lineLength = GetLineMaxColumn(mState.mCursorPosition.mLine); + return GetText( + Coordinates(mState.mCursorPosition.mLine, 0), + Coordinates(mState.mCursorPosition.mLine, lineLength)); +} + +void TextEditor::ProcessInputs() +{ +} + +void TextEditor::Colorize(int aFromLine, int aLines) +{ + int toLine = aLines == -1 ? (int) mLines.size() : std::min((int) mLines.size(), aFromLine + aLines); + mColorRangeMin = std::min(mColorRangeMin, aFromLine); + mColorRangeMax = std::max(mColorRangeMax, toLine); + mColorRangeMin = std::max(0, mColorRangeMin); + mColorRangeMax = std::max(mColorRangeMin, mColorRangeMax); + mCheckComments = true; +} + +void TextEditor::ColorizeRange(int aFromLine, int aToLine) +{ + if (mLines.empty() || aFromLine >= aToLine) + return; + + std::string buffer; + std::cmatch results; + std::string id; + + int endLine = std::max(0, std::min((int) mLines.size(), aToLine)); + for (int i = aFromLine; i < endLine; ++i) + { + auto& line = mLines[i]; + + if (line.empty()) + continue; + + buffer.resize(line.size()); + for (size_t j = 0; j < line.size(); ++j) + { + auto& col = line[j]; + buffer[j] = static_cast(col.mChar); + col.mColorIndex = PaletteIndex::Default; + } + + const char* bufferBegin = &buffer.front(); + const char* bufferEnd = bufferBegin + buffer.size(); + + auto last = bufferEnd; + + for (auto first = bufferBegin; first != last;) + { + const char* token_begin = nullptr; + const char* token_end = nullptr; + PaletteIndex token_color = PaletteIndex::Default; + + bool hasTokenizeResult = false; + + if (mLanguageDefinition.mTokenize != nullptr) + { + if (mLanguageDefinition.mTokenize(first, last, token_begin, token_end, token_color)) + hasTokenizeResult = true; + } + + if (hasTokenizeResult == false) + { + // todo : remove + //printf("using regex for %.*s\n", first + 10 < last ? 10 : int(last - first), first); + + for (auto& p : mRegexList) + { + if (std::regex_search(first, last, results, p.first, std::regex_constants::match_continuous)) + { + hasTokenizeResult = true; + + auto& v = *results.begin(); + token_begin = v.first; + token_end = v.second; + token_color = p.second; + break; + } + } + } + + if (hasTokenizeResult == false) + { + first++; + } + else + { + const size_t token_length = token_end - token_begin; + + if (token_color == PaletteIndex::Identifier) + { + id.assign(token_begin, token_end); + + // todo : allmost all language definitions use lower case to specify keywords, so shouldn't this use ::tolower ? + if (!mLanguageDefinition.mCaseSensitive) + std::transform(id.begin(), id.end(), id.begin(), ::toupper); + + if (!line[first - bufferBegin].mPreprocessor) + { + if (mLanguageDefinition.mKeywords.count(id) != 0) + token_color = PaletteIndex::Keyword; + else if (mLanguageDefinition.mIdentifiers.count(id) != 0) + token_color = PaletteIndex::KnownIdentifier; + else if (mLanguageDefinition.mPreprocIdentifiers.count(id) != 0) + token_color = PaletteIndex::PreprocIdentifier; + } + else + { + if (mLanguageDefinition.mPreprocIdentifiers.count(id) != 0) + token_color = PaletteIndex::PreprocIdentifier; + } + } + + for (size_t j = 0; j < token_length; ++j) + line[(token_begin - bufferBegin) + j].mColorIndex = token_color; + + first = token_end; + } + } + } +} + +void TextEditor::ColorizeInternal() +{ + if (mLines.empty() || !mColorizerEnabled) + return; + + if (mCheckComments) + { + auto endLine = mLines.size(); + auto endIndex = 0; + auto commentStartLine = endLine; + auto commentStartIndex = endIndex; + auto withinString = false; + auto withinSingleLineComment = false; + auto withinPreproc = false; + auto firstChar = true; // there is no other non-whitespace characters in the line before + auto concatenate = false; // '\' on the very end of the line + auto currentLine = 0; + auto currentIndex = 0; + while (currentLine < endLine || currentIndex < endIndex) + { + auto& line = mLines[currentLine]; + + if (currentIndex == 0 && !concatenate) + { + withinSingleLineComment = false; + withinPreproc = false; + firstChar = true; + } + + concatenate = false; + + if (!line.empty()) + { + auto& g = line[currentIndex]; + auto c = g.mChar; + + if (c != mLanguageDefinition.mPreprocChar && !isspace(c)) + firstChar = false; + + if (currentIndex == (int) line.size() - 1 && line[line.size() - 1].mChar == '\\') + concatenate = true; + + bool inComment = (commentStartLine < currentLine || (commentStartLine == currentLine && commentStartIndex <= currentIndex)); + + if (withinString) + { + line[currentIndex].mMultiLineComment = inComment; + + if (c == '\"') + { + if (currentIndex + 1 < (int) line.size() && line[currentIndex + 1].mChar == '\"') + { + currentIndex += 1; + if (currentIndex < (int) line.size()) + line[currentIndex].mMultiLineComment = inComment; + } + else + withinString = false; + } + else if (c == '\\') + { + currentIndex += 1; + if (currentIndex < (int) line.size()) + line[currentIndex].mMultiLineComment = inComment; + } + } + else + { + if (firstChar && c == mLanguageDefinition.mPreprocChar) + withinPreproc = true; + + if (c == '\"') + { + withinString = true; + line[currentIndex].mMultiLineComment = inComment; + } + else + { + auto pred = [](const char& a, const Glyph& b) { return a == b.mChar; }; + auto from = line.begin() + currentIndex; + auto& startStr = mLanguageDefinition.mCommentStart; + auto& singleStartStr = mLanguageDefinition.mSingleLineComment; + + if (!singleStartStr.empty() && + currentIndex + singleStartStr.size() <= line.size() && + equals(singleStartStr.begin(), singleStartStr.end(), from, from + static_cast(singleStartStr.size()), pred)) + { + withinSingleLineComment = true; + } + else if (!withinSingleLineComment && currentIndex + startStr.size() <= line.size() && + equals(startStr.begin(), startStr.end(), from, from + static_cast(startStr.size()), pred)) + { + commentStartLine = currentLine; + commentStartIndex = currentIndex; + } + + inComment = inComment = (commentStartLine < currentLine || (commentStartLine == currentLine && commentStartIndex <= currentIndex)); + + line[currentIndex].mMultiLineComment = inComment; + line[currentIndex].mComment = withinSingleLineComment; + + auto& endStr = mLanguageDefinition.mCommentEnd; + if (currentIndex + 1 >= static_cast(endStr.size()) && + equals(endStr.begin(), endStr.end(), from + 1 - static_cast(endStr.size()), from + 1, pred)) + { + commentStartIndex = endIndex; + commentStartLine = endLine; + } + } + } + line[currentIndex].mPreprocessor = withinPreproc; + currentIndex += UTF8CharLength(c); + if (currentIndex >= static_cast(line.size())) + { + currentIndex = 0; + ++currentLine; + } + } + else + { + currentIndex = 0; + ++currentLine; + } + } + mCheckComments = false; + } + + if (mColorRangeMin < mColorRangeMax) + { + const int increment = (mLanguageDefinition.mTokenize == nullptr) ? 10 : 10000; + const int to = std::min(mColorRangeMin + increment, mColorRangeMax); + ColorizeRange(mColorRangeMin, to); + mColorRangeMin = to; + + if (mColorRangeMax == mColorRangeMin) + { + mColorRangeMin = std::numeric_limits::max(); + mColorRangeMax = 0; + } + return; + } +} + +float TextEditor::TextDistanceToLineStart(const Coordinates& aFrom) const +{ + auto& line = mLines[aFrom.mLine]; + float distance = 0.0f; + float spaceSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, " ", nullptr, nullptr).x; + int colIndex = GetCharacterIndex(aFrom); + for (size_t it = 0u; it < line.size() && it < colIndex;) + { + if (line[it].mChar == '\t') + { + distance = (1.0f + std::floor((1.0f + distance) / (float(mTabSize) * spaceSize))) * (float(mTabSize) * spaceSize); + ++it; + } + else + { + auto d = UTF8CharLength(line[it].mChar); + char tempCString[7]; + int i = 0; + for (; i < 6 && d-- > 0 && it < (int) line.size(); i++, it++) + tempCString[i] = static_cast(line[it].mChar); + + tempCString[i] = '\0'; + distance += ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, tempCString, nullptr, nullptr).x; + } + } + + return distance; +} + +void TextEditor::EnsureCursorVisible() +{ + if (!mWithinRender) + { + mScrollToCursor = true; + return; + } + + float scrollX = ImGui::GetScrollX(); + float scrollY = ImGui::GetScrollY(); + + auto height = ImGui::GetWindowHeight(); + auto width = ImGui::GetWindowWidth(); + + auto top = 1 + static_cast(std::ceil(scrollY / mCharAdvance.y)); + auto bottom = static_cast(std::ceil((scrollY + height) / mCharAdvance.y)); + + auto left = static_cast(std::ceil(scrollX / mCharAdvance.x)); + auto right = static_cast(std::ceil((scrollX + width) / mCharAdvance.x)); + + auto pos = GetActualCursorCoordinates(); + auto len = TextDistanceToLineStart(pos); + + if (pos.mLine < top) + ImGui::SetScrollY(std::max(0.0f, static_cast(pos.mLine - 1) * mCharAdvance.y)); + if (pos.mLine > bottom - 4) + ImGui::SetScrollY(std::max(0.0f, static_cast(pos.mLine + 4) * mCharAdvance.y - height)); + if (len + mTextStart < static_cast(left) + 4) + ImGui::SetScrollX(std::max(0.0f, len + mTextStart - 4)); + if (len + mTextStart > static_cast(right) - 4) + ImGui::SetScrollX(std::max(0.0f, len + mTextStart + 4 - width)); +} + +int TextEditor::GetPageSize() const +{ + auto height = ImGui::GetWindowHeight() - 20.0f; + return static_cast(std::floor(height / mCharAdvance.y)); +} + +TextEditor::UndoRecord::UndoRecord( + std::string aAdded, + const TextEditor::Coordinates aAddedStart, + const TextEditor::Coordinates aAddedEnd, + std::string aRemoved, + const TextEditor::Coordinates aRemovedStart, + const TextEditor::Coordinates aRemovedEnd, + const TextEditor::EditorState& aBefore, + const TextEditor::EditorState& aAfter) + : mAdded(std::move(aAdded)) + , mAddedStart(aAddedStart) + , mAddedEnd(aAddedEnd) + , mRemoved(std::move(aRemoved)) + , mRemovedStart(aRemovedStart) + , mRemovedEnd(aRemovedEnd) + , mBefore(aBefore) + , mAfter(aAfter) +{ + assert(mAddedStart <= mAddedEnd); + assert(mRemovedStart <= mRemovedEnd); +} + +void TextEditor::UndoRecord::Undo(TextEditor* aEditor) +{ + if (!mAdded.empty()) + { + aEditor->DeleteRange(mAddedStart, mAddedEnd); + aEditor->Colorize(mAddedStart.mLine - 1, mAddedEnd.mLine - mAddedStart.mLine + 2); + } + + if (!mRemoved.empty()) + { + auto start = mRemovedStart; + aEditor->InsertTextAt(start, mRemoved.c_str()); + aEditor->Colorize(mRemovedStart.mLine - 1, mRemovedEnd.mLine - mRemovedStart.mLine + 2); + } + + aEditor->mState = mBefore; + aEditor->EnsureCursorVisible(); +} + +void TextEditor::UndoRecord::Redo(TextEditor* aEditor) +{ + if (!mRemoved.empty()) + { + aEditor->DeleteRange(mRemovedStart, mRemovedEnd); + aEditor->Colorize(mRemovedStart.mLine - 1, mRemovedEnd.mLine - mRemovedStart.mLine + 1); + } + + if (!mAdded.empty()) + { + auto start = mAddedStart; + aEditor->InsertTextAt(start, mAdded.c_str()); + aEditor->Colorize(mAddedStart.mLine - 1, mAddedEnd.mLine - mAddedStart.mLine + 1); + } + + aEditor->mState = mAfter; + aEditor->EnsureCursorVisible(); +} + +static bool TokenizeCStyleString(const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end) +{ + const char* p = in_begin; + + if (*p == '"') + { + p++; + + while (p < in_end) + { + // handle end of string + if (*p == '"') + { + out_begin = in_begin; + out_end = p + 1; + return true; + } + + // handle escape character for " + if (*p == '\\' && p + 1 < in_end && p[1] == '"') + p++; + + p++; + } + } + + return false; +} + +static bool TokenizeCStyleCharacterLiteral(const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end) +{ + const char* p = in_begin; + + if (*p == '\'') + { + p++; + + // handle escape characters + if (p < in_end && *p == '\\') + p++; + + if (p < in_end) + p++; + + // handle end of character literal + if (p < in_end && *p == '\'') + { + out_begin = in_begin; + out_end = p + 1; + return true; + } + } + + return false; +} + +static bool TokenizeCStyleIdentifier(const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end) +{ + const char* p = in_begin; + + if ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || *p == '_') + { + p++; + + while ((p < in_end) && ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') || *p == '_')) + p++; + + out_begin = in_begin; + out_end = p; + return true; + } + + return false; +} + +static bool TokenizeCStyleNumber(const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end) +{ + const char* p = in_begin; + + const bool startsWithNumber = *p >= '0' && *p <= '9'; + + if (*p != '+' && *p != '-' && !startsWithNumber) + return false; + + p++; + + bool hasNumber = startsWithNumber; + + while (p < in_end && (*p >= '0' && *p <= '9')) + { + hasNumber = true; + + p++; + } + + if (hasNumber == false) + return false; + + bool isFloat = false; + bool isHex = false; + bool isBinary = false; + + if (p < in_end) + { + if (*p == '.') + { + isFloat = true; + + p++; + + while (p < in_end && (*p >= '0' && *p <= '9')) + p++; + } + else if (*p == 'x' || *p == 'X') + { + // hex formatted integer of the type 0xef80 + + isHex = true; + + p++; + + while (p < in_end && ((*p >= '0' && *p <= '9') || (*p >= 'a' && *p <= 'f') || (*p >= 'A' && *p <= 'F'))) + p++; + } + else if (*p == 'b' || *p == 'B') + { + // binary formatted integer of the type 0b01011101 + + isBinary = true; + + p++; + + while (p < in_end && (*p >= '0' && *p <= '1')) + p++; + } + } + + if (isHex == false && isBinary == false) + { + // floating point exponent + if (p < in_end && (*p == 'e' || *p == 'E')) + { + isFloat = true; + + p++; + + if (p < in_end && (*p == '+' || *p == '-')) + p++; + + bool hasDigits = false; + + while (p < in_end && (*p >= '0' && *p <= '9')) + { + hasDigits = true; + + p++; + } + + if (hasDigits == false) + return false; + } + + // single precision floating point type + if (p < in_end && *p == 'f') + p++; + } + + if (isFloat == false) + { + // integer size type + while (p < in_end && (*p == 'u' || *p == 'U' || *p == 'l' || *p == 'L')) + p++; + } + + out_begin = in_begin; + out_end = p; + return true; +} + +static bool TokenizeCStylePunctuation(const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end) +{ + (void) in_end; + + switch (*in_begin) + { + case '[': + case ']': + case '{': + case '}': + case '!': + case '%': + case '^': + case '&': + case '*': + case '(': + case ')': + case '-': + case '+': + case '=': + case '~': + case '|': + case '<': + case '>': + case '?': + case ':': + case '/': + case ';': + case ',': + case '.': + out_begin = in_begin; + out_end = in_begin + 1; + return true; + default: + return false; + } +} + +static bool TokenizeMilkdropConstant(const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end) +{ + const char* p = in_begin; + + if (*p != '$') + { + return false; + } + + p++; + + // Ordinal constant: $'n' + if (*p == '\'') + { + if (p + 2 < in_end && *(p + 2) == '\'') + { + out_begin = in_begin; + out_end = p + 3; + return true; + } + } + + // PI or PHI constant: $PI and $PHI + if (*p == 'p' || *p == 'P' && p + 1 < in_end) + { + if (*(p + 1) == 'i' || *(p + 1) == 'I') + { + out_begin = in_begin; + out_end = p + 2; + return true; + } + if ((*(p + 1) == 'h' || *(p + 1) == 'H') && p + 2 < in_end && (*(p + 2) == 'i' || *(p + 2) == 'I')) + { + out_begin = in_begin; + out_end = p + 3; + return true; + } + } + + // E constant: $E + if (*p == 'e' || *p == 'E') + { + out_begin = in_begin; + out_end = p + 1; + return true; + } + + // Only hex constant left to check: $XAABBCCDD + if (*p != 'x' & *p != 'X') + { + return false; + } + + p++; + + while (p < in_end) + { + // handle end of hex constant + static const char hexChars[] = "0123456789aAbBcCdDeEfF"; + + bool digitValid = false; + for (auto hexDigit : hexChars) + { + if (*p == hexDigit) + { + digitValid = true; + break; + } + } + + if (!digitValid) + { + break; + } + + p++; + } + + // Only found $X, but no other valid digits -> don't recognize as constant. + if (p == in_begin + 2) + { + return false; + } + + out_begin = in_begin; + out_end = p; + return true; +} + +static bool TokenizeMilkdropNumber(const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end) +{ + const char* p = in_begin; + + const bool startsWithNumber = *p >= '0' && *p <= '9'; + + if (*p != '+' && *p != '-' && !startsWithNumber) + return false; + + p++; + + bool hasNumber = startsWithNumber; + + while (p < in_end && (*p >= '0' && *p <= '9')) + { + hasNumber = true; + + p++; + } + + if (hasNumber == false) + return false; + + + if (p < in_end) + { + if (*p == '.') + { + p++; + + while (p < in_end && (*p >= '0' && *p <= '9')) + p++; + } + } + + // floating point exponent + if (p < in_end && (*p == 'e' || *p == 'E')) + { + p++; + + if (p < in_end && (*p == '+' || *p == '-')) + p++; + + bool hasDigits = false; + + while (p < in_end && (*p >= '0' && *p <= '9')) + { + hasDigits = true; + + p++; + } + + if (hasDigits == false) + return false; + } + + out_begin = in_begin; + out_end = p; + return true; +} + +static bool TokenizeMilkdropPunctuation(const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end) +{ + (void) in_end; + + switch (*in_begin) + { + case '[': + case ']': + case '!': + case '%': + case '^': + case '&': + case '*': + case '(': + case ')': + case '-': + case '+': + case '=': + case '|': + case '<': + case '>': + case '?': + case ':': + case '/': + case ';': + case ',': + out_begin = in_begin; + out_end = in_begin + 1; + return true; + default: + return false; + } +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::CPlusPlus() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const cppKeywords[] = { + "alignas", "alignof", "and", "and_eq", "asm", "atomic_cancel", "atomic_commit", "atomic_noexcept", "auto", "bitand", "bitor", "bool", "break", "case", "catch", "char", "char16_t", "char32_t", "class", + "compl", "concept", "const", "constexpr", "const_cast", "continue", "decltype", "default", "delete", "do", "double", "dynamic_cast", "else", "enum", "explicit", "export", "extern", "false", "float", + "for", "friend", "goto", "if", "import", "inline", "int", "long", "module", "mutable", "namespace", "new", "noexcept", "not", "not_eq", "nullptr", "operator", "or", "or_eq", "private", "protected", "public", + "register", "reinterpret_cast", "requires", "return", "short", "signed", "sizeof", "static", "static_assert", "static_cast", "struct", "switch", "synchronized", "template", "this", "thread_local", + "throw", "true", "try", "typedef", "typeid", "typename", "union", "unsigned", "using", "virtual", "void", "volatile", "wchar_t", "while", "xor", "xor_eq"}; + for (auto& k : cppKeywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "abort", "abs", "acos", "asin", "atan", "atexit", "atof", "atoi", "atol", "ceil", "clock", "cosh", "ctime", "div", "exit", "fabs", "floor", "fmod", "getchar", "getenv", "isalnum", "isalpha", "isdigit", "isgraph", + "ispunct", "isspace", "isupper", "kbhit", "log10", "log2", "log", "memcmp", "modf", "pow", "printf", "sprintf", "snprintf", "putchar", "putenv", "puts", "rand", "remove", "rename", "sinh", "sqrt", "srand", "strcat", "strcmp", "strerror", "time", "tolower", "toupper", + "std", "string", "vector", "map", "unordered_map", "set", "unordered_set", "min", "max"}; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenize = [](const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end, PaletteIndex& paletteIndex) -> bool { + paletteIndex = PaletteIndex::Max; + + while (in_begin < in_end && isascii(*in_begin) && isblank(*in_begin)) + in_begin++; + + if (in_begin == in_end) + { + out_begin = in_end; + out_end = in_end; + paletteIndex = PaletteIndex::Default; + } + else if (TokenizeCStyleString(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::String; + else if (TokenizeCStyleCharacterLiteral(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::CharLiteral; + else if (TokenizeCStyleIdentifier(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Identifier; + else if (TokenizeCStyleNumber(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Number; + else if (TokenizeCStylePunctuation(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Punctuation; + + return paletteIndex != PaletteIndex::Max; + }; + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = true; + + langDef.mName = "C++"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::MilkdropExpression() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const nseel2Keywords[] = { + "loop", "while", "bnot", "equal", "below", "above", "assign", "sin", "cos", "tan", "asin", "acos", "atan", "atan2", "sqr", "sqrt", "pow", "exp", "_neg", "log", "log10", "abs", "min", + "max", "sign", "rand", "floor", "int", "ceil", "invsqrt", "sigmoid", "band", "bor", "exec2", "exec3", "_mem", "megabuf", "gmem", "gmegabuf", "freembuf", "memcpy", "memset"}; + for (auto& k : nseel2Keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "_if", "_and", "_or", "_not", "_equal", "_noteq", "_below", "_above", "_beleq", "_aboeq", "_set", "_add", "_sub", "_mul", "_div", "_mod", "_mulop", "_divop", "_orop", "_andop", "_subop", + "_modop", "_powop", "_neg", "_gmem"}; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Internal function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenize = [](const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end, PaletteIndex& paletteIndex) -> bool { + paletteIndex = PaletteIndex::Max; + + while (in_begin < in_end && isascii(*in_begin) && isblank(*in_begin)) + in_begin++; + + if (in_begin == in_end) + { + out_begin = in_end; + out_end = in_end; + paletteIndex = PaletteIndex::Default; + } + else if (TokenizeMilkdropConstant(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::CharLiteral; + else if (TokenizeCStyleIdentifier(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Identifier; + else if (TokenizeMilkdropNumber(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Number; + else if (TokenizeCStylePunctuation(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Punctuation; + + return paletteIndex != PaletteIndex::Max; + }; + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = true; + + langDef.mName = "Milkdrop Expression"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::HLSL() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "AppendStructuredBuffer", + "asm", + "asm_fragment", + "BlendState", + "bool", + "break", + "Buffer", + "ByteAddressBuffer", + "case", + "cbuffer", + "centroid", + "class", + "column_major", + "compile", + "compile_fragment", + "CompileShader", + "const", + "continue", + "ComputeShader", + "ConsumeStructuredBuffer", + "default", + "DepthStencilState", + "DepthStencilView", + "discard", + "do", + "double", + "DomainShader", + "dword", + "else", + "export", + "extern", + "false", + "float", + "for", + "fxgroup", + "GeometryShader", + "groupshared", + "half", + "Hullshader", + "if", + "in", + "inline", + "inout", + "InputPatch", + "int", + "interface", + "line", + "lineadj", + "linear", + "LineStream", + "matrix", + "min16float", + "min10float", + "min16int", + "min12int", + "min16uint", + "namespace", + "nointerpolation", + "noperspective", + "NULL", + "out", + "OutputPatch", + "packoffset", + "pass", + "pixelfragment", + "PixelShader", + "point", + "PointStream", + "precise", + "RasterizerState", + "RenderTargetView", + "return", + "register", + "row_major", + "RWBuffer", + "RWByteAddressBuffer", + "RWStructuredBuffer", + "RWTexture1D", + "RWTexture1DArray", + "RWTexture2D", + "RWTexture2DArray", + "RWTexture3D", + "sample", + "sampler", + "SamplerState", + "SamplerComparisonState", + "shared", + "snorm", + "stateblock", + "stateblock_state", + "static", + "string", + "struct", + "switch", + "StructuredBuffer", + "tbuffer", + "technique", + "technique10", + "technique11", + "texture", + "Texture1D", + "Texture1DArray", + "Texture2D", + "Texture2DArray", + "Texture2DMS", + "Texture2DMSArray", + "Texture3D", + "TextureCube", + "TextureCubeArray", + "true", + "typedef", + "triangle", + "triangleadj", + "TriangleStream", + "uint", + "uniform", + "unorm", + "unsigned", + "vector", + "vertexfragment", + "VertexShader", + "void", + "volatile", + "while", + "bool1", + "bool2", + "bool3", + "bool4", + "double1", + "double2", + "double3", + "double4", + "float1", + "float2", + "float3", + "float4", + "int1", + "int2", + "int3", + "int4", + "in", + "out", + "inout", + "uint1", + "uint2", + "uint3", + "uint4", + "dword1", + "dword2", + "dword3", + "dword4", + "half1", + "half2", + "half3", + "half4", + "float1x1", + "float2x1", + "float3x1", + "float4x1", + "float1x2", + "float2x2", + "float3x2", + "float4x2", + "float1x3", + "float2x3", + "float3x3", + "float4x3", + "float1x4", + "float2x4", + "float3x4", + "float4x4", + "half1x1", + "half2x1", + "half3x1", + "half4x1", + "half1x2", + "half2x2", + "half3x2", + "half4x2", + "half1x3", + "half2x3", + "half3x3", + "half4x3", + "half1x4", + "half2x4", + "half3x4", + "half4x4", + }; + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "abort", "abs", "acos", "all", "AllMemoryBarrier", "AllMemoryBarrierWithGroupSync", "any", "asdouble", "asfloat", "asin", "asint", "asint", "asuint", + "asuint", "atan", "atan2", "ceil", "CheckAccessFullyMapped", "clamp", "clip", "cos", "cosh", "countbits", "cross", "D3DCOLORtoUBYTE4", "ddx", + "ddx_coarse", "ddx_fine", "ddy", "ddy_coarse", "ddy_fine", "degrees", "determinant", "DeviceMemoryBarrier", "DeviceMemoryBarrierWithGroupSync", + "distance", "dot", "dst", "errorf", "EvaluateAttributeAtCentroid", "EvaluateAttributeAtSample", "EvaluateAttributeSnapped", "exp", "exp2", + "f16tof32", "f32tof16", "faceforward", "firstbithigh", "firstbitlow", "floor", "fma", "fmod", "frac", "frexp", "fwidth", "GetRenderTargetSampleCount", + "GetRenderTargetSamplePosition", "GroupMemoryBarrier", "GroupMemoryBarrierWithGroupSync", "InterlockedAdd", "InterlockedAnd", "InterlockedCompareExchange", + "InterlockedCompareStore", "InterlockedExchange", "InterlockedMax", "InterlockedMin", "InterlockedOr", "InterlockedXor", "isfinite", "isinf", "isnan", + "ldexp", "length", "lerp", "lit", "log", "log10", "log2", "mad", "max", "min", "modf", "msad4", "mul", "noise", "normalize", "pow", "printf", + "Process2DQuadTessFactorsAvg", "Process2DQuadTessFactorsMax", "Process2DQuadTessFactorsMin", "ProcessIsolineTessFactors", "ProcessQuadTessFactorsAvg", + "ProcessQuadTessFactorsMax", "ProcessQuadTessFactorsMin", "ProcessTriTessFactorsAvg", "ProcessTriTessFactorsMax", "ProcessTriTessFactorsMin", + "radians", "rcp", "reflect", "refract", "reversebits", "round", "rsqrt", "saturate", "sign", "sin", "sincos", "sinh", "smoothstep", "sqrt", "step", + "tan", "tanh", "tex1D", "tex1D", "tex1Dbias", "tex1Dgrad", "tex1Dlod", "tex1Dproj", "tex2D", "tex2D", "tex2Dbias", "tex2Dgrad", "tex2Dlod", "tex2Dproj", + "tex3D", "tex3D", "tex3Dbias", "tex3Dgrad", "tex3Dlod", "tex3Dproj", "texCUBE", "texCUBE", "texCUBEbias", "texCUBEgrad", "texCUBElod", "texCUBEproj", "transpose", "trunc"}; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back(std::make_pair("[ \\t]*#[ \\t]*[a-zA-Z_]+", PaletteIndex::Preprocessor)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"(L?\"(\\.|[^\"])*\")", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"(\'\\?[^\']\')", PaletteIndex::CharLiteral)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?[0-9]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[0-7]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[a-zA-Z_][a-zA-Z0-9_]*", PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"([\[\]\{\}\!\%\^\&\*\(\)\-\+\=\~\|\<\>\?\/\;\,\.])", PaletteIndex::Punctuation)); + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = true; + + langDef.mName = "HLSL"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::GLSL() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "auto", "break", "case", "char", "const", "continue", "default", "do", "double", "else", "enum", "extern", "float", "for", "goto", "if", "inline", "int", "long", "register", "restrict", "return", "short", + "signed", "sizeof", "static", "struct", "switch", "typedef", "union", "unsigned", "void", "volatile", "while", "_Alignas", "_Alignof", "_Atomic", "_Bool", "_Complex", "_Generic", "_Imaginary", + "_Noreturn", "_Static_assert", "_Thread_local"}; + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "abort", "abs", "acos", "asin", "atan", "atexit", "atof", "atoi", "atol", "ceil", "clock", "cosh", "ctime", "div", "exit", "fabs", "floor", "fmod", "getchar", "getenv", "isalnum", "isalpha", "isdigit", "isgraph", + "ispunct", "isspace", "isupper", "kbhit", "log10", "log2", "log", "memcmp", "modf", "pow", "putchar", "putenv", "puts", "rand", "remove", "rename", "sinh", "sqrt", "srand", "strcat", "strcmp", "strerror", "time", "tolower", "toupper"}; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back(std::make_pair("[ \\t]*#[ \\t]*[a-zA-Z_]+", PaletteIndex::Preprocessor)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"(L?\"(\\.|[^\"])*\")", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"(\'\\?[^\']\')", PaletteIndex::CharLiteral)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?[0-9]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[0-7]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[a-zA-Z_][a-zA-Z0-9_]*", PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"([\[\]\{\}\!\%\^\&\*\(\)\-\+\=\~\|\<\>\?\/\;\,\.])", PaletteIndex::Punctuation)); + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = true; + + langDef.mName = "GLSL"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::C() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "auto", "break", "case", "char", "const", "continue", "default", "do", "double", "else", "enum", "extern", "float", "for", "goto", "if", "inline", "int", "long", "register", "restrict", "return", "short", + "signed", "sizeof", "static", "struct", "switch", "typedef", "union", "unsigned", "void", "volatile", "while", "_Alignas", "_Alignof", "_Atomic", "_Bool", "_Complex", "_Generic", "_Imaginary", + "_Noreturn", "_Static_assert", "_Thread_local"}; + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "abort", "abs", "acos", "asin", "atan", "atexit", "atof", "atoi", "atol", "ceil", "clock", "cosh", "ctime", "div", "exit", "fabs", "floor", "fmod", "getchar", "getenv", "isalnum", "isalpha", "isdigit", "isgraph", + "ispunct", "isspace", "isupper", "kbhit", "log10", "log2", "log", "memcmp", "modf", "pow", "putchar", "putenv", "puts", "rand", "remove", "rename", "sinh", "sqrt", "srand", "strcat", "strcmp", "strerror", "time", "tolower", "toupper"}; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenize = [](const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end, PaletteIndex& paletteIndex) -> bool { + paletteIndex = PaletteIndex::Max; + + while (in_begin < in_end && isascii(*in_begin) && isblank(*in_begin)) + in_begin++; + + if (in_begin == in_end) + { + out_begin = in_end; + out_end = in_end; + paletteIndex = PaletteIndex::Default; + } + else if (TokenizeCStyleString(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::String; + else if (TokenizeCStyleCharacterLiteral(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::CharLiteral; + else if (TokenizeCStyleIdentifier(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Identifier; + else if (TokenizeCStyleNumber(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Number; + else if (TokenizeCStylePunctuation(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Punctuation; + + return paletteIndex != PaletteIndex::Max; + }; + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = true; + + langDef.mName = "C"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::SQL() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "ADD", "EXCEPT", "PERCENT", "ALL", "EXEC", "PLAN", "ALTER", "EXECUTE", "PRECISION", "AND", "EXISTS", "PRIMARY", "ANY", "EXIT", "PRINT", "AS", "FETCH", "PROC", "ASC", "FILE", "PROCEDURE", + "AUTHORIZATION", "FILLFACTOR", "PUBLIC", "BACKUP", "FOR", "RAISERROR", "BEGIN", "FOREIGN", "READ", "BETWEEN", "FREETEXT", "READTEXT", "BREAK", "FREETEXTTABLE", "RECONFIGURE", + "BROWSE", "FROM", "REFERENCES", "BULK", "FULL", "REPLICATION", "BY", "FUNCTION", "RESTORE", "CASCADE", "GOTO", "RESTRICT", "CASE", "GRANT", "RETURN", "CHECK", "GROUP", "REVOKE", + "CHECKPOINT", "HAVING", "RIGHT", "CLOSE", "HOLDLOCK", "ROLLBACK", "CLUSTERED", "IDENTITY", "ROWCOUNT", "COALESCE", "IDENTITY_INSERT", "ROWGUIDCOL", "COLLATE", "IDENTITYCOL", "RULE", + "COLUMN", "IF", "SAVE", "COMMIT", "IN", "SCHEMA", "COMPUTE", "INDEX", "SELECT", "CONSTRAINT", "INNER", "SESSION_USER", "CONTAINS", "INSERT", "SET", "CONTAINSTABLE", "INTERSECT", "SETUSER", + "CONTINUE", "INTO", "SHUTDOWN", "CONVERT", "IS", "SOME", "CREATE", "JOIN", "STATISTICS", "CROSS", "KEY", "SYSTEM_USER", "CURRENT", "KILL", "TABLE", "CURRENT_DATE", "LEFT", "TEXTSIZE", + "CURRENT_TIME", "LIKE", "THEN", "CURRENT_TIMESTAMP", "LINENO", "TO", "CURRENT_USER", "LOAD", "TOP", "CURSOR", "NATIONAL", "TRAN", "DATABASE", "NOCHECK", "TRANSACTION", + "DBCC", "NONCLUSTERED", "TRIGGER", "DEALLOCATE", "NOT", "TRUNCATE", "DECLARE", "NULL", "TSEQUAL", "DEFAULT", "NULLIF", "UNION", "DELETE", "OF", "UNIQUE", "DENY", "OFF", "UPDATE", + "DESC", "OFFSETS", "UPDATETEXT", "DISK", "ON", "USE", "DISTINCT", "OPEN", "USER", "DISTRIBUTED", "OPENDATASOURCE", "VALUES", "DOUBLE", "OPENQUERY", "VARYING", "DROP", "OPENROWSET", "VIEW", + "DUMMY", "OPENXML", "WAITFOR", "DUMP", "OPTION", "WHEN", "ELSE", "OR", "WHERE", "END", "ORDER", "WHILE", "ERRLVL", "OUTER", "WITH", "ESCAPE", "OVER", "WRITETEXT"}; + + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "ABS", "ACOS", "ADD_MONTHS", "ASCII", "ASCIISTR", "ASIN", "ATAN", "ATAN2", "AVG", "BFILENAME", "BIN_TO_NUM", "BITAND", "CARDINALITY", "CASE", "CAST", "CEIL", + "CHARTOROWID", "CHR", "COALESCE", "COMPOSE", "CONCAT", "CONVERT", "CORR", "COS", "COSH", "COUNT", "COVAR_POP", "COVAR_SAMP", "CUME_DIST", "CURRENT_DATE", + "CURRENT_TIMESTAMP", "DBTIMEZONE", "DECODE", "DECOMPOSE", "DENSE_RANK", "DUMP", "EMPTY_BLOB", "EMPTY_CLOB", "EXP", "EXTRACT", "FIRST_VALUE", "FLOOR", "FROM_TZ", "GREATEST", + "GROUP_ID", "HEXTORAW", "INITCAP", "INSTR", "INSTR2", "INSTR4", "INSTRB", "INSTRC", "LAG", "LAST_DAY", "LAST_VALUE", "LEAD", "LEAST", "LENGTH", "LENGTH2", "LENGTH4", + "LENGTHB", "LENGTHC", "LISTAGG", "LN", "LNNVL", "LOCALTIMESTAMP", "LOG", "LOWER", "LPAD", "LTRIM", "MAX", "MEDIAN", "MIN", "MOD", "MONTHS_BETWEEN", "NANVL", "NCHR", + "NEW_TIME", "NEXT_DAY", "NTH_VALUE", "NULLIF", "NUMTODSINTERVAL", "NUMTOYMINTERVAL", "NVL", "NVL2", "POWER", "RANK", "RAWTOHEX", "REGEXP_COUNT", "REGEXP_INSTR", + "REGEXP_REPLACE", "REGEXP_SUBSTR", "REMAINDER", "REPLACE", "ROUND", "ROWNUM", "RPAD", "RTRIM", "SESSIONTIMEZONE", "SIGN", "SIN", "SINH", + "SOUNDEX", "SQRT", "STDDEV", "SUBSTR", "SUM", "SYS_CONTEXT", "SYSDATE", "SYSTIMESTAMP", "TAN", "TANH", "TO_CHAR", "TO_CLOB", "TO_DATE", "TO_DSINTERVAL", "TO_LOB", + "TO_MULTI_BYTE", "TO_NCLOB", "TO_NUMBER", "TO_SINGLE_BYTE", "TO_TIMESTAMP", "TO_TIMESTAMP_TZ", "TO_YMINTERVAL", "TRANSLATE", "TRIM", "TRUNC", "TZ_OFFSET", "UID", "UPPER", + "USER", "USERENV", "VAR_POP", "VAR_SAMP", "VARIANCE", "VSIZE "}; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back(std::make_pair(R"(L?\"(\\.|[^\"])*\")", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"(\'[^\']*\')", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?[0-9]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[0-7]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[a-zA-Z_][a-zA-Z0-9_]*", PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"([\[\]\{\}\!\%\^\&\*\(\)\-\+\=\~\|\<\>\?\/\;\,\.])", PaletteIndex::Punctuation)); + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = false; + langDef.mAutoIndentation = false; + + langDef.mName = "SQL"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::AngelScript() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "and", "abstract", "auto", "bool", "break", "case", "cast", "class", "const", "continue", "default", "do", "double", "else", "enum", "false", "final", "float", "for", + "from", "funcdef", "function", "get", "if", "import", "in", "inout", "int", "interface", "int8", "int16", "int32", "int64", "is", "mixin", "namespace", "not", + "null", "or", "out", "override", "private", "protected", "return", "set", "shared", "super", "switch", "this ", "true", "typedef", "uint", "uint8", "uint16", "uint32", + "uint64", "void", "while", "xor"}; + + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "cos", "sin", "tab", "acos", "asin", "atan", "atan2", "cosh", "sinh", "tanh", "log", "log10", "pow", "sqrt", "abs", "ceil", "floor", "fraction", "closeTo", "fpFromIEEE", "fpToIEEE", + "complex", "opEquals", "opAddAssign", "opSubAssign", "opMulAssign", "opDivAssign", "opAdd", "opSub", "opMul", "opDiv"}; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back(std::make_pair(R"(L?\"(\\.|[^\"])*\")", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"(\'\\?[^\']\')", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?[0-9]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[0-7]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[a-zA-Z_][a-zA-Z0-9_]*", PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"([\[\]\{\}\!\%\^\&\*\(\)\-\+\=\~\|\<\>\?\/\;\,\.])", PaletteIndex::Punctuation)); + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = true; + + langDef.mName = "AngelScript"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::Lua() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "and", "break", "do", "", "else", "elseif", "end", "false", "for", "function", "if", "in", "", "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while"}; + + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "assert", "collectgarbage", "dofile", "error", "getmetatable", "ipairs", "loadfile", "load", "loadstring", "next", "pairs", "pcall", "print", "rawequal", "rawlen", "rawget", "rawset", + "select", "setmetatable", "tonumber", "tostring", "type", "xpcall", "_G", "_VERSION", "arshift", "band", "bnot", "bor", "bxor", "btest", "extract", "lrotate", "lshift", "replace", + "rrotate", "rshift", "create", "resume", "running", "status", "wrap", "yield", "isyieldable", "debug", "getuservalue", "gethook", "getinfo", "getlocal", "getregistry", "getmetatable", + "getupvalue", "upvaluejoin", "upvalueid", "setuservalue", "sethook", "setlocal", "setmetatable", "setupvalue", "traceback", "close", "flush", "input", "lines", "open", "output", "popen", + "read", "tmpfile", "type", "write", "close", "flush", "lines", "read", "seek", "setvbuf", "write", "__gc", "__tostring", "abs", "acos", "asin", "atan", "ceil", "cos", "deg", "exp", "tointeger", + "floor", "fmod", "ult", "log", "max", "min", "modf", "rad", "random", "randomseed", "sin", "sqrt", "string", "tan", "type", "atan2", "cosh", "sinh", "tanh", + "pow", "frexp", "ldexp", "log10", "pi", "huge", "maxinteger", "mininteger", "loadlib", "searchpath", "seeall", "preload", "cpath", "path", "searchers", "loaded", "module", "require", "clock", + "date", "difftime", "execute", "exit", "getenv", "remove", "rename", "setlocale", "time", "tmpname", "byte", "char", "dump", "find", "format", "gmatch", "gsub", "len", "lower", "match", "rep", + "reverse", "sub", "upper", "pack", "packsize", "unpack", "concat", "maxn", "insert", "pack", "unpack", "remove", "move", "sort", "offset", "codepoint", "char", "len", "codes", "charpattern", + "coroutine", "table", "io", "os", "string", "utf8", "bit32", "math", "debug", "package"}; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back(std::make_pair(R"(L?\"(\\.|[^\"])*\")", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"(\'[^\']*\')", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?[0-9]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[a-zA-Z_][a-zA-Z0-9_]*", PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"([\[\]\{\}\!\%\^\&\*\(\)\-\+\=\~\|\<\>\?\/\;\,\.])", PaletteIndex::Punctuation)); + + langDef.mCommentStart = "--[["; + langDef.mCommentEnd = "]]"; + langDef.mSingleLineComment = "--"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = false; + + langDef.mName = "Lua"; + + inited = true; + } + return langDef; +} diff --git a/src/gui/preset_editor/imgui_color_text_editor/TextEditor.h b/src/gui/preset_editor/imgui_color_text_editor/TextEditor.h new file mode 100644 index 0000000..1b6aada --- /dev/null +++ b/src/gui/preset_editor/imgui_color_text_editor/TextEditor.h @@ -0,0 +1,467 @@ +#pragma once + +#include "imgui.h" +#include +#include +#include +#include +#include +#include +#include +#include + +class TextEditor +{ +public: + enum class PaletteIndex + { + Default, + Keyword, + Number, + String, + CharLiteral, + Punctuation, + Preprocessor, + Identifier, + KnownIdentifier, + PreprocIdentifier, + Comment, + MultiLineComment, + Background, + Cursor, + Selection, + ErrorMarker, + Breakpoint, + LineNumber, + CurrentLineFill, + CurrentLineFillInactive, + CurrentLineEdge, + Max + }; + + enum class SelectionMode + { + Normal, + Word, + Line + }; + + struct Breakpoint { + int mLine; + bool mEnabled; + std::string mCondition; + + Breakpoint() + : mLine(-1) + , mEnabled(false) + { + } + }; + + // Represents a character coordinate from the user's point of view, + // i. e. consider an uniform grid (assuming fixed-width font) on the + // screen as it is rendered, and each cell has its own coordinate, starting from 0. + // Tabs are counted as [1..mTabSize] count empty spaces, depending on + // how many space is necessary to reach the next tab stop. + // For example, coordinate (1, 5) represents the character 'B' in a line "\tABC", when mTabSize = 4, + // because it is rendered as " ABC" on the screen. + struct Coordinates { + int mLine, mColumn; + Coordinates() + : mLine(0) + , mColumn(0) + { + } + Coordinates(int aLine, int aColumn) + : mLine(aLine) + , mColumn(aColumn) + { + assert(aLine >= 0); + assert(aColumn >= 0); + } + static Coordinates Invalid() + { + static Coordinates invalid(-1, -1); + return invalid; + } + + bool operator==(const Coordinates& o) const + { + return mLine == o.mLine && + mColumn == o.mColumn; + } + + bool operator!=(const Coordinates& o) const + { + return mLine != o.mLine || + mColumn != o.mColumn; + } + + bool operator<(const Coordinates& o) const + { + if (mLine != o.mLine) + return mLine < o.mLine; + return mColumn < o.mColumn; + } + + bool operator>(const Coordinates& o) const + { + if (mLine != o.mLine) + return mLine > o.mLine; + return mColumn > o.mColumn; + } + + bool operator<=(const Coordinates& o) const + { + if (mLine != o.mLine) + return mLine < o.mLine; + return mColumn <= o.mColumn; + } + + bool operator>=(const Coordinates& o) const + { + if (mLine != o.mLine) + return mLine > o.mLine; + return mColumn >= o.mColumn; + } + }; + + struct Identifier { + Coordinates mLocation; + std::string mDeclaration; + }; + + typedef std::string String; + typedef std::unordered_map Identifiers; + typedef std::unordered_set Keywords; + typedef std::map ErrorMarkers; + typedef std::unordered_set Breakpoints; + typedef std::array Palette; + typedef uint8_t Char; + + struct Glyph { + Char mChar; + PaletteIndex mColorIndex = PaletteIndex::Default; + bool mComment : 1; + bool mMultiLineComment : 1; + bool mPreprocessor : 1; + + Glyph(Char aChar, PaletteIndex aColorIndex) + : mChar(aChar) + , mColorIndex(aColorIndex) + , mComment(false) + , mMultiLineComment(false) + , mPreprocessor(false) + { + } + }; + + typedef std::vector Line; + typedef std::vector Lines; + + struct LanguageDefinition { + typedef std::pair TokenRegexString; + typedef std::vector TokenRegexStrings; + typedef bool (*TokenizeCallback)(const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end, PaletteIndex& paletteIndex); + + std::string mName; + Keywords mKeywords; + Identifiers mIdentifiers; + Identifiers mPreprocIdentifiers; + std::string mCommentStart, mCommentEnd, mSingleLineComment; + char mPreprocChar; + bool mAutoIndentation; + + TokenizeCallback mTokenize; + + TokenRegexStrings mTokenRegexStrings; + + bool mCaseSensitive; + + LanguageDefinition() + : mPreprocChar('#') + , mAutoIndentation(true) + , mTokenize(nullptr) + , mCaseSensitive(true) + { + } + + static const LanguageDefinition& CPlusPlus(); + static const LanguageDefinition& MilkdropExpression(); + static const LanguageDefinition& HLSL(); + static const LanguageDefinition& GLSL(); + static const LanguageDefinition& C(); + static const LanguageDefinition& SQL(); + static const LanguageDefinition& AngelScript(); + static const LanguageDefinition& Lua(); + }; + + TextEditor(); + ~TextEditor() = default; + + void SetLanguageDefinition(const LanguageDefinition& aLanguageDef); + const LanguageDefinition& GetLanguageDefinition() const + { + return mLanguageDefinition; + } + + const Palette& GetPalette() const + { + return mPaletteBase; + } + void SetPalette(const Palette& aValue); + + void SetErrorMarkers(const ErrorMarkers& aMarkers) + { + mErrorMarkers = aMarkers; + } + void SetBreakpoints(const Breakpoints& aMarkers) + { + mBreakpoints = aMarkers; + } + + void Render(const char* aTitle, const ImVec2& aSize = ImVec2(), bool aBorder = false); + void SetText(const std::string& aText); + std::string GetText() const; + + void SetTextLines(const std::vector& aLines); + std::vector GetTextLines() const; + + std::string GetSelectedText() const; + std::string GetCurrentLineText() const; + + int GetTotalLines() const + { + return (int) mLines.size(); + } + bool IsOverwrite() const + { + return mOverwrite; + } + + void SetReadOnly(bool aValue); + bool IsReadOnly() const + { + return mReadOnly; + } + bool IsTextChanged() const + { + return mTextChanged; + } + bool IsCursorPositionChanged() const + { + return mCursorPositionChanged; + } + + bool IsColorizerEnabled() const + { + return mColorizerEnabled; + } + void SetColorizerEnable(bool aValue); + + Coordinates GetCursorPosition() const + { + return GetActualCursorCoordinates(); + } + void SetCursorPosition(const Coordinates& aPosition); + + inline void SetHandleMouseInputs(bool aValue) + { + mHandleMouseInputs = aValue; + } + inline bool IsHandleMouseInputsEnabled() const + { + return mHandleKeyboardInputs; + } + + inline void SetHandleKeyboardInputs(bool aValue) + { + mHandleKeyboardInputs = aValue; + } + inline bool IsHandleKeyboardInputsEnabled() const + { + return mHandleKeyboardInputs; + } + + inline void SetImGuiChildIgnored(bool aValue) + { + mIgnoreImGuiChild = aValue; + } + inline bool IsImGuiChildIgnored() const + { + return mIgnoreImGuiChild; + } + + inline void SetShowWhitespaces(bool aValue) + { + mShowWhitespaces = aValue; + } + inline bool IsShowingWhitespaces() const + { + return mShowWhitespaces; + } + + void SetTabSize(int aValue); + inline int GetTabSize() const + { + return mTabSize; + } + + void InsertText(const std::string& aValue); + void InsertText(const char* aValue); + + void MoveUp(int aAmount = 1, bool aSelect = false); + void MoveDown(int aAmount = 1, bool aSelect = false); + void MoveLeft(int aAmount = 1, bool aSelect = false, bool aWordMode = false); + void MoveRight(int aAmount = 1, bool aSelect = false, bool aWordMode = false); + void MoveTop(bool aSelect = false); + void MoveBottom(bool aSelect = false); + void MoveHome(bool aSelect = false); + void MoveEnd(bool aSelect = false); + + void SetSelectionStart(const Coordinates& aPosition); + void SetSelectionEnd(const Coordinates& aPosition); + void SetSelection(const Coordinates& aStart, const Coordinates& aEnd, SelectionMode aMode = SelectionMode::Normal); + void SelectWordUnderCursor(); + void SelectAll(); + bool HasSelection() const; + + void Copy(); + void Cut(); + void Paste(); + void Delete(); + + bool CanUndo() const; + bool CanRedo() const; + void Undo(int aSteps = 1); + void Redo(int aSteps = 1); + + static const Palette& GetDarkPalette(); + static const Palette& GetLightPalette(); + static const Palette& GetRetroBluePalette(); + +private: + typedef std::vector> RegexList; + + struct EditorState { + Coordinates mSelectionStart; + Coordinates mSelectionEnd; + Coordinates mCursorPosition; + }; + + class UndoRecord + { + public: + UndoRecord() + { + } + ~UndoRecord() + { + } + + UndoRecord( + std::string aAdded, + const TextEditor::Coordinates aAddedStart, + const TextEditor::Coordinates aAddedEnd, + + std::string aRemoved, + const TextEditor::Coordinates aRemovedStart, + const TextEditor::Coordinates aRemovedEnd, + + const TextEditor::EditorState& aBefore, + const TextEditor::EditorState& aAfter); + + void Undo(TextEditor* aEditor); + void Redo(TextEditor* aEditor); + + std::string mAdded; + Coordinates mAddedStart; + Coordinates mAddedEnd; + + std::string mRemoved; + Coordinates mRemovedStart; + Coordinates mRemovedEnd; + + EditorState mBefore; + EditorState mAfter; + }; + + typedef std::vector UndoBuffer; + + void ProcessInputs(); + void Colorize(int aFromLine = 0, int aCount = -1); + void ColorizeRange(int aFromLine = 0, int aToLine = 0); + void ColorizeInternal(); + float TextDistanceToLineStart(const Coordinates& aFrom) const; + void EnsureCursorVisible(); + int GetPageSize() const; + std::string GetText(const Coordinates& aStart, const Coordinates& aEnd) const; + Coordinates GetActualCursorCoordinates() const; + Coordinates SanitizeCoordinates(const Coordinates& aValue) const; + void Advance(Coordinates& aCoordinates) const; + void DeleteRange(const Coordinates& aStart, const Coordinates& aEnd); + int InsertTextAt(Coordinates& aWhere, const char* aValue); + void AddUndo(const UndoRecord& aValue); + Coordinates ScreenPosToCoordinates(const ImVec2& aPosition) const; + Coordinates FindWordStart(const Coordinates& aFrom) const; + Coordinates FindWordEnd(const Coordinates& aFrom) const; + Coordinates FindNextWord(const Coordinates& aFrom) const; + int GetCharacterIndex(const Coordinates& aCoordinates) const; + int GetCharacterColumn(int aLine, int aIndex) const; + int GetLineCharacterCount(int aLine) const; + int GetLineMaxColumn(int aLine) const; + bool IsOnWordBoundary(const Coordinates& aAt) const; + void RemoveLine(int aStart, int aEnd); + void RemoveLine(int aIndex); + Line& InsertLine(int aIndex); + void EnterCharacter(ImWchar aChar, bool aShift); + void Backspace(); + void DeleteSelection(); + std::string GetWordUnderCursor() const; + std::string GetWordAt(const Coordinates& aCoords) const; + ImU32 GetGlyphColor(const Glyph& aGlyph) const; + + void HandleKeyboardInputs(); + void HandleMouseInputs(); + void Render(); + + float mLineSpacing{1.0f}; + Lines mLines; + EditorState mState; + UndoBuffer mUndoBuffer; + int mUndoIndex{0}; + + int mTabSize{4}; + bool mOverwrite{false}; + bool mReadOnly{false}; + bool mWithinRender{false}; + bool mScrollToCursor{false}; + bool mScrollToTop{false}; + bool mTextChanged{false}; + bool mColorizerEnabled{true}; + float mTextStart{20.0f}; // position (in pixels) where a code line starts relative to the left of the TextEditor. + int mLeftMargin{10}; + bool mCursorPositionChanged{false}; + int mColorRangeMin{0}; + int mColorRangeMax{0}; + SelectionMode mSelectionMode{SelectionMode::Normal}; + bool mHandleKeyboardInputs{true}; + bool mHandleMouseInputs{true}; + bool mIgnoreImGuiChild{false}; + bool mShowWhitespaces{true}; + + Palette mPaletteBase{{}}; + Palette mPalette{{}}; + LanguageDefinition mLanguageDefinition; + RegexList mRegexList; + + bool mCheckComments{true}; + Breakpoints mBreakpoints; + ErrorMarkers mErrorMarkers; + ImVec2 mCharAdvance; + Coordinates mInteractiveStart, mInteractiveEnd; + std::string mLineBuffer; + uint64_t mStartTime{0}; + + float mLastClick{-1.0f}; +}; diff --git a/src/notifications/UpdateWindowTitleNotification.h b/src/notifications/UpdateWindowTitleNotification.h index d6cb0a4..96a1d43 100644 --- a/src/notifications/UpdateWindowTitleNotification.h +++ b/src/notifications/UpdateWindowTitleNotification.h @@ -2,11 +2,21 @@ #include +#include + /** * @brief Informs the application that the window title should be updated. */ class UpdateWindowTitleNotification : public Poco::Notification { public: + UpdateWindowTitleNotification() = default; + + explicit UpdateWindowTitleNotification(std::string customTitle) + : _customTitle(std::move(customTitle)) + {} + std::string name() const override; + + std::string _customTitle; }; diff --git a/vcpkg.json b/vcpkg.json index 924009d..38f5627 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -9,7 +9,8 @@ "util" ] }, + "freetype", "projectm", - "freetype" + "projectm-eval" ] } \ No newline at end of file From f68fcd043eca14c06222fd6fbb9c323d773b74a1 Mon Sep 17 00:00:00 2001 From: Kai Blaschke Date: Mon, 8 Sep 2025 18:57:14 +0200 Subject: [PATCH 02/14] Add icons and variable highlighting, implement shape/wave settings --- src/gui/AboutWindow.cpp | 1 + src/gui/CMakeLists.txt | 13 + src/gui/IconsFontAwesome7.h | 1421 +++++++++++++++++ src/gui/MainMenu.cpp | 32 +- src/gui/ProjectMGUI.cpp | 35 +- src/gui/ProjectMGUI.h | 12 + src/gui/preset_editor/CMakeLists.txt | 11 +- .../preset_editor/CodeContextInformation.cpp | 941 +++++++++++ .../preset_editor/CodeContextInformation.h | 52 + src/gui/preset_editor/CodeEditorTab.cpp | 13 + src/gui/preset_editor/CodeEditorTab.h | 15 + src/gui/preset_editor/CodeEditorWindow.cpp | 13 + src/gui/preset_editor/CodeEditorWindow.h | 15 + src/gui/preset_editor/EditorMenu.cpp | 23 +- src/gui/preset_editor/EditorPreset.cpp | 6 +- src/gui/preset_editor/EditorPreset.h | 2 +- src/gui/preset_editor/ExpressionCodeTypes.h | 22 + src/gui/preset_editor/PresetEditorGUI.cpp | 706 ++++++-- src/gui/preset_editor/PresetEditorGUI.h | 11 +- .../imgui_color_text_editor/TextEditor.cpp | 10 +- src/resources/FontAwesome License.txt | 165 ++ src/resources/fa-regular-400.ttf | Bin 0 -> 48076 bytes src/resources/fa-solid-900.ttf | Bin 0 -> 314852 bytes 23 files changed, 3320 insertions(+), 199 deletions(-) create mode 100644 src/gui/IconsFontAwesome7.h create mode 100644 src/gui/preset_editor/CodeContextInformation.cpp create mode 100644 src/gui/preset_editor/CodeContextInformation.h create mode 100644 src/gui/preset_editor/CodeEditorTab.cpp create mode 100644 src/gui/preset_editor/CodeEditorTab.h create mode 100644 src/gui/preset_editor/CodeEditorWindow.cpp create mode 100644 src/gui/preset_editor/CodeEditorWindow.h create mode 100644 src/gui/preset_editor/ExpressionCodeTypes.h create mode 100644 src/resources/FontAwesome License.txt create mode 100644 src/resources/fa-regular-400.ttf create mode 100644 src/resources/fa-solid-900.ttf diff --git a/src/gui/AboutWindow.cpp b/src/gui/AboutWindow.cpp index 932b3af..b9d1d8d 100644 --- a/src/gui/AboutWindow.cpp +++ b/src/gui/AboutWindow.cpp @@ -59,6 +59,7 @@ void AboutWindow::Draw() ImGui::BulletText("Dear ImGui by Omar Cornut and contributors (MIT)"); ImGui::BulletText("The POCO C++ Framework by Applied Informatics GmbH (MIT)"); ImGui::BulletText("FreeType 2 (FreeType License / GNU GPL v2)"); + ImGui::BulletText("FontAwesome Free Icons v7 (FontAwesome License / SIL OFL 1.1)"); ImGui::Dummy({.0f, 10.0f}); ImGui::TextUnformatted("Via libprojectM:"); diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 3a08546..b23f2d6 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -18,15 +18,28 @@ add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/LiberationSansFont.h" MAIN_DEPENDENCY "${CMAKE_SOURCE_DIR}/src/resources/LiberationSans-Regular.ttf" ) +add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/FontAwesomeIconsRegular7.h" + COMMAND ${BINARY_TO_COMPRESSED_EXECUTABLE} "${CMAKE_SOURCE_DIR}/src/resources/fa-regular-400.ttf" FontAwesomeIconsRegular7 > "${CMAKE_CURRENT_BINARY_DIR}/FontAwesomeIconsRegular7.h" + MAIN_DEPENDENCY "${CMAKE_SOURCE_DIR}/src/resources/fa-regular-400.ttf" + ) + +add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/FontAwesomeIconsSolid7.h" + COMMAND ${BINARY_TO_COMPRESSED_EXECUTABLE} "${CMAKE_SOURCE_DIR}/src/resources/fa-solid-900.ttf" FontAwesomeIconsSolid7 > "${CMAKE_CURRENT_BINARY_DIR}/FontAwesomeIconsSolid7.h" + MAIN_DEPENDENCY "${CMAKE_SOURCE_DIR}/src/resources/fa-solid-900.ttf" + ) + add_library(ProjectMSDL-GUI STATIC "${CMAKE_CURRENT_BINARY_DIR}/AnonymousProFont.h" "${CMAKE_CURRENT_BINARY_DIR}/LiberationSansFont.h" + "${CMAKE_CURRENT_BINARY_DIR}/FontAwesomeIconsRegular7.h" + "${CMAKE_CURRENT_BINARY_DIR}/FontAwesomeIconsSolid7.h" AboutWindow.cpp AboutWindow.h FileChooser.cpp FileChooser.h HelpWindow.cpp HelpWindow.h + IconsFontAwesome7.h MainMenu.cpp MainMenu.h PresetSelection.cpp diff --git a/src/gui/IconsFontAwesome7.h b/src/gui/IconsFontAwesome7.h new file mode 100644 index 0000000..7e840f8 --- /dev/null +++ b/src/gui/IconsFontAwesome7.h @@ -0,0 +1,1421 @@ +// Generated by https://github.com/juliettef/IconFontCppHeaders script GenerateIconFontCppHeaders.py +// for C and C++ +// from codepoints https://github.com/FortAwesome/Font-Awesome/raw/7.x/metadata/icons.yml +// for use with font https://github.com/FortAwesome/Font-Awesome/blob/7.x/webfonts/fa-regular-400.woff2 (You may need to convert the .woff2 files to .ttf depending upon your loader.), https://github.com/FortAwesome/Font-Awesome/blob/7.x/webfonts/fa-solid-900.woff2 (You may need to convert the .woff2 files to .ttf depending upon your loader.) + +#pragma once + +#define FONT_ICON_FILE_NAME_FAR "fa-regular-400.woff2" +#define FONT_ICON_FILE_NAME_FAS "fa-solid-900.woff2" + +#define ICON_MIN_FA 0xe005 +#define ICON_MAX_16_FA 0xf8ff +#define ICON_MAX_FA 0xf8ff + +#define ICON_FA_0 "0" // U+0030 +#define ICON_FA_1 "1" // U+0031 +#define ICON_FA_2 "2" // U+0032 +#define ICON_FA_3 "3" // U+0033 +#define ICON_FA_4 "4" // U+0034 +#define ICON_FA_5 "5" // U+0035 +#define ICON_FA_6 "6" // U+0036 +#define ICON_FA_7 "7" // U+0037 +#define ICON_FA_8 "8" // U+0038 +#define ICON_FA_9 "9" // U+0039 +#define ICON_FA_A "A" // U+0041 +#define ICON_FA_ADDRESS_BOOK "\xef\x8a\xb9" // U+f2b9 +#define ICON_FA_ADDRESS_CARD "\xef\x8a\xbb" // U+f2bb +#define ICON_FA_ALARM_CLOCK "\xef\x8d\x8e" // U+f34e +#define ICON_FA_ALIGN_CENTER "\xef\x80\xb7" // U+f037 +#define ICON_FA_ALIGN_JUSTIFY "\xef\x80\xb9" // U+f039 +#define ICON_FA_ALIGN_LEFT "\xef\x80\xb6" // U+f036 +#define ICON_FA_ALIGN_RIGHT "\xef\x80\xb8" // U+f038 +#define ICON_FA_ANCHOR "\xef\x84\xbd" // U+f13d +#define ICON_FA_ANCHOR_CIRCLE_CHECK "\xee\x92\xaa" // U+e4aa +#define ICON_FA_ANCHOR_CIRCLE_EXCLAMATION "\xee\x92\xab" // U+e4ab +#define ICON_FA_ANCHOR_CIRCLE_XMARK "\xee\x92\xac" // U+e4ac +#define ICON_FA_ANCHOR_LOCK "\xee\x92\xad" // U+e4ad +#define ICON_FA_ANGLE_DOWN "\xef\x84\x87" // U+f107 +#define ICON_FA_ANGLE_LEFT "\xef\x84\x84" // U+f104 +#define ICON_FA_ANGLE_RIGHT "\xef\x84\x85" // U+f105 +#define ICON_FA_ANGLE_UP "\xef\x84\x86" // U+f106 +#define ICON_FA_ANGLES_DOWN "\xef\x84\x83" // U+f103 +#define ICON_FA_ANGLES_LEFT "\xef\x84\x80" // U+f100 +#define ICON_FA_ANGLES_RIGHT "\xef\x84\x81" // U+f101 +#define ICON_FA_ANGLES_UP "\xef\x84\x82" // U+f102 +#define ICON_FA_ANKH "\xef\x99\x84" // U+f644 +#define ICON_FA_APPLE_WHOLE "\xef\x97\x91" // U+f5d1 +#define ICON_FA_ARCHWAY "\xef\x95\x97" // U+f557 +#define ICON_FA_ARROW_DOWN "\xef\x81\xa3" // U+f063 +#define ICON_FA_ARROW_DOWN_1_9 "\xef\x85\xa2" // U+f162 +#define ICON_FA_ARROW_DOWN_9_1 "\xef\xa2\x86" // U+f886 +#define ICON_FA_ARROW_DOWN_A_Z "\xef\x85\x9d" // U+f15d +#define ICON_FA_ARROW_DOWN_LONG "\xef\x85\xb5" // U+f175 +#define ICON_FA_ARROW_DOWN_SHORT_WIDE "\xef\xa2\x84" // U+f884 +#define ICON_FA_ARROW_DOWN_UP_ACROSS_LINE "\xee\x92\xaf" // U+e4af +#define ICON_FA_ARROW_DOWN_UP_LOCK "\xee\x92\xb0" // U+e4b0 +#define ICON_FA_ARROW_DOWN_WIDE_SHORT "\xef\x85\xa0" // U+f160 +#define ICON_FA_ARROW_DOWN_Z_A "\xef\xa2\x81" // U+f881 +#define ICON_FA_ARROW_LEFT "\xef\x81\xa0" // U+f060 +#define ICON_FA_ARROW_LEFT_LONG "\xef\x85\xb7" // U+f177 +#define ICON_FA_ARROW_POINTER "\xef\x89\x85" // U+f245 +#define ICON_FA_ARROW_RIGHT "\xef\x81\xa1" // U+f061 +#define ICON_FA_ARROW_RIGHT_ARROW_LEFT "\xef\x83\xac" // U+f0ec +#define ICON_FA_ARROW_RIGHT_FROM_BRACKET "\xef\x82\x8b" // U+f08b +#define ICON_FA_ARROW_RIGHT_LONG "\xef\x85\xb8" // U+f178 +#define ICON_FA_ARROW_RIGHT_TO_BRACKET "\xef\x82\x90" // U+f090 +#define ICON_FA_ARROW_RIGHT_TO_CITY "\xee\x92\xb3" // U+e4b3 +#define ICON_FA_ARROW_ROTATE_LEFT "\xef\x83\xa2" // U+f0e2 +#define ICON_FA_ARROW_ROTATE_RIGHT "\xef\x80\x9e" // U+f01e +#define ICON_FA_ARROW_TREND_DOWN "\xee\x82\x97" // U+e097 +#define ICON_FA_ARROW_TREND_UP "\xee\x82\x98" // U+e098 +#define ICON_FA_ARROW_TURN_DOWN "\xef\x85\x89" // U+f149 +#define ICON_FA_ARROW_TURN_UP "\xef\x85\x88" // U+f148 +#define ICON_FA_ARROW_UP "\xef\x81\xa2" // U+f062 +#define ICON_FA_ARROW_UP_1_9 "\xef\x85\xa3" // U+f163 +#define ICON_FA_ARROW_UP_9_1 "\xef\xa2\x87" // U+f887 +#define ICON_FA_ARROW_UP_A_Z "\xef\x85\x9e" // U+f15e +#define ICON_FA_ARROW_UP_FROM_BRACKET "\xee\x82\x9a" // U+e09a +#define ICON_FA_ARROW_UP_FROM_GROUND_WATER "\xee\x92\xb5" // U+e4b5 +#define ICON_FA_ARROW_UP_FROM_WATER_PUMP "\xee\x92\xb6" // U+e4b6 +#define ICON_FA_ARROW_UP_LONG "\xef\x85\xb6" // U+f176 +#define ICON_FA_ARROW_UP_RIGHT_DOTS "\xee\x92\xb7" // U+e4b7 +#define ICON_FA_ARROW_UP_RIGHT_FROM_SQUARE "\xef\x82\x8e" // U+f08e +#define ICON_FA_ARROW_UP_SHORT_WIDE "\xef\xa2\x85" // U+f885 +#define ICON_FA_ARROW_UP_WIDE_SHORT "\xef\x85\xa1" // U+f161 +#define ICON_FA_ARROW_UP_Z_A "\xef\xa2\x82" // U+f882 +#define ICON_FA_ARROWS_DOWN_TO_LINE "\xee\x92\xb8" // U+e4b8 +#define ICON_FA_ARROWS_DOWN_TO_PEOPLE "\xee\x92\xb9" // U+e4b9 +#define ICON_FA_ARROWS_LEFT_RIGHT "\xef\x81\xbe" // U+f07e +#define ICON_FA_ARROWS_LEFT_RIGHT_TO_LINE "\xee\x92\xba" // U+e4ba +#define ICON_FA_ARROWS_ROTATE "\xef\x80\xa1" // U+f021 +#define ICON_FA_ARROWS_SPIN "\xee\x92\xbb" // U+e4bb +#define ICON_FA_ARROWS_SPLIT_UP_AND_LEFT "\xee\x92\xbc" // U+e4bc +#define ICON_FA_ARROWS_TO_CIRCLE "\xee\x92\xbd" // U+e4bd +#define ICON_FA_ARROWS_TO_DOT "\xee\x92\xbe" // U+e4be +#define ICON_FA_ARROWS_TO_EYE "\xee\x92\xbf" // U+e4bf +#define ICON_FA_ARROWS_TURN_RIGHT "\xee\x93\x80" // U+e4c0 +#define ICON_FA_ARROWS_TURN_TO_DOTS "\xee\x93\x81" // U+e4c1 +#define ICON_FA_ARROWS_UP_DOWN "\xef\x81\xbd" // U+f07d +#define ICON_FA_ARROWS_UP_DOWN_LEFT_RIGHT "\xef\x81\x87" // U+f047 +#define ICON_FA_ARROWS_UP_TO_LINE "\xee\x93\x82" // U+e4c2 +#define ICON_FA_ASTERISK "*" // U+002a +#define ICON_FA_AT "@" // U+0040 +#define ICON_FA_ATOM "\xef\x97\x92" // U+f5d2 +#define ICON_FA_AUDIO_DESCRIPTION "\xef\x8a\x9e" // U+f29e +#define ICON_FA_AUSTRAL_SIGN "\xee\x82\xa9" // U+e0a9 +#define ICON_FA_AWARD "\xef\x95\x99" // U+f559 +#define ICON_FA_B "B" // U+0042 +#define ICON_FA_BABY "\xef\x9d\xbc" // U+f77c +#define ICON_FA_BABY_CARRIAGE "\xef\x9d\xbd" // U+f77d +#define ICON_FA_BACKWARD "\xef\x81\x8a" // U+f04a +#define ICON_FA_BACKWARD_FAST "\xef\x81\x89" // U+f049 +#define ICON_FA_BACKWARD_STEP "\xef\x81\x88" // U+f048 +#define ICON_FA_BACON "\xef\x9f\xa5" // U+f7e5 +#define ICON_FA_BACTERIA "\xee\x81\x99" // U+e059 +#define ICON_FA_BACTERIUM "\xee\x81\x9a" // U+e05a +#define ICON_FA_BAG_SHOPPING "\xef\x8a\x90" // U+f290 +#define ICON_FA_BAHAI "\xef\x99\xa6" // U+f666 +#define ICON_FA_BAHT_SIGN "\xee\x82\xac" // U+e0ac +#define ICON_FA_BAN "\xef\x81\x9e" // U+f05e +#define ICON_FA_BAN_SMOKING "\xef\x95\x8d" // U+f54d +#define ICON_FA_BANDAGE "\xef\x91\xa2" // U+f462 +#define ICON_FA_BANGLADESHI_TAKA_SIGN "\xee\x8b\xa6" // U+e2e6 +#define ICON_FA_BARCODE "\xef\x80\xaa" // U+f02a +#define ICON_FA_BARS "\xef\x83\x89" // U+f0c9 +#define ICON_FA_BARS_PROGRESS "\xef\xa0\xa8" // U+f828 +#define ICON_FA_BARS_STAGGERED "\xef\x95\x90" // U+f550 +#define ICON_FA_BASEBALL "\xef\x90\xb3" // U+f433 +#define ICON_FA_BASEBALL_BAT_BALL "\xef\x90\xb2" // U+f432 +#define ICON_FA_BASKET_SHOPPING "\xef\x8a\x91" // U+f291 +#define ICON_FA_BASKETBALL "\xef\x90\xb4" // U+f434 +#define ICON_FA_BATH "\xef\x8b\x8d" // U+f2cd +#define ICON_FA_BATTERY_EMPTY "\xef\x89\x84" // U+f244 +#define ICON_FA_BATTERY_FULL "\xef\x89\x80" // U+f240 +#define ICON_FA_BATTERY_HALF "\xef\x89\x82" // U+f242 +#define ICON_FA_BATTERY_QUARTER "\xef\x89\x83" // U+f243 +#define ICON_FA_BATTERY_THREE_QUARTERS "\xef\x89\x81" // U+f241 +#define ICON_FA_BED "\xef\x88\xb6" // U+f236 +#define ICON_FA_BED_PULSE "\xef\x92\x87" // U+f487 +#define ICON_FA_BEER_MUG_EMPTY "\xef\x83\xbc" // U+f0fc +#define ICON_FA_BELL "\xef\x83\xb3" // U+f0f3 +#define ICON_FA_BELL_CONCIERGE "\xef\x95\xa2" // U+f562 +#define ICON_FA_BELL_SLASH "\xef\x87\xb6" // U+f1f6 +#define ICON_FA_BEZIER_CURVE "\xef\x95\x9b" // U+f55b +#define ICON_FA_BICYCLE "\xef\x88\x86" // U+f206 +#define ICON_FA_BINOCULARS "\xef\x87\xa5" // U+f1e5 +#define ICON_FA_BIOHAZARD "\xef\x9e\x80" // U+f780 +#define ICON_FA_BITCOIN_SIGN "\xee\x82\xb4" // U+e0b4 +#define ICON_FA_BLENDER "\xef\x94\x97" // U+f517 +#define ICON_FA_BLENDER_PHONE "\xef\x9a\xb6" // U+f6b6 +#define ICON_FA_BLOG "\xef\x9e\x81" // U+f781 +#define ICON_FA_BOLD "\xef\x80\xb2" // U+f032 +#define ICON_FA_BOLT "\xef\x83\xa7" // U+f0e7 +#define ICON_FA_BOLT_LIGHTNING "\xee\x82\xb7" // U+e0b7 +#define ICON_FA_BOMB "\xef\x87\xa2" // U+f1e2 +#define ICON_FA_BONE "\xef\x97\x97" // U+f5d7 +#define ICON_FA_BONG "\xef\x95\x9c" // U+f55c +#define ICON_FA_BOOK "\xef\x80\xad" // U+f02d +#define ICON_FA_BOOK_ATLAS "\xef\x95\x98" // U+f558 +#define ICON_FA_BOOK_BIBLE "\xef\x99\x87" // U+f647 +#define ICON_FA_BOOK_BOOKMARK "\xee\x82\xbb" // U+e0bb +#define ICON_FA_BOOK_JOURNAL_WHILLS "\xef\x99\xaa" // U+f66a +#define ICON_FA_BOOK_MEDICAL "\xef\x9f\xa6" // U+f7e6 +#define ICON_FA_BOOK_OPEN "\xef\x94\x98" // U+f518 +#define ICON_FA_BOOK_OPEN_READER "\xef\x97\x9a" // U+f5da +#define ICON_FA_BOOK_QURAN "\xef\x9a\x87" // U+f687 +#define ICON_FA_BOOK_SKULL "\xef\x9a\xb7" // U+f6b7 +#define ICON_FA_BOOK_TANAKH "\xef\xa0\xa7" // U+f827 +#define ICON_FA_BOOKMARK "\xef\x80\xae" // U+f02e +#define ICON_FA_BORDER_ALL "\xef\xa1\x8c" // U+f84c +#define ICON_FA_BORDER_NONE "\xef\xa1\x90" // U+f850 +#define ICON_FA_BORDER_TOP_LEFT "\xef\xa1\x93" // U+f853 +#define ICON_FA_BORE_HOLE "\xee\x93\x83" // U+e4c3 +#define ICON_FA_BOTTLE_DROPLET "\xee\x93\x84" // U+e4c4 +#define ICON_FA_BOTTLE_WATER "\xee\x93\x85" // U+e4c5 +#define ICON_FA_BOWL_FOOD "\xee\x93\x86" // U+e4c6 +#define ICON_FA_BOWL_RICE "\xee\x8b\xab" // U+e2eb +#define ICON_FA_BOWLING_BALL "\xef\x90\xb6" // U+f436 +#define ICON_FA_BOX "\xef\x91\xa6" // U+f466 +#define ICON_FA_BOX_ARCHIVE "\xef\x86\x87" // U+f187 +#define ICON_FA_BOX_OPEN "\xef\x92\x9e" // U+f49e +#define ICON_FA_BOX_TISSUE "\xee\x81\x9b" // U+e05b +#define ICON_FA_BOXES_PACKING "\xee\x93\x87" // U+e4c7 +#define ICON_FA_BOXES_STACKED "\xef\x91\xa8" // U+f468 +#define ICON_FA_BRAILLE "\xef\x8a\xa1" // U+f2a1 +#define ICON_FA_BRAIN "\xef\x97\x9c" // U+f5dc +#define ICON_FA_BRAZILIAN_REAL_SIGN "\xee\x91\xac" // U+e46c +#define ICON_FA_BREAD_SLICE "\xef\x9f\xac" // U+f7ec +#define ICON_FA_BRIDGE "\xee\x93\x88" // U+e4c8 +#define ICON_FA_BRIDGE_CIRCLE_CHECK "\xee\x93\x89" // U+e4c9 +#define ICON_FA_BRIDGE_CIRCLE_EXCLAMATION "\xee\x93\x8a" // U+e4ca +#define ICON_FA_BRIDGE_CIRCLE_XMARK "\xee\x93\x8b" // U+e4cb +#define ICON_FA_BRIDGE_LOCK "\xee\x93\x8c" // U+e4cc +#define ICON_FA_BRIDGE_WATER "\xee\x93\x8e" // U+e4ce +#define ICON_FA_BRIEFCASE "\xef\x82\xb1" // U+f0b1 +#define ICON_FA_BRIEFCASE_MEDICAL "\xef\x91\xa9" // U+f469 +#define ICON_FA_BROOM "\xef\x94\x9a" // U+f51a +#define ICON_FA_BROOM_BALL "\xef\x91\x98" // U+f458 +#define ICON_FA_BRUSH "\xef\x95\x9d" // U+f55d +#define ICON_FA_BUCKET "\xee\x93\x8f" // U+e4cf +#define ICON_FA_BUG "\xef\x86\x88" // U+f188 +#define ICON_FA_BUG_SLASH "\xee\x92\x90" // U+e490 +#define ICON_FA_BUGS "\xee\x93\x90" // U+e4d0 +#define ICON_FA_BUILDING "\xef\x86\xad" // U+f1ad +#define ICON_FA_BUILDING_CIRCLE_ARROW_RIGHT "\xee\x93\x91" // U+e4d1 +#define ICON_FA_BUILDING_CIRCLE_CHECK "\xee\x93\x92" // U+e4d2 +#define ICON_FA_BUILDING_CIRCLE_EXCLAMATION "\xee\x93\x93" // U+e4d3 +#define ICON_FA_BUILDING_CIRCLE_XMARK "\xee\x93\x94" // U+e4d4 +#define ICON_FA_BUILDING_COLUMNS "\xef\x86\x9c" // U+f19c +#define ICON_FA_BUILDING_FLAG "\xee\x93\x95" // U+e4d5 +#define ICON_FA_BUILDING_LOCK "\xee\x93\x96" // U+e4d6 +#define ICON_FA_BUILDING_NGO "\xee\x93\x97" // U+e4d7 +#define ICON_FA_BUILDING_SHIELD "\xee\x93\x98" // U+e4d8 +#define ICON_FA_BUILDING_UN "\xee\x93\x99" // U+e4d9 +#define ICON_FA_BUILDING_USER "\xee\x93\x9a" // U+e4da +#define ICON_FA_BUILDING_WHEAT "\xee\x93\x9b" // U+e4db +#define ICON_FA_BULLHORN "\xef\x82\xa1" // U+f0a1 +#define ICON_FA_BULLSEYE "\xef\x85\x80" // U+f140 +#define ICON_FA_BURGER "\xef\xa0\x85" // U+f805 +#define ICON_FA_BURST "\xee\x93\x9c" // U+e4dc +#define ICON_FA_BUS "\xef\x88\x87" // U+f207 +#define ICON_FA_BUS_SIDE "\xee\xa0\x9d" // U+e81d +#define ICON_FA_BUS_SIMPLE "\xef\x95\x9e" // U+f55e +#define ICON_FA_BUSINESS_TIME "\xef\x99\x8a" // U+f64a +#define ICON_FA_C "C" // U+0043 +#define ICON_FA_CABLE_CAR "\xef\x9f\x9a" // U+f7da +#define ICON_FA_CAKE_CANDLES "\xef\x87\xbd" // U+f1fd +#define ICON_FA_CALCULATOR "\xef\x87\xac" // U+f1ec +#define ICON_FA_CALENDAR "\xef\x84\xb3" // U+f133 +#define ICON_FA_CALENDAR_CHECK "\xef\x89\xb4" // U+f274 +#define ICON_FA_CALENDAR_DAY "\xef\x9e\x83" // U+f783 +#define ICON_FA_CALENDAR_DAYS "\xef\x81\xb3" // U+f073 +#define ICON_FA_CALENDAR_MINUS "\xef\x89\xb2" // U+f272 +#define ICON_FA_CALENDAR_PLUS "\xef\x89\xb1" // U+f271 +#define ICON_FA_CALENDAR_WEEK "\xef\x9e\x84" // U+f784 +#define ICON_FA_CALENDAR_XMARK "\xef\x89\xb3" // U+f273 +#define ICON_FA_CAMERA "\xef\x80\xb0" // U+f030 +#define ICON_FA_CAMERA_RETRO "\xef\x82\x83" // U+f083 +#define ICON_FA_CAMERA_ROTATE "\xee\x83\x98" // U+e0d8 +#define ICON_FA_CAMPGROUND "\xef\x9a\xbb" // U+f6bb +#define ICON_FA_CANDY_CANE "\xef\x9e\x86" // U+f786 +#define ICON_FA_CANNABIS "\xef\x95\x9f" // U+f55f +#define ICON_FA_CAPSULES "\xef\x91\xab" // U+f46b +#define ICON_FA_CAR "\xef\x86\xb9" // U+f1b9 +#define ICON_FA_CAR_BATTERY "\xef\x97\x9f" // U+f5df +#define ICON_FA_CAR_BURST "\xef\x97\xa1" // U+f5e1 +#define ICON_FA_CAR_ON "\xee\x93\x9d" // U+e4dd +#define ICON_FA_CAR_REAR "\xef\x97\x9e" // U+f5de +#define ICON_FA_CAR_SIDE "\xef\x97\xa4" // U+f5e4 +#define ICON_FA_CAR_TUNNEL "\xee\x93\x9e" // U+e4de +#define ICON_FA_CARAVAN "\xef\xa3\xbf" // U+f8ff +#define ICON_FA_CARET_DOWN "\xef\x83\x97" // U+f0d7 +#define ICON_FA_CARET_LEFT "\xef\x83\x99" // U+f0d9 +#define ICON_FA_CARET_RIGHT "\xef\x83\x9a" // U+f0da +#define ICON_FA_CARET_UP "\xef\x83\x98" // U+f0d8 +#define ICON_FA_CARROT "\xef\x9e\x87" // U+f787 +#define ICON_FA_CART_ARROW_DOWN "\xef\x88\x98" // U+f218 +#define ICON_FA_CART_FLATBED "\xef\x91\xb4" // U+f474 +#define ICON_FA_CART_FLATBED_SUITCASE "\xef\x96\x9d" // U+f59d +#define ICON_FA_CART_PLUS "\xef\x88\x97" // U+f217 +#define ICON_FA_CART_SHOPPING "\xef\x81\xba" // U+f07a +#define ICON_FA_CASH_REGISTER "\xef\x9e\x88" // U+f788 +#define ICON_FA_CAT "\xef\x9a\xbe" // U+f6be +#define ICON_FA_CEDI_SIGN "\xee\x83\x9f" // U+e0df +#define ICON_FA_CENT_SIGN "\xee\x8f\xb5" // U+e3f5 +#define ICON_FA_CERTIFICATE "\xef\x82\xa3" // U+f0a3 +#define ICON_FA_CHAIR "\xef\x9b\x80" // U+f6c0 +#define ICON_FA_CHALKBOARD "\xef\x94\x9b" // U+f51b +#define ICON_FA_CHALKBOARD_USER "\xef\x94\x9c" // U+f51c +#define ICON_FA_CHAMPAGNE_GLASSES "\xef\x9e\x9f" // U+f79f +#define ICON_FA_CHARGING_STATION "\xef\x97\xa7" // U+f5e7 +#define ICON_FA_CHART_AREA "\xef\x87\xbe" // U+f1fe +#define ICON_FA_CHART_BAR "\xef\x82\x80" // U+f080 +#define ICON_FA_CHART_COLUMN "\xee\x83\xa3" // U+e0e3 +#define ICON_FA_CHART_DIAGRAM "\xee\x9a\x95" // U+e695 +#define ICON_FA_CHART_GANTT "\xee\x83\xa4" // U+e0e4 +#define ICON_FA_CHART_LINE "\xef\x88\x81" // U+f201 +#define ICON_FA_CHART_PIE "\xef\x88\x80" // U+f200 +#define ICON_FA_CHART_SIMPLE "\xee\x91\xb3" // U+e473 +#define ICON_FA_CHECK "\xef\x80\x8c" // U+f00c +#define ICON_FA_CHECK_DOUBLE "\xef\x95\xa0" // U+f560 +#define ICON_FA_CHECK_TO_SLOT "\xef\x9d\xb2" // U+f772 +#define ICON_FA_CHEESE "\xef\x9f\xaf" // U+f7ef +#define ICON_FA_CHESS "\xef\x90\xb9" // U+f439 +#define ICON_FA_CHESS_BISHOP "\xef\x90\xba" // U+f43a +#define ICON_FA_CHESS_BOARD "\xef\x90\xbc" // U+f43c +#define ICON_FA_CHESS_KING "\xef\x90\xbf" // U+f43f +#define ICON_FA_CHESS_KNIGHT "\xef\x91\x81" // U+f441 +#define ICON_FA_CHESS_PAWN "\xef\x91\x83" // U+f443 +#define ICON_FA_CHESS_QUEEN "\xef\x91\x85" // U+f445 +#define ICON_FA_CHESS_ROOK "\xef\x91\x87" // U+f447 +#define ICON_FA_CHEVRON_DOWN "\xef\x81\xb8" // U+f078 +#define ICON_FA_CHEVRON_LEFT "\xef\x81\x93" // U+f053 +#define ICON_FA_CHEVRON_RIGHT "\xef\x81\x94" // U+f054 +#define ICON_FA_CHEVRON_UP "\xef\x81\xb7" // U+f077 +#define ICON_FA_CHILD "\xef\x86\xae" // U+f1ae +#define ICON_FA_CHILD_COMBATANT "\xee\x93\xa0" // U+e4e0 +#define ICON_FA_CHILD_DRESS "\xee\x96\x9c" // U+e59c +#define ICON_FA_CHILD_REACHING "\xee\x96\x9d" // U+e59d +#define ICON_FA_CHILDREN "\xee\x93\xa1" // U+e4e1 +#define ICON_FA_CHURCH "\xef\x94\x9d" // U+f51d +#define ICON_FA_CIRCLE "\xef\x84\x91" // U+f111 +#define ICON_FA_CIRCLE_ARROW_DOWN "\xef\x82\xab" // U+f0ab +#define ICON_FA_CIRCLE_ARROW_LEFT "\xef\x82\xa8" // U+f0a8 +#define ICON_FA_CIRCLE_ARROW_RIGHT "\xef\x82\xa9" // U+f0a9 +#define ICON_FA_CIRCLE_ARROW_UP "\xef\x82\xaa" // U+f0aa +#define ICON_FA_CIRCLE_CHECK "\xef\x81\x98" // U+f058 +#define ICON_FA_CIRCLE_CHEVRON_DOWN "\xef\x84\xba" // U+f13a +#define ICON_FA_CIRCLE_CHEVRON_LEFT "\xef\x84\xb7" // U+f137 +#define ICON_FA_CIRCLE_CHEVRON_RIGHT "\xef\x84\xb8" // U+f138 +#define ICON_FA_CIRCLE_CHEVRON_UP "\xef\x84\xb9" // U+f139 +#define ICON_FA_CIRCLE_DOLLAR_TO_SLOT "\xef\x92\xb9" // U+f4b9 +#define ICON_FA_CIRCLE_DOT "\xef\x86\x92" // U+f192 +#define ICON_FA_CIRCLE_DOWN "\xef\x8d\x98" // U+f358 +#define ICON_FA_CIRCLE_EXCLAMATION "\xef\x81\xaa" // U+f06a +#define ICON_FA_CIRCLE_H "\xef\x91\xbe" // U+f47e +#define ICON_FA_CIRCLE_HALF_STROKE "\xef\x81\x82" // U+f042 +#define ICON_FA_CIRCLE_INFO "\xef\x81\x9a" // U+f05a +#define ICON_FA_CIRCLE_LEFT "\xef\x8d\x99" // U+f359 +#define ICON_FA_CIRCLE_MINUS "\xef\x81\x96" // U+f056 +#define ICON_FA_CIRCLE_NODES "\xee\x93\xa2" // U+e4e2 +#define ICON_FA_CIRCLE_NOTCH "\xef\x87\x8e" // U+f1ce +#define ICON_FA_CIRCLE_PAUSE "\xef\x8a\x8b" // U+f28b +#define ICON_FA_CIRCLE_PLAY "\xef\x85\x84" // U+f144 +#define ICON_FA_CIRCLE_PLUS "\xef\x81\x95" // U+f055 +#define ICON_FA_CIRCLE_QUESTION "\xef\x81\x99" // U+f059 +#define ICON_FA_CIRCLE_RADIATION "\xef\x9e\xba" // U+f7ba +#define ICON_FA_CIRCLE_RIGHT "\xef\x8d\x9a" // U+f35a +#define ICON_FA_CIRCLE_STOP "\xef\x8a\x8d" // U+f28d +#define ICON_FA_CIRCLE_UP "\xef\x8d\x9b" // U+f35b +#define ICON_FA_CIRCLE_USER "\xef\x8a\xbd" // U+f2bd +#define ICON_FA_CIRCLE_XMARK "\xef\x81\x97" // U+f057 +#define ICON_FA_CITY "\xef\x99\x8f" // U+f64f +#define ICON_FA_CLAPPERBOARD "\xee\x84\xb1" // U+e131 +#define ICON_FA_CLIPBOARD "\xef\x8c\xa8" // U+f328 +#define ICON_FA_CLIPBOARD_CHECK "\xef\x91\xac" // U+f46c +#define ICON_FA_CLIPBOARD_LIST "\xef\x91\xad" // U+f46d +#define ICON_FA_CLIPBOARD_QUESTION "\xee\x93\xa3" // U+e4e3 +#define ICON_FA_CLIPBOARD_USER "\xef\x9f\xb3" // U+f7f3 +#define ICON_FA_CLOCK "\xef\x80\x97" // U+f017 +#define ICON_FA_CLOCK_ROTATE_LEFT "\xef\x87\x9a" // U+f1da +#define ICON_FA_CLONE "\xef\x89\x8d" // U+f24d +#define ICON_FA_CLOSED_CAPTIONING "\xef\x88\x8a" // U+f20a +#define ICON_FA_CLOUD "\xef\x83\x82" // U+f0c2 +#define ICON_FA_CLOUD_ARROW_DOWN "\xef\x83\xad" // U+f0ed +#define ICON_FA_CLOUD_ARROW_UP "\xef\x83\xae" // U+f0ee +#define ICON_FA_CLOUD_BOLT "\xef\x9d\xac" // U+f76c +#define ICON_FA_CLOUD_MEATBALL "\xef\x9c\xbb" // U+f73b +#define ICON_FA_CLOUD_MOON "\xef\x9b\x83" // U+f6c3 +#define ICON_FA_CLOUD_MOON_RAIN "\xef\x9c\xbc" // U+f73c +#define ICON_FA_CLOUD_RAIN "\xef\x9c\xbd" // U+f73d +#define ICON_FA_CLOUD_SHOWERS_HEAVY "\xef\x9d\x80" // U+f740 +#define ICON_FA_CLOUD_SHOWERS_WATER "\xee\x93\xa4" // U+e4e4 +#define ICON_FA_CLOUD_SUN "\xef\x9b\x84" // U+f6c4 +#define ICON_FA_CLOUD_SUN_RAIN "\xef\x9d\x83" // U+f743 +#define ICON_FA_CLOVER "\xee\x84\xb9" // U+e139 +#define ICON_FA_CODE "\xef\x84\xa1" // U+f121 +#define ICON_FA_CODE_BRANCH "\xef\x84\xa6" // U+f126 +#define ICON_FA_CODE_COMMIT "\xef\x8e\x86" // U+f386 +#define ICON_FA_CODE_COMPARE "\xee\x84\xba" // U+e13a +#define ICON_FA_CODE_FORK "\xee\x84\xbb" // U+e13b +#define ICON_FA_CODE_MERGE "\xef\x8e\x87" // U+f387 +#define ICON_FA_CODE_PULL_REQUEST "\xee\x84\xbc" // U+e13c +#define ICON_FA_COINS "\xef\x94\x9e" // U+f51e +#define ICON_FA_COLON_SIGN "\xee\x85\x80" // U+e140 +#define ICON_FA_COMMENT "\xef\x81\xb5" // U+f075 +#define ICON_FA_COMMENT_DOLLAR "\xef\x99\x91" // U+f651 +#define ICON_FA_COMMENT_DOTS "\xef\x92\xad" // U+f4ad +#define ICON_FA_COMMENT_MEDICAL "\xef\x9f\xb5" // U+f7f5 +#define ICON_FA_COMMENT_NODES "\xee\x9a\x96" // U+e696 +#define ICON_FA_COMMENT_SLASH "\xef\x92\xb3" // U+f4b3 +#define ICON_FA_COMMENT_SMS "\xef\x9f\x8d" // U+f7cd +#define ICON_FA_COMMENTS "\xef\x82\x86" // U+f086 +#define ICON_FA_COMMENTS_DOLLAR "\xef\x99\x93" // U+f653 +#define ICON_FA_COMPACT_DISC "\xef\x94\x9f" // U+f51f +#define ICON_FA_COMPASS "\xef\x85\x8e" // U+f14e +#define ICON_FA_COMPASS_DRAFTING "\xef\x95\xa8" // U+f568 +#define ICON_FA_COMPRESS "\xef\x81\xa6" // U+f066 +#define ICON_FA_COMPUTER "\xee\x93\xa5" // U+e4e5 +#define ICON_FA_COMPUTER_MOUSE "\xef\xa3\x8c" // U+f8cc +#define ICON_FA_COOKIE "\xef\x95\xa3" // U+f563 +#define ICON_FA_COOKIE_BITE "\xef\x95\xa4" // U+f564 +#define ICON_FA_COPY "\xef\x83\x85" // U+f0c5 +#define ICON_FA_COPYRIGHT "\xef\x87\xb9" // U+f1f9 +#define ICON_FA_COUCH "\xef\x92\xb8" // U+f4b8 +#define ICON_FA_COW "\xef\x9b\x88" // U+f6c8 +#define ICON_FA_CREDIT_CARD "\xef\x82\x9d" // U+f09d +#define ICON_FA_CROP "\xef\x84\xa5" // U+f125 +#define ICON_FA_CROP_SIMPLE "\xef\x95\xa5" // U+f565 +#define ICON_FA_CROSS "\xef\x99\x94" // U+f654 +#define ICON_FA_CROSSHAIRS "\xef\x81\x9b" // U+f05b +#define ICON_FA_CROW "\xef\x94\xa0" // U+f520 +#define ICON_FA_CROWN "\xef\x94\xa1" // U+f521 +#define ICON_FA_CRUTCH "\xef\x9f\xb7" // U+f7f7 +#define ICON_FA_CRUZEIRO_SIGN "\xee\x85\x92" // U+e152 +#define ICON_FA_CUBE "\xef\x86\xb2" // U+f1b2 +#define ICON_FA_CUBES "\xef\x86\xb3" // U+f1b3 +#define ICON_FA_CUBES_STACKED "\xee\x93\xa6" // U+e4e6 +#define ICON_FA_D "D" // U+0044 +#define ICON_FA_DATABASE "\xef\x87\x80" // U+f1c0 +#define ICON_FA_DELETE_LEFT "\xef\x95\x9a" // U+f55a +#define ICON_FA_DEMOCRAT "\xef\x9d\x87" // U+f747 +#define ICON_FA_DESKTOP "\xef\x8e\x90" // U+f390 +#define ICON_FA_DHARMACHAKRA "\xef\x99\x95" // U+f655 +#define ICON_FA_DIAGRAM_NEXT "\xee\x91\xb6" // U+e476 +#define ICON_FA_DIAGRAM_PREDECESSOR "\xee\x91\xb7" // U+e477 +#define ICON_FA_DIAGRAM_PROJECT "\xef\x95\x82" // U+f542 +#define ICON_FA_DIAGRAM_SUCCESSOR "\xee\x91\xba" // U+e47a +#define ICON_FA_DIAMOND "\xef\x88\x99" // U+f219 +#define ICON_FA_DIAMOND_TURN_RIGHT "\xef\x97\xab" // U+f5eb +#define ICON_FA_DICE "\xef\x94\xa2" // U+f522 +#define ICON_FA_DICE_D20 "\xef\x9b\x8f" // U+f6cf +#define ICON_FA_DICE_D6 "\xef\x9b\x91" // U+f6d1 +#define ICON_FA_DICE_FIVE "\xef\x94\xa3" // U+f523 +#define ICON_FA_DICE_FOUR "\xef\x94\xa4" // U+f524 +#define ICON_FA_DICE_ONE "\xef\x94\xa5" // U+f525 +#define ICON_FA_DICE_SIX "\xef\x94\xa6" // U+f526 +#define ICON_FA_DICE_THREE "\xef\x94\xa7" // U+f527 +#define ICON_FA_DICE_TWO "\xef\x94\xa8" // U+f528 +#define ICON_FA_DISEASE "\xef\x9f\xba" // U+f7fa +#define ICON_FA_DISPLAY "\xee\x85\xa3" // U+e163 +#define ICON_FA_DIVIDE "\xef\x94\xa9" // U+f529 +#define ICON_FA_DNA "\xef\x91\xb1" // U+f471 +#define ICON_FA_DOG "\xef\x9b\x93" // U+f6d3 +#define ICON_FA_DOLLAR_SIGN "$" // U+0024 +#define ICON_FA_DOLLY "\xef\x91\xb2" // U+f472 +#define ICON_FA_DONG_SIGN "\xee\x85\xa9" // U+e169 +#define ICON_FA_DOOR_CLOSED "\xef\x94\xaa" // U+f52a +#define ICON_FA_DOOR_OPEN "\xef\x94\xab" // U+f52b +#define ICON_FA_DOVE "\xef\x92\xba" // U+f4ba +#define ICON_FA_DOWN_LEFT_AND_UP_RIGHT_TO_CENTER "\xef\x90\xa2" // U+f422 +#define ICON_FA_DOWN_LONG "\xef\x8c\x89" // U+f309 +#define ICON_FA_DOWNLOAD "\xef\x80\x99" // U+f019 +#define ICON_FA_DRAGON "\xef\x9b\x95" // U+f6d5 +#define ICON_FA_DRAW_POLYGON "\xef\x97\xae" // U+f5ee +#define ICON_FA_DROPLET "\xef\x81\x83" // U+f043 +#define ICON_FA_DROPLET_SLASH "\xef\x97\x87" // U+f5c7 +#define ICON_FA_DRUM "\xef\x95\xa9" // U+f569 +#define ICON_FA_DRUM_STEELPAN "\xef\x95\xaa" // U+f56a +#define ICON_FA_DRUMSTICK_BITE "\xef\x9b\x97" // U+f6d7 +#define ICON_FA_DUMBBELL "\xef\x91\x8b" // U+f44b +#define ICON_FA_DUMPSTER "\xef\x9e\x93" // U+f793 +#define ICON_FA_DUMPSTER_FIRE "\xef\x9e\x94" // U+f794 +#define ICON_FA_DUNGEON "\xef\x9b\x99" // U+f6d9 +#define ICON_FA_E "E" // U+0045 +#define ICON_FA_EAR_DEAF "\xef\x8a\xa4" // U+f2a4 +#define ICON_FA_EAR_LISTEN "\xef\x8a\xa2" // U+f2a2 +#define ICON_FA_EARTH_AFRICA "\xef\x95\xbc" // U+f57c +#define ICON_FA_EARTH_AMERICAS "\xef\x95\xbd" // U+f57d +#define ICON_FA_EARTH_ASIA "\xef\x95\xbe" // U+f57e +#define ICON_FA_EARTH_EUROPE "\xef\x9e\xa2" // U+f7a2 +#define ICON_FA_EARTH_OCEANIA "\xee\x91\xbb" // U+e47b +#define ICON_FA_EGG "\xef\x9f\xbb" // U+f7fb +#define ICON_FA_EJECT "\xef\x81\x92" // U+f052 +#define ICON_FA_ELEVATOR "\xee\x85\xad" // U+e16d +#define ICON_FA_ELLIPSIS "\xef\x85\x81" // U+f141 +#define ICON_FA_ELLIPSIS_VERTICAL "\xef\x85\x82" // U+f142 +#define ICON_FA_ENVELOPE "\xef\x83\xa0" // U+f0e0 +#define ICON_FA_ENVELOPE_CIRCLE_CHECK "\xee\x93\xa8" // U+e4e8 +#define ICON_FA_ENVELOPE_OPEN "\xef\x8a\xb6" // U+f2b6 +#define ICON_FA_ENVELOPE_OPEN_TEXT "\xef\x99\x98" // U+f658 +#define ICON_FA_ENVELOPES_BULK "\xef\x99\xb4" // U+f674 +#define ICON_FA_EQUALS "=" // U+003d +#define ICON_FA_ERASER "\xef\x84\xad" // U+f12d +#define ICON_FA_ETHERNET "\xef\x9e\x96" // U+f796 +#define ICON_FA_EURO_SIGN "\xef\x85\x93" // U+f153 +#define ICON_FA_EXCLAMATION "!" // U+0021 +#define ICON_FA_EXPAND "\xef\x81\xa5" // U+f065 +#define ICON_FA_EXPLOSION "\xee\x93\xa9" // U+e4e9 +#define ICON_FA_EYE "\xef\x81\xae" // U+f06e +#define ICON_FA_EYE_DROPPER "\xef\x87\xbb" // U+f1fb +#define ICON_FA_EYE_LOW_VISION "\xef\x8a\xa8" // U+f2a8 +#define ICON_FA_EYE_SLASH "\xef\x81\xb0" // U+f070 +#define ICON_FA_F "F" // U+0046 +#define ICON_FA_FACE_ANGRY "\xef\x95\x96" // U+f556 +#define ICON_FA_FACE_DIZZY "\xef\x95\xa7" // U+f567 +#define ICON_FA_FACE_FLUSHED "\xef\x95\xb9" // U+f579 +#define ICON_FA_FACE_FROWN "\xef\x84\x99" // U+f119 +#define ICON_FA_FACE_FROWN_OPEN "\xef\x95\xba" // U+f57a +#define ICON_FA_FACE_GRIMACE "\xef\x95\xbf" // U+f57f +#define ICON_FA_FACE_GRIN "\xef\x96\x80" // U+f580 +#define ICON_FA_FACE_GRIN_BEAM "\xef\x96\x82" // U+f582 +#define ICON_FA_FACE_GRIN_BEAM_SWEAT "\xef\x96\x83" // U+f583 +#define ICON_FA_FACE_GRIN_HEARTS "\xef\x96\x84" // U+f584 +#define ICON_FA_FACE_GRIN_SQUINT "\xef\x96\x85" // U+f585 +#define ICON_FA_FACE_GRIN_SQUINT_TEARS "\xef\x96\x86" // U+f586 +#define ICON_FA_FACE_GRIN_STARS "\xef\x96\x87" // U+f587 +#define ICON_FA_FACE_GRIN_TEARS "\xef\x96\x88" // U+f588 +#define ICON_FA_FACE_GRIN_TONGUE "\xef\x96\x89" // U+f589 +#define ICON_FA_FACE_GRIN_TONGUE_SQUINT "\xef\x96\x8a" // U+f58a +#define ICON_FA_FACE_GRIN_TONGUE_WINK "\xef\x96\x8b" // U+f58b +#define ICON_FA_FACE_GRIN_WIDE "\xef\x96\x81" // U+f581 +#define ICON_FA_FACE_GRIN_WINK "\xef\x96\x8c" // U+f58c +#define ICON_FA_FACE_KISS "\xef\x96\x96" // U+f596 +#define ICON_FA_FACE_KISS_BEAM "\xef\x96\x97" // U+f597 +#define ICON_FA_FACE_KISS_WINK_HEART "\xef\x96\x98" // U+f598 +#define ICON_FA_FACE_LAUGH "\xef\x96\x99" // U+f599 +#define ICON_FA_FACE_LAUGH_BEAM "\xef\x96\x9a" // U+f59a +#define ICON_FA_FACE_LAUGH_SQUINT "\xef\x96\x9b" // U+f59b +#define ICON_FA_FACE_LAUGH_WINK "\xef\x96\x9c" // U+f59c +#define ICON_FA_FACE_MEH "\xef\x84\x9a" // U+f11a +#define ICON_FA_FACE_MEH_BLANK "\xef\x96\xa4" // U+f5a4 +#define ICON_FA_FACE_ROLLING_EYES "\xef\x96\xa5" // U+f5a5 +#define ICON_FA_FACE_SAD_CRY "\xef\x96\xb3" // U+f5b3 +#define ICON_FA_FACE_SAD_TEAR "\xef\x96\xb4" // U+f5b4 +#define ICON_FA_FACE_SMILE "\xef\x84\x98" // U+f118 +#define ICON_FA_FACE_SMILE_BEAM "\xef\x96\xb8" // U+f5b8 +#define ICON_FA_FACE_SMILE_WINK "\xef\x93\x9a" // U+f4da +#define ICON_FA_FACE_SURPRISE "\xef\x97\x82" // U+f5c2 +#define ICON_FA_FACE_TIRED "\xef\x97\x88" // U+f5c8 +#define ICON_FA_FAN "\xef\xa1\xa3" // U+f863 +#define ICON_FA_FAUCET "\xee\x80\x85" // U+e005 +#define ICON_FA_FAUCET_DRIP "\xee\x80\x86" // U+e006 +#define ICON_FA_FAX "\xef\x86\xac" // U+f1ac +#define ICON_FA_FEATHER "\xef\x94\xad" // U+f52d +#define ICON_FA_FEATHER_POINTED "\xef\x95\xab" // U+f56b +#define ICON_FA_FERRY "\xee\x93\xaa" // U+e4ea +#define ICON_FA_FILE "\xef\x85\x9b" // U+f15b +#define ICON_FA_FILE_ARROW_DOWN "\xef\x95\xad" // U+f56d +#define ICON_FA_FILE_ARROW_UP "\xef\x95\xb4" // U+f574 +#define ICON_FA_FILE_AUDIO "\xef\x87\x87" // U+f1c7 +#define ICON_FA_FILE_CIRCLE_CHECK "\xee\x96\xa0" // U+e5a0 +#define ICON_FA_FILE_CIRCLE_EXCLAMATION "\xee\x93\xab" // U+e4eb +#define ICON_FA_FILE_CIRCLE_MINUS "\xee\x93\xad" // U+e4ed +#define ICON_FA_FILE_CIRCLE_PLUS "\xee\x92\x94" // U+e494 +#define ICON_FA_FILE_CIRCLE_QUESTION "\xee\x93\xaf" // U+e4ef +#define ICON_FA_FILE_CIRCLE_XMARK "\xee\x96\xa1" // U+e5a1 +#define ICON_FA_FILE_CODE "\xef\x87\x89" // U+f1c9 +#define ICON_FA_FILE_CONTRACT "\xef\x95\xac" // U+f56c +#define ICON_FA_FILE_CSV "\xef\x9b\x9d" // U+f6dd +#define ICON_FA_FILE_EXCEL "\xef\x87\x83" // U+f1c3 +#define ICON_FA_FILE_EXPORT "\xef\x95\xae" // U+f56e +#define ICON_FA_FILE_FRAGMENT "\xee\x9a\x97" // U+e697 +#define ICON_FA_FILE_HALF_DASHED "\xee\x9a\x98" // U+e698 +#define ICON_FA_FILE_IMAGE "\xef\x87\x85" // U+f1c5 +#define ICON_FA_FILE_IMPORT "\xef\x95\xaf" // U+f56f +#define ICON_FA_FILE_INVOICE "\xef\x95\xb0" // U+f570 +#define ICON_FA_FILE_INVOICE_DOLLAR "\xef\x95\xb1" // U+f571 +#define ICON_FA_FILE_LINES "\xef\x85\x9c" // U+f15c +#define ICON_FA_FILE_MEDICAL "\xef\x91\xb7" // U+f477 +#define ICON_FA_FILE_PDF "\xef\x87\x81" // U+f1c1 +#define ICON_FA_FILE_PEN "\xef\x8c\x9c" // U+f31c +#define ICON_FA_FILE_POWERPOINT "\xef\x87\x84" // U+f1c4 +#define ICON_FA_FILE_PRESCRIPTION "\xef\x95\xb2" // U+f572 +#define ICON_FA_FILE_SHIELD "\xee\x93\xb0" // U+e4f0 +#define ICON_FA_FILE_SIGNATURE "\xef\x95\xb3" // U+f573 +#define ICON_FA_FILE_VIDEO "\xef\x87\x88" // U+f1c8 +#define ICON_FA_FILE_WAVEFORM "\xef\x91\xb8" // U+f478 +#define ICON_FA_FILE_WORD "\xef\x87\x82" // U+f1c2 +#define ICON_FA_FILE_ZIPPER "\xef\x87\x86" // U+f1c6 +#define ICON_FA_FILL "\xef\x95\xb5" // U+f575 +#define ICON_FA_FILL_DRIP "\xef\x95\xb6" // U+f576 +#define ICON_FA_FILM "\xef\x80\x88" // U+f008 +#define ICON_FA_FILTER "\xef\x82\xb0" // U+f0b0 +#define ICON_FA_FILTER_CIRCLE_DOLLAR "\xef\x99\xa2" // U+f662 +#define ICON_FA_FILTER_CIRCLE_XMARK "\xee\x85\xbb" // U+e17b +#define ICON_FA_FINGERPRINT "\xef\x95\xb7" // U+f577 +#define ICON_FA_FIRE "\xef\x81\xad" // U+f06d +#define ICON_FA_FIRE_BURNER "\xee\x93\xb1" // U+e4f1 +#define ICON_FA_FIRE_EXTINGUISHER "\xef\x84\xb4" // U+f134 +#define ICON_FA_FIRE_FLAME_CURVED "\xef\x9f\xa4" // U+f7e4 +#define ICON_FA_FIRE_FLAME_SIMPLE "\xef\x91\xaa" // U+f46a +#define ICON_FA_FISH "\xef\x95\xb8" // U+f578 +#define ICON_FA_FISH_FINS "\xee\x93\xb2" // U+e4f2 +#define ICON_FA_FLAG "\xef\x80\xa4" // U+f024 +#define ICON_FA_FLAG_CHECKERED "\xef\x84\x9e" // U+f11e +#define ICON_FA_FLAG_USA "\xef\x9d\x8d" // U+f74d +#define ICON_FA_FLASK "\xef\x83\x83" // U+f0c3 +#define ICON_FA_FLASK_VIAL "\xee\x93\xb3" // U+e4f3 +#define ICON_FA_FLOPPY_DISK "\xef\x83\x87" // U+f0c7 +#define ICON_FA_FLORIN_SIGN "\xee\x86\x84" // U+e184 +#define ICON_FA_FOLDER "\xef\x81\xbb" // U+f07b +#define ICON_FA_FOLDER_CLOSED "\xee\x86\x85" // U+e185 +#define ICON_FA_FOLDER_MINUS "\xef\x99\x9d" // U+f65d +#define ICON_FA_FOLDER_OPEN "\xef\x81\xbc" // U+f07c +#define ICON_FA_FOLDER_PLUS "\xef\x99\x9e" // U+f65e +#define ICON_FA_FOLDER_TREE "\xef\xa0\x82" // U+f802 +#define ICON_FA_FONT "\xef\x80\xb1" // U+f031 +#define ICON_FA_FONT_AWESOME "\xef\x8a\xb4" // U+f2b4 +#define ICON_FA_FOOTBALL "\xef\x91\x8e" // U+f44e +#define ICON_FA_FORWARD "\xef\x81\x8e" // U+f04e +#define ICON_FA_FORWARD_FAST "\xef\x81\x90" // U+f050 +#define ICON_FA_FORWARD_STEP "\xef\x81\x91" // U+f051 +#define ICON_FA_FRANC_SIGN "\xee\x86\x8f" // U+e18f +#define ICON_FA_FROG "\xef\x94\xae" // U+f52e +#define ICON_FA_FUTBOL "\xef\x87\xa3" // U+f1e3 +#define ICON_FA_G "G" // U+0047 +#define ICON_FA_GAMEPAD "\xef\x84\x9b" // U+f11b +#define ICON_FA_GAS_PUMP "\xef\x94\xaf" // U+f52f +#define ICON_FA_GAUGE "\xef\x98\xa4" // U+f624 +#define ICON_FA_GAUGE_HIGH "\xef\x98\xa5" // U+f625 +#define ICON_FA_GAUGE_SIMPLE "\xef\x98\xa9" // U+f629 +#define ICON_FA_GAUGE_SIMPLE_HIGH "\xef\x98\xaa" // U+f62a +#define ICON_FA_GAVEL "\xef\x83\xa3" // U+f0e3 +#define ICON_FA_GEAR "\xef\x80\x93" // U+f013 +#define ICON_FA_GEARS "\xef\x82\x85" // U+f085 +#define ICON_FA_GEM "\xef\x8e\xa5" // U+f3a5 +#define ICON_FA_GENDERLESS "\xef\x88\xad" // U+f22d +#define ICON_FA_GHOST "\xef\x9b\xa2" // U+f6e2 +#define ICON_FA_GIFT "\xef\x81\xab" // U+f06b +#define ICON_FA_GIFTS "\xef\x9e\x9c" // U+f79c +#define ICON_FA_GLASS_WATER "\xee\x93\xb4" // U+e4f4 +#define ICON_FA_GLASS_WATER_DROPLET "\xee\x93\xb5" // U+e4f5 +#define ICON_FA_GLASSES "\xef\x94\xb0" // U+f530 +#define ICON_FA_GLOBE "\xef\x82\xac" // U+f0ac +#define ICON_FA_GOLF_BALL_TEE "\xef\x91\x90" // U+f450 +#define ICON_FA_GOPURAM "\xef\x99\xa4" // U+f664 +#define ICON_FA_GRADUATION_CAP "\xef\x86\x9d" // U+f19d +#define ICON_FA_GREATER_THAN ">" // U+003e +#define ICON_FA_GREATER_THAN_EQUAL "\xef\x94\xb2" // U+f532 +#define ICON_FA_GRIP "\xef\x96\x8d" // U+f58d +#define ICON_FA_GRIP_LINES "\xef\x9e\xa4" // U+f7a4 +#define ICON_FA_GRIP_LINES_VERTICAL "\xef\x9e\xa5" // U+f7a5 +#define ICON_FA_GRIP_VERTICAL "\xef\x96\x8e" // U+f58e +#define ICON_FA_GROUP_ARROWS_ROTATE "\xee\x93\xb6" // U+e4f6 +#define ICON_FA_GUARANI_SIGN "\xee\x86\x9a" // U+e19a +#define ICON_FA_GUITAR "\xef\x9e\xa6" // U+f7a6 +#define ICON_FA_GUN "\xee\x86\x9b" // U+e19b +#define ICON_FA_H "H" // U+0048 +#define ICON_FA_HAMMER "\xef\x9b\xa3" // U+f6e3 +#define ICON_FA_HAMSA "\xef\x99\xa5" // U+f665 +#define ICON_FA_HAND "\xef\x89\x96" // U+f256 +#define ICON_FA_HAND_BACK_FIST "\xef\x89\x95" // U+f255 +#define ICON_FA_HAND_DOTS "\xef\x91\xa1" // U+f461 +#define ICON_FA_HAND_FIST "\xef\x9b\x9e" // U+f6de +#define ICON_FA_HAND_HOLDING "\xef\x92\xbd" // U+f4bd +#define ICON_FA_HAND_HOLDING_DOLLAR "\xef\x93\x80" // U+f4c0 +#define ICON_FA_HAND_HOLDING_DROPLET "\xef\x93\x81" // U+f4c1 +#define ICON_FA_HAND_HOLDING_HAND "\xee\x93\xb7" // U+e4f7 +#define ICON_FA_HAND_HOLDING_HEART "\xef\x92\xbe" // U+f4be +#define ICON_FA_HAND_HOLDING_MEDICAL "\xee\x81\x9c" // U+e05c +#define ICON_FA_HAND_LIZARD "\xef\x89\x98" // U+f258 +#define ICON_FA_HAND_MIDDLE_FINGER "\xef\xa0\x86" // U+f806 +#define ICON_FA_HAND_PEACE "\xef\x89\x9b" // U+f25b +#define ICON_FA_HAND_POINT_DOWN "\xef\x82\xa7" // U+f0a7 +#define ICON_FA_HAND_POINT_LEFT "\xef\x82\xa5" // U+f0a5 +#define ICON_FA_HAND_POINT_RIGHT "\xef\x82\xa4" // U+f0a4 +#define ICON_FA_HAND_POINT_UP "\xef\x82\xa6" // U+f0a6 +#define ICON_FA_HAND_POINTER "\xef\x89\x9a" // U+f25a +#define ICON_FA_HAND_SCISSORS "\xef\x89\x97" // U+f257 +#define ICON_FA_HAND_SPARKLES "\xee\x81\x9d" // U+e05d +#define ICON_FA_HAND_SPOCK "\xef\x89\x99" // U+f259 +#define ICON_FA_HANDCUFFS "\xee\x93\xb8" // U+e4f8 +#define ICON_FA_HANDS "\xef\x8a\xa7" // U+f2a7 +#define ICON_FA_HANDS_ASL_INTERPRETING "\xef\x8a\xa3" // U+f2a3 +#define ICON_FA_HANDS_BOUND "\xee\x93\xb9" // U+e4f9 +#define ICON_FA_HANDS_BUBBLES "\xee\x81\x9e" // U+e05e +#define ICON_FA_HANDS_CLAPPING "\xee\x86\xa8" // U+e1a8 +#define ICON_FA_HANDS_HOLDING "\xef\x93\x82" // U+f4c2 +#define ICON_FA_HANDS_HOLDING_CHILD "\xee\x93\xba" // U+e4fa +#define ICON_FA_HANDS_HOLDING_CIRCLE "\xee\x93\xbb" // U+e4fb +#define ICON_FA_HANDS_PRAYING "\xef\x9a\x84" // U+f684 +#define ICON_FA_HANDSHAKE "\xef\x8a\xb5" // U+f2b5 +#define ICON_FA_HANDSHAKE_ANGLE "\xef\x93\x84" // U+f4c4 +#define ICON_FA_HANDSHAKE_SLASH "\xee\x81\xa0" // U+e060 +#define ICON_FA_HANUKIAH "\xef\x9b\xa6" // U+f6e6 +#define ICON_FA_HARD_DRIVE "\xef\x82\xa0" // U+f0a0 +#define ICON_FA_HASHTAG "#" // U+0023 +#define ICON_FA_HAT_COWBOY "\xef\xa3\x80" // U+f8c0 +#define ICON_FA_HAT_COWBOY_SIDE "\xef\xa3\x81" // U+f8c1 +#define ICON_FA_HAT_WIZARD "\xef\x9b\xa8" // U+f6e8 +#define ICON_FA_HEAD_SIDE_COUGH "\xee\x81\xa1" // U+e061 +#define ICON_FA_HEAD_SIDE_COUGH_SLASH "\xee\x81\xa2" // U+e062 +#define ICON_FA_HEAD_SIDE_MASK "\xee\x81\xa3" // U+e063 +#define ICON_FA_HEAD_SIDE_VIRUS "\xee\x81\xa4" // U+e064 +#define ICON_FA_HEADING "\xef\x87\x9c" // U+f1dc +#define ICON_FA_HEADPHONES "\xef\x80\xa5" // U+f025 +#define ICON_FA_HEADSET "\xef\x96\x90" // U+f590 +#define ICON_FA_HEART "\xef\x80\x84" // U+f004 +#define ICON_FA_HEART_CIRCLE_BOLT "\xee\x93\xbc" // U+e4fc +#define ICON_FA_HEART_CIRCLE_CHECK "\xee\x93\xbd" // U+e4fd +#define ICON_FA_HEART_CIRCLE_EXCLAMATION "\xee\x93\xbe" // U+e4fe +#define ICON_FA_HEART_CIRCLE_MINUS "\xee\x93\xbf" // U+e4ff +#define ICON_FA_HEART_CIRCLE_PLUS "\xee\x94\x80" // U+e500 +#define ICON_FA_HEART_CIRCLE_XMARK "\xee\x94\x81" // U+e501 +#define ICON_FA_HEART_CRACK "\xef\x9e\xa9" // U+f7a9 +#define ICON_FA_HEART_PULSE "\xef\x88\x9e" // U+f21e +#define ICON_FA_HELICOPTER "\xef\x94\xb3" // U+f533 +#define ICON_FA_HELICOPTER_SYMBOL "\xee\x94\x82" // U+e502 +#define ICON_FA_HELMET_SAFETY "\xef\xa0\x87" // U+f807 +#define ICON_FA_HELMET_UN "\xee\x94\x83" // U+e503 +#define ICON_FA_HEXAGON "\xef\x8c\x92" // U+f312 +#define ICON_FA_HEXAGON_NODES "\xee\x9a\x99" // U+e699 +#define ICON_FA_HEXAGON_NODES_BOLT "\xee\x9a\x9a" // U+e69a +#define ICON_FA_HIGHLIGHTER "\xef\x96\x91" // U+f591 +#define ICON_FA_HILL_AVALANCHE "\xee\x94\x87" // U+e507 +#define ICON_FA_HILL_ROCKSLIDE "\xee\x94\x88" // U+e508 +#define ICON_FA_HIPPO "\xef\x9b\xad" // U+f6ed +#define ICON_FA_HOCKEY_PUCK "\xef\x91\x93" // U+f453 +#define ICON_FA_HOLLY_BERRY "\xef\x9e\xaa" // U+f7aa +#define ICON_FA_HORSE "\xef\x9b\xb0" // U+f6f0 +#define ICON_FA_HORSE_HEAD "\xef\x9e\xab" // U+f7ab +#define ICON_FA_HOSPITAL "\xef\x83\xb8" // U+f0f8 +#define ICON_FA_HOSPITAL_USER "\xef\xa0\x8d" // U+f80d +#define ICON_FA_HOT_TUB_PERSON "\xef\x96\x93" // U+f593 +#define ICON_FA_HOTDOG "\xef\xa0\x8f" // U+f80f +#define ICON_FA_HOTEL "\xef\x96\x94" // U+f594 +#define ICON_FA_HOURGLASS "\xef\x89\x94" // U+f254 +#define ICON_FA_HOURGLASS_END "\xef\x89\x93" // U+f253 +#define ICON_FA_HOURGLASS_HALF "\xef\x89\x92" // U+f252 +#define ICON_FA_HOURGLASS_START "\xef\x89\x91" // U+f251 +#define ICON_FA_HOUSE "\xef\x80\x95" // U+f015 +#define ICON_FA_HOUSE_CHIMNEY "\xee\x8e\xaf" // U+e3af +#define ICON_FA_HOUSE_CHIMNEY_CRACK "\xef\x9b\xb1" // U+f6f1 +#define ICON_FA_HOUSE_CHIMNEY_MEDICAL "\xef\x9f\xb2" // U+f7f2 +#define ICON_FA_HOUSE_CHIMNEY_USER "\xee\x81\xa5" // U+e065 +#define ICON_FA_HOUSE_CHIMNEY_WINDOW "\xee\x80\x8d" // U+e00d +#define ICON_FA_HOUSE_CIRCLE_CHECK "\xee\x94\x89" // U+e509 +#define ICON_FA_HOUSE_CIRCLE_EXCLAMATION "\xee\x94\x8a" // U+e50a +#define ICON_FA_HOUSE_CIRCLE_XMARK "\xee\x94\x8b" // U+e50b +#define ICON_FA_HOUSE_CRACK "\xee\x8e\xb1" // U+e3b1 +#define ICON_FA_HOUSE_FIRE "\xee\x94\x8c" // U+e50c +#define ICON_FA_HOUSE_FLAG "\xee\x94\x8d" // U+e50d +#define ICON_FA_HOUSE_FLOOD_WATER "\xee\x94\x8e" // U+e50e +#define ICON_FA_HOUSE_FLOOD_WATER_CIRCLE_ARROW_RIGHT "\xee\x94\x8f" // U+e50f +#define ICON_FA_HOUSE_LAPTOP "\xee\x81\xa6" // U+e066 +#define ICON_FA_HOUSE_LOCK "\xee\x94\x90" // U+e510 +#define ICON_FA_HOUSE_MEDICAL "\xee\x8e\xb2" // U+e3b2 +#define ICON_FA_HOUSE_MEDICAL_CIRCLE_CHECK "\xee\x94\x91" // U+e511 +#define ICON_FA_HOUSE_MEDICAL_CIRCLE_EXCLAMATION "\xee\x94\x92" // U+e512 +#define ICON_FA_HOUSE_MEDICAL_CIRCLE_XMARK "\xee\x94\x93" // U+e513 +#define ICON_FA_HOUSE_MEDICAL_FLAG "\xee\x94\x94" // U+e514 +#define ICON_FA_HOUSE_SIGNAL "\xee\x80\x92" // U+e012 +#define ICON_FA_HOUSE_TSUNAMI "\xee\x94\x95" // U+e515 +#define ICON_FA_HOUSE_USER "\xee\x86\xb0" // U+e1b0 +#define ICON_FA_HRYVNIA_SIGN "\xef\x9b\xb2" // U+f6f2 +#define ICON_FA_HURRICANE "\xef\x9d\x91" // U+f751 +#define ICON_FA_I "I" // U+0049 +#define ICON_FA_I_CURSOR "\xef\x89\x86" // U+f246 +#define ICON_FA_ICE_CREAM "\xef\xa0\x90" // U+f810 +#define ICON_FA_ICICLES "\xef\x9e\xad" // U+f7ad +#define ICON_FA_ICONS "\xef\xa1\xad" // U+f86d +#define ICON_FA_ID_BADGE "\xef\x8b\x81" // U+f2c1 +#define ICON_FA_ID_CARD "\xef\x8b\x82" // U+f2c2 +#define ICON_FA_ID_CARD_CLIP "\xef\x91\xbf" // U+f47f +#define ICON_FA_IGLOO "\xef\x9e\xae" // U+f7ae +#define ICON_FA_IMAGE "\xef\x80\xbe" // U+f03e +#define ICON_FA_IMAGE_PORTRAIT "\xef\x8f\xa0" // U+f3e0 +#define ICON_FA_IMAGES "\xef\x8c\x82" // U+f302 +#define ICON_FA_INBOX "\xef\x80\x9c" // U+f01c +#define ICON_FA_INDENT "\xef\x80\xbc" // U+f03c +#define ICON_FA_INDIAN_RUPEE_SIGN "\xee\x86\xbc" // U+e1bc +#define ICON_FA_INDUSTRY "\xef\x89\xb5" // U+f275 +#define ICON_FA_INFINITY "\xef\x94\xb4" // U+f534 +#define ICON_FA_INFO "\xef\x84\xa9" // U+f129 +#define ICON_FA_ITALIC "\xef\x80\xb3" // U+f033 +#define ICON_FA_J "J" // U+004a +#define ICON_FA_JAR "\xee\x94\x96" // U+e516 +#define ICON_FA_JAR_WHEAT "\xee\x94\x97" // U+e517 +#define ICON_FA_JEDI "\xef\x99\xa9" // U+f669 +#define ICON_FA_JET_FIGHTER "\xef\x83\xbb" // U+f0fb +#define ICON_FA_JET_FIGHTER_UP "\xee\x94\x98" // U+e518 +#define ICON_FA_JOINT "\xef\x96\x95" // U+f595 +#define ICON_FA_JUG_DETERGENT "\xee\x94\x99" // U+e519 +#define ICON_FA_K "K" // U+004b +#define ICON_FA_KAABA "\xef\x99\xab" // U+f66b +#define ICON_FA_KEY "\xef\x82\x84" // U+f084 +#define ICON_FA_KEYBOARD "\xef\x84\x9c" // U+f11c +#define ICON_FA_KHANDA "\xef\x99\xad" // U+f66d +#define ICON_FA_KIP_SIGN "\xee\x87\x84" // U+e1c4 +#define ICON_FA_KIT_MEDICAL "\xef\x91\xb9" // U+f479 +#define ICON_FA_KITCHEN_SET "\xee\x94\x9a" // U+e51a +#define ICON_FA_KIWI_BIRD "\xef\x94\xb5" // U+f535 +#define ICON_FA_L "L" // U+004c +#define ICON_FA_LAND_MINE_ON "\xee\x94\x9b" // U+e51b +#define ICON_FA_LANDMARK "\xef\x99\xaf" // U+f66f +#define ICON_FA_LANDMARK_DOME "\xef\x9d\x92" // U+f752 +#define ICON_FA_LANDMARK_FLAG "\xee\x94\x9c" // U+e51c +#define ICON_FA_LANGUAGE "\xef\x86\xab" // U+f1ab +#define ICON_FA_LAPTOP "\xef\x84\x89" // U+f109 +#define ICON_FA_LAPTOP_CODE "\xef\x97\xbc" // U+f5fc +#define ICON_FA_LAPTOP_FILE "\xee\x94\x9d" // U+e51d +#define ICON_FA_LAPTOP_MEDICAL "\xef\xa0\x92" // U+f812 +#define ICON_FA_LARI_SIGN "\xee\x87\x88" // U+e1c8 +#define ICON_FA_LAYER_GROUP "\xef\x97\xbd" // U+f5fd +#define ICON_FA_LEAF "\xef\x81\xac" // U+f06c +#define ICON_FA_LEFT_LONG "\xef\x8c\x8a" // U+f30a +#define ICON_FA_LEFT_RIGHT "\xef\x8c\xb7" // U+f337 +#define ICON_FA_LEMON "\xef\x82\x94" // U+f094 +#define ICON_FA_LESS_THAN "<" // U+003c +#define ICON_FA_LESS_THAN_EQUAL "\xef\x94\xb7" // U+f537 +#define ICON_FA_LIFE_RING "\xef\x87\x8d" // U+f1cd +#define ICON_FA_LIGHTBULB "\xef\x83\xab" // U+f0eb +#define ICON_FA_LINES_LEANING "\xee\x94\x9e" // U+e51e +#define ICON_FA_LINK "\xef\x83\x81" // U+f0c1 +#define ICON_FA_LINK_SLASH "\xef\x84\xa7" // U+f127 +#define ICON_FA_LIRA_SIGN "\xef\x86\x95" // U+f195 +#define ICON_FA_LIST "\xef\x80\xba" // U+f03a +#define ICON_FA_LIST_CHECK "\xef\x82\xae" // U+f0ae +#define ICON_FA_LIST_OL "\xef\x83\x8b" // U+f0cb +#define ICON_FA_LIST_UL "\xef\x83\x8a" // U+f0ca +#define ICON_FA_LITECOIN_SIGN "\xee\x87\x93" // U+e1d3 +#define ICON_FA_LOCATION_ARROW "\xef\x84\xa4" // U+f124 +#define ICON_FA_LOCATION_CROSSHAIRS "\xef\x98\x81" // U+f601 +#define ICON_FA_LOCATION_DOT "\xef\x8f\x85" // U+f3c5 +#define ICON_FA_LOCATION_PIN "\xef\x81\x81" // U+f041 +#define ICON_FA_LOCATION_PIN_LOCK "\xee\x94\x9f" // U+e51f +#define ICON_FA_LOCK "\xef\x80\xa3" // U+f023 +#define ICON_FA_LOCK_OPEN "\xef\x8f\x81" // U+f3c1 +#define ICON_FA_LOCUST "\xee\x94\xa0" // U+e520 +#define ICON_FA_LUNGS "\xef\x98\x84" // U+f604 +#define ICON_FA_LUNGS_VIRUS "\xee\x81\xa7" // U+e067 +#define ICON_FA_M "M" // U+004d +#define ICON_FA_MAGNET "\xef\x81\xb6" // U+f076 +#define ICON_FA_MAGNIFYING_GLASS "\xef\x80\x82" // U+f002 +#define ICON_FA_MAGNIFYING_GLASS_ARROW_RIGHT "\xee\x94\xa1" // U+e521 +#define ICON_FA_MAGNIFYING_GLASS_CHART "\xee\x94\xa2" // U+e522 +#define ICON_FA_MAGNIFYING_GLASS_DOLLAR "\xef\x9a\x88" // U+f688 +#define ICON_FA_MAGNIFYING_GLASS_LOCATION "\xef\x9a\x89" // U+f689 +#define ICON_FA_MAGNIFYING_GLASS_MINUS "\xef\x80\x90" // U+f010 +#define ICON_FA_MAGNIFYING_GLASS_PLUS "\xef\x80\x8e" // U+f00e +#define ICON_FA_MANAT_SIGN "\xee\x87\x95" // U+e1d5 +#define ICON_FA_MAP "\xef\x89\xb9" // U+f279 +#define ICON_FA_MAP_LOCATION "\xef\x96\x9f" // U+f59f +#define ICON_FA_MAP_LOCATION_DOT "\xef\x96\xa0" // U+f5a0 +#define ICON_FA_MAP_PIN "\xef\x89\xb6" // U+f276 +#define ICON_FA_MARKER "\xef\x96\xa1" // U+f5a1 +#define ICON_FA_MARS "\xef\x88\xa2" // U+f222 +#define ICON_FA_MARS_AND_VENUS "\xef\x88\xa4" // U+f224 +#define ICON_FA_MARS_AND_VENUS_BURST "\xee\x94\xa3" // U+e523 +#define ICON_FA_MARS_DOUBLE "\xef\x88\xa7" // U+f227 +#define ICON_FA_MARS_STROKE "\xef\x88\xa9" // U+f229 +#define ICON_FA_MARS_STROKE_RIGHT "\xef\x88\xab" // U+f22b +#define ICON_FA_MARS_STROKE_UP "\xef\x88\xaa" // U+f22a +#define ICON_FA_MARTINI_GLASS "\xef\x95\xbb" // U+f57b +#define ICON_FA_MARTINI_GLASS_CITRUS "\xef\x95\xa1" // U+f561 +#define ICON_FA_MARTINI_GLASS_EMPTY "\xef\x80\x80" // U+f000 +#define ICON_FA_MASK "\xef\x9b\xba" // U+f6fa +#define ICON_FA_MASK_FACE "\xee\x87\x97" // U+e1d7 +#define ICON_FA_MASK_VENTILATOR "\xee\x94\xa4" // U+e524 +#define ICON_FA_MASKS_THEATER "\xef\x98\xb0" // U+f630 +#define ICON_FA_MATTRESS_PILLOW "\xee\x94\xa5" // U+e525 +#define ICON_FA_MAXIMIZE "\xef\x8c\x9e" // U+f31e +#define ICON_FA_MEDAL "\xef\x96\xa2" // U+f5a2 +#define ICON_FA_MEMORY "\xef\x94\xb8" // U+f538 +#define ICON_FA_MENORAH "\xef\x99\xb6" // U+f676 +#define ICON_FA_MERCURY "\xef\x88\xa3" // U+f223 +#define ICON_FA_MESSAGE "\xef\x89\xba" // U+f27a +#define ICON_FA_METEOR "\xef\x9d\x93" // U+f753 +#define ICON_FA_MICROCHIP "\xef\x8b\x9b" // U+f2db +#define ICON_FA_MICROPHONE "\xef\x84\xb0" // U+f130 +#define ICON_FA_MICROPHONE_LINES "\xef\x8f\x89" // U+f3c9 +#define ICON_FA_MICROPHONE_LINES_SLASH "\xef\x94\xb9" // U+f539 +#define ICON_FA_MICROPHONE_SLASH "\xef\x84\xb1" // U+f131 +#define ICON_FA_MICROSCOPE "\xef\x98\x90" // U+f610 +#define ICON_FA_MILL_SIGN "\xee\x87\xad" // U+e1ed +#define ICON_FA_MINIMIZE "\xef\x9e\x8c" // U+f78c +#define ICON_FA_MINUS "\xef\x81\xa8" // U+f068 +#define ICON_FA_MITTEN "\xef\x9e\xb5" // U+f7b5 +#define ICON_FA_MOBILE "\xef\x8f\x8e" // U+f3ce +#define ICON_FA_MOBILE_BUTTON "\xef\x84\x8b" // U+f10b +#define ICON_FA_MOBILE_RETRO "\xee\x94\xa7" // U+e527 +#define ICON_FA_MOBILE_SCREEN "\xef\x8f\x8f" // U+f3cf +#define ICON_FA_MOBILE_SCREEN_BUTTON "\xef\x8f\x8d" // U+f3cd +#define ICON_FA_MOBILE_VIBRATE "\xee\xa0\x96" // U+e816 +#define ICON_FA_MONEY_BILL "\xef\x83\x96" // U+f0d6 +#define ICON_FA_MONEY_BILL_1 "\xef\x8f\x91" // U+f3d1 +#define ICON_FA_MONEY_BILL_1_WAVE "\xef\x94\xbb" // U+f53b +#define ICON_FA_MONEY_BILL_TRANSFER "\xee\x94\xa8" // U+e528 +#define ICON_FA_MONEY_BILL_TREND_UP "\xee\x94\xa9" // U+e529 +#define ICON_FA_MONEY_BILL_WAVE "\xef\x94\xba" // U+f53a +#define ICON_FA_MONEY_BILL_WHEAT "\xee\x94\xaa" // U+e52a +#define ICON_FA_MONEY_BILLS "\xee\x87\xb3" // U+e1f3 +#define ICON_FA_MONEY_CHECK "\xef\x94\xbc" // U+f53c +#define ICON_FA_MONEY_CHECK_DOLLAR "\xef\x94\xbd" // U+f53d +#define ICON_FA_MONUMENT "\xef\x96\xa6" // U+f5a6 +#define ICON_FA_MOON "\xef\x86\x86" // U+f186 +#define ICON_FA_MORTAR_PESTLE "\xef\x96\xa7" // U+f5a7 +#define ICON_FA_MOSQUE "\xef\x99\xb8" // U+f678 +#define ICON_FA_MOSQUITO "\xee\x94\xab" // U+e52b +#define ICON_FA_MOSQUITO_NET "\xee\x94\xac" // U+e52c +#define ICON_FA_MOTORCYCLE "\xef\x88\x9c" // U+f21c +#define ICON_FA_MOUND "\xee\x94\xad" // U+e52d +#define ICON_FA_MOUNTAIN "\xef\x9b\xbc" // U+f6fc +#define ICON_FA_MOUNTAIN_CITY "\xee\x94\xae" // U+e52e +#define ICON_FA_MOUNTAIN_SUN "\xee\x94\xaf" // U+e52f +#define ICON_FA_MUG_HOT "\xef\x9e\xb6" // U+f7b6 +#define ICON_FA_MUG_SAUCER "\xef\x83\xb4" // U+f0f4 +#define ICON_FA_MUSIC "\xef\x80\x81" // U+f001 +#define ICON_FA_N "N" // U+004e +#define ICON_FA_NAIRA_SIGN "\xee\x87\xb6" // U+e1f6 +#define ICON_FA_NETWORK_WIRED "\xef\x9b\xbf" // U+f6ff +#define ICON_FA_NEUTER "\xef\x88\xac" // U+f22c +#define ICON_FA_NEWSPAPER "\xef\x87\xaa" // U+f1ea +#define ICON_FA_NON_BINARY "\xee\xa0\x87" // U+e807 +#define ICON_FA_NOT_EQUAL "\xef\x94\xbe" // U+f53e +#define ICON_FA_NOTDEF "\xee\x87\xbe" // U+e1fe +#define ICON_FA_NOTE_STICKY "\xef\x89\x89" // U+f249 +#define ICON_FA_NOTES_MEDICAL "\xef\x92\x81" // U+f481 +#define ICON_FA_O "O" // U+004f +#define ICON_FA_OBJECT_GROUP "\xef\x89\x87" // U+f247 +#define ICON_FA_OBJECT_UNGROUP "\xef\x89\x88" // U+f248 +#define ICON_FA_OCTAGON "\xef\x8c\x86" // U+f306 +#define ICON_FA_OIL_CAN "\xef\x98\x93" // U+f613 +#define ICON_FA_OIL_WELL "\xee\x94\xb2" // U+e532 +#define ICON_FA_OM "\xef\x99\xb9" // U+f679 +#define ICON_FA_OTTER "\xef\x9c\x80" // U+f700 +#define ICON_FA_OUTDENT "\xef\x80\xbb" // U+f03b +#define ICON_FA_P "P" // U+0050 +#define ICON_FA_PAGER "\xef\xa0\x95" // U+f815 +#define ICON_FA_PAINT_ROLLER "\xef\x96\xaa" // U+f5aa +#define ICON_FA_PAINTBRUSH "\xef\x87\xbc" // U+f1fc +#define ICON_FA_PALETTE "\xef\x94\xbf" // U+f53f +#define ICON_FA_PALLET "\xef\x92\x82" // U+f482 +#define ICON_FA_PANORAMA "\xee\x88\x89" // U+e209 +#define ICON_FA_PAPER_PLANE "\xef\x87\x98" // U+f1d8 +#define ICON_FA_PAPERCLIP "\xef\x83\x86" // U+f0c6 +#define ICON_FA_PARACHUTE_BOX "\xef\x93\x8d" // U+f4cd +#define ICON_FA_PARAGRAPH "\xef\x87\x9d" // U+f1dd +#define ICON_FA_PASSPORT "\xef\x96\xab" // U+f5ab +#define ICON_FA_PASTE "\xef\x83\xaa" // U+f0ea +#define ICON_FA_PAUSE "\xef\x81\x8c" // U+f04c +#define ICON_FA_PAW "\xef\x86\xb0" // U+f1b0 +#define ICON_FA_PEACE "\xef\x99\xbc" // U+f67c +#define ICON_FA_PEN "\xef\x8c\x84" // U+f304 +#define ICON_FA_PEN_CLIP "\xef\x8c\x85" // U+f305 +#define ICON_FA_PEN_FANCY "\xef\x96\xac" // U+f5ac +#define ICON_FA_PEN_NIB "\xef\x96\xad" // U+f5ad +#define ICON_FA_PEN_RULER "\xef\x96\xae" // U+f5ae +#define ICON_FA_PEN_TO_SQUARE "\xef\x81\x84" // U+f044 +#define ICON_FA_PENCIL "\xef\x8c\x83" // U+f303 +#define ICON_FA_PENTAGON "\xee\x9e\x90" // U+e790 +#define ICON_FA_PEOPLE_ARROWS "\xee\x81\xa8" // U+e068 +#define ICON_FA_PEOPLE_CARRY_BOX "\xef\x93\x8e" // U+f4ce +#define ICON_FA_PEOPLE_GROUP "\xee\x94\xb3" // U+e533 +#define ICON_FA_PEOPLE_LINE "\xee\x94\xb4" // U+e534 +#define ICON_FA_PEOPLE_PULLING "\xee\x94\xb5" // U+e535 +#define ICON_FA_PEOPLE_ROBBERY "\xee\x94\xb6" // U+e536 +#define ICON_FA_PEOPLE_ROOF "\xee\x94\xb7" // U+e537 +#define ICON_FA_PEPPER_HOT "\xef\xa0\x96" // U+f816 +#define ICON_FA_PERCENT "%" // U+0025 +#define ICON_FA_PERSON "\xef\x86\x83" // U+f183 +#define ICON_FA_PERSON_ARROW_DOWN_TO_LINE "\xee\x94\xb8" // U+e538 +#define ICON_FA_PERSON_ARROW_UP_FROM_LINE "\xee\x94\xb9" // U+e539 +#define ICON_FA_PERSON_BIKING "\xef\xa1\x8a" // U+f84a +#define ICON_FA_PERSON_BOOTH "\xef\x9d\x96" // U+f756 +#define ICON_FA_PERSON_BREASTFEEDING "\xee\x94\xba" // U+e53a +#define ICON_FA_PERSON_BURST "\xee\x94\xbb" // U+e53b +#define ICON_FA_PERSON_CANE "\xee\x94\xbc" // U+e53c +#define ICON_FA_PERSON_CHALKBOARD "\xee\x94\xbd" // U+e53d +#define ICON_FA_PERSON_CIRCLE_CHECK "\xee\x94\xbe" // U+e53e +#define ICON_FA_PERSON_CIRCLE_EXCLAMATION "\xee\x94\xbf" // U+e53f +#define ICON_FA_PERSON_CIRCLE_MINUS "\xee\x95\x80" // U+e540 +#define ICON_FA_PERSON_CIRCLE_PLUS "\xee\x95\x81" // U+e541 +#define ICON_FA_PERSON_CIRCLE_QUESTION "\xee\x95\x82" // U+e542 +#define ICON_FA_PERSON_CIRCLE_XMARK "\xee\x95\x83" // U+e543 +#define ICON_FA_PERSON_DIGGING "\xef\xa1\x9e" // U+f85e +#define ICON_FA_PERSON_DOTS_FROM_LINE "\xef\x91\xb0" // U+f470 +#define ICON_FA_PERSON_DRESS "\xef\x86\x82" // U+f182 +#define ICON_FA_PERSON_DRESS_BURST "\xee\x95\x84" // U+e544 +#define ICON_FA_PERSON_DROWNING "\xee\x95\x85" // U+e545 +#define ICON_FA_PERSON_FALLING "\xee\x95\x86" // U+e546 +#define ICON_FA_PERSON_FALLING_BURST "\xee\x95\x87" // U+e547 +#define ICON_FA_PERSON_HALF_DRESS "\xee\x95\x88" // U+e548 +#define ICON_FA_PERSON_HARASSING "\xee\x95\x89" // U+e549 +#define ICON_FA_PERSON_HIKING "\xef\x9b\xac" // U+f6ec +#define ICON_FA_PERSON_MILITARY_POINTING "\xee\x95\x8a" // U+e54a +#define ICON_FA_PERSON_MILITARY_RIFLE "\xee\x95\x8b" // U+e54b +#define ICON_FA_PERSON_MILITARY_TO_PERSON "\xee\x95\x8c" // U+e54c +#define ICON_FA_PERSON_PRAYING "\xef\x9a\x83" // U+f683 +#define ICON_FA_PERSON_PREGNANT "\xee\x8c\x9e" // U+e31e +#define ICON_FA_PERSON_RAYS "\xee\x95\x8d" // U+e54d +#define ICON_FA_PERSON_RIFLE "\xee\x95\x8e" // U+e54e +#define ICON_FA_PERSON_RUNNING "\xef\x9c\x8c" // U+f70c +#define ICON_FA_PERSON_SHELTER "\xee\x95\x8f" // U+e54f +#define ICON_FA_PERSON_SKATING "\xef\x9f\x85" // U+f7c5 +#define ICON_FA_PERSON_SKIING "\xef\x9f\x89" // U+f7c9 +#define ICON_FA_PERSON_SKIING_NORDIC "\xef\x9f\x8a" // U+f7ca +#define ICON_FA_PERSON_SNOWBOARDING "\xef\x9f\x8e" // U+f7ce +#define ICON_FA_PERSON_SWIMMING "\xef\x97\x84" // U+f5c4 +#define ICON_FA_PERSON_THROUGH_WINDOW "\xee\x96\xa9" // U+e5a9 +#define ICON_FA_PERSON_WALKING "\xef\x95\x94" // U+f554 +#define ICON_FA_PERSON_WALKING_ARROW_LOOP_LEFT "\xee\x95\x91" // U+e551 +#define ICON_FA_PERSON_WALKING_ARROW_RIGHT "\xee\x95\x92" // U+e552 +#define ICON_FA_PERSON_WALKING_DASHED_LINE_ARROW_RIGHT "\xee\x95\x93" // U+e553 +#define ICON_FA_PERSON_WALKING_LUGGAGE "\xee\x95\x94" // U+e554 +#define ICON_FA_PERSON_WALKING_WITH_CANE "\xef\x8a\x9d" // U+f29d +#define ICON_FA_PESETA_SIGN "\xee\x88\xa1" // U+e221 +#define ICON_FA_PESO_SIGN "\xee\x88\xa2" // U+e222 +#define ICON_FA_PHONE "\xef\x82\x95" // U+f095 +#define ICON_FA_PHONE_FLIP "\xef\xa1\xb9" // U+f879 +#define ICON_FA_PHONE_SLASH "\xef\x8f\x9d" // U+f3dd +#define ICON_FA_PHONE_VOLUME "\xef\x8a\xa0" // U+f2a0 +#define ICON_FA_PHOTO_FILM "\xef\xa1\xbc" // U+f87c +#define ICON_FA_PIGGY_BANK "\xef\x93\x93" // U+f4d3 +#define ICON_FA_PILLS "\xef\x92\x84" // U+f484 +#define ICON_FA_PIZZA_SLICE "\xef\xa0\x98" // U+f818 +#define ICON_FA_PLACE_OF_WORSHIP "\xef\x99\xbf" // U+f67f +#define ICON_FA_PLANE "\xef\x81\xb2" // U+f072 +#define ICON_FA_PLANE_ARRIVAL "\xef\x96\xaf" // U+f5af +#define ICON_FA_PLANE_CIRCLE_CHECK "\xee\x95\x95" // U+e555 +#define ICON_FA_PLANE_CIRCLE_EXCLAMATION "\xee\x95\x96" // U+e556 +#define ICON_FA_PLANE_CIRCLE_XMARK "\xee\x95\x97" // U+e557 +#define ICON_FA_PLANE_DEPARTURE "\xef\x96\xb0" // U+f5b0 +#define ICON_FA_PLANE_LOCK "\xee\x95\x98" // U+e558 +#define ICON_FA_PLANE_SLASH "\xee\x81\xa9" // U+e069 +#define ICON_FA_PLANE_UP "\xee\x88\xad" // U+e22d +#define ICON_FA_PLANT_WILT "\xee\x96\xaa" // U+e5aa +#define ICON_FA_PLATE_WHEAT "\xee\x95\x9a" // U+e55a +#define ICON_FA_PLAY "\xef\x81\x8b" // U+f04b +#define ICON_FA_PLUG "\xef\x87\xa6" // U+f1e6 +#define ICON_FA_PLUG_CIRCLE_BOLT "\xee\x95\x9b" // U+e55b +#define ICON_FA_PLUG_CIRCLE_CHECK "\xee\x95\x9c" // U+e55c +#define ICON_FA_PLUG_CIRCLE_EXCLAMATION "\xee\x95\x9d" // U+e55d +#define ICON_FA_PLUG_CIRCLE_MINUS "\xee\x95\x9e" // U+e55e +#define ICON_FA_PLUG_CIRCLE_PLUS "\xee\x95\x9f" // U+e55f +#define ICON_FA_PLUG_CIRCLE_XMARK "\xee\x95\xa0" // U+e560 +#define ICON_FA_PLUS "+" // U+002b +#define ICON_FA_PLUS_MINUS "\xee\x90\xbc" // U+e43c +#define ICON_FA_PODCAST "\xef\x8b\x8e" // U+f2ce +#define ICON_FA_POO "\xef\x8b\xbe" // U+f2fe +#define ICON_FA_POO_STORM "\xef\x9d\x9a" // U+f75a +#define ICON_FA_POOP "\xef\x98\x99" // U+f619 +#define ICON_FA_POWER_OFF "\xef\x80\x91" // U+f011 +#define ICON_FA_PRESCRIPTION "\xef\x96\xb1" // U+f5b1 +#define ICON_FA_PRESCRIPTION_BOTTLE "\xef\x92\x85" // U+f485 +#define ICON_FA_PRESCRIPTION_BOTTLE_MEDICAL "\xef\x92\x86" // U+f486 +#define ICON_FA_PRINT "\xef\x80\xaf" // U+f02f +#define ICON_FA_PUMP_MEDICAL "\xee\x81\xaa" // U+e06a +#define ICON_FA_PUMP_SOAP "\xee\x81\xab" // U+e06b +#define ICON_FA_PUZZLE_PIECE "\xef\x84\xae" // U+f12e +#define ICON_FA_Q "Q" // U+0051 +#define ICON_FA_QRCODE "\xef\x80\xa9" // U+f029 +#define ICON_FA_QUESTION "?" // U+003f +#define ICON_FA_QUOTE_LEFT "\xef\x84\x8d" // U+f10d +#define ICON_FA_QUOTE_RIGHT "\xef\x84\x8e" // U+f10e +#define ICON_FA_R "R" // U+0052 +#define ICON_FA_RADIATION "\xef\x9e\xb9" // U+f7b9 +#define ICON_FA_RADIO "\xef\xa3\x97" // U+f8d7 +#define ICON_FA_RAINBOW "\xef\x9d\x9b" // U+f75b +#define ICON_FA_RANKING_STAR "\xee\x95\xa1" // U+e561 +#define ICON_FA_RECEIPT "\xef\x95\x83" // U+f543 +#define ICON_FA_RECORD_VINYL "\xef\xa3\x99" // U+f8d9 +#define ICON_FA_RECTANGLE_AD "\xef\x99\x81" // U+f641 +#define ICON_FA_RECTANGLE_LIST "\xef\x80\xa2" // U+f022 +#define ICON_FA_RECTANGLE_XMARK "\xef\x90\x90" // U+f410 +#define ICON_FA_RECYCLE "\xef\x86\xb8" // U+f1b8 +#define ICON_FA_REGISTERED "\xef\x89\x9d" // U+f25d +#define ICON_FA_REPEAT "\xef\x8d\xa3" // U+f363 +#define ICON_FA_REPLY "\xef\x8f\xa5" // U+f3e5 +#define ICON_FA_REPLY_ALL "\xef\x84\xa2" // U+f122 +#define ICON_FA_REPUBLICAN "\xef\x9d\x9e" // U+f75e +#define ICON_FA_RESTROOM "\xef\x9e\xbd" // U+f7bd +#define ICON_FA_RETWEET "\xef\x81\xb9" // U+f079 +#define ICON_FA_RIBBON "\xef\x93\x96" // U+f4d6 +#define ICON_FA_RIGHT_FROM_BRACKET "\xef\x8b\xb5" // U+f2f5 +#define ICON_FA_RIGHT_LEFT "\xef\x8d\xa2" // U+f362 +#define ICON_FA_RIGHT_LONG "\xef\x8c\x8b" // U+f30b +#define ICON_FA_RIGHT_TO_BRACKET "\xef\x8b\xb6" // U+f2f6 +#define ICON_FA_RING "\xef\x9c\x8b" // U+f70b +#define ICON_FA_ROAD "\xef\x80\x98" // U+f018 +#define ICON_FA_ROAD_BARRIER "\xee\x95\xa2" // U+e562 +#define ICON_FA_ROAD_BRIDGE "\xee\x95\xa3" // U+e563 +#define ICON_FA_ROAD_CIRCLE_CHECK "\xee\x95\xa4" // U+e564 +#define ICON_FA_ROAD_CIRCLE_EXCLAMATION "\xee\x95\xa5" // U+e565 +#define ICON_FA_ROAD_CIRCLE_XMARK "\xee\x95\xa6" // U+e566 +#define ICON_FA_ROAD_LOCK "\xee\x95\xa7" // U+e567 +#define ICON_FA_ROAD_SPIKES "\xee\x95\xa8" // U+e568 +#define ICON_FA_ROBOT "\xef\x95\x84" // U+f544 +#define ICON_FA_ROCKET "\xef\x84\xb5" // U+f135 +#define ICON_FA_ROTATE "\xef\x8b\xb1" // U+f2f1 +#define ICON_FA_ROTATE_LEFT "\xef\x8b\xaa" // U+f2ea +#define ICON_FA_ROTATE_RIGHT "\xef\x8b\xb9" // U+f2f9 +#define ICON_FA_ROUTE "\xef\x93\x97" // U+f4d7 +#define ICON_FA_RSS "\xef\x82\x9e" // U+f09e +#define ICON_FA_RUBLE_SIGN "\xef\x85\x98" // U+f158 +#define ICON_FA_RUG "\xee\x95\xa9" // U+e569 +#define ICON_FA_RULER "\xef\x95\x85" // U+f545 +#define ICON_FA_RULER_COMBINED "\xef\x95\x86" // U+f546 +#define ICON_FA_RULER_HORIZONTAL "\xef\x95\x87" // U+f547 +#define ICON_FA_RULER_VERTICAL "\xef\x95\x88" // U+f548 +#define ICON_FA_RUPEE_SIGN "\xef\x85\x96" // U+f156 +#define ICON_FA_RUPIAH_SIGN "\xee\x88\xbd" // U+e23d +#define ICON_FA_S "S" // U+0053 +#define ICON_FA_SACK_DOLLAR "\xef\xa0\x9d" // U+f81d +#define ICON_FA_SACK_XMARK "\xee\x95\xaa" // U+e56a +#define ICON_FA_SAILBOAT "\xee\x91\x85" // U+e445 +#define ICON_FA_SATELLITE "\xef\x9e\xbf" // U+f7bf +#define ICON_FA_SATELLITE_DISH "\xef\x9f\x80" // U+f7c0 +#define ICON_FA_SCALE_BALANCED "\xef\x89\x8e" // U+f24e +#define ICON_FA_SCALE_UNBALANCED "\xef\x94\x95" // U+f515 +#define ICON_FA_SCALE_UNBALANCED_FLIP "\xef\x94\x96" // U+f516 +#define ICON_FA_SCHOOL "\xef\x95\x89" // U+f549 +#define ICON_FA_SCHOOL_CIRCLE_CHECK "\xee\x95\xab" // U+e56b +#define ICON_FA_SCHOOL_CIRCLE_EXCLAMATION "\xee\x95\xac" // U+e56c +#define ICON_FA_SCHOOL_CIRCLE_XMARK "\xee\x95\xad" // U+e56d +#define ICON_FA_SCHOOL_FLAG "\xee\x95\xae" // U+e56e +#define ICON_FA_SCHOOL_LOCK "\xee\x95\xaf" // U+e56f +#define ICON_FA_SCISSORS "\xef\x83\x84" // U+f0c4 +#define ICON_FA_SCREWDRIVER "\xef\x95\x8a" // U+f54a +#define ICON_FA_SCREWDRIVER_WRENCH "\xef\x9f\x99" // U+f7d9 +#define ICON_FA_SCROLL "\xef\x9c\x8e" // U+f70e +#define ICON_FA_SCROLL_TORAH "\xef\x9a\xa0" // U+f6a0 +#define ICON_FA_SD_CARD "\xef\x9f\x82" // U+f7c2 +#define ICON_FA_SECTION "\xee\x91\x87" // U+e447 +#define ICON_FA_SEEDLING "\xef\x93\x98" // U+f4d8 +#define ICON_FA_SEPTAGON "\xee\xa0\xa0" // U+e820 +#define ICON_FA_SERVER "\xef\x88\xb3" // U+f233 +#define ICON_FA_SHAPES "\xef\x98\x9f" // U+f61f +#define ICON_FA_SHARE "\xef\x81\xa4" // U+f064 +#define ICON_FA_SHARE_FROM_SQUARE "\xef\x85\x8d" // U+f14d +#define ICON_FA_SHARE_NODES "\xef\x87\xa0" // U+f1e0 +#define ICON_FA_SHEET_PLASTIC "\xee\x95\xb1" // U+e571 +#define ICON_FA_SHEKEL_SIGN "\xef\x88\x8b" // U+f20b +#define ICON_FA_SHIELD "\xef\x84\xb2" // U+f132 +#define ICON_FA_SHIELD_CAT "\xee\x95\xb2" // U+e572 +#define ICON_FA_SHIELD_DOG "\xee\x95\xb3" // U+e573 +#define ICON_FA_SHIELD_HALVED "\xef\x8f\xad" // U+f3ed +#define ICON_FA_SHIELD_HEART "\xee\x95\xb4" // U+e574 +#define ICON_FA_SHIELD_VIRUS "\xee\x81\xac" // U+e06c +#define ICON_FA_SHIP "\xef\x88\x9a" // U+f21a +#define ICON_FA_SHIRT "\xef\x95\x93" // U+f553 +#define ICON_FA_SHOE_PRINTS "\xef\x95\x8b" // U+f54b +#define ICON_FA_SHOP "\xef\x95\x8f" // U+f54f +#define ICON_FA_SHOP_LOCK "\xee\x92\xa5" // U+e4a5 +#define ICON_FA_SHOP_SLASH "\xee\x81\xb0" // U+e070 +#define ICON_FA_SHOWER "\xef\x8b\x8c" // U+f2cc +#define ICON_FA_SHRIMP "\xee\x91\x88" // U+e448 +#define ICON_FA_SHUFFLE "\xef\x81\xb4" // U+f074 +#define ICON_FA_SHUTTLE_SPACE "\xef\x86\x97" // U+f197 +#define ICON_FA_SIGN_HANGING "\xef\x93\x99" // U+f4d9 +#define ICON_FA_SIGNAL "\xef\x80\x92" // U+f012 +#define ICON_FA_SIGNATURE "\xef\x96\xb7" // U+f5b7 +#define ICON_FA_SIGNS_POST "\xef\x89\xb7" // U+f277 +#define ICON_FA_SIM_CARD "\xef\x9f\x84" // U+f7c4 +#define ICON_FA_SINGLE_QUOTE_LEFT "\xee\xa0\x9b" // U+e81b +#define ICON_FA_SINGLE_QUOTE_RIGHT "\xee\xa0\x9c" // U+e81c +#define ICON_FA_SINK "\xee\x81\xad" // U+e06d +#define ICON_FA_SITEMAP "\xef\x83\xa8" // U+f0e8 +#define ICON_FA_SKULL "\xef\x95\x8c" // U+f54c +#define ICON_FA_SKULL_CROSSBONES "\xef\x9c\x94" // U+f714 +#define ICON_FA_SLASH "\xef\x9c\x95" // U+f715 +#define ICON_FA_SLEIGH "\xef\x9f\x8c" // U+f7cc +#define ICON_FA_SLIDERS "\xef\x87\x9e" // U+f1de +#define ICON_FA_SMOG "\xef\x9d\x9f" // U+f75f +#define ICON_FA_SMOKING "\xef\x92\x8d" // U+f48d +#define ICON_FA_SNOWFLAKE "\xef\x8b\x9c" // U+f2dc +#define ICON_FA_SNOWMAN "\xef\x9f\x90" // U+f7d0 +#define ICON_FA_SNOWPLOW "\xef\x9f\x92" // U+f7d2 +#define ICON_FA_SOAP "\xee\x81\xae" // U+e06e +#define ICON_FA_SOCKS "\xef\x9a\x96" // U+f696 +#define ICON_FA_SOLAR_PANEL "\xef\x96\xba" // U+f5ba +#define ICON_FA_SORT "\xef\x83\x9c" // U+f0dc +#define ICON_FA_SORT_DOWN "\xef\x83\x9d" // U+f0dd +#define ICON_FA_SORT_UP "\xef\x83\x9e" // U+f0de +#define ICON_FA_SPA "\xef\x96\xbb" // U+f5bb +#define ICON_FA_SPAGHETTI_MONSTER_FLYING "\xef\x99\xbb" // U+f67b +#define ICON_FA_SPELL_CHECK "\xef\xa2\x91" // U+f891 +#define ICON_FA_SPIDER "\xef\x9c\x97" // U+f717 +#define ICON_FA_SPINNER "\xef\x84\x90" // U+f110 +#define ICON_FA_SPIRAL "\xee\xa0\x8a" // U+e80a +#define ICON_FA_SPLOTCH "\xef\x96\xbc" // U+f5bc +#define ICON_FA_SPOON "\xef\x8b\xa5" // U+f2e5 +#define ICON_FA_SPRAY_CAN "\xef\x96\xbd" // U+f5bd +#define ICON_FA_SPRAY_CAN_SPARKLES "\xef\x97\x90" // U+f5d0 +#define ICON_FA_SQUARE "\xef\x83\x88" // U+f0c8 +#define ICON_FA_SQUARE_ARROW_UP_RIGHT "\xef\x85\x8c" // U+f14c +#define ICON_FA_SQUARE_BINARY "\xee\x9a\x9b" // U+e69b +#define ICON_FA_SQUARE_CARET_DOWN "\xef\x85\x90" // U+f150 +#define ICON_FA_SQUARE_CARET_LEFT "\xef\x86\x91" // U+f191 +#define ICON_FA_SQUARE_CARET_RIGHT "\xef\x85\x92" // U+f152 +#define ICON_FA_SQUARE_CARET_UP "\xef\x85\x91" // U+f151 +#define ICON_FA_SQUARE_CHECK "\xef\x85\x8a" // U+f14a +#define ICON_FA_SQUARE_ENVELOPE "\xef\x86\x99" // U+f199 +#define ICON_FA_SQUARE_FULL "\xef\x91\x9c" // U+f45c +#define ICON_FA_SQUARE_H "\xef\x83\xbd" // U+f0fd +#define ICON_FA_SQUARE_MINUS "\xef\x85\x86" // U+f146 +#define ICON_FA_SQUARE_NFI "\xee\x95\xb6" // U+e576 +#define ICON_FA_SQUARE_PARKING "\xef\x95\x80" // U+f540 +#define ICON_FA_SQUARE_PEN "\xef\x85\x8b" // U+f14b +#define ICON_FA_SQUARE_PERSON_CONFINED "\xee\x95\xb7" // U+e577 +#define ICON_FA_SQUARE_PHONE "\xef\x82\x98" // U+f098 +#define ICON_FA_SQUARE_PHONE_FLIP "\xef\xa1\xbb" // U+f87b +#define ICON_FA_SQUARE_PLUS "\xef\x83\xbe" // U+f0fe +#define ICON_FA_SQUARE_POLL_HORIZONTAL "\xef\x9a\x82" // U+f682 +#define ICON_FA_SQUARE_POLL_VERTICAL "\xef\x9a\x81" // U+f681 +#define ICON_FA_SQUARE_ROOT_VARIABLE "\xef\x9a\x98" // U+f698 +#define ICON_FA_SQUARE_RSS "\xef\x85\x83" // U+f143 +#define ICON_FA_SQUARE_SHARE_NODES "\xef\x87\xa1" // U+f1e1 +#define ICON_FA_SQUARE_UP_RIGHT "\xef\x8d\xa0" // U+f360 +#define ICON_FA_SQUARE_VIRUS "\xee\x95\xb8" // U+e578 +#define ICON_FA_SQUARE_XMARK "\xef\x8b\x93" // U+f2d3 +#define ICON_FA_STAFF_SNAKE "\xee\x95\xb9" // U+e579 +#define ICON_FA_STAIRS "\xee\x8a\x89" // U+e289 +#define ICON_FA_STAMP "\xef\x96\xbf" // U+f5bf +#define ICON_FA_STAPLER "\xee\x96\xaf" // U+e5af +#define ICON_FA_STAR "\xef\x80\x85" // U+f005 +#define ICON_FA_STAR_AND_CRESCENT "\xef\x9a\x99" // U+f699 +#define ICON_FA_STAR_HALF "\xef\x82\x89" // U+f089 +#define ICON_FA_STAR_HALF_STROKE "\xef\x97\x80" // U+f5c0 +#define ICON_FA_STAR_OF_DAVID "\xef\x9a\x9a" // U+f69a +#define ICON_FA_STAR_OF_LIFE "\xef\x98\xa1" // U+f621 +#define ICON_FA_STERLING_SIGN "\xef\x85\x94" // U+f154 +#define ICON_FA_STETHOSCOPE "\xef\x83\xb1" // U+f0f1 +#define ICON_FA_STOP "\xef\x81\x8d" // U+f04d +#define ICON_FA_STOPWATCH "\xef\x8b\xb2" // U+f2f2 +#define ICON_FA_STOPWATCH_20 "\xee\x81\xaf" // U+e06f +#define ICON_FA_STORE "\xef\x95\x8e" // U+f54e +#define ICON_FA_STORE_SLASH "\xee\x81\xb1" // U+e071 +#define ICON_FA_STREET_VIEW "\xef\x88\x9d" // U+f21d +#define ICON_FA_STRIKETHROUGH "\xef\x83\x8c" // U+f0cc +#define ICON_FA_STROOPWAFEL "\xef\x95\x91" // U+f551 +#define ICON_FA_SUBSCRIPT "\xef\x84\xac" // U+f12c +#define ICON_FA_SUITCASE "\xef\x83\xb2" // U+f0f2 +#define ICON_FA_SUITCASE_MEDICAL "\xef\x83\xba" // U+f0fa +#define ICON_FA_SUITCASE_ROLLING "\xef\x97\x81" // U+f5c1 +#define ICON_FA_SUN "\xef\x86\x85" // U+f185 +#define ICON_FA_SUN_PLANT_WILT "\xee\x95\xba" // U+e57a +#define ICON_FA_SUPERSCRIPT "\xef\x84\xab" // U+f12b +#define ICON_FA_SWATCHBOOK "\xef\x97\x83" // U+f5c3 +#define ICON_FA_SYNAGOGUE "\xef\x9a\x9b" // U+f69b +#define ICON_FA_SYRINGE "\xef\x92\x8e" // U+f48e +#define ICON_FA_T "T" // U+0054 +#define ICON_FA_TABLE "\xef\x83\x8e" // U+f0ce +#define ICON_FA_TABLE_CELLS "\xef\x80\x8a" // U+f00a +#define ICON_FA_TABLE_CELLS_COLUMN_LOCK "\xee\x99\xb8" // U+e678 +#define ICON_FA_TABLE_CELLS_LARGE "\xef\x80\x89" // U+f009 +#define ICON_FA_TABLE_CELLS_ROW_LOCK "\xee\x99\xba" // U+e67a +#define ICON_FA_TABLE_CELLS_ROW_UNLOCK "\xee\x9a\x91" // U+e691 +#define ICON_FA_TABLE_COLUMNS "\xef\x83\x9b" // U+f0db +#define ICON_FA_TABLE_LIST "\xef\x80\x8b" // U+f00b +#define ICON_FA_TABLE_TENNIS_PADDLE_BALL "\xef\x91\x9d" // U+f45d +#define ICON_FA_TABLET "\xef\x8f\xbb" // U+f3fb +#define ICON_FA_TABLET_BUTTON "\xef\x84\x8a" // U+f10a +#define ICON_FA_TABLET_SCREEN_BUTTON "\xef\x8f\xba" // U+f3fa +#define ICON_FA_TABLETS "\xef\x92\x90" // U+f490 +#define ICON_FA_TACHOGRAPH_DIGITAL "\xef\x95\xa6" // U+f566 +#define ICON_FA_TAG "\xef\x80\xab" // U+f02b +#define ICON_FA_TAGS "\xef\x80\xac" // U+f02c +#define ICON_FA_TAPE "\xef\x93\x9b" // U+f4db +#define ICON_FA_TARP "\xee\x95\xbb" // U+e57b +#define ICON_FA_TARP_DROPLET "\xee\x95\xbc" // U+e57c +#define ICON_FA_TAXI "\xef\x86\xba" // U+f1ba +#define ICON_FA_TEETH "\xef\x98\xae" // U+f62e +#define ICON_FA_TEETH_OPEN "\xef\x98\xaf" // U+f62f +#define ICON_FA_TEMPERATURE_ARROW_DOWN "\xee\x80\xbf" // U+e03f +#define ICON_FA_TEMPERATURE_ARROW_UP "\xee\x81\x80" // U+e040 +#define ICON_FA_TEMPERATURE_EMPTY "\xef\x8b\x8b" // U+f2cb +#define ICON_FA_TEMPERATURE_FULL "\xef\x8b\x87" // U+f2c7 +#define ICON_FA_TEMPERATURE_HALF "\xef\x8b\x89" // U+f2c9 +#define ICON_FA_TEMPERATURE_HIGH "\xef\x9d\xa9" // U+f769 +#define ICON_FA_TEMPERATURE_LOW "\xef\x9d\xab" // U+f76b +#define ICON_FA_TEMPERATURE_QUARTER "\xef\x8b\x8a" // U+f2ca +#define ICON_FA_TEMPERATURE_THREE_QUARTERS "\xef\x8b\x88" // U+f2c8 +#define ICON_FA_TENGE_SIGN "\xef\x9f\x97" // U+f7d7 +#define ICON_FA_TENT "\xee\x95\xbd" // U+e57d +#define ICON_FA_TENT_ARROW_DOWN_TO_LINE "\xee\x95\xbe" // U+e57e +#define ICON_FA_TENT_ARROW_LEFT_RIGHT "\xee\x95\xbf" // U+e57f +#define ICON_FA_TENT_ARROW_TURN_LEFT "\xee\x96\x80" // U+e580 +#define ICON_FA_TENT_ARROWS_DOWN "\xee\x96\x81" // U+e581 +#define ICON_FA_TENTS "\xee\x96\x82" // U+e582 +#define ICON_FA_TERMINAL "\xef\x84\xa0" // U+f120 +#define ICON_FA_TEXT_HEIGHT "\xef\x80\xb4" // U+f034 +#define ICON_FA_TEXT_SLASH "\xef\xa1\xbd" // U+f87d +#define ICON_FA_TEXT_WIDTH "\xef\x80\xb5" // U+f035 +#define ICON_FA_THERMOMETER "\xef\x92\x91" // U+f491 +#define ICON_FA_THUMBS_DOWN "\xef\x85\xa5" // U+f165 +#define ICON_FA_THUMBS_UP "\xef\x85\xa4" // U+f164 +#define ICON_FA_THUMBTACK "\xef\x82\x8d" // U+f08d +#define ICON_FA_THUMBTACK_SLASH "\xee\x9a\x8f" // U+e68f +#define ICON_FA_TICKET "\xef\x85\x85" // U+f145 +#define ICON_FA_TICKET_SIMPLE "\xef\x8f\xbf" // U+f3ff +#define ICON_FA_TIMELINE "\xee\x8a\x9c" // U+e29c +#define ICON_FA_TOGGLE_OFF "\xef\x88\x84" // U+f204 +#define ICON_FA_TOGGLE_ON "\xef\x88\x85" // U+f205 +#define ICON_FA_TOILET "\xef\x9f\x98" // U+f7d8 +#define ICON_FA_TOILET_PAPER "\xef\x9c\x9e" // U+f71e +#define ICON_FA_TOILET_PAPER_SLASH "\xee\x81\xb2" // U+e072 +#define ICON_FA_TOILET_PORTABLE "\xee\x96\x83" // U+e583 +#define ICON_FA_TOILETS_PORTABLE "\xee\x96\x84" // U+e584 +#define ICON_FA_TOOLBOX "\xef\x95\x92" // U+f552 +#define ICON_FA_TOOTH "\xef\x97\x89" // U+f5c9 +#define ICON_FA_TORII_GATE "\xef\x9a\xa1" // U+f6a1 +#define ICON_FA_TORNADO "\xef\x9d\xaf" // U+f76f +#define ICON_FA_TOWER_BROADCAST "\xef\x94\x99" // U+f519 +#define ICON_FA_TOWER_CELL "\xee\x96\x85" // U+e585 +#define ICON_FA_TOWER_OBSERVATION "\xee\x96\x86" // U+e586 +#define ICON_FA_TRACTOR "\xef\x9c\xa2" // U+f722 +#define ICON_FA_TRADEMARK "\xef\x89\x9c" // U+f25c +#define ICON_FA_TRAFFIC_LIGHT "\xef\x98\xb7" // U+f637 +#define ICON_FA_TRAILER "\xee\x81\x81" // U+e041 +#define ICON_FA_TRAIN "\xef\x88\xb8" // U+f238 +#define ICON_FA_TRAIN_SUBWAY "\xef\x88\xb9" // U+f239 +#define ICON_FA_TRAIN_TRAM "\xee\x96\xb4" // U+e5b4 +#define ICON_FA_TRANSGENDER "\xef\x88\xa5" // U+f225 +#define ICON_FA_TRASH "\xef\x87\xb8" // U+f1f8 +#define ICON_FA_TRASH_ARROW_UP "\xef\xa0\xa9" // U+f829 +#define ICON_FA_TRASH_CAN "\xef\x8b\xad" // U+f2ed +#define ICON_FA_TRASH_CAN_ARROW_UP "\xef\xa0\xaa" // U+f82a +#define ICON_FA_TREE "\xef\x86\xbb" // U+f1bb +#define ICON_FA_TREE_CITY "\xee\x96\x87" // U+e587 +#define ICON_FA_TRIANGLE_EXCLAMATION "\xef\x81\xb1" // U+f071 +#define ICON_FA_TROPHY "\xef\x82\x91" // U+f091 +#define ICON_FA_TROWEL "\xee\x96\x89" // U+e589 +#define ICON_FA_TROWEL_BRICKS "\xee\x96\x8a" // U+e58a +#define ICON_FA_TRUCK "\xef\x83\x91" // U+f0d1 +#define ICON_FA_TRUCK_ARROW_RIGHT "\xee\x96\x8b" // U+e58b +#define ICON_FA_TRUCK_DROPLET "\xee\x96\x8c" // U+e58c +#define ICON_FA_TRUCK_FAST "\xef\x92\x8b" // U+f48b +#define ICON_FA_TRUCK_FIELD "\xee\x96\x8d" // U+e58d +#define ICON_FA_TRUCK_FIELD_UN "\xee\x96\x8e" // U+e58e +#define ICON_FA_TRUCK_FRONT "\xee\x8a\xb7" // U+e2b7 +#define ICON_FA_TRUCK_MEDICAL "\xef\x83\xb9" // U+f0f9 +#define ICON_FA_TRUCK_MONSTER "\xef\x98\xbb" // U+f63b +#define ICON_FA_TRUCK_MOVING "\xef\x93\x9f" // U+f4df +#define ICON_FA_TRUCK_PICKUP "\xef\x98\xbc" // U+f63c +#define ICON_FA_TRUCK_PLANE "\xee\x96\x8f" // U+e58f +#define ICON_FA_TRUCK_RAMP_BOX "\xef\x93\x9e" // U+f4de +#define ICON_FA_TTY "\xef\x87\xa4" // U+f1e4 +#define ICON_FA_TURKISH_LIRA_SIGN "\xee\x8a\xbb" // U+e2bb +#define ICON_FA_TURN_DOWN "\xef\x8e\xbe" // U+f3be +#define ICON_FA_TURN_UP "\xef\x8e\xbf" // U+f3bf +#define ICON_FA_TV "\xef\x89\xac" // U+f26c +#define ICON_FA_U "U" // U+0055 +#define ICON_FA_UMBRELLA "\xef\x83\xa9" // U+f0e9 +#define ICON_FA_UMBRELLA_BEACH "\xef\x97\x8a" // U+f5ca +#define ICON_FA_UNDERLINE "\xef\x83\x8d" // U+f0cd +#define ICON_FA_UNIVERSAL_ACCESS "\xef\x8a\x9a" // U+f29a +#define ICON_FA_UNLOCK "\xef\x82\x9c" // U+f09c +#define ICON_FA_UNLOCK_KEYHOLE "\xef\x84\xbe" // U+f13e +#define ICON_FA_UP_DOWN "\xef\x8c\xb8" // U+f338 +#define ICON_FA_UP_DOWN_LEFT_RIGHT "\xef\x82\xb2" // U+f0b2 +#define ICON_FA_UP_LONG "\xef\x8c\x8c" // U+f30c +#define ICON_FA_UP_RIGHT_AND_DOWN_LEFT_FROM_CENTER "\xef\x90\xa4" // U+f424 +#define ICON_FA_UP_RIGHT_FROM_SQUARE "\xef\x8d\x9d" // U+f35d +#define ICON_FA_UPLOAD "\xef\x82\x93" // U+f093 +#define ICON_FA_USER "\xef\x80\x87" // U+f007 +#define ICON_FA_USER_ASTRONAUT "\xef\x93\xbb" // U+f4fb +#define ICON_FA_USER_CHECK "\xef\x93\xbc" // U+f4fc +#define ICON_FA_USER_CLOCK "\xef\x93\xbd" // U+f4fd +#define ICON_FA_USER_DOCTOR "\xef\x83\xb0" // U+f0f0 +#define ICON_FA_USER_GEAR "\xef\x93\xbe" // U+f4fe +#define ICON_FA_USER_GRADUATE "\xef\x94\x81" // U+f501 +#define ICON_FA_USER_GROUP "\xef\x94\x80" // U+f500 +#define ICON_FA_USER_INJURED "\xef\x9c\xa8" // U+f728 +#define ICON_FA_USER_LOCK "\xef\x94\x82" // U+f502 +#define ICON_FA_USER_MINUS "\xef\x94\x83" // U+f503 +#define ICON_FA_USER_NINJA "\xef\x94\x84" // U+f504 +#define ICON_FA_USER_NURSE "\xef\xa0\xaf" // U+f82f +#define ICON_FA_USER_PEN "\xef\x93\xbf" // U+f4ff +#define ICON_FA_USER_PLUS "\xef\x88\xb4" // U+f234 +#define ICON_FA_USER_SECRET "\xef\x88\x9b" // U+f21b +#define ICON_FA_USER_SHIELD "\xef\x94\x85" // U+f505 +#define ICON_FA_USER_SLASH "\xef\x94\x86" // U+f506 +#define ICON_FA_USER_TAG "\xef\x94\x87" // U+f507 +#define ICON_FA_USER_TIE "\xef\x94\x88" // U+f508 +#define ICON_FA_USER_XMARK "\xef\x88\xb5" // U+f235 +#define ICON_FA_USERS "\xef\x83\x80" // U+f0c0 +#define ICON_FA_USERS_BETWEEN_LINES "\xee\x96\x91" // U+e591 +#define ICON_FA_USERS_GEAR "\xef\x94\x89" // U+f509 +#define ICON_FA_USERS_LINE "\xee\x96\x92" // U+e592 +#define ICON_FA_USERS_RAYS "\xee\x96\x93" // U+e593 +#define ICON_FA_USERS_RECTANGLE "\xee\x96\x94" // U+e594 +#define ICON_FA_USERS_SLASH "\xee\x81\xb3" // U+e073 +#define ICON_FA_USERS_VIEWFINDER "\xee\x96\x95" // U+e595 +#define ICON_FA_UTENSILS "\xef\x8b\xa7" // U+f2e7 +#define ICON_FA_V "V" // U+0056 +#define ICON_FA_VAN_SHUTTLE "\xef\x96\xb6" // U+f5b6 +#define ICON_FA_VAULT "\xee\x8b\x85" // U+e2c5 +#define ICON_FA_VENUS "\xef\x88\xa1" // U+f221 +#define ICON_FA_VENUS_DOUBLE "\xef\x88\xa6" // U+f226 +#define ICON_FA_VENUS_MARS "\xef\x88\xa8" // U+f228 +#define ICON_FA_VEST "\xee\x82\x85" // U+e085 +#define ICON_FA_VEST_PATCHES "\xee\x82\x86" // U+e086 +#define ICON_FA_VIAL "\xef\x92\x92" // U+f492 +#define ICON_FA_VIAL_CIRCLE_CHECK "\xee\x96\x96" // U+e596 +#define ICON_FA_VIAL_VIRUS "\xee\x96\x97" // U+e597 +#define ICON_FA_VIALS "\xef\x92\x93" // U+f493 +#define ICON_FA_VIDEO "\xef\x80\xbd" // U+f03d +#define ICON_FA_VIDEO_SLASH "\xef\x93\xa2" // U+f4e2 +#define ICON_FA_VIHARA "\xef\x9a\xa7" // U+f6a7 +#define ICON_FA_VIRUS "\xee\x81\xb4" // U+e074 +#define ICON_FA_VIRUS_COVID "\xee\x92\xa8" // U+e4a8 +#define ICON_FA_VIRUS_COVID_SLASH "\xee\x92\xa9" // U+e4a9 +#define ICON_FA_VIRUS_SLASH "\xee\x81\xb5" // U+e075 +#define ICON_FA_VIRUSES "\xee\x81\xb6" // U+e076 +#define ICON_FA_VOICEMAIL "\xef\xa2\x97" // U+f897 +#define ICON_FA_VOLCANO "\xef\x9d\xb0" // U+f770 +#define ICON_FA_VOLLEYBALL "\xef\x91\x9f" // U+f45f +#define ICON_FA_VOLUME_HIGH "\xef\x80\xa8" // U+f028 +#define ICON_FA_VOLUME_LOW "\xef\x80\xa7" // U+f027 +#define ICON_FA_VOLUME_OFF "\xef\x80\xa6" // U+f026 +#define ICON_FA_VOLUME_XMARK "\xef\x9a\xa9" // U+f6a9 +#define ICON_FA_VR_CARDBOARD "\xef\x9c\xa9" // U+f729 +#define ICON_FA_W "W" // U+0057 +#define ICON_FA_WALKIE_TALKIE "\xef\xa3\xaf" // U+f8ef +#define ICON_FA_WALLET "\xef\x95\x95" // U+f555 +#define ICON_FA_WAND_MAGIC "\xef\x83\x90" // U+f0d0 +#define ICON_FA_WAND_MAGIC_SPARKLES "\xee\x8b\x8a" // U+e2ca +#define ICON_FA_WAND_SPARKLES "\xef\x9c\xab" // U+f72b +#define ICON_FA_WAREHOUSE "\xef\x92\x94" // U+f494 +#define ICON_FA_WATER "\xef\x9d\xb3" // U+f773 +#define ICON_FA_WATER_LADDER "\xef\x97\x85" // U+f5c5 +#define ICON_FA_WAVE_SQUARE "\xef\xa0\xbe" // U+f83e +#define ICON_FA_WEB_AWESOME "\xee\x9a\x82" // U+e682 +#define ICON_FA_WEIGHT_HANGING "\xef\x97\x8d" // U+f5cd +#define ICON_FA_WEIGHT_SCALE "\xef\x92\x96" // U+f496 +#define ICON_FA_WHEAT_AWN "\xee\x8b\x8d" // U+e2cd +#define ICON_FA_WHEAT_AWN_CIRCLE_EXCLAMATION "\xee\x96\x98" // U+e598 +#define ICON_FA_WHEELCHAIR "\xef\x86\x93" // U+f193 +#define ICON_FA_WHEELCHAIR_MOVE "\xee\x8b\x8e" // U+e2ce +#define ICON_FA_WHISKEY_GLASS "\xef\x9e\xa0" // U+f7a0 +#define ICON_FA_WIFI "\xef\x87\xab" // U+f1eb +#define ICON_FA_WIND "\xef\x9c\xae" // U+f72e +#define ICON_FA_WINDOW_MAXIMIZE "\xef\x8b\x90" // U+f2d0 +#define ICON_FA_WINDOW_MINIMIZE "\xef\x8b\x91" // U+f2d1 +#define ICON_FA_WINDOW_RESTORE "\xef\x8b\x92" // U+f2d2 +#define ICON_FA_WINE_BOTTLE "\xef\x9c\xaf" // U+f72f +#define ICON_FA_WINE_GLASS "\xef\x93\xa3" // U+f4e3 +#define ICON_FA_WINE_GLASS_EMPTY "\xef\x97\x8e" // U+f5ce +#define ICON_FA_WON_SIGN "\xef\x85\x99" // U+f159 +#define ICON_FA_WORM "\xee\x96\x99" // U+e599 +#define ICON_FA_WRENCH "\xef\x82\xad" // U+f0ad +#define ICON_FA_X "X" // U+0058 +#define ICON_FA_X_RAY "\xef\x92\x97" // U+f497 +#define ICON_FA_XMARK "\xef\x80\x8d" // U+f00d +#define ICON_FA_XMARKS_LINES "\xee\x96\x9a" // U+e59a +#define ICON_FA_Y "Y" // U+0059 +#define ICON_FA_YEN_SIGN "\xef\x85\x97" // U+f157 +#define ICON_FA_YIN_YANG "\xef\x9a\xad" // U+f6ad +#define ICON_FA_Z "Z" // U+005a diff --git a/src/gui/MainMenu.cpp b/src/gui/MainMenu.cpp index 9fe7881..e6b0a19 100644 --- a/src/gui/MainMenu.cpp +++ b/src/gui/MainMenu.cpp @@ -1,6 +1,7 @@ #include "gui/MainMenu.h" #include "AudioCapture.h" +#include "IconsFontAwesome7.h" #include "ProjectMSDLApplication.h" #include "ProjectMWrapper.h" @@ -53,9 +54,9 @@ void MainMenu::DrawFileMenu() { if (ImGui::BeginMenu("File")) { - if (ImGui::BeginMenu("Preset Editor")) + if (ImGui::BeginMenu(ICON_FA_PENCIL " Preset Editor")) { - if (ImGui::MenuItem("Edit Current Preset", "Ctrl+e")) + if (ImGui::MenuItem(ICON_FA_PENCIL " Edit Current Preset", "Ctrl+e")) { auto currentPreset = _projectMWrapper.CurrentPresetFileName(); if (currentPreset.empty()) @@ -67,12 +68,12 @@ void MainMenu::DrawFileMenu() _gui.ShowPresetEditor(currentPreset); } } - if (ImGui::MenuItem("Select Preset From Disk...", "Ctrl+l")) + if (ImGui::MenuItem(ICON_FA_FOLDER_OPEN " Select Preset From Disk...", "Ctrl+l")) { _presetChooser.Title("Select a Preset for Editing"); _presetChooser.Show(); } - if (ImGui::MenuItem("Create New Preset", "Ctrl+Shift+n")) + if (ImGui::MenuItem(ICON_FA_SQUARE_PLUS " Create New Preset", "Ctrl+Shift+n")) { _gui.ShowPresetEditor({}); } @@ -82,14 +83,14 @@ void MainMenu::DrawFileMenu() ImGui::Separator(); - if (ImGui::MenuItem("Settings...", "Ctrl+s")) + if (ImGui::MenuItem(ICON_FA_GEAR " Settings...", "Ctrl+s")) { _gui.ShowSettingsWindow(); } ImGui::Separator(); - if (ImGui::MenuItem("Quit projectM", "Ctrl+q")) + if (ImGui::MenuItem(ICON_FA_DOOR_OPEN " Quit projectM", "Ctrl+q")) { _notificationCenter.postNotification(new QuitNotification); } @@ -104,30 +105,30 @@ void MainMenu::DrawPlaybackMenu() { auto& app = ProjectMSDLApplication::instance(); - if (ImGui::MenuItem("Play Next Preset", "n")) + if (ImGui::MenuItem(ICON_FA_FORWARD_STEP " Play Next Preset", "n")) { _notificationCenter.postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::LastPreset)); } - if (ImGui::MenuItem("Play Previous Preset", "p")) + if (ImGui::MenuItem(ICON_FA_BACKWARD_STEP " Play Previous Preset", "p")) { _notificationCenter.postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::PreviousPreset)); } - if (ImGui::MenuItem("Go Back One Preset", "Backspace")) + if (ImGui::MenuItem(ICON_FA_ROTATE_LEFT " Go Back One Preset", "Backspace")) { _notificationCenter.postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::LastPreset)); } - if (ImGui::MenuItem("Random Preset", "r")) + if (ImGui::MenuItem(ICON_FA_WAND_MAGIC_SPARKLES " Random Preset", "r")) { _notificationCenter.postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::RandomPreset)); } ImGui::Separator(); - if (ImGui::MenuItem("Lock Preset", "Spacebar", app.config().getBool("projectM.presetLocked", false))) + if (ImGui::MenuItem(ICON_FA_LOCK " Lock Preset", "Spacebar", app.config().getBool("projectM.presetLocked", false))) { _notificationCenter.postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::TogglePresetLocked)); } - if (ImGui::MenuItem("Enable Shuffle", "y", app.config().getBool("projectM.shuffleEnabled", true))) + if (ImGui::MenuItem(ICON_FA_SHUFFLE " Enable Shuffle", "y", app.config().getBool("projectM.shuffleEnabled", true))) { _notificationCenter.postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::ToggleShuffle)); } @@ -207,18 +208,19 @@ void MainMenu::DrawHelpMenu() ImGui::Separator(); - if (ImGui::MenuItem("Visit the projectM Wiki on GitHub")) + if (ImGui::MenuItem(ICON_FA_ARROW_UP_RIGHT_FROM_SQUARE " Visit the projectM Wiki on GitHub")) { SystemBrowser::OpenURL("https://github.com/projectM-visualizer/projectm/wiki"); } - if (ImGui::MenuItem("Report a Bug or Request a Feature")) + if (ImGui::MenuItem(ICON_FA_ARROW_UP_RIGHT_FROM_SQUARE " Report a Bug or Request a Feature")) { SystemBrowser::OpenURL("https://github.com/projectM-visualizer/projectm/issues/new/choose"); } - if (ImGui::MenuItem("Sponsor projectM on OpenCollective")) + if (ImGui::MenuItem(ICON_FA_ARROW_UP_RIGHT_FROM_SQUARE " Sponsor projectM on OpenCollective")) { SystemBrowser::OpenURL("https://opencollective.com/projectm"); } + ImGui::EndMenu(); } } diff --git a/src/gui/ProjectMGUI.cpp b/src/gui/ProjectMGUI.cpp index 498cbef..8d9d33a 100644 --- a/src/gui/ProjectMGUI.cpp +++ b/src/gui/ProjectMGUI.cpp @@ -1,10 +1,13 @@ #include "ProjectMGUI.h" -#include "AnonymousProFont.h" -#include "LiberationSansFont.h" #include "ProjectMWrapper.h" #include "SDLRenderingWindow.h" +#include "AnonymousProFont.h" +#include "FontAwesomeIconsRegular7.h" +#include "FontAwesomeIconsSolid7.h" +#include "LiberationSansFont.h" + #include "imgui.h" #include "imgui_impl_opengl3.h" #include "imgui_impl_sdl2.h" @@ -90,12 +93,20 @@ void ProjectMGUI::UpdateFontSize() _textScalingFactor = newScalingFactor; - ImFontConfig config; - config.MergeMode = true; - io.Fonts->Clear(); - _uiFont = io.Fonts->AddFontFromMemoryCompressedTTF(&AnonymousPro_compressed_data, AnonymousPro_compressed_size, floor(24.0f * _textScalingFactor)); - _toastFont = io.Fonts->AddFontFromMemoryCompressedTTF(&LiberationSans_compressed_data, LiberationSans_compressed_size, floor(40.0f * _textScalingFactor)); + + ImFontConfig configMainFont; + configMainFont.MergeMode = false; + + ImFontConfig configIconFont; + configIconFont.MergeMode = true; + configIconFont.GlyphMinAdvanceX = floor(24.0f * _textScalingFactor); + + _uiFont = io.Fonts->AddFontFromMemoryCompressedTTF(&AnonymousPro_compressed_data, AnonymousPro_compressed_size, floor(24.0f * _textScalingFactor), &configMainFont); + io.Fonts->AddFontFromMemoryCompressedTTF(&FontAwesomeIconsSolid7_compressed_data, FontAwesomeIconsSolid7_compressed_size, floor(24.0f * _textScalingFactor), &configIconFont); + io.Fonts->AddFontFromMemoryCompressedTTF(&FontAwesomeIconsRegular7_compressed_data, FontAwesomeIconsRegular7_compressed_size, floor(24.0f * _textScalingFactor), &configIconFont); + + _toastFont = io.Fonts->AddFontFromMemoryCompressedTTF(&LiberationSans_compressed_data, LiberationSans_compressed_size, floor(40.0f * _textScalingFactor), &configMainFont); io.Fonts->Build(); ImGui::GetStyle().ScaleAllSizes(1.0); @@ -195,6 +206,16 @@ void ProjectMGUI::PushUIFont() ImGui::PushFont(_uiFont); } +void ProjectMGUI::PushSolidIconsFont() +{ + ImGui::PushFont(_fontAwesomeIconsSolid); +} + +void ProjectMGUI::PushRegularIconsFont() +{ + ImGui::PushFont(_fontAwesomeIconsRegular); +} + void ProjectMGUI::PopFont() { ImGui::PopFont(); diff --git a/src/gui/ProjectMGUI.h b/src/gui/ProjectMGUI.h index ac7e35c..19a7d9c 100644 --- a/src/gui/ProjectMGUI.h +++ b/src/gui/ProjectMGUI.h @@ -85,6 +85,16 @@ class ProjectMGUI : public Poco::Util::Subsystem */ void PushUIFont(); + /** + * @brief Pushes the "Solid" icon font to the render stack + */ + void PushSolidIconsFont(); + + /** + * @brief Pushes the "Regular" icon font to the render stack + */ + void PushRegularIconsFont(); + /** * @brief Pops the last font from the stack */ @@ -128,6 +138,8 @@ class ProjectMGUI : public Poco::Util::Subsystem SDL_GLContext _glContext{nullptr}; //!< Pointer to the OpenGL context associated with the window. ImFont* _uiFont{nullptr}; //!< Main UI font (monospaced). ImFont* _toastFont{nullptr}; //!< Toast message font (sans-serif, larger). + ImFont* _fontAwesomeIconsSolid{nullptr}; //!< "Solid" icons set by FontAwesome. + ImFont* _fontAwesomeIconsRegular{nullptr}; //!< "Regular" icons set by FontAwesome. uint64_t _lastFrameTicks{0}; //!< Tick count of the last frame (see SDL_GetTicks64) diff --git a/src/gui/preset_editor/CMakeLists.txt b/src/gui/preset_editor/CMakeLists.txt index f5dae8a..bfcdc30 100644 --- a/src/gui/preset_editor/CMakeLists.txt +++ b/src/gui/preset_editor/CMakeLists.txt @@ -1,14 +1,21 @@ add_subdirectory(imgui_color_text_editor) add_library(PresetEditor STATIC + CodeContextInformation.cpp + CodeContextInformation.h + CodeEditorTab.cpp + CodeEditorTab.h + CodeEditorWindow.cpp + CodeEditorWindow.h + EditorMenu.cpp + EditorMenu.h EditorPreset.cpp EditorPreset.h + ExpressionCodeTypes.h PresetEditorGUI.cpp PresetEditorGUI.h PresetFile.cpp PresetFile.h - EditorMenu.cpp - EditorMenu.h ) target_link_libraries(PresetEditor diff --git a/src/gui/preset_editor/CodeContextInformation.cpp b/src/gui/preset_editor/CodeContextInformation.cpp new file mode 100644 index 0000000..e696def --- /dev/null +++ b/src/gui/preset_editor/CodeContextInformation.cpp @@ -0,0 +1,941 @@ +#include "CodeContextInformation.h" + +#include "IconsFontAwesome7.h" + +#include + +namespace Editor { + +std::string CodeContextInformation::GetContextName(ExpressionCodeTypes type, int index) +{ + switch (type) + { + case ExpressionCodeTypes::PerFrameInit: + return "Per-Frame Init"; + case ExpressionCodeTypes::PerFrame: + return "Per-Frame"; + case ExpressionCodeTypes::PerVertex: + return "Per-Vertex"; + case ExpressionCodeTypes::CustomWaveInit: + return "Wave " + std::to_string(index) + " Init"; + case ExpressionCodeTypes::CustomWavePerFrame: + return "Wave " + std::to_string(index) + " Per-Frame"; + case ExpressionCodeTypes::CustomWavePerPoint: + return "Wave " + std::to_string(index) + " Per-Point"; + case ExpressionCodeTypes::CustomShapeInit: + return "Shape " + std::to_string(index) + " Init"; + case ExpressionCodeTypes::CustomShapePerFrame: + return "Shape " + std::to_string(index) + " Per-Frame"; + case ExpressionCodeTypes::WarpShader: + return "Warp Shader"; + case ExpressionCodeTypes::CompositeShader: + return "Composite Shader"; + } + + throw std::runtime_error(std::string("Unknown value for argument ExpressionCodeTypes type in CodeContextInformation::GetContextName()")); +} + +std::vector> CodeContextInformation::GetIdentifierList(ExpressionCodeTypes type) +{ + static std::map>> identifierList{ + {ExpressionCodeTypes::PerFrameInit, {{"zoom", "Controls inward/outward motion.\nWritable."}, // + {"zoomexp", "Controls the curvature of the zoom.\nWritable."}, // + {"rot", "Controls the amount of rotation.\nWritable."}, // + {"warp", "Controls the magnitude of the warping.\nWritable."}, // + {"cx", "Controls where the center of rotation and stretching is, horizontally.\nWritable."}, // + {"cy", "Controls where the center of rotation and stretching is, vertically.\nWritable."}, // + {"dx", "Controls amount of constant horizontal motion.\nWritable."}, // + {"dy", "Controls amount of constant vertical motion.\nWritable."}, // + {"sx", "Controls amount of constant horizontal stretching.\nWritable."}, // + {"sy", "Controls amount of constant vertical stretching.\nWritable."}, // + {"wave_mode", "Controls which of the 8/16 types of waveform is drawn.\nWritable."}, // + {"wave_x", "Horizontal position of the waveform (0..1).\nWritable."}, // + {"wave_y", "Vertical position of the waveform (0..1).\nWritable."}, // + {"wave_r", "Amount of red color in the wave (0..1).\nWritable."}, // + {"wave_g", "Amount of green color in the wave (0..1).\nWritable."}, // + {"wave_b", "Amount of blue color in the wave (0..1).\nWritable."}, // + {"wave_a", "Opacity of the wave (0..1).\nWritable."}, // + {"wave_mystery", "This value does different things for each waveform (-1..1).\nWritable."}, // + {"wave_usedots", "If 1, the waveform is drawn as dots instead of lines (0/1).\nWritable."}, // + {"wave_thick", "If 1, the waveform's lines (or dots) are drawn with double thickness (0/1).\nWritable."}, // + {"wave_additive", "If 1, the wave is drawn additively, saturating the image at white (0/1).\nWritable."}, // + {"wave_brighten", "If 1, all 3 r/g/b colors will be scaled up until at least one reaches 1.0 (0/1)\nWritable."}, // + {"ob_size", "Thickness of the outer border drawn at the edges of the screen every frame (0..0.5).\nWritable."}, // + {"ob_r", "Amount of red color in the outer border (0..1).\nWritable."}, // + {"ob_g", "Amount of green color in the outer border (0..1).\nWritable."}, // + {"ob_b", "Amount of blue color in the outer border (0..1).\nWritable."}, // + {"ob_a", "Opacity of the outer border (0=transparent, 1=opaque).\nWritable."}, // + {"ib_size", "Thickness of the inner border drawn at the edges of the screen every frame (0..0.5).\nWritable."}, // + {"ib_r", "Amount of red color in the inner border (0..1).\nWritable."}, // + {"ib_g", "Amount of green color in the inner border (0..1).\nWritable."}, // + {"ib_b", "Amount of blue color in the inner border (0..1).\nWritable."}, // + {"ib_a", "Opacity of the inner border (0=transparent, 1=opaque).\nWritable."}, // + {"mv_r", "Amount of red color in the motion vectors (0..1).\nWritable."}, // + {"mv_g", "Amount of green color in the motion vectors (0..1).\nWritable."}, // + {"mv_b", "Amount of blue color in the motion vectors (0..1).\nWritable."}, // + {"mv_a", "Opacity of the motion vectors (0=transparent, 1=opaque).\nWritable."}, // + {"mv_x", "The number of motion vectors in the X direction (0..64).\nWritable."}, // + {"mv_y", "The number of motion vectors in the Y direction (0..48).\nWritable."}, // + {"mv_l", "The length of the motion vectors (0=no trail, 1=normal, 2=double...).\nWritable."}, // + {"mv_dx", "Horizontal placement offset of the motion vectors (-1..1).\nWritable."}, // + {"mv_dy", "Vertical placement offset of the motion vectors (-1..1).\nWritable."}, // + {"decay", "Controls the eventual fade to black (1=no fade, 0.9=strong fade, 0.98=recommended).\nWritable."}, // + {"gamma", "Controls display brightness (1=normal, 2=double, 3=triple, etc.).\nWritable."}, // + {"echo_zoom", "Controls the size of the second graphics layer (>0).\nWritable."}, // + {"echo_alpha", "Controls the opacity of the second graphics layer (0=transparent/off, 0.5=half-mix, 1=opaque).\nWritable."}, // + {"echo_orient", "Selects an orientation for the second graphics layer (0=normal, 1=flip on x, 2=flip on y, 3=flip on both).\nWritable."}, // + {"darken_center", "If 1, help keeps the image from getting too bright by continually dimming the center point (0/1).\nWritable."}, // + {"wrap", "Sets whether or not screen elements can drift off of one side and onto the other (0/1).\nWritable."}, // + {"invert", "Inverts the colors in the image (0/1).\nWritable."}, // + {"brighten", "Brightens the darker parts of the image (0/1).\nWritable."}, // + {"darken", "Darkens the brighter parts of the image (0/1).\nWritable."}, // + {"solarize", "Emphasizes mid-range colors (0/1).\nWritable."}, // + {"monitor", "Value for debugging preset code. Unsupported in projectM.\nWritable."}, // + {"time", "Retrieves the current time, in seconds, since program start.\nREAD-ONLY."}, // + {"fps", "Retrieves the current framerate, in frames per second.\nREAD-ONLY."}, // + {"frame", "Retrieves the number of frames of animation elapsed since the program started.\nREAD-ONLY."}, // + {"progress", "Progress through the current preset.\nREAD-ONLY."}, // + {"bass", "Retrieves the current amount of bass (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"mid", "Retrieves the current amount of middles (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"treb", "Retrieves the current amount of treble (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"bass_att", "Retrieves the attenuated amount of bass (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"mid_att", "Retrieves the attenuated amount of middles (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"treb_att", "Retrieves the attenuated amount of treble (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"meshx", "Tells you the user's per-vertex mesh size in the X direction. Always an integer value.\nREAD-ONLY."}, // + {"meshy", "Tells you the user's per-vertex mesh size in the Y direction. Always an integer value.\nREAD-ONLY."}, // + {"pixelsx", "Width of the rendering canvas, in pixels.\nREAD-ONLY."}, // + {"pixelsy", "Height of the rendering canvas, in pixels.\nREAD-ONLY."}, // + {"aspectx", "Multiply an X coordinate by this to make the preset look the same at any aspect (>0).\nREAD-ONLY."}, // + {"aspecty", "Multiply a Y coordinate by this to make the preset look the same at any aspect (>0).\nREAD-ONLY."}, // + {"blur1_min", "Blur level 1 range minimum (0..1).\nWritable."}, // + {"blur1_max", "Blur level 1 range maximum (0..1).\nWritable."}, // + {"blur2_min", "Blur level 2 range minimum (0..1).\nWritable."}, // + {"blur2_max", "Blur level 2 range maximum (0..1).\nWritable."}, // + {"blur3_min", "Blur level 3 range minimum (0..1).\nWritable."}, // + {"blur3_max", "Blur level 3 range maximum (0..1).\nWritable."}, // + {"blur1_edge_darken", "Amount of edge darkening at blur level 1 (0..1).\nWritable."}, // + {"q1", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q2", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q3", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q4", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q5", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q6", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q7", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q8", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q9", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q10", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q11", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q12", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q13", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q14", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q15", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q16", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q17", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q18", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q19", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q20", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q21", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q22", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q23", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q24", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q25", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q26", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q27", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q28", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q29", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q30", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q31", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q32", "q1 to q32 values are carried along a chain to each code block.\nWritable."}}}, + + {ExpressionCodeTypes::PerFrame, {{"zoom", "Controls inward/outward motion.\nWritable."}, // + {"zoomexp", "Controls the curvature of the zoom.\nWritable."}, // + {"rot", "Controls the amount of rotation.\nWritable."}, // + {"warp", "Controls the magnitude of the warping.\nWritable."}, // + {"cx", "Controls where the center of rotation and stretching is, horizontally.\nWritable."}, // + {"cy", "Controls where the center of rotation and stretching is, vertically.\nWritable."}, // + {"dx", "Controls amount of constant horizontal motion.\nWritable."}, // + {"dy", "Controls amount of constant vertical motion.\nWritable."}, // + {"sx", "Controls amount of constant horizontal stretching.\nWritable."}, // + {"sy", "Controls amount of constant vertical stretching.\nWritable."}, // + {"wave_mode", "Controls which of the 8/16 types of waveform is drawn.\nWritable."}, // + {"wave_x", "Horizontal position of the waveform (0..1).\nWritable."}, // + {"wave_y", "Vertical position of the waveform (0..1).\nWritable."}, // + {"wave_r", "Amount of red color in the wave (0..1).\nWritable."}, // + {"wave_g", "Amount of green color in the wave (0..1).\nWritable."}, // + {"wave_b", "Amount of blue color in the wave (0..1).\nWritable."}, // + {"wave_a", "Opacity of the wave (0..1).\nWritable."}, // + {"wave_mystery", "This value does different things for each waveform (-1..1).\nWritable."}, // + {"wave_usedots", "If 1, the waveform is drawn as dots instead of lines (0/1).\nWritable."}, // + {"wave_thick", "If 1, the waveform's lines (or dots) are drawn with double thickness (0/1).\nWritable."}, // + {"wave_additive", "If 1, the wave is drawn additively, saturating the image at white (0/1).\nWritable."}, // + {"wave_brighten", "If 1, all 3 r/g/b colors will be scaled up until at least one reaches 1.0 (0/1)\nWritable."}, // + {"ob_size", "Thickness of the outer border drawn at the edges of the screen every frame (0..0.5).\nWritable."}, // + {"ob_r", "Amount of red color in the outer border (0..1).\nWritable."}, // + {"ob_g", "Amount of green color in the outer border (0..1).\nWritable."}, // + {"ob_b", "Amount of blue color in the outer border (0..1).\nWritable."}, // + {"ob_a", "Opacity of the outer border (0=transparent, 1=opaque).\nWritable."}, // + {"ib_size", "Thickness of the inner border drawn at the edges of the screen every frame (0..0.5).\nWritable."}, // + {"ib_r", "Amount of red color in the inner border (0..1).\nWritable."}, // + {"ib_g", "Amount of green color in the inner border (0..1).\nWritable."}, // + {"ib_b", "Amount of blue color in the inner border (0..1).\nWritable."}, // + {"ib_a", "Opacity of the inner border (0=transparent, 1=opaque).\nWritable."}, // + {"mv_r", "Amount of red color in the motion vectors (0..1).\nWritable."}, // + {"mv_g", "Amount of green color in the motion vectors (0..1).\nWritable."}, // + {"mv_b", "Amount of blue color in the motion vectors (0..1).\nWritable."}, // + {"mv_a", "Opacity of the motion vectors (0=transparent, 1=opaque).\nWritable."}, // + {"mv_x", "The number of motion vectors in the X direction (0..64).\nWritable."}, // + {"mv_y", "The number of motion vectors in the Y direction (0..48).\nWritable."}, // + {"mv_l", "The length of the motion vectors (0=no trail, 1=normal, 2=double...).\nWritable."}, // + {"mv_dx", "Horizontal placement offset of the motion vectors (-1..1).\nWritable."}, // + {"mv_dy", "Vertical placement offset of the motion vectors (-1..1).\nWritable."}, // + {"decay", "Controls the eventual fade to black (1=no fade, 0.9=strong fade, 0.98=recommended).\nWritable."}, // + {"gamma", "Controls display brightness (1=normal, 2=double, 3=triple, etc.).\nWritable."}, // + {"echo_zoom", "Controls the size of the second graphics layer (>0).\nWritable."}, // + {"echo_alpha", "Controls the opacity of the second graphics layer (0=transparent/off, 0.5=half-mix, 1=opaque).\nWritable."}, // + {"echo_orient", "Selects an orientation for the second graphics layer (0=normal, 1=flip on x, 2=flip on y, 3=flip on both).\nWritable."}, // + {"darken_center", "If 1, help keeps the image from getting too bright by continually dimming the center point (0/1).\nWritable."}, // + {"wrap", "Sets whether or not screen elements can drift off of one side and onto the other (0/1).\nWritable."}, // + {"invert", "Inverts the colors in the image (0/1).\nWritable."}, // + {"brighten", "Brightens the darker parts of the image (0/1).\nWritable."}, // + {"darken", "Darkens the brighter parts of the image (0/1).\nWritable."}, // + {"solarize", "Emphasizes mid-range colors (0/1).\nWritable."}, // + {"monitor", "Value for debugging preset code. Unsupported in projectM.\nWritable."}, // + {"time", "Retrieves the current time, in seconds, since program start.\nREAD-ONLY."}, // + {"fps", "Retrieves the current framerate, in frames per second.\nREAD-ONLY."}, // + {"frame", "Retrieves the number of frames of animation elapsed since the program started.\nREAD-ONLY."}, // + {"progress", "Progress through the current preset.\nREAD-ONLY."}, // + {"bass", "Retrieves the current amount of bass (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"mid", "Retrieves the current amount of middles (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"treb", "Retrieves the current amount of treble (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"bass_att", "Retrieves the attenuated amount of bass (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"mid_att", "Retrieves the attenuated amount of middles (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"treb_att", "Retrieves the attenuated amount of treble (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"meshx", "Tells you the user's per-vertex mesh size in the X direction. Always an integer value.\nREAD-ONLY."}, // + {"meshy", "Tells you the user's per-vertex mesh size in the Y direction. Always an integer value.\nREAD-ONLY."}, // + {"pixelsx", "Width of the rendering canvas, in pixels.\nREAD-ONLY."}, // + {"pixelsy", "Height of the rendering canvas, in pixels.\nREAD-ONLY."}, // + {"aspectx", "Multiply an X coordinate by this to make the preset look the same at any aspect (>0).\nREAD-ONLY."}, // + {"aspecty", "Multiply a Y coordinate by this to make the preset look the same at any aspect (>0).\nREAD-ONLY."}, // + {"blur1_min", "Blur level 1 range minimum (0..1).\nWritable."}, // + {"blur1_max", "Blur level 1 range maximum (0..1).\nWritable."}, // + {"blur2_min", "Blur level 2 range minimum (0..1).\nWritable."}, // + {"blur2_max", "Blur level 2 range maximum (0..1).\nWritable."}, // + {"blur3_min", "Blur level 3 range minimum (0..1).\nWritable."}, // + {"blur3_max", "Blur level 3 range maximum (0..1).\nWritable."}, // + {"blur1_edge_darken", "Amount of edge darkening at blur level 1 (0..1).\nWritable."}, // + {"q1", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q2", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q3", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q4", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q5", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q6", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q7", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q8", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q9", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q10", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q11", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q12", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q13", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q14", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q15", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q16", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q17", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q18", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q19", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q20", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q21", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q22", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q23", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q24", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q25", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q26", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q27", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q28", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q29", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q30", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q31", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q32", "q1 to q32 values are carried along a chain to each code block.\nWritable."}}}, + + {ExpressionCodeTypes::PerVertex, {{"x", "Retrieves the X position of the current pixel (0..1).\nREAD-ONLY."}, // + {"y", "Retrieves the Y position of the current pixel (0..1).\nREAD-ONLY."}, // + {"rad", "Retrieves the distance of the pixel from the center of the screen.\nAt the center of the screen this will be zero, and at the corners, 1.\nREAD-ONLY."}, // + {"ang", "Retrieves the angle of the current pixel, with respect to the center of the screen.\nThis is equal to the arctangent of y over x.\nREAD-ONLY."}, // + {"zoom", "Controls inward/outward motion.\nWritable."}, // + {"zoomexp", "Controls the curvature of the zoom.\nWritable."}, // + {"rot", "Controls the amount of rotation.\nWritable."}, // + {"warp", "Controls the magnitude of the warping.\nWritable."}, // + {"cx", "Controls where the center of rotation and stretching is, horizontally.\nWritable."}, // + {"cy", "Controls where the center of rotation and stretching is, vertically.\nWritable."}, // + {"dx", "Controls amount of constant horizontal motion.\nWritable."}, // + {"dy", "Controls amount of constant vertical motion.\nWritable."}, // + {"sx", "Controls amount of constant horizontal stretching.\nWritable."}, // + {"sy", "Controls amount of constant vertical stretching.\nWritable."}, // + {"time", "Retrieves the current time, in seconds, since program start.\nREAD-ONLY."}, // + {"fps", "Retrieves the current framerate, in frames per second.\nREAD-ONLY."}, // + {"frame", "Retrieves the number of frames of animation elapsed since the program started.\nREAD-ONLY."}, // + {"progress", "Progress through the current preset.\nREAD-ONLY."}, // + {"bass", "Retrieves the current amount of bass (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"mid", "Retrieves the current amount of middles (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"treb", "Retrieves the current amount of treble (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"bass_att", "Retrieves the attenuated amount of bass (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"mid_att", "Retrieves the attenuated amount of middles (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"treb_att", "Retrieves the attenuated amount of treble (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"meshx", "Tells you the user's per-vertex mesh size in the X direction. Always an integer value.\nREAD-ONLY."}, // + {"meshy", "Tells you the user's per-vertex mesh size in the Y direction. Always an integer value.\nREAD-ONLY."}, // + {"pixelsx", "Width of the rendering canvas, in pixels.\nREAD-ONLY."}, // + {"pixelsy", "Height of the rendering canvas, in pixels.\nREAD-ONLY."}, // + {"aspectx", "Multiply an X coordinate by this to make the preset look the same at any aspect (>0).\nREAD-ONLY."}, // + {"aspecty", "Multiply a Y coordinate by this to make the preset look the same at any aspect (>0).\nREAD-ONLY."}, // + {"q1", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q2", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q3", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q4", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q5", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q6", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q7", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q8", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q9", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q10", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q11", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q12", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q13", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q14", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q15", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q16", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q17", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q18", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q19", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q20", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q21", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q22", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q23", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q24", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q25", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q26", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q27", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q28", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q29", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q30", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q31", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q32", "q1 to q32 values are carried along a chain to each code block.\nWritable."}}}, + + {ExpressionCodeTypes::CustomWaveInit, {{"r", "Base amount of red color in the wave (0..1).\nWritable."}, // + {"g", "Base amount of green color in the wave (0..1).\nWritable."}, // + {"b", "Base amount of blue color in the wave (0..1).\nWritable."}, // + {"a", "Base opacity of the waveform (0=transparent, 1=opaque).\nWritable."}, // + {"samples", "Read: retrieves the # of samples specified for this custom wave (from the menu).\nWrite: lets you dynamically change that #, frame to frame (0..512).\nWritable."}, // + {"time", "Retrieves the current time, in seconds, since program start.\nREAD-ONLY."}, // + {"fps", "Retrieves the current framerate, in frames per second.\nREAD-ONLY."}, // + {"frame", "Retrieves the number of frames of animation elapsed since the program started.\nREAD-ONLY."}, // + {"progress", "Progress through the current preset.\nREAD-ONLY."}, // + {"bass", "Retrieves the current amount of bass (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"mid", "Retrieves the current amount of middles (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"treb", "Retrieves the current amount of treble (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"bass_att", "Retrieves the attenuated amount of bass (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"mid_att", "Retrieves the attenuated amount of middles (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"treb_att", "Retrieves the attenuated amount of treble (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"q1", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q2", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q3", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q4", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q5", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q6", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q7", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q8", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q9", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q10", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q11", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q12", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q13", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q14", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q15", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q16", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q17", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q18", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q19", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q20", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q21", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q22", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q23", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q24", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q25", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q26", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q27", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q28", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q29", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q30", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q31", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q32", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"t1", "t1 to t8 values are carried from each wave init to per-frame to per-point.\nWritable."}, // + {"t2", "t1 to t8 values are carried from each wave init to per-frame to per-point.\nWritable."}, // + {"t3", "t1 to t8 values are carried from each wave init to per-frame to per-point.\nWritable."}, // + {"t4", "t1 to t8 values are carried from each wave init to per-frame to per-point.\nWritable."}, // + {"t5", "t1 to t8 values are carried from each wave init to per-frame to per-point.\nWritable."}, // + {"t6", "t1 to t8 values are carried from each wave init to per-frame to per-point.\nWritable."}, // + {"t7", "t1 to t8 values are carried from each wave init to per-frame to per-point.\nWritable."}, // + {"t8", "t1 to t8 values are carried from each wave init to per-frame to per-point.\nWritable."}}}, + + {ExpressionCodeTypes::CustomWavePerFrame, {{"r", "Base amount of red color in the wave (0..1).\nWritable."}, // + {"g", "Base amount of green color in the wave (0..1).\nWritable."}, // + {"b", "Base amount of blue color in the wave (0..1).\nWritable."}, // + {"a", "Base opacity of the waveform (0=transparent, 1=opaque).\nWritable."}, // + {"samples", "Read: retrieves the # of samples specified for this custom wave (from the menu).\nWrite: lets you dynamically change that #, frame to frame (0..512).\nWritable."}, // + {"time", "Retrieves the current time, in seconds, since program start.\nREAD-ONLY."}, // + {"fps", "Retrieves the current framerate, in frames per second.\nREAD-ONLY."}, // + {"frame", "Retrieves the number of frames of animation elapsed since the program started.\nREAD-ONLY."}, // + {"progress", "Progress through the current preset.\nREAD-ONLY."}, // + {"bass", "Retrieves the current amount of bass (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"mid", "Retrieves the current amount of middles (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"treb", "Retrieves the current amount of treble (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"bass_att", "Retrieves the attenuated amount of bass (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"mid_att", "Retrieves the attenuated amount of middles (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"treb_att", "Retrieves the attenuated amount of treble (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"q1", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q2", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q3", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q4", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q5", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q6", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q7", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q8", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q9", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q10", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q11", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q12", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q13", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q14", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q15", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q16", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q17", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q18", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q19", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q20", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q21", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q22", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q23", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q24", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q25", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q26", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q27", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q28", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q29", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q30", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q31", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q32", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"t1", "t1 to t8 values are carried from each wave init to per-frame to per-point.\nWritable."}, // + {"t2", "t1 to t8 values are carried from each wave init to per-frame to per-point.\nWritable."}, // + {"t3", "t1 to t8 values are carried from each wave init to per-frame to per-point.\nWritable."}, // + {"t4", "t1 to t8 values are carried from each wave init to per-frame to per-point.\nWritable."}, // + {"t5", "t1 to t8 values are carried from each wave init to per-frame to per-point.\nWritable."}, // + {"t6", "t1 to t8 values are carried from each wave init to per-frame to per-point.\nWritable."}, // + {"t7", "t1 to t8 values are carried from each wave init to per-frame to per-point.\nWritable."}, // + {"t8", "t1 to t8 values are carried from each wave init to per-frame to per-point.\nWritable."}}}, + + {ExpressionCodeTypes::CustomWavePerPoint, {{"r", "Base amount of red color in the wave (0..1).\nWritable."}, // + {"g", "Base amount of green color in the wave (0..1).\nWritable."}, // + {"b", "Base amount of blue color in the wave (0..1).\nWritable."}, // + {"a", "Base opacity of the waveform (0=transparent, 1=opaque).\nWritable."}, // + {"x", "The X position of this point that makes up the wave (0=left, 1=right).\nWritable."}, // + {"y", "The Y position of this point that makes up the wave (0=left, 1=right).\nWritable."}, // + {"sample", "How far along we are, through the samples that make up the waveform (0=first sample, 0.5 = half-way through, 1=last sample).\nREAD-ONLY."}, // + {"value1", "The value of the left audio channel sample or spectrum at this point in the waveform.\nREAD-ONLY."}, // + {"value2", "The value of the right audio channel sample or spectrum at this point in the waveform.\nREAD-ONLY."}, // + {"time", "Retrieves the current time, in seconds, since program start.\nREAD-ONLY."}, // + {"fps", "Retrieves the current framerate, in frames per second.\nREAD-ONLY."}, // + {"frame", "Retrieves the number of frames of animation elapsed since the program started.\nREAD-ONLY."}, // + {"progress", "Progress through the current preset.\nREAD-ONLY."}, // + {"bass", "Retrieves the current amount of bass (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"mid", "Retrieves the current amount of middles (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"treb", "Retrieves the current amount of treble (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"bass_att", "Retrieves the attenuated amount of bass (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"mid_att", "Retrieves the attenuated amount of middles (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"treb_att", "Retrieves the attenuated amount of treble (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"q1", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q2", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q3", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q4", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q5", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q6", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q7", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q8", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q9", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q10", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q11", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q12", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q13", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q14", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q15", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q16", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q17", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q18", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q19", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q20", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q21", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q22", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q23", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q24", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q25", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q26", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q27", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q28", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q29", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q30", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q31", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q32", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"t1", "t1 to t8 values are carried from each wave init to per-frame to per-point.\nWritable."}, // + {"t2", "t1 to t8 values are carried from each wave init to per-frame to per-point.\nWritable."}, // + {"t3", "t1 to t8 values are carried from each wave init to per-frame to per-point.\nWritable."}, // + {"t4", "t1 to t8 values are carried from each wave init to per-frame to per-point.\nWritable."}, // + {"t5", "t1 to t8 values are carried from each wave init to per-frame to per-point.\nWritable."}, // + {"t6", "t1 to t8 values are carried from each wave init to per-frame to per-point.\nWritable."}, // + {"t7", "t1 to t8 values are carried from each wave init to per-frame to per-point.\nWritable."}, // + {"t8", "t1 to t8 values are carried from each wave init to per-frame to per-point.\nWritable."}}}, + + {ExpressionCodeTypes::CustomShapeInit, {{"num_inst", "The total # of shape instances (1..1024).\nREAD-ONLY."}, // + {"sides", "The default number of sides that make up the polygonal shape (3..100).\nWritable."}, // + {"thick", "If non-zero, the border will be overdrawn 4 times to make it thicker, bolder, and more visible (0/1).\nWritable."}, // + {"additive", "If non-zero, the shape will add color to saturate the image towards white; otherwise, it will replace what's there (0/1).\nWritable."}, // + {"x", "Default X position of the shape (0..1; 0=left side, 1=right side).\nWritable."}, // + {"y", "Default Y position of the shape (0..1; 0=left side, 1=right side).\nWritable."}, // + {"rad", "Default radius of the shape (>=0).\nWritable."}, // + {"ang", "Default rotation angle of the shape (0...2*pi).\nWritable."}, // + {"textured", "If non-zero, the shape will be textured with the image from the previous frame (0/1).\nWritable."}, // + {"tex_zoom", "The portion of the previous frame's image to use with the shape (>0).\nWritable."}, // + {"tex_ang", "The angle at which to rotate the previous frame's image before applying it to the shape (0..2*PI).\nWritable."}, // + {"r", "Default amount of red color toward the center of the shape (0..1).\nWritable."}, // + {"g", "Default amount of green color toward the center of the shape (0..1).\nWritable."}, // + {"b", "Default amount of blue color toward the center of the shape (0..1).\nWritable."}, // + {"a", "Default opacity of the center of the shape (0=transparent, 1=opaque).\nWritable."}, // + {"r2", "Default amount of red color toward the outer edge of the shape (0..1).\nWritable."}, // + {"g2", "Default amount of green color toward the outer edge of the shape (0..1).\nWritable."}, // + {"b2", "Default amount of blue color toward the outer edge of the shape (0..1).\nWritable."}, // + {"a2", "Default opacity of the outer edge of the shape (0=transparent, 1=opaque).\nWritable."}, // + {"border_r", "Default amount of red color in the shape's border (0..1).\nWritable."}, // + {"border_g", "Default amount of green color in the shape's border (0..1).\nWritable."}, // + {"border_b", "Default amount of blue color in the shape's border (0..1).\nWritable."}, // + {"border_a", "Default opacity of the shape's border (0=transparent, 1=opaque).\nWritable."}, // + {"time", "Retrieves the current time, in seconds, since program start.\nREAD-ONLY."}, // + {"fps", "Retrieves the current framerate, in frames per second.\nREAD-ONLY."}, // + {"frame", "Retrieves the number of frames of animation elapsed since the program started.\nREAD-ONLY."}, // + {"progress", "Progress through the current preset.\nREAD-ONLY."}, // + {"bass", "Retrieves the current amount of bass (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"mid", "Retrieves the current amount of middles (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"treb", "Retrieves the current amount of treble (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"bass_att", "Retrieves the attenuated amount of bass (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"mid_att", "Retrieves the attenuated amount of middles (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"treb_att", "Retrieves the attenuated amount of treble (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"q1", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q2", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q3", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q4", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q5", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q6", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q7", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q8", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q9", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q10", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q11", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q12", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q13", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q14", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q15", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q16", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q17", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q18", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q19", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q20", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q21", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q22", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q23", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q24", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q25", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q26", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q27", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q28", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q29", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q30", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q31", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q32", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"t1", "t1 to t8 values are carried from each shape init to per-frame.\nWritable."}, // + {"t2", "t1 to t8 values are carried from each shape init to per-frame.\nWritable."}, // + {"t3", "t1 to t8 values are carried from each shape init to per-frame.\nWritable."}, // + {"t4", "t1 to t8 values are carried from each shape init to per-frame.\nWritable."}, // + {"t5", "t1 to t8 values are carried from each shape init to per-frame.\nWritable."}, // + {"t6", "t1 to t8 values are carried from each shape init to per-frame.\nWritable."}, // + {"t7", "t1 to t8 values are carried from each shape init to per-frame.\nWritable."}, // + {"t8", "t1 to t8 values are carried from each shape init to per-frame.\nWritable."}}}, + + {ExpressionCodeTypes::CustomShapePerFrame, {{"num_inst", "The total # of shape instances (1..1024).\nREAD-ONLY."}, // + {"instance", "The current instance number that this code is being executed for (0..num_inst-1).\nREAD-ONLY."}, // + {"sides", "The default number of sides that make up the polygonal shape (3..100).\nWritable."}, // + {"thick", "If non-zero, the border will be overdrawn 4 times to make it thicker, bolder, and more visible (0/1).\nWritable."}, // + {"additive", "If non-zero, the shape will add color to saturate the image towards white; otherwise, it will replace what's there (0/1).\nWritable."}, // + {"x", "Default X position of the shape (0..1; 0=left side, 1=right side).\nWritable."}, // + {"y", "Default Y position of the shape (0..1; 0=left side, 1=right side).\nWritable."}, // + {"rad", "Default radius of the shape (>=0).\nWritable."}, // + {"ang", "Default rotation angle of the shape (0...2*pi).\nWritable."}, // + {"textured", "If non-zero, the shape will be textured with the image from the previous frame (0/1).\nWritable."}, // + {"tex_zoom", "The portion of the previous frame's image to use with the shape (>0).\nWritable."}, // + {"tex_ang", "The angle at which to rotate the previous frame's image before applying it to the shape (0..2*PI).\nWritable."}, // + {"r", "Default amount of red color toward the center of the shape (0..1).\nWritable."}, // + {"g", "Default amount of green color toward the center of the shape (0..1).\nWritable."}, // + {"b", "Default amount of blue color toward the center of the shape (0..1).\nWritable."}, // + {"a", "Default opacity of the center of the shape (0=transparent, 1=opaque).\nWritable."}, // + {"r2", "Default amount of red color toward the outer edge of the shape (0..1).\nWritable."}, // + {"g2", "Default amount of green color toward the outer edge of the shape (0..1).\nWritable."}, // + {"b2", "Default amount of blue color toward the outer edge of the shape (0..1).\nWritable."}, // + {"a2", "Default opacity of the outer edge of the shape (0=transparent, 1=opaque).\nWritable."}, // + {"border_r", "Default amount of red color in the shape's border (0..1).\nWritable."}, // + {"border_g", "Default amount of green color in the shape's border (0..1).\nWritable."}, // + {"border_b", "Default amount of blue color in the shape's border (0..1).\nWritable."}, // + {"border_a", "Default opacity of the shape's border (0=transparent, 1=opaque).\nWritable."}, // + {"time", "Retrieves the current time, in seconds, since program start.\nREAD-ONLY."}, // + {"fps", "Retrieves the current framerate, in frames per second.\nREAD-ONLY."}, // + {"frame", "Retrieves the number of frames of animation elapsed since the program started.\nREAD-ONLY."}, // + {"progress", "Progress through the current preset.\nREAD-ONLY."}, // + {"bass", "Retrieves the current amount of bass (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"mid", "Retrieves the current amount of middles (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"treb", "Retrieves the current amount of treble (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"bass_att", "Retrieves the attenuated amount of bass (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"mid_att", "Retrieves the attenuated amount of middles (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"treb_att", "Retrieves the attenuated amount of treble (1=normal, <~0.7=quiet, >1.3=loud).\nREAD-ONLY."}, // + {"q1", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q2", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q3", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q4", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q5", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q6", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q7", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q8", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q9", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q10", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q11", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q12", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q13", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q14", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q15", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q16", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q17", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q18", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q19", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q20", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q21", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q22", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q23", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q24", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q25", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q26", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q27", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q28", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q29", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q30", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q31", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"q32", "q1 to q32 values are carried along a chain to each code block.\nWritable."}, // + {"t1", "t1 to t8 values are carried from each shape init to per-frame.\nWritable."}, // + {"t2", "t1 to t8 values are carried from each shape init to per-frame.\nWritable."}, // + {"t3", "t1 to t8 values are carried from each shape init to per-frame.\nWritable."}, // + {"t4", "t1 to t8 values are carried from each shape init to per-frame.\nWritable."}, // + {"t5", "t1 to t8 values are carried from each shape init to per-frame.\nWritable."}, // + {"t6", "t1 to t8 values are carried from each shape init to per-frame.\nWritable."}, // + {"t7", "t1 to t8 values are carried from each shape init to per-frame.\nWritable."}, // + {"t8", "t1 to t8 values are carried from each shape init to per-frame.\nWritable."}}}, + + {ExpressionCodeTypes::WarpShader, {{"shader_body", ICON_FA_PAPER_PLANE " Shader entry point.\nWill be replaced with the appropriate function declaration at runtime."}, // + {"ret", ICON_FA_CUBE " float3\n" ICON_FA_RIGHT_FROM_BRACKET " Shader output RGB color."}, // + {"uv", ICON_FA_CUBE " float2\nWarped UV coordinates (approx. 0..1)."}, // + {"uv_orig", ICON_FA_CUBE " float2\nOriginal, unwarped UV coordinates (0..1)."}, // + {"rad", ICON_FA_CUBE " float\nRadius of the current pixel from center of screen (0..1)."}, // + {"ang", ICON_FA_CUBE " float\nAngle of the current pixel from center of screen (0..2*PI)."}, // + {"rand_preset", ICON_FA_CUBE " float4\n4 random floats [0..1], updated once per preset."}, // + {"rand_frame", ICON_FA_CUBE " float4\n4 random floats [0..1], updated each frame."}, // + {"time", ICON_FA_CUBE " float\nThe time, in seconds, starting at zero when the *preset* starts."}, // + {"fps", ICON_FA_CUBE " float\nThe current framerate (frames per second)."}, // + {"frame", ICON_FA_CUBE " float\nThe current frame #."}, // + {"progress", ICON_FA_CUBE " float\nThe progress through the current preset (0..1)."}, // + {"bass", ICON_FA_CUBE " float\nThe current amount of bass (1=normal, <~0.7=quiet, >1.3=loud)."}, // + {"mid", ICON_FA_CUBE " float\nThe current amount of middles (1=normal, <~0.7=quiet, >1.3=loud)."}, // + {"treb", ICON_FA_CUBE " float\nThe current amount of treble (1=normal, <~0.7=quiet, >1.3=loud)."}, // + {"vol", ICON_FA_CUBE " float\nThe current volume of all frequencies (1=normal, <~0.7=quiet, >1.3=loud)."}, // + {"bass_att", ICON_FA_CUBE " float\nThe attenuated amount of bass (1=normal, <~0.7=quiet, >1.3=loud)."}, // + {"mid_att", ICON_FA_CUBE " float\nThe attenuated amount of middles (1=normal, <~0.7=quiet, >1.3=loud)."}, // + {"treb_att", ICON_FA_CUBE " float\nThe attenuated amount of treble (1=normal, <~0.7=quiet, >1.3=loud)."}, // + {"vol_att", ICON_FA_CUBE " float\nThe attenuated volume of all frequencies (1=normal, <~0.7=quiet, >1.3=loud)."}, // + {"aspect", ICON_FA_CUBE " float4\n.xy: Multiplier to use on UV's to paste an image fullscreen, *aspect-aware*\n.zw: Inverse (1/x) values as in .xy."}, // + {"texsize", ICON_FA_CUBE " float4\nInfo about the size of the drawing canvas, in pixels.\n.xy: (width,height)\n.zw: (1/(float)w, 1/(float)h)"}, // + {"slow_roam_cos", ICON_FA_CUBE " float4\nFour values that slowly roam around in the [0..1] range at varying speeds.\n.xyzw: 0.5 + 0.5*cos(time * float4(~0.005, ~0.008, ~0.013, ~0.022))"}, // + {"roam_cos", ICON_FA_CUBE " float4\nFour values that roam around in the [0..1] range.\n.xyzw: 0.5 + 0.5*cos(time * float4(~0.3, ~1.3, ~5, ~20))"}, // + {"slow_roam_sin", ICON_FA_CUBE " float4\nFour values that slowly roam around in the [0..1] range at varying speeds.\n.xyzw: 0.5 + 0.5*sin(time * float4(~0.005, ~0.008, ~0.013, ~0.022))"}, // + {"roam_sin", ICON_FA_CUBE " float4\nFour values that roam around in the [0..1] range at varying speeds.\n.xyzw: 0.5 + 0.5*sin(time * float4(~0.3, ~1.3, ~5, ~20))"}, // + {"q1", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q2", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q3", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q4", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q5", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q6", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q7", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q8", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q9", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q10", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q11", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q12", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q13", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q14", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q15", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q16", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q17", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q18", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q19", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q20", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q21", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q22", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q23", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q24", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q25", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q26", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q27", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q28", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q29", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q30", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q31", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q32", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"_qa", ICON_FA_CUBE " float4\nA 4-component vector of the q1-q4 variables. Can be swizzled."}, // + {"_qb", ICON_FA_CUBE " float4\nA 4-component vector of the q5-q8 variables. Can be swizzled."}, // + {"_qc", ICON_FA_CUBE " float4\nA 4-component vector of the q9-q12 variables. Can be swizzled."}, // + {"_qd", ICON_FA_CUBE " float4\nA 4-component vector of the q13-q46 variables. Can be swizzled."}, // + {"_qe", ICON_FA_CUBE " float4\nA 4-component vector of the q17-q20 variables. Can be swizzled."}, // + {"_qf", ICON_FA_CUBE " float4\nA 4-component vector of the q21-q24 variables. Can be swizzled."}, // + {"_qg", ICON_FA_CUBE " float4\nA 4-component vector of the q25-q28 variables. Can be swizzled."}, // + {"_qh", ICON_FA_CUBE " float4\nA 4-component vector of the q29-q32 variables. Can be swizzled."}, // + {"blur1_min", ICON_FA_CUBE " float\nBlur level 1 range minimum (0..1)."}, // + {"blur1_max", ICON_FA_CUBE " float\nBlur level 1 range maximum (0..1)."}, // + {"blur2_min", ICON_FA_CUBE " float\nBlur level 2 range minimum (0..1)."}, // + {"blur2_max", ICON_FA_CUBE " float\nBlur level 2 range maximum (0..1)."}, // + {"blur3_min", ICON_FA_CUBE " float\nBlur level 3 range minimum (0..1)."}, // + {"blur3_max", ICON_FA_CUBE " float\nBlur level 3 range maximum (0..1)."}, // + {"rot_s1", ICON_FA_CUBE " float4x3\nFour random, static rotations, randomized at preset load time.\nMinor translation component (<1)."}, // + {"rot_s2", ICON_FA_CUBE " float4x3\nFour random, static rotations, randomized at preset load time.\nMinor translation component (<1)."}, // + {"rot_s3", ICON_FA_CUBE " float4x3\nFour random, static rotations, randomized at preset load time.\nMinor translation component (<1)."}, // + {"rot_s4", ICON_FA_CUBE " float4x3\nFour random, static rotations, randomized at preset load time.\nMinor translation component (<1)."}, // + {"rot_d1", ICON_FA_CUBE " float4x3\nFour random, slowly changing rotations."}, // + {"rot_d2", ICON_FA_CUBE " float4x3\nFour random, slowly changing rotations."}, // + {"rot_d3", ICON_FA_CUBE " float4x3\nFour random, slowly changing rotations."}, // + {"rot_d4", ICON_FA_CUBE " float4x3\nFour random, slowly changing rotations."}, // + {"rot_f1", ICON_FA_CUBE " float4x3\nFour random, faster-changing rotations."}, // + {"rot_f2", ICON_FA_CUBE " float4x3\nFour random, faster-changing rotations."}, // + {"rot_f3", ICON_FA_CUBE " float4x3\nFour random, faster-changing rotations."}, // + {"rot_f4", ICON_FA_CUBE " float4x3\nFour random, faster-changing rotations."}, // + {"rot_vf1", ICON_FA_CUBE " float4x3\nFour random, very-fast-changing rotations."}, // + {"rot_vf2", ICON_FA_CUBE " float4x3\nFour random, very-fast-changing rotations."}, // + {"rot_vf3", ICON_FA_CUBE " float4x3\nFour random, very-fast-changing rotations."}, // + {"rot_vf4", ICON_FA_CUBE " float4x3\nFour random, very-fast-changing rotations."}, // + {"rot_uf1", ICON_FA_CUBE " float4x3\nFour random, ultra-fast-changing rotations."}, // + {"rot_uf2", ICON_FA_CUBE " float4x3\nFour random, ultra-fast-changing rotations."}, // + {"rot_uf3", ICON_FA_CUBE " float4x3\nFour random, ultra-fast-changing rotations."}, // + {"rot_uf4", ICON_FA_CUBE " float4x3\nFour random, ultra-fast-changing rotations."}, // + {"rot_rand1", ICON_FA_CUBE " float4x3\nRandom rotation on every frame."}, // + {"rot_rand2", ICON_FA_CUBE " float4x3\nRandom rotation on every frame."}, // + {"rot_rand3", ICON_FA_CUBE " float4x3\nRandom rotation on every frame."}, // + {"rot_rand4", ICON_FA_CUBE " float4x3\nRandom rotation on every frame."}, // + {"sampler_main", ICON_FA_CUBE " sampler\nMain preset image texture sampler.\nBilinear filtering, wraps around."}, // + {"sampler_fw_main", ICON_FA_CUBE " sampler\nMain preset image texture sampler.\nBilinear filtering, wraps around."}, // + {"sampler_fc_main", ICON_FA_CUBE " sampler\nMain preset image texture sampler.\nBilinear filtering, clamps to edge."}, // + {"sampler_pw_main", ICON_FA_CUBE " sampler\nMain preset image texture sampler.\nPoint sampling, wraps around."}, // + {"sampler_pc_main", ICON_FA_CUBE " sampler\nMain preset image texture sampler.\nPoint sampling, clamps to edge."}, // + {"sampler_noise_lq", ICON_FA_CUBE " sampler\n256x256 2D 4-channel low-quality noise texture.\nBilinear filtering, wraps around."}, // + {"sampler_fw_noise_lq", ICON_FA_CUBE " sampler\n256x256 2D 4-channel low-quality noise texture.\nBilinear filtering, wraps around."}, // + {"sampler_fc_noise_lq", ICON_FA_CUBE " sampler\n256x256 2D 4-channel low-quality noise texture.\nBilinear filtering, clamps to edge."}, // + {"sampler_pw_noise_lq", ICON_FA_CUBE " sampler\n256x256 2D 4-channel low-quality noise texture.\nPoint sampling, wraps around."}, // + {"sampler_pc_noise_lq", ICON_FA_CUBE " sampler\n256x256 2D 4-channel low-quality noise texture.\nPoint sampling, clamps to edge."}, // + {"sampler_noise_lq_lite", ICON_FA_CUBE " sampler\n32x32 2D 4-channel low-quality noise texture.\nBilinear filtering, wraps around."}, // + {"sampler_fw_noise_lq_lite", ICON_FA_CUBE " sampler\n32x32 2D 4-channel low-quality noise texture.\nBilinear filtering, wraps around."}, // + {"sampler_fc_noise_lq_lite", ICON_FA_CUBE " sampler\n32x32 2D 4-channel low-quality noise texture.\nBilinear filtering, clamps to edge."}, // + {"sampler_pw_noise_lq_lite", ICON_FA_CUBE " sampler\n32x32 2D 4-channel low-quality noise texture.\nPoint sampling, wraps around."}, // + {"sampler_pc_noise_lq_lite", ICON_FA_CUBE " sampler\n32x32 2D 4-channel low-quality noise texture.\nPoint sampling, clamps to edge."}, // + {"sampler_noise_mq", ICON_FA_CUBE " sampler\n64x64 2D 4-channel medium-quality noise texture.\nBilinear filtering, wraps around."}, // + {"sampler_fw_noise_mq", ICON_FA_CUBE " sampler\n64x64 2D 4-channel medium-quality noise texture.\nBilinear filtering, wraps around."}, // + {"sampler_fc_noise_mq", ICON_FA_CUBE " sampler\n64x64 2D 4-channel medium-quality noise texture.\nBilinear filtering, clamps to edge."}, // + {"sampler_pw_noise_mq", ICON_FA_CUBE " sampler\n64x64 2D 4-channel medium-quality noise texture.\nPoint sampling, wraps around."}, // + {"sampler_pc_noise_mq", ICON_FA_CUBE " sampler\n64x64 2D 4-channel medium-quality noise texture.\nPoint sampling, clamps to edge."}, // + {"sampler_noise_hq", ICON_FA_CUBE " sampler\n32x32 2D 4-channel high-quality noise texture.\nBilinear filtering, wraps around."}, // + {"sampler_fw_noise_hq", ICON_FA_CUBE " sampler\n32x32 2D 4-channel high-quality noise texture.\nBilinear filtering, wraps around."}, // + {"sampler_fc_noise_hq", ICON_FA_CUBE " sampler\n32x32 2D 4-channel high-quality noise texture.\nBilinear filtering, clamps to edge."}, // + {"sampler_pw_noise_hq", ICON_FA_CUBE " sampler\n32x32 2D 4-channel high-quality noise texture.\nPoint sampling, wraps around."}, // + {"sampler_pc_noise_hq", ICON_FA_CUBE " sampler\n32x32 2D 4-channel high-quality noise texture.\nPoint sampling, clamps to edge."}, // + {"sampler_noisevol_lq", ICON_FA_CUBE " sampler\n32x32x32 3D 4-channel low-quality volumetric noise texture.\nBilinear filtering, wraps around."}, // + {"sampler_fw_noisevol_lq", ICON_FA_CUBE " sampler\n32x32x32 3D 4-channel low-quality volumetric noise texture.\nBilinear filtering, wraps around."}, // + {"sampler_fc_noisevol_lq", ICON_FA_CUBE " sampler\n32x32x32 3D 4-channel low-quality volumetric noise texture.\nBilinear filtering, clamps to edge."}, // + {"sampler_pw_noisevol_lq", ICON_FA_CUBE " sampler\n32x32x32 3D 4-channel low-quality volumetric noise texture.\nPoint sampling, wraps around."}, // + {"sampler_pc_noisevol_lq", ICON_FA_CUBE " sampler\n32x32x32 3D 4-channel low-quality volumetric noise texture.\nPoint sampling, clamps to edge."}, // + {"sampler_noisevol_hq", ICON_FA_CUBE " sampler\n8x8x8 3D 4-channel high-quality volumetric noise texture.\nBilinear filtering, wraps around."}, // + {"sampler_fw_noisevol_hq", ICON_FA_CUBE " sampler\n8x8x8 3D 4-channel high-quality volumetric noise texture.\nBilinear filtering, wraps around."}, // + {"sampler_fc_noisevol_hq", ICON_FA_CUBE " sampler\n8x8x8 3D 4-channel high-quality volumetric noise texture.\nBilinear filtering, clamps to edge."}, // + {"sampler_pw_noisevol_hq", ICON_FA_CUBE " sampler\n8x8x8 3D 4-channel high-quality volumetric noise texture.\nPoint sampling, wraps around."}, // + {"sampler_pc_noisevol_hq", ICON_FA_CUBE " sampler\n8x8x8 3D 4-channel high-quality volumetric noise texture.\nPoint sampling, clamps to edge."}}}, + + {ExpressionCodeTypes::CompositeShader, {{"shader_body", ICON_FA_PAPER_PLANE " Shader entry point.\nWill be replaced with the appropriate function declaration at runtime."}, // + {"ret", ICON_FA_CUBE " float3\n" ICON_FA_RIGHT_FROM_BRACKET " Shader output RGB color."}, // + {"uv", ICON_FA_CUBE " float2\nUnwarped UV coordinates (0..1)."}, // + {"hue_shader", ICON_FA_CUBE " float3\nA color that varies across the screen (the old 'hue shader' effect from MilkDrop 1)."}, // + {"rad", ICON_FA_CUBE " float\nRadius of the current pixel from center of screen (0..1)."}, // + {"ang", ICON_FA_CUBE " float\nAngle of the current pixel from center of screen (0..2*PI)."}, // + {"rand_preset", ICON_FA_CUBE " float4\n4 random floats [0..1], updated once per preset."}, // + {"rand_frame", ICON_FA_CUBE " float4\n4 random floats [0..1], updated each frame."}, // + {"time", ICON_FA_CUBE " float\nThe time, in seconds, starting at zero when the *preset* starts."}, // + {"fps", ICON_FA_CUBE " float\nThe current framerate (frames per second)."}, // + {"frame", ICON_FA_CUBE " float\nThe current frame #."}, // + {"progress", ICON_FA_CUBE " float\nThe progress through the current preset (0..1)."}, // + {"bass", ICON_FA_CUBE " float\nThe current amount of bass (1=normal, <~0.7=quiet, >1.3=loud)."}, // + {"mid", ICON_FA_CUBE " float\nThe current amount of middles (1=normal, <~0.7=quiet, >1.3=loud)."}, // + {"treb", ICON_FA_CUBE " float\nThe current amount of treble (1=normal, <~0.7=quiet, >1.3=loud)."}, // + {"vol", ICON_FA_CUBE " float\nThe current volume of all frequencies (1=normal, <~0.7=quiet, >1.3=loud)."}, // + {"bass_att", ICON_FA_CUBE " float\nThe attenuated amount of bass (1=normal, <~0.7=quiet, >1.3=loud)."}, // + {"mid_att", ICON_FA_CUBE " float\nThe attenuated amount of middles (1=normal, <~0.7=quiet, >1.3=loud)."}, // + {"treb_att", ICON_FA_CUBE " float\nThe attenuated amount of treble (1=normal, <~0.7=quiet, >1.3=loud)."}, // + {"vol_att", ICON_FA_CUBE " float\nThe attenuated volume of all frequencies (1=normal, <~0.7=quiet, >1.3=loud)."}, // + {"aspect", ICON_FA_CUBE " float4\n.xy: Multiplier to use on UV's to paste an image fullscreen, *aspect-aware*\n.zw: Inverse (1/x) values as in .xy."}, // + {"texsize", ICON_FA_CUBE " float4\nInfo about the size of the drawing canvas, in pixels.\n.xy: (width,height)\n.zw: (1/(float)w, 1/(float)h)"}, // + {"slow_roam_cos", ICON_FA_CUBE " float4\nFour values that slowly roam around in the [0..1] range at varying speeds.\n.xyzw: 0.5 + 0.5*cos(time * float4(~0.005, ~0.008, ~0.013, ~0.022))"}, // + {"roam_cos", ICON_FA_CUBE " float4\nFour values that roam around in the [0..1] range.\n.xyzw: 0.5 + 0.5*cos(time * float4(~0.3, ~1.3, ~5, ~20))"}, // + {"slow_roam_sin", ICON_FA_CUBE " float4\nFour values that slowly roam around in the [0..1] range at varying speeds.\n.xyzw: 0.5 + 0.5*sin(time * float4(~0.005, ~0.008, ~0.013, ~0.022))"}, // + {"roam_sin", ICON_FA_CUBE " float4\nFour values that roam around in the [0..1] range at varying speeds.\n.xyzw: 0.5 + 0.5*sin(time * float4(~0.3, ~1.3, ~5, ~20))"}, // + {"q1", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q2", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q3", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q4", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q5", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q6", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q7", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q8", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q9", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q10", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q11", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q12", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q13", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q14", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q15", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q16", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q17", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q18", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q19", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q20", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q21", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q22", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q23", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q24", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q25", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q26", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q27", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q28", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q29", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q30", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q31", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"q32", ICON_FA_CUBE " float\nq1 to q32 values are copied from the per-frame code into the shader."}, // + {"_qa", ICON_FA_CUBE " float4\nA 4-component vector of the q1-q4 variables. Can be swizzled."}, // + {"_qb", ICON_FA_CUBE " float4\nA 4-component vector of the q5-q8 variables. Can be swizzled."}, // + {"_qc", ICON_FA_CUBE " float4\nA 4-component vector of the q9-q12 variables. Can be swizzled."}, // + {"_qd", ICON_FA_CUBE " float4\nA 4-component vector of the q13-q46 variables. Can be swizzled."}, // + {"_qe", ICON_FA_CUBE " float4\nA 4-component vector of the q17-q20 variables. Can be swizzled."}, // + {"_qf", ICON_FA_CUBE " float4\nA 4-component vector of the q21-q24 variables. Can be swizzled."}, // + {"_qg", ICON_FA_CUBE " float4\nA 4-component vector of the q25-q28 variables. Can be swizzled."}, // + {"_qh", ICON_FA_CUBE " float4\nA 4-component vector of the q29-q32 variables. Can be swizzled."}, // + {"blur1_min", ICON_FA_CUBE " float\nBlur level 1 range minimum (0..1)."}, // + {"blur1_max", ICON_FA_CUBE " float\nBlur level 1 range maximum (0..1)."}, // + {"blur2_min", ICON_FA_CUBE " float\nBlur level 2 range minimum (0..1)."}, // + {"blur2_max", ICON_FA_CUBE " float\nBlur level 2 range maximum (0..1)."}, // + {"blur3_min", ICON_FA_CUBE " float\nBlur level 3 range minimum (0..1)."}, // + {"blur3_max", ICON_FA_CUBE " float\nBlur level 3 range maximum (0..1)."}, // + {"rot_s1", ICON_FA_CUBE " float4x3\nFour random, static rotations, randomized at preset load time.\nMinor translation component (<1)."}, // + {"rot_s2", ICON_FA_CUBE " float4x3\nFour random, static rotations, randomized at preset load time.\nMinor translation component (<1)."}, // + {"rot_s3", ICON_FA_CUBE " float4x3\nFour random, static rotations, randomized at preset load time.\nMinor translation component (<1)."}, // + {"rot_s4", ICON_FA_CUBE " float4x3\nFour random, static rotations, randomized at preset load time.\nMinor translation component (<1)."}, // + {"rot_d1", ICON_FA_CUBE " float4x3\nFour random, slowly changing rotations."}, // + {"rot_d2", ICON_FA_CUBE " float4x3\nFour random, slowly changing rotations."}, // + {"rot_d3", ICON_FA_CUBE " float4x3\nFour random, slowly changing rotations."}, // + {"rot_d4", ICON_FA_CUBE " float4x3\nFour random, slowly changing rotations."}, // + {"rot_f1", ICON_FA_CUBE " float4x3\nFour random, faster-changing rotations."}, // + {"rot_f2", ICON_FA_CUBE " float4x3\nFour random, faster-changing rotations."}, // + {"rot_f3", ICON_FA_CUBE " float4x3\nFour random, faster-changing rotations."}, // + {"rot_f4", ICON_FA_CUBE " float4x3\nFour random, faster-changing rotations."}, // + {"rot_vf1", ICON_FA_CUBE " float4x3\nFour random, very-fast-changing rotations."}, // + {"rot_vf2", ICON_FA_CUBE " float4x3\nFour random, very-fast-changing rotations."}, // + {"rot_vf3", ICON_FA_CUBE " float4x3\nFour random, very-fast-changing rotations."}, // + {"rot_vf4", ICON_FA_CUBE " float4x3\nFour random, very-fast-changing rotations."}, // + {"rot_uf1", ICON_FA_CUBE " float4x3\nFour random, ultra-fast-changing rotations."}, // + {"rot_uf2", ICON_FA_CUBE " float4x3\nFour random, ultra-fast-changing rotations."}, // + {"rot_uf3", ICON_FA_CUBE " float4x3\nFour random, ultra-fast-changing rotations."}, // + {"rot_uf4", ICON_FA_CUBE " float4x3\nFour random, ultra-fast-changing rotations."}, // + {"rot_rand1", ICON_FA_CUBE " float4x3\nRandom rotation on every frame."}, // + {"rot_rand2", ICON_FA_CUBE " float4x3\nRandom rotation on every frame."}, // + {"rot_rand3", ICON_FA_CUBE " float4x3\nRandom rotation on every frame."}, // + {"rot_rand4", ICON_FA_CUBE " float4x3\nRandom rotation on every frame."}, // + {"sampler_main", ICON_FA_CUBE " sampler\nMain preset image texture sampler.\nBilinear filtering, wraps around."}, // + {"sampler_fw_main", ICON_FA_CUBE " sampler\nMain preset image texture sampler.\nBilinear filtering, wraps around."}, // + {"sampler_fc_main", ICON_FA_CUBE " sampler\nMain preset image texture sampler.\nBilinear filtering, clamps to edge."}, // + {"sampler_pw_main", ICON_FA_CUBE " sampler\nMain preset image texture sampler.\nPoint sampling, wraps around."}, // + {"sampler_pc_main", ICON_FA_CUBE " sampler\nMain preset image texture sampler.\nPoint sampling, clamps to edge."}, // + {"sampler_noise_lq", ICON_FA_CUBE " sampler\n256x256 2D 4-channel low-quality noise texture.\nBilinear filtering, wraps around."}, // + {"sampler_fw_noise_lq", ICON_FA_CUBE " sampler\n256x256 2D 4-channel low-quality noise texture.\nBilinear filtering, wraps around."}, // + {"sampler_fc_noise_lq", ICON_FA_CUBE " sampler\n256x256 2D 4-channel low-quality noise texture.\nBilinear filtering, clamps to edge."}, // + {"sampler_pw_noise_lq", ICON_FA_CUBE " sampler\n256x256 2D 4-channel low-quality noise texture.\nPoint sampling, wraps around."}, // + {"sampler_pc_noise_lq", ICON_FA_CUBE " sampler\n256x256 2D 4-channel low-quality noise texture.\nPoint sampling, clamps to edge."}, // + {"sampler_noise_lq_lite", ICON_FA_CUBE " sampler\n32x32 2D 4-channel low-quality noise texture.\nBilinear filtering, wraps around."}, // + {"sampler_fw_noise_lq_lite", ICON_FA_CUBE " sampler\n32x32 2D 4-channel low-quality noise texture.\nBilinear filtering, wraps around."}, // + {"sampler_fc_noise_lq_lite", ICON_FA_CUBE " sampler\n32x32 2D 4-channel low-quality noise texture.\nBilinear filtering, clamps to edge."}, // + {"sampler_pw_noise_lq_lite", ICON_FA_CUBE " sampler\n32x32 2D 4-channel low-quality noise texture.\nPoint sampling, wraps around."}, // + {"sampler_pc_noise_lq_lite", ICON_FA_CUBE " sampler\n32x32 2D 4-channel low-quality noise texture.\nPoint sampling, clamps to edge."}, // + {"sampler_noise_mq", ICON_FA_CUBE " sampler\n64x64 2D 4-channel medium-quality noise texture.\nBilinear filtering, wraps around."}, // + {"sampler_fw_noise_mq", ICON_FA_CUBE " sampler\n64x64 2D 4-channel medium-quality noise texture.\nBilinear filtering, wraps around."}, // + {"sampler_fc_noise_mq", ICON_FA_CUBE " sampler\n64x64 2D 4-channel medium-quality noise texture.\nBilinear filtering, clamps to edge."}, // + {"sampler_pw_noise_mq", ICON_FA_CUBE " sampler\n64x64 2D 4-channel medium-quality noise texture.\nPoint sampling, wraps around."}, // + {"sampler_pc_noise_mq", ICON_FA_CUBE " sampler\n64x64 2D 4-channel medium-quality noise texture.\nPoint sampling, clamps to edge."}, // + {"sampler_noise_hq", ICON_FA_CUBE " sampler\n32x32 2D 4-channel high-quality noise texture.\nBilinear filtering, wraps around."}, // + {"sampler_fw_noise_hq", ICON_FA_CUBE " sampler\n32x32 2D 4-channel high-quality noise texture.\nBilinear filtering, wraps around."}, // + {"sampler_fc_noise_hq", ICON_FA_CUBE " sampler\n32x32 2D 4-channel high-quality noise texture.\nBilinear filtering, clamps to edge."}, // + {"sampler_pw_noise_hq", ICON_FA_CUBE " sampler\n32x32 2D 4-channel high-quality noise texture.\nPoint sampling, wraps around."}, // + {"sampler_pc_noise_hq", ICON_FA_CUBE " sampler\n32x32 2D 4-channel high-quality noise texture.\nPoint sampling, clamps to edge."}, // + {"sampler_noisevol_lq", ICON_FA_CUBE " sampler\n32x32x32 3D 4-channel low-quality volumetric noise texture.\nBilinear filtering, wraps around."}, // + {"sampler_fw_noisevol_lq", ICON_FA_CUBE " sampler\n32x32x32 3D 4-channel low-quality volumetric noise texture.\nBilinear filtering, wraps around."}, // + {"sampler_fc_noisevol_lq", ICON_FA_CUBE " sampler\n32x32x32 3D 4-channel low-quality volumetric noise texture.\nBilinear filtering, clamps to edge."}, // + {"sampler_pw_noisevol_lq", ICON_FA_CUBE " sampler\n32x32x32 3D 4-channel low-quality volumetric noise texture.\nPoint sampling, wraps around."}, // + {"sampler_pc_noisevol_lq", ICON_FA_CUBE " sampler\n32x32x32 3D 4-channel low-quality volumetric noise texture.\nPoint sampling, clamps to edge."}, // + {"sampler_noisevol_hq", ICON_FA_CUBE " sampler\n8x8x8 3D 4-channel high-quality volumetric noise texture.\nBilinear filtering, wraps around."}, // + {"sampler_fw_noisevol_hq", ICON_FA_CUBE " sampler\n8x8x8 3D 4-channel high-quality volumetric noise texture.\nBilinear filtering, wraps around."}, // + {"sampler_fc_noisevol_hq", ICON_FA_CUBE " sampler\n8x8x8 3D 4-channel high-quality volumetric noise texture.\nBilinear filtering, clamps to edge."}, // + {"sampler_pw_noisevol_hq", ICON_FA_CUBE " sampler\n8x8x8 3D 4-channel high-quality volumetric noise texture.\nPoint sampling, wraps around."}, // + {"sampler_pc_noisevol_hq", ICON_FA_CUBE " sampler\n8x8x8 3D 4-channel high-quality volumetric noise texture.\nPoint sampling, clamps to edge."}}}}; + + return identifierList.at(type); +} + +const TextEditor::LanguageDefinition& CodeContextInformation::GetLanguageDefinition(ExpressionCodeTypes type) +{ + static std::map languageDefinitions{ + {ExpressionCodeTypes::PerFrameInit, PopulateLanguageDefinitionForType(ExpressionCodeTypes::PerFrameInit)}, + {ExpressionCodeTypes::PerFrame, PopulateLanguageDefinitionForType(ExpressionCodeTypes::PerFrame)}, + {ExpressionCodeTypes::PerVertex, PopulateLanguageDefinitionForType(ExpressionCodeTypes::PerVertex)}, + {ExpressionCodeTypes::CustomWaveInit, PopulateLanguageDefinitionForType(ExpressionCodeTypes::CustomWaveInit)}, + {ExpressionCodeTypes::CustomWavePerFrame, PopulateLanguageDefinitionForType(ExpressionCodeTypes::CustomWavePerFrame)}, + {ExpressionCodeTypes::CustomWavePerPoint, PopulateLanguageDefinitionForType(ExpressionCodeTypes::CustomWavePerPoint)}, + {ExpressionCodeTypes::CustomShapeInit, PopulateLanguageDefinitionForType(ExpressionCodeTypes::CustomShapeInit)}, + {ExpressionCodeTypes::CustomShapePerFrame, PopulateLanguageDefinitionForType(ExpressionCodeTypes::CustomShapePerFrame)}, + {ExpressionCodeTypes::WarpShader, PopulateLanguageDefinitionForType(ExpressionCodeTypes::WarpShader)}, + {ExpressionCodeTypes::CompositeShader, PopulateLanguageDefinitionForType(ExpressionCodeTypes::CompositeShader)}}; + + return languageDefinitions.at(type); +} + +TextEditor::LanguageDefinition CodeContextInformation::PopulateLanguageDefinitionForType(ExpressionCodeTypes type) +{ + + auto definition = TextEditor::LanguageDefinition::MilkdropExpression(); + for (const auto& sourceIdentifier : GetIdentifierList(type)) + { + TextEditor::Identifier id; + id.mDeclaration = sourceIdentifier.second; + definition.mIdentifiers.insert(std::make_pair(sourceIdentifier.first, id)); + } + + return definition; +} + +} // namespace Editor diff --git a/src/gui/preset_editor/CodeContextInformation.h b/src/gui/preset_editor/CodeContextInformation.h new file mode 100644 index 0000000..e670cc0 --- /dev/null +++ b/src/gui/preset_editor/CodeContextInformation.h @@ -0,0 +1,52 @@ +#pragma once + +#include "ExpressionCodeTypes.h" + +#include "TextEditor.h" + +#include +#include + +namespace Editor { + +/** + * @class CodeContextInformation + * @brief Retains a static list of identifiers specific to each code context. + * + * This is used in the code editor to highlight the pre-defined variables and shader inputs, + * giving the user additional hints about which variable is user-defined in the context and + * which is not. + */ +class CodeContextInformation final +{ +public: + CodeContextInformation() = delete; + + /** + * @brief Returns the name of the context, to be displayed in the editor tab. + * @param type The type to return the context name for. + * @param index The index of a custom wave or shape. + * @return The name of the code context for displaying in the editor tab. + */ + static auto GetContextName(ExpressionCodeTypes type, int index = 0) -> std::string; + + /** + * @brief Return a list of pre-defined variables/inputs for a given code context. + * @param type The type to return the identifier list for. + * @return A list of identifiers with description representing the pre-defined in/out variables for this expression context. + */ + static auto GetIdentifierList(ExpressionCodeTypes type) -> std::vector>; + + /** + * @brief Returns the correct editor language definition for the given type. + * The context-specific identifiers are already set up properly. + * @param type The type to return the language definition for. + * @return + */ + static const TextEditor::LanguageDefinition& GetLanguageDefinition(ExpressionCodeTypes type); + +private: + static auto PopulateLanguageDefinitionForType(ExpressionCodeTypes type) -> TextEditor::LanguageDefinition; +}; + +} // namespace Editor diff --git a/src/gui/preset_editor/CodeEditorTab.cpp b/src/gui/preset_editor/CodeEditorTab.cpp new file mode 100644 index 0000000..16399eb --- /dev/null +++ b/src/gui/preset_editor/CodeEditorTab.cpp @@ -0,0 +1,13 @@ +#include "CodeEditorTab.h" + +namespace Editor { + +CodeEditorTab::CodeEditorTab() +{ +} + +CodeEditorTab::~CodeEditorTab() +{ +} + +} // namespace Editor diff --git a/src/gui/preset_editor/CodeEditorTab.h b/src/gui/preset_editor/CodeEditorTab.h new file mode 100644 index 0000000..1451f48 --- /dev/null +++ b/src/gui/preset_editor/CodeEditorTab.h @@ -0,0 +1,15 @@ +#pragma once + +namespace Editor { + +class CodeEditorTab +{ +public: + CodeEditorTab(); + + virtual ~CodeEditorTab(); + +private: +}; + +} // namespace Editor diff --git a/src/gui/preset_editor/CodeEditorWindow.cpp b/src/gui/preset_editor/CodeEditorWindow.cpp new file mode 100644 index 0000000..b6e8003 --- /dev/null +++ b/src/gui/preset_editor/CodeEditorWindow.cpp @@ -0,0 +1,13 @@ +#include "CodeEditorWindow.h" + +namespace Editor { + +CodeEditorWindow::CodeEditorWindow() +{ +} + +CodeEditorWindow::~CodeEditorWindow() +{ +} + +} // namespace Editor diff --git a/src/gui/preset_editor/CodeEditorWindow.h b/src/gui/preset_editor/CodeEditorWindow.h new file mode 100644 index 0000000..e2078b3 --- /dev/null +++ b/src/gui/preset_editor/CodeEditorWindow.h @@ -0,0 +1,15 @@ +#pragma once + +namespace Editor { + +class CodeEditorWindow +{ +public: + CodeEditorWindow(); + + virtual ~CodeEditorWindow(); + +private: +}; + +} // namespace Editor diff --git a/src/gui/preset_editor/EditorMenu.cpp b/src/gui/preset_editor/EditorMenu.cpp index 3ea9950..7004794 100644 --- a/src/gui/preset_editor/EditorMenu.cpp +++ b/src/gui/preset_editor/EditorMenu.cpp @@ -1,5 +1,6 @@ #include "EditorMenu.h" +#include "IconsFontAwesome7.h" #include "PresetEditorGUI.h" #include "gui/SystemBrowser.h" @@ -23,7 +24,7 @@ void EditorMenu::Draw() DrawFileMenu(); ImGui::Separator(); - if (ImGui::Button("Update Preset Preview")) + if (ImGui::Button(ICON_FA_PLAY " Update Preset Preview")) { _presetEditorGUI.UpdatePresetPreview(); } @@ -39,33 +40,33 @@ void EditorMenu::DrawFileMenu() { if (ImGui::BeginMenu("File")) { - if (ImGui::MenuItem("New Preset")) + if (ImGui::MenuItem(ICON_FA_SQUARE_PLUS " New Preset")) { _presetEditorGUI.Show(""); } - if (ImGui::MenuItem("Open Preset...")) + if (ImGui::MenuItem(ICON_FA_FOLDER_OPEN " Open Preset...")) { } - if (ImGui::MenuItem("Save Preset")) + if (ImGui::MenuItem(ICON_FA_FLOPPY_DISK " Save Preset")) { } - if (ImGui::MenuItem("Save Preset As...")) + if (ImGui::MenuItem(ICON_FA_FLOPPY_DISK " Save Preset As...")) { } ImGui::Separator(); - if (ImGui::MenuItem("Exit Preset Editor")) + if (ImGui::MenuItem(ICON_FA_CIRCLE_XMARK " Exit Preset Editor")) { _presetEditorGUI.Close(); } ImGui::Separator(); - if (ImGui::MenuItem("Quit projectM", "Ctrl+q")) + if (ImGui::MenuItem(ICON_FA_DOOR_OPEN " Quit projectM", "Ctrl+q")) { _notificationCenter.postNotification(new QuitNotification); } @@ -80,19 +81,19 @@ void EditorMenu::DrawHelpMenu() if (ImGui::BeginMenu("Help")) { - if (ImGui::BeginMenu("Online Documentation")) + if (ImGui::BeginMenu(ICON_FA_INFO " Online Documentation")) { - if (ImGui::MenuItem("Milkdrop Preset Authoring Guide")) + if (ImGui::MenuItem(ICON_FA_ARROW_UP_RIGHT_FROM_SQUARE " Milkdrop Preset Authoring Guide")) { SystemBrowser::OpenURL("https://www.geisswerks.com/milkdrop/milkdrop_preset_authoring.html"); } - if (ImGui::MenuItem("Milkdrop Expression Syntax")) + if (ImGui::MenuItem(ICON_FA_ARROW_UP_RIGHT_FROM_SQUARE " Milkdrop Expression Syntax")) { SystemBrowser::OpenURL("https://github.com/projectM-visualizer/projectm-eval/blob/master/docs/Expression-Syntax.md"); } - if (ImGui::MenuItem("DirectX HLSL Reference")) + if (ImGui::MenuItem(ICON_FA_ARROW_UP_RIGHT_FROM_SQUARE " DirectX HLSL Reference")) { SystemBrowser::OpenURL("https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-reference"); } diff --git a/src/gui/preset_editor/EditorPreset.cpp b/src/gui/preset_editor/EditorPreset.cpp index 7421278..3d85faf 100644 --- a/src/gui/preset_editor/EditorPreset.cpp +++ b/src/gui/preset_editor/EditorPreset.cpp @@ -114,7 +114,7 @@ void EditorPreset::FromParsedFile(const PresetFile& parsedFile) std::string const wavecodePrefix = "wavecode_" + std::to_string(index) + "_"; wave.index = index; - wave.enabled = parsedFile.GetInt(wavecodePrefix + "enabled", wave.enabled); + wave.enabled = parsedFile.GetBool(wavecodePrefix + "enabled", wave.enabled); wave.samples = parsedFile.GetInt(wavecodePrefix + "samples", wave.samples); wave.sep = parsedFile.GetInt(wavecodePrefix + "sep", wave.sep); wave.spectrum = parsedFile.GetBool(wavecodePrefix + "bSpectrum", wave.spectrum); @@ -148,7 +148,7 @@ void EditorPreset::FromParsedFile(const PresetFile& parsedFile) shape.additive = parsedFile.GetBool(shapecodePrefix + "additive", shape.additive); shape.thickOutline = parsedFile.GetBool(shapecodePrefix + "thickOutline", shape.thickOutline); shape.textured = parsedFile.GetBool(shapecodePrefix + "textured", shape.textured); - shape.instances = parsedFile.GetInt(shapecodePrefix + "nushape.inst", shape.instances); + shape.instances = parsedFile.GetInt(shapecodePrefix + "num_inst", shape.instances); shape.x = parsedFile.GetFloat(shapecodePrefix + "x", shape.x); shape.y = parsedFile.GetFloat(shapecodePrefix + "y", shape.y); shape.radius = parsedFile.GetFloat(shapecodePrefix + "rad", shape.radius); @@ -285,7 +285,7 @@ void EditorPreset::ToParsedFile(PresetFile& parsedFile) std::string const wavecodePrefix = "wavecode_" + std::to_string(index) + "_"; - parsedFile.SetInt(wavecodePrefix + "enabled", wave.enabled); + parsedFile.SetBool(wavecodePrefix + "enabled", wave.enabled); parsedFile.SetInt(wavecodePrefix + "samples", wave.samples); parsedFile.SetInt(wavecodePrefix + "sep", wave.sep); parsedFile.SetBool(wavecodePrefix + "bSpectrum", wave.spectrum); diff --git a/src/gui/preset_editor/EditorPreset.h b/src/gui/preset_editor/EditorPreset.h index 58ed86e..ddb87dd 100644 --- a/src/gui/preset_editor/EditorPreset.h +++ b/src/gui/preset_editor/EditorPreset.h @@ -33,7 +33,7 @@ class EditorPreset { public: int index{0}; //!< Custom waveform index in the preset. - int enabled{0}; //!< Render waveform if non-zero. + bool enabled{false}; //!< Render waveform if true. int samples{512}; //!< Number of samples/vertices in the waveform. int sep{0}; //!< Separation distance of dual waveforms. diff --git a/src/gui/preset_editor/ExpressionCodeTypes.h b/src/gui/preset_editor/ExpressionCodeTypes.h new file mode 100644 index 0000000..4e9b821 --- /dev/null +++ b/src/gui/preset_editor/ExpressionCodeTypes.h @@ -0,0 +1,22 @@ +#pragma once + +namespace Editor { + +/** + * @brief An enum with the different expression code contexts used in Milkdrop. + */ +enum class ExpressionCodeTypes : int +{ + PerFrameInit, + PerFrame, + PerVertex, + CustomWaveInit, + CustomWavePerFrame, + CustomWavePerPoint, + CustomShapeInit, + CustomShapePerFrame, + WarpShader, + CompositeShader +}; + +} // namespace Editor diff --git a/src/gui/preset_editor/PresetEditorGUI.cpp b/src/gui/preset_editor/PresetEditorGUI.cpp index 5c6e368..d2e0fb7 100644 --- a/src/gui/preset_editor/PresetEditorGUI.cpp +++ b/src/gui/preset_editor/PresetEditorGUI.cpp @@ -1,5 +1,7 @@ #include "PresetEditorGUI.h" +#include "CodeContextInformation.h" +#include "IconsFontAwesome7.h" #include "ProjectMGUI.h" #include "ProjectMSDLApplication.h" @@ -13,6 +15,8 @@ #include #include +#include + namespace Editor { PresetEditorGUI::PresetEditorGUI(ProjectMGUI& gui) @@ -126,6 +130,25 @@ void PresetEditorGUI::ReleaseProjectMControl() Poco::NotificationCenter::defaultCenter().postNotification(new UpdateWindowTitleNotification()); } +void PresetEditorGUI::EditCode(ExpressionCodeTypes type, std::string& code, int index) +{ + _textEditor.SetLanguageDefinition(CodeContextInformation::GetLanguageDefinition(type)); + _textEditor.SetText(code); +} + +unsigned long PresetEditorGUI::GetLoC(const std::string& code) +{ + std::istringstream iss(code); + unsigned long loc = 0; + std::string line; + while (std::getline(iss, line)) + { + ++loc; + } + + return loc; +} + void PresetEditorGUI::DrawLeftSideBar() { ImGuiWindowFlags window_flags = ImGuiWindowFlags_None; @@ -137,132 +160,31 @@ void PresetEditorGUI::DrawLeftSideBar() DrawGeneralParameters(); DrawDefaultWaveformSettings(); DrawMotionVectorSettings(); + DrawWarpMotionSettings(); + DrawMotionCodeSettings(); + DrawBorderSettings(); - if (ImGui::CollapsingHeader("Warping and Motion")) + if (ImGui::CollapsingHeader("Custom Waveforms")) { - ImGui::TextUnformatted("Translation"); - ImGui::Indent(16.0f); - - ImGui::SliderFloat("Horizontal Motion", &_editorPreset.xPush, -1.00f, 1.0f); - DrawHelpTooltip("Controls amount of constant horizontal motion- -0.01 = move left 1% per frame, 0=none, 0.01 = move right 1%"); - - ImGui::SliderFloat("Vertical Motion", &_editorPreset.yPush, -1.00f, 1.0f); - DrawHelpTooltip("Controls amount of constant vertical motion. -0.01 = move up 1% per frame, 0=none, 0.01 = move down 1%"); - - ImGui::Unindent(16.0f); - ImGui::Spacing(); - - ImGui::TextUnformatted("Rotation"); - ImGui::Indent(16.0f); - - ImGui::SliderFloat("Rotation##WarpRotation", &_editorPreset.rot, -1.00f, 1.0f); - DrawHelpTooltip("Controls the amount of rotation. 0=none, 0.1=slightly right, -0.1=slightly clockwise, 0.1=CCW"); - - ImGui::SliderFloat("Center X##WarpCenterX", &_editorPreset.rotCX, 0.00f, 1.0f); - DrawHelpTooltip("Controls where the center of rotation and stretching is, horizontally. 0=left, 0.5=center, 1=right"); - - ImGui::SliderFloat("Center Y##WarpCenterY", &_editorPreset.rotCY, 0.00f, 1.0f); - DrawHelpTooltip("Controls where the center of rotation and stretching is, vertically. 0=top, 0.5=center, 1=bottom"); - - ImGui::Unindent(16.0f); - ImGui::Spacing(); - - ImGui::TextUnformatted("Scaling"); - ImGui::Indent(16.0f); - - ImGui::SliderFloat("Stretch X##WarpStretchX", &_editorPreset.stretchX, 0.00f, 2.0f); - DrawHelpTooltip("Controls amount of constant horizontal stretching. 0.99=shrink 1%, 1=normal, 1.01=stretch 1%"); - - ImGui::SliderFloat("Stretch Y##WarpStretchY", &_editorPreset.stretchY, 0.00f, 2.0f); - DrawHelpTooltip("Controls amount of constant vertical stretching. 0.99=shrink 1%, 1=normal, 1.01=stretch 1%"); - - ImGui::SliderFloat("Zoom##WarpZoom", &_editorPreset.zoom, 0.00f, 2.0f); - DrawHelpTooltip("Controls inward/outward motion. 0.9=zoom out 10% per frame, 1.0=no zoom, 1.1=zoom in 10%"); - - ImGui::SliderFloat("Zoom Exponent##Warp", &_editorPreset.zoomExponent, 0.00f, 5.0f); - DrawHelpTooltip("Controls the curvature of the zoom; 1=normal"); - - ImGui::Unindent(16.0f); - ImGui::Spacing(); - - ImGui::TextUnformatted("Warping"); - ImGui::Indent(16.0f); - - ImGui::SliderFloat("Warp Amount##WarpAmount", &_editorPreset.warpAmount, 0.00f, 10.0f); - DrawHelpTooltip("Controls the magnitude of the warping. 0=none, 1=normal, 2=major warping..."); - - ImGui::SliderFloat("Warp Scale##WarpScale", &_editorPreset.warpScale, 0.00f, 10.0f); - DrawHelpTooltip("Controls the scale of the warp effect."); - - ImGui::SliderFloat("Warp Animation Speed##WarpAnimSpeed", &_editorPreset.warpAnimSpeed, 0.00f, 5.0f); - DrawHelpTooltip("Controls the speed of the warp effect."); - - ImGui::Unindent(16.0f); - ImGui::Spacing(); - - ImGui::TextUnformatted("Options"); ImGui::Indent(16.0f); - - ImGui::Unindent(16.0f); - ImGui::Spacing(); - - } - - if (ImGui::CollapsingHeader("Motion Code")) - { - - if (ImGui::Button("Per-Frame Init")) + for (auto& wave : _editorPreset.waves) { - EditCode(_editorPreset.perFrameInitCode, false); + DrawCustomWaveSettings(wave); } - if (ImGui::Button("Per-Frame Code")) - { - EditCode(_editorPreset.perFrameCode, false); - } - if (ImGui::Button("Per-Vertex Code")) - { - EditCode(_editorPreset.perPixelCode, false); - } - } - - DrawBorderSettings(); - - if (ImGui::CollapsingHeader("Custom Waveforms")) - { + ImGui::Unindent(16.0f); } if (ImGui::CollapsingHeader("Custom Shapes")) { - } - - bool shaderTabDisabled = _editorPreset.presetVersion < 200 || (_editorPreset.warpShaderVersion < 2 && _editorPreset.compositeShaderVersion < 2); - - ImGui::BeginDisabled(shaderTabDisabled); - if (ImGui::CollapsingHeader("Warp / Composite Shaders")) - { - if (!_editorPreset.warpShader.empty()) - { - if (ImGui::Button("Warp Shader")) - { - EditCode(_editorPreset.warpShader, true); - } - } - if (!_editorPreset.compositeShader.empty()) + ImGui::Indent(16.0f); + for (auto& shape : _editorPreset.shapes) { - if (ImGui::Button("Composite Shader")) - { - EditCode(_editorPreset.compositeShader, true); - } + DrawCustomShapeSettings(shape); } + ImGui::Unindent(16.0f); } - if (shaderTabDisabled) - { - DrawHelpTooltip("To enable HLSL shaders, open the compatibility settings and set the " - "preset version to 200 or higher and the PS version to 2 or higher."); - } - ImGui::EndDisabled(); - DrawShaderLossWarning(); + DrawShaderCodeSettings(); ImGui::EndChild(); @@ -271,7 +193,9 @@ void PresetEditorGUI::DrawLeftSideBar() ImGui::SetNextWindowBgAlpha(0.5f); ImGui::BeginChild("CodeEditor", ImVec2(0, 500), ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeY, window_flags); + ImGui::PushID("CodeEditor"); _textEditor.Render("Code Editor"); + ImGui::PopID(); ImGui::EndChild(); @@ -304,9 +228,12 @@ void PresetEditorGUI::DrawPresetCompatibilitySettings() case 0: default: _editorPreset.presetVersion = 140; + _editorPreset.warpShaderVersion = 0; + _editorPreset.compositeShaderVersion = 0; break; case 1: _editorPreset.presetVersion = 200; + _editorPreset.compositeShaderVersion = _editorPreset.warpShaderVersion; break; case 2: _editorPreset.presetVersion = 201; @@ -332,7 +259,7 @@ void PresetEditorGUI::DrawPresetCompatibilitySettings() { _editorPreset.compositeShaderVersion = _editorPreset.warpShaderVersion; } - DrawHelpTooltip("Minimum required DirectX Pixel Shader version. 0/1=No PS, 2=PS 2.0, 3=PS 2.x, 4=PS 3.0 (Ctrl-click to set higher value)"); + DrawHelpTooltip("Minimum required DirectX Pixel Shader version.\n0/1=No PS, 2=PS 2.0, 3=PS 2.x, 4=PS 3.0 (Ctrl-click to set higher value)"); ImGui::Unindent(16.0f); } @@ -342,9 +269,9 @@ void PresetEditorGUI::DrawPresetCompatibilitySettings() ImGui::Indent(16.0f); ImGui::SliderInt("Warp PS Version", &_editorPreset.warpShaderVersion, 0, 4); - DrawHelpTooltip("Minimum required DirectX Pixel Shader version for the warp shader. 0/1=No PS, 2=PS 2.0, 3=PS 2.x, 4=PS 3.0 (Ctrl-click to set higher value)"); + DrawHelpTooltip("Minimum required DirectX Pixel Shader version for the warp shader.\n0/1=No PS, 2=PS 2.0, 3=PS 2.x, 4=PS 3.0 (Ctrl-click to set higher value)"); ImGui::SliderInt("Composite PS Version", &_editorPreset.compositeShaderVersion, 0, 4); - DrawHelpTooltip("Minimum required DirectX Pixel Shader version for the composite shader. 0/1=No PS, 2=PS 2.0, 3=PS 2.x, 4=PS 3.0 (Ctrl-click to set higher value)"); + DrawHelpTooltip("Minimum required DirectX Pixel Shader version for the composite shader.\n0/1=No PS, 2=PS 2.0, 3=PS 2.x, 4=PS 3.0 (Ctrl-click to set higher value)"); ImGui::Unindent(16.0f); } @@ -355,26 +282,30 @@ void PresetEditorGUI::DrawGeneralParameters() { if (ImGui::CollapsingHeader("General Parameters")) { + bool usesCompositeShader = _editorPreset.presetVersion >= 200 && _editorPreset.compositeShaderVersion >= 2; + ImGui::TextUnformatted("Post-Processing Filters"); ImGui::Indent(16.0f); ImGui::SliderFloat("Decay##PerFrameDecay", &_editorPreset.decay, 0.00f, 1.0f); - DrawHelpTooltip("Controls the eventual fade to black. 1=no fade, 0.9=strong fade, 0.98=recommended"); + DrawHelpTooltip("Controls the eventual fade to black.\n1=no fade, 0.9=strong fade, 0.98=recommended"); + + ImGui::BeginDisabled(usesCompositeShader); ImGui::SliderFloat("Gamma Adjustment##GammaAdjustment", &_editorPreset.gammaAdj, 0.00f, 10.0f); - DrawHelpTooltip("Controls display brightness. 1=normal, 2=double, 3=triple, etc."); + DrawHelpTooltip("Controls display brightness.\n1=normal, 2=double, 3=triple, etc.\nOnly applied if no composite shader is used!"); ImGui::Checkbox("Brighten", &_editorPreset.brighten); - DrawHelpTooltip("Brightens the darker parts of the image (nonlinear; square root filter)"); + DrawHelpTooltip("Brightens the darker parts of the image (nonlinear; square root filter)\nOnly applied if no composite shader is used!"); ImGui::Checkbox("Darken", &_editorPreset.darken); - DrawHelpTooltip("Darkens the brighter parts of the image (nonlinear; squaring filter)"); + DrawHelpTooltip("Darkens the brighter parts of the image (nonlinear; squaring filter)\nOnly applied if no composite shader is used!"); ImGui::Checkbox("Solarize", &_editorPreset.solarize); - DrawHelpTooltip("Emphasizes mid-range colors"); + DrawHelpTooltip("Emphasizes mid-range colors\nOnly applied if no composite shader is used!"); ImGui::Checkbox("Invert", &_editorPreset.invert); - DrawHelpTooltip("Inverts the colors in the image"); + DrawHelpTooltip("Inverts the colors in the image\nOnly applied if no composite shader is used!"); ImGui::Checkbox("Darken Center", &_editorPreset.darkenCenter); DrawHelpTooltip("Darkens a diamond-shaped area in the center of the image.\nApplied after drawing shapes/waveforms, but before drawing borders."); @@ -391,10 +322,10 @@ void PresetEditorGUI::DrawGeneralParameters() ImGui::TextUnformatted("Video Echo"); ImGui::Indent(16.0f); ImGui::SliderFloat("Zoom##VideoEchoZoom", &_editorPreset.videoEchoZoom, 0.01f, 10.0f); - DrawHelpTooltip("Controls the size of the second graphics layer"); + DrawHelpTooltip("Controls the size of the second graphics layer\nOnly applied if no composite shader is used!"); ImGui::SliderFloat("Alpha##VideoEchoAlpha", &_editorPreset.videoEchoAlpha, 0.00f, 1.0f); - DrawHelpTooltip("Controls the opacity of the second graphics layer. 0=transparent (off), 0.5=half-mix, 1=opaque"); + DrawHelpTooltip("Controls the opacity of the second graphics layer.\n0=transparent (off), 0.5=half-mix, 1=opaque\nOnly applied if no composite shader is used!"); { int selectionIndex = _editorPreset.videoEchoOrientation % 4; @@ -415,10 +346,12 @@ void PresetEditorGUI::DrawGeneralParameters() ImGui::EndCombo(); } - DrawHelpTooltip("Selects an orientation for the second graphics layer."); + DrawHelpTooltip("Selects an orientation for the second graphics layer.\nOnly applied if no composite shader is used!"); } - ImGui::Unindent(16.0f); + ImGui::EndDisabled(); + + ImGui::Unindent(16.0f); ImGui::Spacing(); ImGui::TextUnformatted("Blur Texture Value Range"); @@ -463,13 +396,13 @@ void PresetEditorGUI::DrawDefaultWaveformSettings() DrawHelpTooltip("The waveform's lines (or dots) are drawn with double thickness"); ImGui::SliderFloat("Scale##DefaultWaveformScale", &_editorPreset.waveScale, 0.00f, 5.0f); - DrawHelpTooltip("Scaling factor of the waveform. 1 = original size, 2 = twice the size, 0.5 = half the size"); + DrawHelpTooltip("Scaling factor of the waveform.\n1 = original size, 2 = twice the size, 0.5 = half the size"); ImGui::SliderFloat("Smoothing##DefaultWaveformSmoothing", &_editorPreset.waveSmoothing, 0.00f, 1.0f); - DrawHelpTooltip("Smoothing of the waveform. 0 = no smoothing, 0.75 = heavy smoothing"); + DrawHelpTooltip("Smoothing of the waveform.\n0 = no smoothing, 0.75 = heavy smoothing"); ImGui::SliderFloat("Mystery Param##DefaultWaveformParam", &_editorPreset.waveParam, -1.00f, 1.0f); - DrawHelpTooltip("This value does different things depending on the mode. For example, it could control angle at which the waveform was drawn."); + DrawHelpTooltip("This value does different things depending on the mode.\nFor example, it could control angle at which the waveform was drawn."); ImGui::Unindent(16.0f); ImGui::Spacing(); @@ -479,10 +412,10 @@ void PresetEditorGUI::DrawDefaultWaveformSettings() ImGui::Indent(16.0f); ImGui::SliderFloat("X##DefaultWaveformX", &_editorPreset.waveX, 0.00f, 1.0f); - DrawHelpTooltip("Position of the waveform. 0 = far left edge of screen, 0.5 = center, 1 = far right"); + DrawHelpTooltip("Position of the waveform.\n0 = far left edge of screen, 0.5 = center, 1 = far right"); ImGui::SliderFloat("Y##DefaultWaveformY", &_editorPreset.waveY, 0.00f, 1.0f); - DrawHelpTooltip("Position of the waveform. 0 = very bottom of screen, 0.5 = center, 1 = top"); + DrawHelpTooltip("Position of the waveform.\n0 = very bottom of screen, 0.5 = center, 1 = top"); ImGui::Unindent(16.0f); ImGui::Spacing(); @@ -574,20 +507,14 @@ void PresetEditorGUI::DrawMotionVectorSettings() ImGui::Indent(16.0f); - if (ImGui::SliderFloat("Size X##MotionVectorX", &_editorPreset.mvX, 0.00f, 64.0f)) - { - _editorPreset.waveX = std::min(0.0f, std::max(64.0f, _editorPreset.mvX)); - } + ImGui::SliderFloat("Size X##MotionVectorX", &_editorPreset.mvX, 0.00f, 64.0f); DrawHelpTooltip("The number of motion vectors in the X direction"); - if (ImGui::SliderFloat("size Y##MotionVectorY", &_editorPreset.mvY, 0.00f, 48.0f)) - { - _editorPreset.waveY = std::min(0.0f, std::max(48.0f, _editorPreset.mvY)); - } + ImGui::SliderFloat("size Y##MotionVectorY", &_editorPreset.mvY, 0.00f, 48.0f); DrawHelpTooltip("The number of motion vectors in the Y direction"); ImGui::SliderFloat("Length##MotionVectorLength", &_editorPreset.mvL, 0.00f, 5.0f); - DrawHelpTooltip("The length of the motion vectors (0=no trail, 1=normal, 2=double...)"); + DrawHelpTooltip("The length of the motion vectors\n0=no trail, 1=normal, 2=double..."); ImGui::SliderFloat("X Offset##MotionVectorOffsetX", &_editorPreset.mvDX, -1.00f, 1.0f); DrawHelpTooltip("Horizontal placement offset of the motion vectors"); @@ -610,6 +537,147 @@ void PresetEditorGUI::DrawMotionVectorSettings() } } +void PresetEditorGUI::DrawMotionCodeSettings() +{ + if (ImGui::CollapsingHeader("Motion Code")) + { + + ImGui::TextUnformatted("Per-Frame Init"); + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + DrawHelpTooltip("This code will only run once when the preset is loaded.\n" + "It can be used to set up the Per-Frame, qXX, regXX and megabuf variables to known initial (non-zero) values.\n" + "Calculations only required once can also be done in this code."); + + ImGui::Indent(16.0f); + + if (ImGui::Button(ICON_FA_PENCIL " Edit##EditPerFrameInitCode")) + { + EditCode(ExpressionCodeTypes::PerFrameInit, _editorPreset.perFrameInitCode); + } + ImGui::SameLine(); + ImGui::Text("(%lu lines / %lu characters)", GetLoC(_editorPreset.perFrameInitCode), _editorPreset.perFrameInitCode.length()); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + + ImGui::TextUnformatted("Per-Frame"); + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + DrawHelpTooltip("This code runs at the very beginning of each rendered frame.\n" + "Any complex/expensive calculations should be done here, eventually passing the results via qXX or regXX vars" + "or gmegabuf to effects later in the rendering process.\n" + "All qXX var values set here are copied to the Per-Vertex code and all custom waveforms and shapes."); + + ImGui::Indent(16.0f); + + if (ImGui::Button(ICON_FA_PENCIL " Edit##EditPerFrameCode")) + { + EditCode(ExpressionCodeTypes::PerFrame, _editorPreset.perFrameCode); + } + ImGui::SameLine(); + ImGui::Text("(%lu lines / %lu characters)", GetLoC(_editorPreset.perFrameCode), _editorPreset.perFrameCode.length()); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + + ImGui::TextUnformatted("Per-Vertex"); + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + DrawHelpTooltip("This code is executed for every grid vertex in the warp mesh.\n" + "Since this code may run several thousand times PER FRAME, it is key to keep it fast and slim.\n" + "Any values which don't depend on the x/y grid coordinates should be pre-calculated in the Per-Frame code" + "and passed in via qXX or regXX vars or using gmegabuf.\n" + "Also known als \"Per-Pixel\" code."); + + ImGui::Indent(16.0f); + + if (ImGui::Button(ICON_FA_PENCIL " Edit##EditPerVertexCode")) + { + EditCode(ExpressionCodeTypes::PerVertex, _editorPreset.perPixelCode); + } + ImGui::SameLine(); + ImGui::Text("(%lu lines / %lu characters)", GetLoC(_editorPreset.perPixelCode), _editorPreset.perPixelCode.length()); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + } +} + +void PresetEditorGUI::DrawWarpMotionSettings() +{ + if (ImGui::CollapsingHeader("Warping and Motion")) + { + ImGui::TextUnformatted("Translation"); + ImGui::Indent(16.0f); + + ImGui::SliderFloat("Horizontal Motion", &_editorPreset.xPush, -1.00f, 1.0f); + DrawHelpTooltip("Controls amount of constant horizontal motion.\n-0.01 = move left 1% per frame, 0=none, 0.01 = move right 1%"); + + ImGui::SliderFloat("Vertical Motion", &_editorPreset.yPush, -1.00f, 1.0f); + DrawHelpTooltip("Controls amount of constant vertical motion.\n-0.01 = move up 1% per frame, 0=none, 0.01 = move down 1%"); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + ImGui::TextUnformatted("Rotation"); + ImGui::Indent(16.0f); + + ImGui::SliderFloat("Rotation##WarpRotation", &_editorPreset.rot, -1.00f, 1.0f); + DrawHelpTooltip("Controls the amount of rotation.\n0=none, 0.1=slightly right, -0.1=slightly clockwise, 0.1=CCW"); + + ImGui::SliderFloat("Center X##WarpCenterX", &_editorPreset.rotCX, 0.00f, 1.0f); + DrawHelpTooltip("Controls where the center of rotation and stretching is, horizontally.\n0=left, 0.5=center, 1=right"); + + ImGui::SliderFloat("Center Y##WarpCenterY", &_editorPreset.rotCY, 0.00f, 1.0f); + DrawHelpTooltip("Controls where the center of rotation and stretching is, vertically.\n0=top, 0.5=center, 1=bottom"); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + ImGui::TextUnformatted("Scaling"); + ImGui::Indent(16.0f); + + ImGui::SliderFloat("Stretch X##WarpStretchX", &_editorPreset.stretchX, 0.00f, 2.0f); + DrawHelpTooltip("Controls amount of constant horizontal stretching.\n0.99=shrink 1%, 1=normal, 1.01=stretch 1%"); + + ImGui::SliderFloat("Stretch Y##WarpStretchY", &_editorPreset.stretchY, 0.00f, 2.0f); + DrawHelpTooltip("Controls amount of constant vertical stretching.\n0.99=shrink 1%, 1=normal, 1.01=stretch 1%"); + + ImGui::SliderFloat("Zoom##WarpZoom", &_editorPreset.zoom, 0.00f, 2.0f); + DrawHelpTooltip("Controls inward/outward motion.\n0.9=zoom out 10% per frame, 1.0=no zoom, 1.1=zoom in 10%"); + + ImGui::SliderFloat("Zoom Exponent##Warp", &_editorPreset.zoomExponent, 0.00f, 5.0f); + DrawHelpTooltip("Controls the curvature of the zoom; 1=normal"); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + ImGui::TextUnformatted("Warping"); + ImGui::Indent(16.0f); + + ImGui::SliderFloat("Warp Amount##WarpAmount", &_editorPreset.warpAmount, 0.00f, 10.0f); + DrawHelpTooltip("Controls the magnitude of the warping.\n0=none, 1=normal, 2=major warping..."); + + ImGui::SliderFloat("Warp Scale##WarpScale", &_editorPreset.warpScale, 0.00f, 10.0f); + DrawHelpTooltip("Controls the scale of the warp effect."); + + ImGui::SliderFloat("Warp Animation Speed##WarpAnimSpeed", &_editorPreset.warpAnimSpeed, 0.00f, 5.0f); + DrawHelpTooltip("Controls the speed of the warp effect."); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + ImGui::TextUnformatted("Options"); + ImGui::Indent(16.0f); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + } +} + void PresetEditorGUI::DrawBorderSettings() { if (ImGui::CollapsingHeader("Border Effect")) @@ -641,6 +709,350 @@ void PresetEditorGUI::DrawBorderSettings() } } +void PresetEditorGUI::DrawCustomWaveSettings(EditorPreset::Wave& waveform) +{ + std::string idx = std::to_string(waveform.index + 1); + + ImGui::PushID(std::string("CustomWave" + idx).c_str()); + + if (ImGui::CollapsingHeader(std::string("Waveform " + idx).c_str())) + { + ImGui::Checkbox("Enabled", &waveform.enabled); + DrawHelpTooltip("This waveform is only rendered if enabled explicitly."); + + ImGui::Spacing(); + + ImGui::BeginDisabled(!waveform.enabled); + + ImGui::TextUnformatted("Position And Style"); + ImGui::Indent(16.0f); + + ImGui::SliderFloat("X", &waveform.x, 0.0f, 1.0f); + DrawHelpTooltip("Horizontal position of the waveform.\n" + "0=left, 0.5=center, 1=right"); + + ImGui::SliderFloat("Y", &waveform.x, 0.0f, 1.0f); + DrawHelpTooltip("Vertical position of the waveform.\n" + "0=bottom, 0.5=center, 1=top"); + + ImGui::SliderInt("Samples", &waveform.samples, 1, 512); + DrawHelpTooltip("Number of waveform points to draw."); + + ImGui::SliderInt("Separation", &waveform.sep, 0, 255); + DrawHelpTooltip("Separation distance of dual waveforms."); + + ImGui::SliderFloat("Scaling", &waveform.scaling, 0.0f, 10.0f); + DrawHelpTooltip("Waveform value scaling factor."); + + ImGui::SliderFloat("Smoothing", &waveform.scaling, 0.0f, 1.0f); + DrawHelpTooltip("Waveform smoothing value.\n" + "0.0=no smoothing, 0.5=default, 1.0=extreme smoothing"); + + ImGui::ColorEdit4("Color", &waveform.color.red, ImGuiColorEditFlags_Float); + DrawHelpTooltip("The default color of the waveform."); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + ImGui::TextUnformatted("Drawing Flags"); + ImGui::Indent(16.0f); + + ImGui::Checkbox("Spectrum", &waveform.spectrum); + DrawHelpTooltip("Use spectrum instead of oscilloscope data."); + + ImGui::Checkbox("Dots", &waveform.useDots); + DrawHelpTooltip("Draw Waveform as dots instead of lines."); + + ImGui::Checkbox("Thick", &waveform.drawThick); + DrawHelpTooltip("Draw waveform lines or dots twice as thick."); + + ImGui::Checkbox("Additive", &waveform.additive); + DrawHelpTooltip("Use additive color blending when drawing."); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + ImGui::TextUnformatted("Drawing Code"); + ImGui::Indent(16.0f); + + ImGui::TextUnformatted("Initialization"); + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + DrawHelpTooltip("This code will only run once when the preset is loaded.\n" + "It can be used to set up variables specific to this waveform to" + " known initial (non-zero) values. The waveform tX variables can also be initialized here."); + + ImGui::Indent(16.0f); + + if (ImGui::Button(ICON_FA_PENCIL " Edit##InitCode")) + { + EditCode(ExpressionCodeTypes::CustomWaveInit, waveform.initCode, waveform.index + 1); + } + ImGui::SameLine(); + ImGui::Text("(%lu lines / %lu characters)", GetLoC(waveform.initCode), waveform.initCode.length()); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + + ImGui::TextUnformatted("Per-Frame"); + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + DrawHelpTooltip("This code runs once before rendering the waveform.\n" + "Any complex/expensive calculations should be done here, eventually passing the results via qXX/tX vars " + "or (g)megabuf to the per-point code.\n" + "All qXX/tX var values set here are copied to each Per-Point code instance."); + + ImGui::Indent(16.0f); + + if (ImGui::Button(ICON_FA_PENCIL " Edit##PerFrameCode")) + { + EditCode(ExpressionCodeTypes::CustomWavePerFrame, waveform.perFrameCode, waveform.index + 1); + } + ImGui::SameLine(); + ImGui::Text("(%lu lines / %lu characters)", GetLoC(waveform.perFrameCode), waveform.perFrameCode.length()); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + + ImGui::TextUnformatted("Per-Point"); + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + DrawHelpTooltip("This code is executed for every point of the waveform.\n" + "This allows full control over the placement of each vertex of the waveform, " + "enabling drawing arbitrary shapes."); + + ImGui::Indent(16.0f); + + if (ImGui::Button(ICON_FA_PENCIL " Edit##PerPointCode")) + { + EditCode(ExpressionCodeTypes::CustomWavePerPoint, waveform.perPointCode, waveform.index + 1); + } + ImGui::SameLine(); + ImGui::Text("(%lu lines / %lu characters)", GetLoC(waveform.perPointCode), waveform.perPointCode.length()); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + ImGui::EndDisabled(); + } + else if (waveform.enabled) + { + ImGui::SameLine(); + ImGui::TextDisabled(" " ICON_FA_CHECK " Enabled"); + } + + ImGui::PopID(); +} + +void PresetEditorGUI::DrawCustomShapeSettings(EditorPreset::Shape& shape) +{ + std::string idx = std::to_string(shape.index + 1); + + ImGui::PushID(std::string("CustomShape" + idx).c_str()); + + if (ImGui::CollapsingHeader(std::string("Shape " + idx).c_str())) + { + ImGui::Checkbox("Enabled", &shape.enabled); + DrawHelpTooltip("This shape is only rendered if enabled explicitly."); + + ImGui::Spacing(); + + ImGui::BeginDisabled(!shape.enabled); + + ImGui::TextUnformatted("Position And Style"); + ImGui::Indent(16.0f); + + ImGui::SliderFloat("X", &shape.x, 0.0f, 1.0f); + DrawHelpTooltip("Default horizontal position of the shape.\n" + "0=left, 0.5=center, 1=right"); + + ImGui::SliderFloat("Y", &shape.y, 0.0f, 1.0f); + DrawHelpTooltip("Default vertical position of the shape.\n" + "0=bottom, 0.5=center, 1=top"); + + ImGui::SliderFloat("Radius", &shape.radius, 0.001f, 10.0f); + DrawHelpTooltip("Default radius of the shape."); + + ImGui::SliderFloat("Angle", &shape.angle, 0.0f, 2 * 3.14159265358979323846); + DrawHelpTooltip("Default rotation angle of the shape."); + + ImGui::SliderInt("Sides", &shape.sides, 3, 100); + DrawHelpTooltip("The default number of sides that make up the polygonal shape."); + + ImGui::SliderInt("Instances", &shape.instances, 1, 1024); + DrawHelpTooltip("The total number of instances (the number of times to repeat the per-frame equations for, and draw, this shape)."); + + ImGui::ColorEdit4("Inner Color", &shape.color.red, ImGuiColorEditFlags_Float); + DrawHelpTooltip("The default color and opacity towards the center of the shape."); + + ImGui::ColorEdit4("Outer Color", &shape.color2.red, ImGuiColorEditFlags_Float); + DrawHelpTooltip("The default color and opacity towards the outer edge of the shape"); + + ImGui::ColorEdit4("Border Color", &shape.borderColor.red, ImGuiColorEditFlags_Float); + DrawHelpTooltip("The default color and opacity of the border of the shape"); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + ImGui::TextUnformatted("Texturing"); + ImGui::Indent(16.0f); + + ImGui::Checkbox("Textured", &shape.textured); + DrawHelpTooltip("If enabled, the shape will be textured with the image from the previous frame."); + + ImGui::BeginDisabled(!shape.textured); + + ImGui::SliderFloat("Angle##Texture", &shape.tex_ang, 0.0f, 2 * 3.14159265358979323846); + DrawHelpTooltip("The angle at which to rotate the previous frame's image before applying it to the shape."); + + ImGui::SliderFloat("Zoom##Texture", &shape.tex_zoom, 0.001f, 10.0f); + DrawHelpTooltip("The portion of the previous frame's image to use with the shape."); + + ImGui::EndDisabled(); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + ImGui::TextUnformatted("Drawing Flags"); + ImGui::Indent(16.0f); + ImGui::Checkbox("Thick Outline", &shape.thickOutline); + DrawHelpTooltip("If enabled, the border will be overdrawn 4 times to make it thicker, bolder, and more visible."); + + ImGui::Checkbox("Additive", &shape.additive); + DrawHelpTooltip("If enabled, the shape will add color to saturate the image toward white; otherwise, it will replace what's there."); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + ImGui::TextUnformatted("Drawing Code"); + ImGui::Indent(16.0f); + + ImGui::TextUnformatted("Initialization"); + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + DrawHelpTooltip("This code will only run once when the preset is loaded.\n" + "It can be used to set up variables specific to this shape to" + " known initial (non-zero) values. The shape tX variables can ONLY be initialized here."); + + ImGui::Indent(16.0f); + + if (ImGui::Button(ICON_FA_PENCIL " Edit##InitCode")) + { + EditCode(ExpressionCodeTypes::CustomShapeInit, shape.initCode, shape.index + 1); + } + ImGui::SameLine(); + ImGui::Text("(%lu lines / %lu characters)", GetLoC(shape.initCode), shape.initCode.length()); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + + ImGui::TextUnformatted("Per-Frame"); + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + DrawHelpTooltip("This code runs once per shape instance.\n" + "Any complex/expensive calculations should be done in the preset per-frame code, eventually passing the results via qXX/tX vars " + "or gmegabuf to the shape per-frame code. The tX variables are reset to their init values for each instance."); + + ImGui::Indent(16.0f); + + if (ImGui::Button(ICON_FA_PENCIL " Edit##PerFrameCode")) + { + EditCode(ExpressionCodeTypes::CustomShapePerFrame, shape.perFrameCode, shape.index + 1); + } + ImGui::SameLine(); + ImGui::Text("(%lu lines / %lu characters)", GetLoC(shape.perFrameCode), shape.perFrameCode.length()); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + ImGui::EndDisabled(); + } + else if (shape.enabled) + { + ImGui::SameLine(); + ImGui::TextDisabled(" " ICON_FA_CHECK " Enabled"); + } + + ImGui::PopID(); +} + +void PresetEditorGUI::DrawShaderCodeSettings() +{ + bool shaderTabDisabled = _editorPreset.presetVersion < 200 || (_editorPreset.warpShaderVersion < 2 && _editorPreset.compositeShaderVersion < 2); + + ImGui::BeginDisabled(shaderTabDisabled); + if (ImGui::CollapsingHeader("Warp / Composite Shaders")) + { + + ImGui::BeginDisabled(_editorPreset.warpShaderVersion < 2); + + ImGui::TextUnformatted("Warp Shader"); + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + DrawHelpTooltip("The warp shader is used to draw the warp mesh, after the motion vectors were drawn but before " + "any waveforms or shapes are rendered. It can be used to amend or replace the classic Milkdrop 1 " + "warp effect. It is not necessary to use the previous image at all - this shader may create a new" + "image on each frame, onto which waveforms and shapes are drawn.\n\n" ICON_FA_CODE + " This shader must be written in DirectX HLSL."); + + ImGui::Indent(16.0f); + + if (ImGui::Button(ICON_FA_PENCIL " Edit##WarpShader")) + { + EditCode(ExpressionCodeTypes::WarpShader, _editorPreset.warpShader); + } + ImGui::SameLine(); + ImGui::Text("(%lu lines / %lu characters)", GetLoC(_editorPreset.warpShader), _editorPreset.warpShader.length()); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + ImGui::EndDisabled(); + + ImGui::BeginDisabled(_editorPreset.compositeShaderVersion < 2); + + ImGui::TextUnformatted("Composite Shader"); + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + DrawHelpTooltip("The composite shader runs immediately before displaying the preset image to the user, " + "allowing post-processing of the previous rendering steps or even drawing a whole new image " + "on top of it. The input (sampler_main) to the composite shader is passed on to the next frame, " + "the output of the composite shader will only be displayed on screen and then discarded when " + "the next frame is rendered.\n\n" ICON_FA_CODE " This shader must be written in DirectX HLSL."); + + ImGui::Indent(16.0f); + + if (ImGui::Button(ICON_FA_PENCIL " Edit##CompositeShader")) + { + EditCode(ExpressionCodeTypes::CompositeShader, _editorPreset.compositeShader); + } + ImGui::SameLine(); + ImGui::Text("(%lu lines / %lu characters)", GetLoC(_editorPreset.compositeShader), _editorPreset.compositeShader.length()); + + ImGui::Unindent(16.0f); + ImGui::Spacing(); + + ImGui::EndDisabled(); + } + if (shaderTabDisabled) + { + DrawHelpTooltip("To enable HLSL shaders, open the compatibility settings and set the " + "preset version to 200 or higher and the PS version to 2 or higher."); + } + ImGui::EndDisabled(); + + DrawShaderLossWarning(); +} + void PresetEditorGUI::DrawShaderLossWarning() const { if (_editorPreset.presetVersion < 200) @@ -685,18 +1097,4 @@ void PresetEditorGUI::DrawHelpTooltip(const std::string& helpText) } } -void PresetEditorGUI::EditCode(const std::string& code, bool isShaderCode) -{ - if (isShaderCode) - { - _textEditor.SetLanguageDefinition(TextEditor::LanguageDefinition::HLSL()); - } - else - { - _textEditor.SetLanguageDefinition(TextEditor::LanguageDefinition::MilkdropExpression()); - } - - _textEditor.SetText(code); -} - } // namespace Editor \ No newline at end of file diff --git a/src/gui/preset_editor/PresetEditorGUI.h b/src/gui/preset_editor/PresetEditorGUI.h index f6a24f5..658ae89 100644 --- a/src/gui/preset_editor/PresetEditorGUI.h +++ b/src/gui/preset_editor/PresetEditorGUI.h @@ -4,6 +4,7 @@ #include "PresetFile.h" #include "TextEditor.h" #include "EditorMenu.h" +#include "ExpressionCodeTypes.h" #include @@ -47,6 +48,10 @@ class PresetEditorGUI void TakeProjectMControl(); void ReleaseProjectMControl(); + void EditCode(ExpressionCodeTypes type, std::string& code, int index = 0); + + unsigned long GetLoC(const std::string& code); + void DrawLeftSideBar(); void DrawPresetCompatibilitySettings(); @@ -54,12 +59,16 @@ class PresetEditorGUI void DrawDefaultWaveformSettings(); void DrawWaveformModeSelection(); void DrawMotionVectorSettings(); + void DrawMotionCodeSettings(); + void DrawWarpMotionSettings(); void DrawBorderSettings(); + void DrawCustomWaveSettings(EditorPreset::Wave& waveform); + void DrawCustomShapeSettings(EditorPreset::Shape& shape); + void DrawShaderCodeSettings(); void DrawShaderLossWarning() const; static void DrawHelpTooltip(const std::string& helpText); - void EditCode(const std::string& code, bool isShaderCode); ProjectMGUI& _gui; //!< Reference to the projectM GUI instance ProjectMSDLApplication& _application; diff --git a/src/gui/preset_editor/imgui_color_text_editor/TextEditor.cpp b/src/gui/preset_editor/imgui_color_text_editor/TextEditor.cpp index dcb3fd0..d644160 100644 --- a/src/gui/preset_editor/imgui_color_text_editor/TextEditor.cpp +++ b/src/gui/preset_editor/imgui_color_text_editor/TextEditor.cpp @@ -2966,14 +2966,14 @@ const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::MilkdropEx if (!inited) { static const char* const nseel2Keywords[] = { - "loop", "while", "bnot", "equal", "below", "above", "assign", "sin", "cos", "tan", "asin", "acos", "atan", "atan2", "sqr", "sqrt", "pow", "exp", "_neg", "log", "log10", "abs", "min", - "max", "sign", "rand", "floor", "int", "ceil", "invsqrt", "sigmoid", "band", "bor", "exec2", "exec3", "_mem", "megabuf", "gmem", "gmegabuf", "freembuf", "memcpy", "memset"}; + "if", "loop", "while", "bnot", "equal", "below", "above", "assign", "sin", "cos", "tan", "asin", "acos", "atan", "atan2", "sqr", "sqrt", "pow", "exp", "_neg", "log", "log10", "abs", "min", + "max", "sign", "rand", "floor", "int", "ceil", "invsqrt", "sigmoid", "band", "bor", "exec2", "exec3", "_mem", "megabuf", "gmem", "gmegabuf", "freembuf", "memcpy", "memset", + "_if", "_and", "_or", "_not", "_equal", "_noteq", "_below", "_above", "_beleq", "_aboeq", "_set", "_add", "_sub", "_mul", "_div", "_mod", "_mulop", "_divop", "_orop", "_andop", "_subop", + "_modop", "_powop", "_neg", "_gmem"}; for (auto& k : nseel2Keywords) langDef.mKeywords.insert(k); - static const char* const identifiers[] = { - "_if", "_and", "_or", "_not", "_equal", "_noteq", "_below", "_above", "_beleq", "_aboeq", "_set", "_add", "_sub", "_mul", "_div", "_mod", "_mulop", "_divop", "_orop", "_andop", "_subop", - "_modop", "_powop", "_neg", "_gmem"}; + static const char* const identifiers[] = {}; for (auto& k : identifiers) { Identifier id; diff --git a/src/resources/FontAwesome License.txt b/src/resources/FontAwesome License.txt new file mode 100644 index 0000000..45063c1 --- /dev/null +++ b/src/resources/FontAwesome License.txt @@ -0,0 +1,165 @@ +Fonticons, Inc. (https://fontawesome.com) + +-------------------------------------------------------------------------------- + +Font Awesome Free License + +Font Awesome Free is free, open source, and GPL friendly. You can use it for +commercial projects, open source projects, or really almost whatever you want. +Full Font Awesome Free license: https://fontawesome.com/license/free. + +-------------------------------------------------------------------------------- + +# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/) + +The Font Awesome Free download is licensed under a Creative Commons +Attribution 4.0 International License and applies to all icons packaged +as SVG and JS file types. + +-------------------------------------------------------------------------------- + +# Fonts: SIL OFL 1.1 License + +In the Font Awesome Free download, the SIL OFL license applies to all icons +packaged as web and desktop font files. + +Copyright (c) 2025 Fonticons, Inc. (https://fontawesome.com) +with Reserved Font Name: "Font Awesome". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +SIL OPEN FONT LICENSE +Version 1.1 - 26 February 2007 + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting — in part or in whole — any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + +-------------------------------------------------------------------------------- + +# Code: MIT License (https://opensource.org/licenses/MIT) + +In the Font Awesome Free download, the MIT license applies to all non-font and +non-icon files. + +Copyright 2025 Fonticons, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in the +Software without restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +-------------------------------------------------------------------------------- + +# Attribution + +Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font +Awesome Free files already contain embedded comments with sufficient +attribution, so you shouldn't need to do anything additional when using these +files normally. + +We've kept attribution comments terse, so we ask that you do not actively work +to remove them from files, especially code. They're a great way for folks to +learn about Font Awesome. + +-------------------------------------------------------------------------------- + +# Brand Icons + +All brand icons are trademarks of their respective owners. The use of these +trademarks does not indicate endorsement of the trademark holder by Font +Awesome, nor vice versa. **Please do not use brand logos for any purpose except +to represent the company, product, or service to which they refer.** diff --git a/src/resources/fa-regular-400.ttf b/src/resources/fa-regular-400.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ac7fe8dd7fec71657281235741f4e692b093efe7 GIT binary patch literal 48076 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%0D@nbwb3t7G1HMpR` zn(TJ9W%H=yl2xDRJ#)tvpgy+D0PGcXo)dmp_F@Wj-{4@M?X^+I&*$kib;4bhmWSWJ z(KEn$T?jU~d*!$(y6(yi+4A|~gwVw*hyQWoVO`ie((4C({wA~>rhL9Oirga`IT*|! zFOBdQWDs@{cU$huX=OTjB2Q9A$^~;|J&bgxMo+~~Q)hWqsd?44WeY;9e9zmTQC-j6 zELuqiVcJazU9;m^wF28?uU7tvul!;jR=-)N#9TAOr}xY|%{H*})qHBrE!LBQJU*It z8RW>l>2o;NBg!a5P~y+W1fAB^G|Xnk3(>^temk;@oT-wlv2#3HFWp{EWP zE&wSu9B&`!5JH(02L{X$zU!wv;AReQ-S%3$LhZ`7GsEYp1d!$)eN6A*+n+PPdv85c zOSg+Z^sFFdQ&|8UW*>(zQf|pj{X2HN)uB*_vg1Vmha{Vuga#9E(0}VrxSX!^Uu`5DspWbuAdhuD35W&YUq;;--YSR3B&J>v z#thgrgaKP!qU>6@wX|(|@jmR^Hy-^t2%1oEeZ6aDJ;C;l>n6^Eb=bw>n?yegm_!RyHKO`GI+<**LTDK$EFDMx%Vop3hkonbyUqQ*|wB z4cWY9;rM#Gdtwjiou}TK1%Bf@B9*ZfL5TWS)HP&1zgn>CW@HFezW^l{Iw)qzP;Jz8 zS~+=O;vKZbd@2vTCa%YI5XInd!<{xj{*cu$V8|ug?wTM? zM9lBqTVe2|`I{SW3PGJur6b6@gP>$>0)NkXdLQ#gL@Dg2us`Yt8``|z`X+ONa~8q> zdXN&qB#>0h0Kqu$N1tPQC^-fL6J3iM5rdpyobeU`hGD2i7(Epp-r6}IYScF0`8Km*q$jn@8!nJW&l1U?5tbWU7=8oYw_R$(XFSX9KEl?a&+9bjwM zg3B8Yz-gsmtzfG@TFShv$p!pM>z(ivmLj2iVT({0FK4ItM5VI(&@dH-Gy8dK3&MmD zS1-V{OQm6*4WQW$KP)Gj?E^j<4zLhN_K;SDV35r*xruDCFizXeAKrz8#rV#RiqO*1 F@^75eH3t9y literal 0 HcmV?d00001 diff --git a/src/resources/fa-solid-900.ttf b/src/resources/fa-solid-900.ttf new file mode 100644 index 0000000000000000000000000000000000000000..60d024b214d817152aff3366b3f70e648c9241ca GIT binary patch literal 314852 zcmeFa2Y6IP8~1(B%sF9!>?T2iLLelR4MnOTMbsb)3IZZU1nFHmgx*$&h#opf?*dX3 z5D-)pgMuO`ARyR)BqV?!y1UsD2*UfHncWc3=XuL@ec$_C-zB+z`QK;G)O+qTd&-{B zx?Q{0RG3^OD7s10rU_BacikcCTbhi1U$t&qqjt~peMS;7!oTjl2KN~D^CP1R5ykZ; zYQCs{pB}xd?=IgO`&NKQ`y;_3TsXMRyZX>;0b(f{xNh9JeqC07Ph8}<{qyL#Mq8CX1CWGA)sbnIrE}pCvO_!upnOa!& zNR?yJO7ySBFSTvST$Fg{j&6&kQ8MEz?~<+V^WHO_fc% zTZh^%m^W<8{K%JIkK7IuPM0dv!YXaoUHJXmq}!Ek^5g=x3$`RVlqS_xz@`HGq%D#A zlS4K%+&8t4bh_*pPcDG#Zhl!l?7wiH@OHQ^HI}qcUE#76sw1ST4cPyfH(94EABSyd zxR0tGSEy`ARli{K{qxw)emfk(`3BdvoGzR>$_(z zWPJsZbtL~onoO4@hg8~OTzA{a_aE{}f80|>ZWqL*%iWU}-j99vq!YGfngn*Z9iChu zEwoRi(cN@aM&>IBg|=O&D@1-@!}*Z@k7dLA3Hv3_CNY;H<+u?mk%iosy_|v#v{{`3)GQz*H-n1 zw)6A2bR7lfIp0nWl|>%aS8U(YN4Y;E`$u>CbiWVs+XkPLO z)_dBa(0Kcr0Kr?tv|>gZYSK2@R&IENz{I6Qvq9UODLqU!+pzF7t$oi zqX=!wIZ%1Rnii^u?o;gN$vSVrcJlj~ruWO^jwcs@P{tM7mUbF!=s%Ji${Wfj&k6JM z%YA8jzZ!?ML-1r>UwAuTT?NV@uWVD5MS6bw$*?7KFZrKGav#VqC$}ktOi#PJ9Uf$Lo_xeC-*z*hYv`NDr4OsH(m2z5<#*URQx~1@`CH5lU0} zVTWU_poa72kB#p3GrX^$hVzBB;65H|uV9*5`(--Fw_PD0Knv#mC;OguWF2XD%Y^5Y z%XHV@QT2uPh3l5(RUJqVr-k#~vrWh+%ZBoY>z8?yKV7=KvXRD<@2fAAc7K0mzQ5Ha z%ifoMPoLy|Dp1aaZAo$n();uKkM#U@U7>w4ue7C7NTnUd_4j@GW1~v_odyLb@b5v`;GOZJDMZXqe;zwoG#&Kgi#Yv~d4by`g<7HqvxHmH)mns(kXj_3+TP zDl7A2J8Xybp7yc-AM2s}>Mgh(E+f;zw(Fns=f?!{h5eXchEV38$|3JPen`7pxAKE5 z2MXHxV?f#bc4R(m=eHqk3Eivr)Z+@}59KRZS9s03XRc&j1-Y-h+9%Uwp5zeHc7@WU z{ohE&cmS8Ubl!ZQ%I##GCN+N?;WR3+KWxiyqVF8*+caJlLn&N-j`0_N(>^?(UcE-_r+~C%ivwhw`ZQWEzFazz+8z++KcN$Wt(%veS^3e7Am; z*QN8RbPAP8)Ah=_!>Ia`?`{|DOMctPulkOB;WRF735Afh>n;WR8}1LXmS7IGY{R5d z^kv9qxW8OJWOFKneR65X4R0z-=R&qoAdgA4XLygW zC*x?HrC~z7>NTs^s@!DguyG>>^y{CJP^nktgn~H>+V}LlnktD|>O=i#6b+&tG=ln5 z3Vw#sNNPwm@Y9!u(h#KdpfSid5?h0z)v1@V+T!;h8bH0NX((R;HNGcj0@b4g>>Ytp zePI2)#J&0J<>#$dAa6VLOO}>>=t)WFM*{jW6q*8S7_4FWBS=7hWbcMx&q(B#IeK8L zH#7z5vd06FCk3PUU+Z-T)G`8f4G7g*pBm7kC|#3k;in#`kKc7sf2IFYxDhidwLG;_w9d5|Ia?JR-jFJR2%BZ#xpT(m+T~u z3fL8(MewMp6B#8RB{EMUV_|lZ5~(6p#ppZ1qdn2P-WXF~)!za1D!e)fvob`DWVrHJ z3T9a6Ve&T|}g^6z`E>8R;abM!Wa>dHUmlx%Y@^R%Wm9JdBO8KhgmzF`5M|pR7Px+kkx#e$^&nths!epL4bpL8ZWpK^9`c6Pqv?BeX|?B?v_?CTui9Pgaqoa}TtUvo}% zPIIO@Uw1BWE^;n)E_beRu63?+zT;f)+~C~keAl_jx!JkJxz)MT`I&RS^PuyP^DE~C z=Woud&Y<(A^RFaI;z>r5nN&EbNK(&icKn!R3<4gsaDd{NllZU zPimFaHmO}w`=pnWdMCY|v?ytDC@Afq%TX~oc><=?)1Is`_hl6|CoLuJu9PDMx%`6j2AO{Wems|oG~He^^7?g%QH4* z?8x{u<6y?&j8hpuWSq^oo{^Ig$he(ZII~=4wamve-^^T*xi@oP=Aq25Gmm5*%e<6% z?P8&e@fWvW+zfvaVL%54X?lcL&@-_iaxhkIhrm6X|hyVm!q>#XYf}(w;J&cuzS` zc~6q3lBcq#il@4#uBV~r2~Sf`Gfyi|J5PJh%bu>DKAy3jX`UIL*`B$c1)jG(i#&@x zOFb(++dMlwA9!|o_IbYX9P|9>`N{LM=NHd;&n3_Ao`5Ilx#79#x$R}%Lf)d@cyBfD zquvJI=e*6mt-LRIyLbnA$9Ttj$9pGur+F>!Oz$l3Z0|hpeDB-dW!@FuRo?a9P2N4; zz21+#pLq9qKlOg*-S18Fe&PMnd(3;vd)oV}H`{yJd)0g0>-7e_H@vrVB61>gish8f ziO;E&Q#+@APOF^GIbCwP=k&^XEoW-Z8#!}x*5qu+`8?-9&Y_%da*pSGpK~rJJtsTo zdQM)>UpaSt5x!_&IbQ`|MPD7?qrQf|M!qL~jeSjg&-j}8TKHP|UhuW`b@X-db@TP~ z_4N(*jqs)T#`z}srue4%-tbwz*}jFo#lBU(wZ8X#yL_Mc_WQo@9rS(WJLWs(`^9&` zchPsrchg_kU)BGxzox&IzpnpLf3m-^f3|;)f1ZD(f1Uqb{|Elv{*U~h`49Pz_>cKd z`p^1*_W$a?;Lq@9`7isg`Ty|$6<`4)5FL0RP%=<9kQk^Ks1m3ds26AyXdHMt&@9kA z&^pj2&_2*H&^6FA&^ItJFgP$YkP;Xj7#Elrm=Z`0yct*&SRPm#csH;)@P1%#U|(Q= zAT4k(a5!*0@O|J^;D^B3z)ykmfs27lfh&RQ0dL@D;8u`=kwHf=HuzvLK3FzbE?7BO zHCQ9~crZEGB-kw2GT1iQA=o+CEjS=JC^#%QGB_#tS}-*@BRDg-Ft{kVBDf~FF1Rtc zHMlLfC-_P5vtU~Ai{PQ)k>Ii5iQviL&%sN<--CY!bAtY0FqoIia|`7b$t{{2nQPCD z&MlT(BKM)(GPzZ9YvewW+dTLA+!u1&=622Pl{+|hT<)aYskv|D&dXh#dm#7Q+_Skq z(0vos?ZW zyG?ed?BUs?v){~~m%So;P4?#d*7UdY*Yr57>4Y`iz?xpj9UWTJA1tt@Ct^)+tk(2q z?iWI9diT(p{;GQz*7Ruw*7RBKx$Xs_HGPG94c7E`+&kQ9?i21aSkp5@Yx-rkJG7?f zdI)QJVXWzPPjqNae@LzA30TuBgx2(`o|>L|p2t1OSks^Nynr>mgQv5nyJwVVs^@jJ zrq9b?(^q)5sx^J5=Oa&==ezth{Z~)A=c-!MbFrr9dC6;di+CUOCVC(7*7qi3O>c=c zy$#m%;aJnh-Lt08&0o`3de?a0_3r-LntlLl`q$nQSkr&WU(-Eazgp9IPT>M;dOfV^ z9o3rNQ?2RKLu>l}(3<{Tfi?Z+eQSDctmzH@!D92N*T$M&4{Q2USkveF7x-8E z*Za5lclr1DKkSkpTNyJAfr7#tcL5u6xw1*Zk4V@+S6*7Vg_)7J+#2j35V82lJ(`sY~F4+g&p zeyi5>v%xI2rhCv0zkK#Xw-xj|<{+;-B@w4J*#7~c(5T?sD9vxa_#BxQlU_aT#$J;(m-f9d{zm z9v2yBmby_Yx0JsWD`}N{v*hHG<4cZ>SxOYM1T2nOgx_z2h0@6}^TE8BIru$GZCf#K zDV-7X2GXX3*JDy+rp3IbQm0^ha?GTdi7^w@_PChQF{6-{5;HQUZ%m(<9x>fxy2(9A zdnKlG$m$sL61F>l_A%{3X|1r;GUj>wZULHu=VG3XX%_QL%+s){%HjboByo{Xs( zlN94rHO9x3L2BukxR{bLkugPK7m108;n8=Z14Pk&;ET?Qc1K@^bqQogXGLd5r$_$~ zeJc7`^mox;BkdsgB6@f94(N7SYxHWc3drxBQZ&30JwJM0^qlBaSW^K;9_>>5CczpP zJr=)*M-PbZAKf>)cXW^FZqZ$$Uykk&-7dOybkpc(q8nj<_2`n((a}-S_UK4iGU{qn zCVn4_+7tC*)Xu2wQSU`d532h(MF6zaoHc_pkT1B;t zdOoT})U#1dqn?Rs67^(Mqo~KC8b;NNdNis|)FV-~kh@yc!%To$GImSCu9K#)h z9RnQ$9Q_=<96cR99Niq99UUDnIodngIodi}J6btfI-YklcRc54=6KA}(DA6FmZPSl znxmqloFl9KyluH|;m__Z}?O)lywC}g?vwv*gW8ZDxYTshtWPjJb+P>1h!oJ+T#6I6X&py-s zmi-H)3N%jf$@%C}{vG!5+6#Gc~2>TFwAA5IuSNkjW&h}3Bj`kPr>Wjr_ zsfE3{{W*IxdsF*U_6GL4_9T00dz}3tdkK4_-4=N#(ifQ%>5lv}@=|0*L`uZyh=mc)M|>JFBjQ;zy-?|h;t|d9^8|i|M0AXJC1Pwu zt3q`OHH~;7Vv*@Kz2+ad2;YOBw1^1vBajvGJIWM~7#2~={L1_)BHi3#eroP9-!pfc z8zbgMq(*c!*W+h;MCphv=C|e|)G*ULXnqk<#QX$h)6J`3Tg1tTUn9;$?2Py%VmE$1 ziFnUE9ATKZ&D#-uBVNT%*NE;B-OW1YRr6=_NAnE&-9KUw=#D;iH`kz6XT%`0qdCGn zt$vP}C(Xm=3H(epC!*ag5v3v~qpl%l^N3CnPV~29L`}1yS>Jrr>~GeOXl3ri&yUzs z&NyS7#?MLE)s6AyC+0`SSLRM*yfN13ZS*vD82ybu;E97~nz;?FoWSTx8S1!J6G6rd ztp$HaL=@cmKetV_Zp!i|R*3)ge?oqL{r~8nQ5v`S_sgQj9)Pvf_|!QBjGbbcz#54%&BZ0*R#qpJ_*7XP z#AGoUTTja^fzk6T;H2Er|8!Y?FuX0*C~ibc6@BGz{ZCvCTS%V`UI}l@o^kbw6;272 zkh$iDZ0r}A{3d?S1eg#00)G*|PlU^eiAcwIWgo?8o{HZi!W2Bo)K|yDhMWp%M&_>jdt6}pFKzq;pti_ilfq022aPz zQUdPKl*2um3b^;~#Iuh|cqUKZxn!Ur;vv=8MwuNnH zyV!2_8Qaf3XUExT_5(Y^&axlbPwZ#*D?7)|vkaEWF0#w)3j2*+Wq&X~yTR_@J(EJ* z#vkCNcqLwiSLcuNWZsxJ;m`8tcyrzg$3X}F67R@6^H+Em-j(;{y?7tqkH5;N@)`Us zK8w%gZ}UZbF<-)$@@0HEU%}tu>-kp1`cD2a-_MWn@A(;ij$h!{_#fQI{XD>fJeS|# zH+dev#sA{B`5h50iiucJLOdkOh@f})hmBMC%s6ZOY@9PPjH^bD zk!RdC3z-hHj9J&LXFg#zF`qG?C3T$DYNY4dC<#pr!Jy?qMZiH7P!*VdZ3Ww7#yM;bVvv}rlBD!K!=5p;~XABu3~calOzGW!KJ7K#hRn248>J|Liz^l zkwW@tOo)e}SalWEpyNVRhhjxm)PTOD;ftUfG<*qkvxZ-QZq;yHRnRt#kYnGY;on2| zX?REISq)nc4Qj*<=jXE8&={1o&6NF;@wgTM5UtOaj&c34aBOx+Hujbd!cJ zhN7Pmz6`oW!>>W#({NmQCG6H1Ina+Z<}=XGG<+#S5A!4unBPQvzDd|+=sXSk4LV=L z@M)X4K*O#=@i{0Fh^a)hCy}4BiRhn1AQloo(eTDl_)5ZXJ(PG*!=EK8S4<=QQx5%* z7#xR!(C{u$L&Lj5<1```iujOl#Af-*8g>qf_>i#kP{fBso}-kedJxH|0GV(H06{O9YO`^0^vz7J5U&euU;}*t^i% z8u>|I0rMgeh`WkwG`u<#@g`wspy;oJ!{-%GXqZexof4J*7HlAGd00s}>U1&NOM0~({@v!L*=#H*N&(8W9dA8|npyBYd^GS_78*wIUxDWc2hWnwNH1e#@*;ykH%T63i z5`kEDcF~BLP{fl&%z<{($n!iW{3~HAponP+{{uQg!x2Bu@fu}MfDM#A88(nH>~v{_ z?AvP^c_!|hsu4lxG>wpBN!19%kn?qokmFsT!S`Uuxkw{$+&B@-61Ew-Tw}=ouhB5s z?ph6(bG=T(5s%JyH1bT=xn9Hbpc^z?_HCnvBc7ayR|%K>-=yJxL1o*3--d3{$oO>1 zm;=h*2^;V`(9bmd3{?6Nh#Am>8Wod=U;`rq`jy5k1ihf)k3w<0N(>xdP8_cif#cOF z`wh$|pmJ=$I0uz|1p@s{A`M4eCUK3j4cI{0CTt*&g`~n7o(wIbG0_rqwHANKz@!SmCz_#)(eaep@|v;Yf(}yjXY0C zdRik|LYr!o{XA@->S+ZVh!)Vc8k}KLQag>Z+rtLLpf72Z-5WNL$3@cH8l0t45@J#! z*YKpp8u{5*xsFDRg4WfD6r9%BHS+VKYMe$aftJ=NyF6?ldO#~^WGq#ELBm!OrDM$L zXkVTmrK3&>zI8_F@Ues*fnwcAZwwoKNymJocZFRKisM~Ek3+kI(INYF*wbOdAL$rN zI{HN&pvyGW5sG-1kgV@LjXWny->sp}(7hUZ1-ef|U7*J`I2WY!A2ozNr6aZ_B>Qy{ zWWnxEl!5t`$aA}l2A~n_UeIKXJa@}@Q6taYGUPrW&)qUY(5tWqYvj3G#srN#Z_7vp z7_&Tg%fLD)k>_g}7;naM*u$VW-X-$r#NJDQxzt$M=N9GX?&47Lfjv>FS`;vw% z=rs+l1L7*=zrE+4Z#OlvOJ)b&V)Y!Y-0SPL8Rlf4;ifsOdd-mYPYne5#fhM2iDR>NM0j@K~6%q7H` zgrV(AXj8&wK+&#*p)Z%-2Fqc;MRXZ+Az|2erI3bUuCEl)um#W(8iskg5(mm4KkB%W zpkYgh+ypq%mqOu72~UO^z(hL6>W{uVV9uYL0{y*4DF|p zpI>e{1|UDb+ygZ7^VIz+7=rXap~Jvr*gmLB!==9wAMWWGqxAP18vYJcjv4UvP|UrA z+n|_xiJBJ+HsFP!a%{6<7lFz#BQAI(6mj6joO1_skw%`|yK$UL*k@1~2Y|;w<$SM# zT@1Pgtc5M-bDf5_g3A7FL%N*L9bhNy^3XJJ47L+`0(=jTUBi>1(!Upx zF6S*vW4sH^*03AUOB!AXdKvtI{7s;40AKPJP`^gBh6XfTjw7hyA47A&ZRCG}$U^}2 z$!l{Dd@7OmSUvEgrzq0#8R~)WB=VfWV+Rh{J)zM6e&W5M@R3B|*!Lj*CF~9q{*rLn z&O-n(&HF+T%M!K=8n594p$VWI@?f3uz>g9+*PaR>3HDHEB@M}TDr@*iXcdh-zw%TC z)sa61S`)zcd@K}x_cVk(8TvSQ0`^oW=D^bwb}F=)h9kB-&w^G+x1iEr?O@M`w%3q= zb^ti$jv-H1*vp~a0er?+Lq`F$&o@A2d(&WVhQ1DFz}^a-tr4$6=W4_N=sbYf zzsFLZ~7?}L7z5g4aO){8p$ zr_hf8_RDKAPa60J_7~9az%kh0LVwh7Y!gD2k_(2Uo4L=7BYLuM|`v&r$yyvC{_jAdU2W}%B^?3!3Lrc^iz$pKZ6a#<4`$fpzJi*Kz_D*4*dp!UF zC0>O3K>&6;C}L0|IzVr0#7jgu@NZ58(mO&6gGkt2pv5$z2eh-O-3?L_YXlB4frEt&uU{ zE2j~!Ln~62jqb1T+EF@F;AE);X-!~BR%P|Sx!o*VcuA3n^3ypQUG z-+hQ1@jg`g6n+xhq0%ov?1D4CuSq2bKK*;xp)4jq01M7sz$b zC+h;@bLd9^>xf8$$}-q5zJ$u{FJK>n9@L1#(62N?&f9n381f(%eWx_yDD({Y1?k^H z&w~rFPeL=nMcAjImozFi5PLo#GN6b-e_`0D!ygN(!p??1tf6AiYM>_4Z$WEm@a&QN zwLx9A^pgKk*g!se^49~&NH?I3HFACSKLy}V!vRHXNf>Ql)0hc80pK$O{qvvI80eG#XN@r*`m4r}eT1L=@UwxK@WYQ1V-Yk~f+2(-JTc9_maH4gdpT?}rZ7 z$Y%tBp&H{0Xo|+baTFM>F%Ck<0;~fD<~}e1OoWa34@}V*IF16ZfmEa;CIWAOH(?)x zVlE}d_fX7V0P|xYJ_6G3Yhj;(zN;~Qgl^UtKSAHu$Y&OTJpf}heu2vI0OK4~jt3a$ zp=lZe@f46_K;MmYD2{)LkqJGnF)l)7pMa4CJ*6>l%m#kY7&z7gXEnxUsO%Fku0Unm zXw&!&D(gjk24XWH%K+m#ROSN);xzC(K$`I<)D66_J3PrxHO61ic#Uxzig=bNyBus_$hs?m%D9J5(5f1fL#u-tNEgt@ zz~iuyDVVG=ZO|qfvoN%o#w-GDsWD}L+W^F?iGBw=Xv`>RC(s$`(a^4-8|-4x0UA?| zX&@Mcbc`)H6byqc$2S6ugk2Jf7z|E=9S3y*#GiaF7MunUho&6&bT9*UB6Oz4tN>jA z79zbObdknPg09e*(l@KY8sveGg6lM<^wWB<5$O*@H-oLP;jbWKHMk9SZRm#p@oLtA zehfZ={V4P^jaeW1IY>i#1LzkTQ~LWLIE3^@&~E^aC;6Nu_$@dFTh7A?jVb5hq{e(2 zidYT)j6BakvjF1Id=C1%#(W<7r^b|Xm@hzs8dJ`pjI%uCc@cl074ac4 z+d&Z%xrJc2hhpv}@?KUh=3YXxp^+MuZifxbj!?uwF6Q5S8CpzZA|`S%-?=4_{tEOV zjfoh^#k@*P#7ZvaDYpvj?ojw$V)ll@M-sCSw7JIY3&ofvW>F zS>LxBQ`U#MlbDF>+@CZi+Q`k;hy*C+PGU}m-qDy-h)`2rBo% z|HiXW{Otii#&ggp1-=gmjaCp(fRZa%w3^9D~+Y5I+EYR6%AJXbS~#*{@a#GUh;AE41AoHs;Xm1btOOMh$3+ zf^hA|a>gh~`~V%RAfp-dH3cy@bgF`QCUk*<_#WtDg**>rIZGAtnuX=8QV_0vSdNDH zo3P(i$a653vq?clB=mg+8C#$`6(qdS4;93gLDLi@WIfUshz;EDVmV(Z$do?+QbEQI z=ph9eGS62E;&N^eD+t#MEa!-VjOozt6vREy6AHq00n0h1AR)(hS|LAoSk4&*85Z;> z1>rn|<@}-`oF}lHbcHP7`JbNLVmt8pG!dojwv7J43PK`I$J@w7G}7< zk;u;l=7WEMycf-U@H^1e;aQ}qi+D$?{8hSw?KU0`5 zT|xXKXoiB!hEN%EGKNb)Wt`!26W7Sh=T^vjkIW}y#e?+m(13#2CMaSU$Y&D_*MJha zhRd&oNQmV^nS#VnDDnY$?}+&k=YZHAD0~5k?S&Rq$oq=Sk97nP$FbnY~1qma)67_Ma{+HMBB8S>y5@js^^199PRt{{Op^0!cs zkp_KUA+I%>=k$K{!ule#9tHapIo@TYjbwhE7!wPlnD=s95#C1^X@3-wA3d$QS{gsUVJ+^1rPh zvjcRIf=3<%G9n17#w_}5SzlbG3c~X#=D(`IcblO%74n`o3m_|y zZ#*zuvrEY64dn_l+Ci~?0WuH^xQ>^QfmjHb3Nl(lBNSw`ffiPfH~}r9AOpu^z^)+U z1!%N_cz-D3ACS=zj z0Wz9Gt18HN6N+O8kkJBKO+mQ#!f-!9LPjd|5d|5~Kp$1e`!+1_ltMl)W4NCnAp>!X zYj6qv`vT~6g?vuI0*G5cc(%?0vlPTJKY_Uld7p;`<|&9vA1+c5PlGO25bnb;T)#_* zBX$DI6vSn}5L-ZAS2A4FO9ueY*sGPeHg( z$Z+j1!Joi^?obfbg6>qvH&a+Z=EJdo^L&Q;0}?XiJYZb|WHyD$z9PE147vY%g?xL31x_l6-G*i;h)cg+R1lMWzosB#H1u}`8KaIB-(ge^ah`an@X&~_H=tdN}z8`lN`bAszUiMB7pz8tdQqu`a0 zjrk7#7P4_X2Cs%}^f`D<+c)euTyc#c&qHoF6xuEUy9E3b4K1Z0d>@b9K>P#3Jymw2 zyn>7up_LV6V4c3vSV2ZzDB>2#Yif3*iGs{3P{c4GJbz&~G$dxgmgU?n$y6dlh871^rGzdM@ z$HI*q1@V^98wwJ#Zo~|b_i)&aTMD8UP`P~@=}{;?PC){5cN1|5h(8ZS+kkLSjNOD! z02!Fin>b#8ycf-G9#E)zq&Q#vum7058_gJdQ2u4JzyIkqxvN`l9Oaf~xn+26yvQwU z#9yq^IzFS)nmh5mmqo?5sZ!Ro?1&mxBnrPH3&-F$MU=sRE|XR)w{#@MXP2rGDdH<` zyc21}7on1Ei*>WCd?T_DnT$QsvhTRb|F8U=wtx5oZt8E${Z}XbyYpWU{Of^#J@Bsw z{`J7W9{AS-|9aqG5B&el1E$NskQf*VaK}h~Yo0#>aP=?0T`0d9C2$uOSJmoU7V;^q zi8M18r~4zohj`h#Irx&uRtUTda2KI)1%Przo&>l;DT;kXaphMOceElS0iKXW{))fp zQV+ZiaP{h_2++Rc5B%MnhF~^PG_GW$CjeZH#nb{WaEPc_U9b}13iyF009SFvj}XQ7 zz@O#rjsJHt0igYdzQ>E(Xur$=fV}Z&Kj96$J&Wz~$WsCRsCbabITvqZT_>uv25()Mjk9=xYBg?PVEnar;iaiTcv-3uK!5Av>i*G_cwuTCUO<0^ zs3Cm!*m}H#jZ__4uzuPv9xSldpmkM9H0q8g~G=1MpNDQ4`ET6V&kx>TQ}z z)NC}-v(t&58%)%^2yo$o@d={m(f5{kV%idYXjK%TKdmt@t+n5k3T{*;WS=k!v4wc61mXMYcuhp z(sM-94icrli5HU4pXnIW8@L-V!%OrQ(yXRLGn2tBqFI>3*%;gG?L>1h<~c~4`ytUh z^mqQ(L<`WTg-ClF^S7uAUJBYwwA4YgtPj!h!+7}yaky$SUh>HyTAPa(;xS+AF|Liz z;iaDAM4QJFZTSE%!EYpb?+nql2!K9r$6W6Cis%FQcqfjHUDJtn4=4I?2VRUHLbNvu z{6+K;+W7b#qE8SD`o= zC;0K_&P2bUkH06^p7^Gng>QPnpTRhyT(oneKGDr;M0xPb zttW~8Li%5*_x3sbKd+~-Lg5ahygSX$5R*^y@Qo1`@dUu#F`EFN;#PzSz9m)^ch&6u zh&dJ$i^5%`Xx!b2`H|QIxWiH$cY;db4q1s~#2&<*l!rzUD~Y=>rP7JTp`FsWGg)RO zvG`YsmF-C^0e7hqai=TsCt~Fe5-Y!oScOz#6>+D=iTzI0n{Y@Nb3y$ zyn;Gj!5y_>y2Sdxk9|HQ)^{Yaer1XE9|AC*0Y!+t zS`;At)r-UiwgR|oH7Ej10$&ju!U66S4#n7pW)d4#5zHhuJP9l!HX;FBCN}ao{;*DC zu!-0x_-z!%FdBUteU;dlW5mYJ2G@y=Lm$VTCN@3}V7?|SB{uOlVv`U*lMw@x(f*VH zU>h+P{O&r47ulKu%>UF{#HKw6g1FI+yKt|gz1K1R=@wpCdlN6JwI=pv34oY-3;Gu3 z>a97zPt4j*Y$p6a6LT?Z7&u34Hu^AoJa~`ToRVNA2oRgw6`=mPzu?8T?f~&JAMrdN zeOk~3p#6n;_&7$~yp8@YLToSU05%d^j5-&ijU|ZrCFt8yUqB?KON(Z2O7W_I@A-50e%W`v85}i8^+n&fSBE zeVBp=r4s=1?}d-{V(vcbP3+@DfO-041Mm^sHwzDnQO{@X@n8%--@lsJ=j8z6H?0q` z1MtTI^zXm{VqY{O_GK)wgU^91VuvOH`0gvj#@F!qVFNr4(Dvc?K{l~(FxGEie*^nS z88DUDQH<;8a)9=~MZdl~ffwxH|6{)qJ3bi??Mnd6&-XZ{PCf~~Bz6jIpF&*yP@UMB zR>aOWAoimJpdUXC2B_<2=+Bpl{n8WsO6=EC;AOA`oFR6uHh3NUMC?4~?*e>(;ZI`e zi2L;Kh-JWEnHXQ@X<}KJ*Gt6!;_ou@UB>vYM1Zx#etV7B)rG{aVZ2y>+3$#t>*&uP zFM(WQe;&aXX6yj@@WvGLypIRN4}h`Y9KLMP1W&#(pT4*8bgvoMjHeutU_YKNQ~~Gl zAiO?+kMPYUb_4Tw1Mzip2(i3p!C_*zQ130o!(Y(={<%E>4<}LYoh&? z(Hoo~9+e1?7X3MK{GCi*4BN#r@nwnO#EXx>gVsZMxO^Bd^1Vp>AtyjzN+uI8)t-3V z1K>LG(hG@~d6+oXeqI*k%OZcm06Z{%5ZoeO&X1S>R^p2m9f(&<1!&LN8V}v0!4cw> zP;X@uSop&TH9;Qnswh(pd8$W(H;LD95wBUAcrCPF8{@CN2&daI;CZwtS+L*Lr11_9#jKPKK`C-IlQBi^wrc#HVU7*D4<;41OX$p6aI0CpFY z=~@}QjW4ZqB#zGz-lGZl11}q*Up>)ouXpjXA^g_+5+0Pj4o(v9i?;h>yWh)T6CU=W zj{az8KsDm876U=z18?Jn!ZXANWB(A$*)WWEct7GJ5Fhx=;3@FoC>!{d_~>Wxum`>z za}zHN4kbPgF*Pm&4*}bN1Nf3l4S+eGfbmUw1}_pu5uYMJXMj0#VXj>m<5c{SuW9hj zv>%A4t|R_>1>!hGUHtL&$Hs@9XOYp!6u{bY{`21bO7od&>TZu1pg0;lo?gV}&zGx`%#R&j0x1>98 z6JLt{E&Yo)&SCko@9`yoL;xSIXbI4Vm6-Qc&A?B@SGNP`^BTm<8q~2Si}>0X0sOgc zIKFg+{=TyVU%0|pHtZq3u_^I)M-krypKOBPHc!OEwvu2L@vR*I=K8%A#NQuHeA{Gz zV{kjhxE=NF!1f1Ci0{O{U9AA>+`WMKhh2dOU)q~Md@siS5ytp2=J%5#U=Q(qXzx>` ze|nnuXPEo_G2k#BV8ah-rNP(44`6=2fRDc{O8j6&;)me7Lzjtv1wVd``o6~44x`V9 z(YJ5VUz{uRBkvGD+5#LU{%vF8-(hUuT_Ap}Ik-Xm_#NUW(8uqW6F)f&FES<(KfMND zx z`u>D(+?ap&aeO%t^Y49+cn<3E)dbUs`%{SHTNWJO{O9;aJI6P@c@X2pH(@!R%JQ4= zO&->_ys5-*y#&z4Un_~GRG6`xxf;A_>SCbG4c+ma`NF!mcArX-Xc9JOc0SVhp z5{1{2DAEv|BT*Fp6IJ9#B;q(F$C99Jt^$fU8B5p5<(yx;!Gmk`k4ET~n*;*ivM8atjiCsvPn@OVl zbP^S&kf``731>SJNyWhp5|vP2W%RG|VG>mqk*N9=iHF}OQ4RG~uL>@ZsPP#IoU4mk zK@zo3ka%PmKzf~%B#Du>jXd zG$};lY4q{w91_pmA<--fd`jZkm+&PVjHku3czMwx(Q+h-R`1~@#WG+Qi8f2|<+5HR z+QNVBih&PEv>yt5Bs#!;sT@F^9Wlm^@b}9jNOZ!uJKF%p^U44cUEs?uXrt>3B)WAY z(H;KnVJFcO{qOZ4_=!aCz9jlE5`Alf&q?&_Nuob|)c**H0chvd!z2c}Net>kV(>r^ zATb1W4MCejT_lD-3HFf~`2avnq_ie63T=!={iA;;F?Km#4otxp?$DUj)T1P(!N;kIU=E4tMM%7{gT#!VNxa#QgjJTr z%%TAQv)L^859_Cx^B9SFsDIvd67w6VNqkrXppSctgY6_fYE9x})cr|+fcp2Pk@$2piO;H$*pE6t zM_i`OCUKxVfN#D;{a+%_A&mWN>^}^jeG>`RlK2+K*mvVe9Gg$#L^BfKqweqFyOZeO zDfr^lY7(d6=QDFjoJBu=#Jv2tp2SZRz`G=VDGX4@uc-eV`g5M+3m6kgq;~-qNo0;B zaS`RS(4TCSxrAf)N-YwMH`Y483jHj!~P)|j$33zeLzNZ4>Dqq z9*Kco1bv)+D3UAn+p@aXZK;y_SqJOUQ^vU*g{*qwGL{ zw6fR8NI+i_v0d%~u#b%LyUD0f5@e83@eux)UR!{Ak}i``sXzFhjLI{~sPZU4T2<8d z@NzP$jRAMasJ@Mi8ePb!*#Vp)qt<*fYR8iC$VxKmj3T3MOK_HqdQHJHGO!jI4N}Nx zSOQ>-k6k3A(GfBp-$lj~>&SQ#<&)9Z##I1(@l+!)kBla5KqkIyjqyJ7L0-+HhhnpX zrOaBu%3EXZY!e%7W~t5Ed8JJJ?+xu@vtU@kHKt71kL^5UkZH19-nQkj)`qofH(0jd zZp++gtr=wvw^_I9nAWh-qla0xJfse{My;_n+N|6}_HKW>>Tt)-K!aW-q*q)Fo} zTLa4+mRH>Tr}R-3%o*dyyT;A2ji2pWF=wT1RhF5DdjBaME*WP{T57GdSu2+=U1{0Q z!5P8gCep`S6LtEsr7KkW-F{8YYa1(Dm%Wv_W$liSca;f7kO0S>_09%ouBg%}QA@cI^z?rOD=jX}h{u?QK@;PMum?HkW0hq&3;)nrzu7 zTdqac5}UQlY;u2ZgAZEfdTaG+YrV}{H(^!EY+KDGX8ZZQKCuqitgk=)^lQtu2vzMe z9W{dOf~RAxwC;O4ShfyU_wH5)o7G`Y_cY6vX6@NyrP+cD^A`Ts3{!)j#V%fAt)8;P zHhJ+FYw{G^t>U+en^PuRV-`=gEt#^~TC&)dTRgY8Y%L|XU2J&9@*X#X70#Q>7OtMY zaH4JDgxAMToNCLfP#>xF&1F;PjhQ;pHg)2d=@S=7fDWV4T zsTj(uW9HRS&eS2bbP}dMh!hKZtZ`}+y;t)etYgYO;Z0qUENx*AX59+HZ5L0Sx@g%h zTBn*=kJv1PhV@93CXZOQN315Ntcx}aK@zNUEjH|Unc*Ji%M78(apdjTnp-C}+*RF~ zqq1wK%@%*_bG!R{hAdXlhvBOMSCxTTbD#2nitpf)mie{yX(y|-&FVJ1cdzlbys|Ht z{U)aLw|d*GVP-JpZ{fP|Zq$8cT1P)P^z{Z?uW+k_3m5i}G{I}5~WtX*etF;Tq`oOKd?#&Z? zS`G`rTqpmy(ktO3mf6Yb-P`J9vj&>$mTg?P*|P1k%wMdd&8#-Is9~a-JL#+NbtN)N8h@=*l+20zXLu=C}Ofbf89gRgB_a<+>zo7JJ?_lxR&DO?+ z>y}9eTAh0L?qu1@t9}<)9C9r4f_413bwNAumlj9!y{g^(ZHu3^--8w4(0={${ihwe zeS5x#l|yrf=O)H3n2in^(<87%E#->hKV7pZ(*v_0XH&w0v zi{~$1JlB>hla|k4l{$Zd%~4}3mh^kqiQMA%hwv?=Sb6RKm-0Ebp;M_7=8s*TYE%8m zlbxE7nmTs=c-w*rD^pi2&^=f*cfRga>SEVi+pW6j6iTJ0qEmUpj>=h1+p|Z1w5Si7 z?nC?anPMCA+N;B@o;GXb{YTzArZsEktl6_|v**m2J#Uun+tnW*mQ#PR$tk^ts#W!# zW%*j3Po9qG6OeH$_34N{IaxUlry~xv>?c}v>spC6D>0+4-?D{2kMwbqmuTe`3Kp`0 z*dI&`!VD&2X-Kq!h42ZThg3vEhwk0+c{#)ywq*PoTgvFclLuHf#H|XU zOkFe4Hh1oV*$eJAfkXbjkCCmW<*%E87LH;vLMtGmQ%A8{l|t*7jL8wX39-``Pg$To z{ijTOV~VX>UP_0&;*$p=Hs+6Ak!oB1`l|W!SJ<|kHG`cjbIX(F{PC+&m(R1Un2+FC zZkxQdL$G+YV2W*~75ib{n%KMwmU$}J*gTcjSgu(GKVWd=hPoPBe%0VU9j@XtgUTrO&Vq1c9iOG{~b?W4mvg%~o9=T`*%U#U0WnK)HvN9jR zIfgYFryWx!kD-2GsH&n+Rw9FQ*#+u}++71%0tN&rrw6 zBd40+Jmg2~+b@=#w&lg#9en5m7Gbc#dgp(z_a<<5Rb{@Ywyf^uRjaji_r12-EfPfy zO;iF35d{(vtsq0fSP3L#PF1Rss#D{c=V6_B9;)ipl&YjEGYL~bkN^q=yjEgA+h8j~ zi)~P}f!_6dyWYF^|Lvhpol}(}Uct8e-SE@ooU`^?Yp>xO|KIm@1a@$j@)w`|yz*`= z(fh^uuL>E9CcppHSFwcrG4V+Ik*&O74|m#awAw*ijyLb#T)YeS3cGj`%zYj~3F}+S zC?RIyy-852`K5B`X21K~HPY@p_KUU+yHri>WISLzKO^r5S>533?`axXe);Gu_Nk<6 zrL+l5Q@0BQF(F_HK4f-h|sRw#VfGp!fnmdN8w`LMEvd0kGQgESFm z+?(dghvhZcgYnwdG+rT{E5zww3`~7N%Et@-GzKgw|A2je&{!>F#x$PQZMXTk&1a3d zGF&Oi-$%!2SUnBuIG@Vp(eZpIy!!geCWTIo{SmT&Sv; z#)@DnlZr{gA=EV`T~MdS*t)tUlTfaOcJua#(bQ0$FMokK3XyUl$Me~8FlXgIxP+BQ zbNj`?8m1=iS8kBDw%=vDW4?*asAT}XNXD2i}0!nWN5myB(p;#B!NX2X=nlEdnEDw z6DPh8X8EZ(XA$3Q6D=)b8y8JOyT<+eWe0o8{lLwlo{Otjt-2bcoL@PvI#t3q`^D&} z*w4kdX|yTIm*m*W)S5@cQ*^9mGb@cA5R(zg>jvL4t~%5n`o;LDYicJ|eSkVK4xwUV zx|j)NL*;g=j9+#0sm-|;UkrW?8R7-;1}-$dF|PjS%2S`0R57fF%YUsr!%!^q8J6hn zPeO9)^(MQ#W~etf>^{5QtugVGKZmbymOOZ_H^J);(sd~%`rQfi@n^~d>}T?QELO;; z3xXF!-W9X*pDFh-`OoABsOMD4i=ImD<4rVVaiA3OPIaT8L>II)gJ83 zTDg3l@>Qmsr(D4twn%3IoY_&1=sC9T@36`3feVCw~>^ z@pF$)oi8~Hol%RETb+TOQtj6#H9X4xt=M1 zBVSLQ&AzYL%ACk~;tn3Shiq=>9eb6ZGC4ly6r!PB*pt32_vOO*6l?{1<)1MH4fRW1 zC$FxkZTh*u1vgHJMvLfz*Ry`XQX&;dc=?ctG(>}9Xl?3ey3{$`7Qs53w{>k2e3RJL z-?@j+I_{Ik3BpiALoAU{Wkw7jCUxdEOJPQvI+#&rECGwvk!GnRXn^T;3Fa2=fY-}S zrr8{_-dO8b1uHl2?>a2_VX?ozyoVp0^`5GwW3qB*G;yd=Fw+q8CfrfC;1~eY!(i3- zYB9dnZkW7ndYRO{js^W8zwmJpGDbEY2s056#ey;3eGd!9{869aKH(1p{gBdaQ}t3l z*H6OVzkG>_oUuX^l}Ub*9p1{)xDP%%*YA@VgDWeb&a3Jl*(wc14{9=gvGESeeR zV}iB4eD%rC@8P1r@S6ofsDRR#iY60rF0YesW65MDmJvK7GX4})s5_P0Si%?e2salN zo6Q2^=Qutyiw0vB$1AfqZp@+qx_NpriPNv02YV8)u?7C_(w}^?Vj0v-jwz*La;s*h z8rf=m&M;0ImWXQm;)5HHK=akDjdf2$s;trCluOy3UHzNHI#?CHS^s5Ps(RI=a8KXi zy}LmnFJ-5dmn6d;Guks`N^`NtcqOZFs;l43+FWLn*a_`@tob0g$hiDI$?J6oaM)bJ z9rY%7{UpQMmh*X@%MH-k$T-1}zHS_jUQtxJ2UJ4_>GOs< z1$lL{ax1i+l?QP~10pB_T(?}n7IZ{B$VF5{M3irsbu-dz>H_o3=_5m@b_t9dKvKGP z1(fI8Gy(ri4fKm7uNXe7{@RqMB{4XUmac2?b+1BOH%W%g+`-bplt1O?i3CfhOCs&! zX_qj&T-@trF?Y-zagkZuXs(@Nn9PGNzuWKT2`{LH+T7(b3s;)^V{FF`7W2>j+Nu8G zbAIwB7<%%%f?POi&G1>~ieL^gnv6bxD=kW(Gng)BoeHMOes5UqwY!PwM$)lVEXC2a z-duk*44zT{&^+h-w0AF?^EF;D6+>?kF0$A%)!zWa*ijO2GzL;-?AB$bbL4Ys&EQoQ z75Tvp65jUzEWLW*uq$U=dMy6zrPt5lMar55*B4CX0{F=ED`v1F6SI14Zij>S^kVaI zTwUCS!;2%|+e4>zzOVIDJKs-dc*5au+q_n8QjeudgtePp!m;d*cW2Gq;lN>5$4$Gi z0BN4hvXZ0E*M}=$uB8-1tdPj3vRRs=-aL&ffUAZ%VcF*UT0i6S{d9&Wve{HVQQ$)r z?x;~GGj2L2<|xq|^>Ov3!&{iMcp+?0i78<)qZ)<2iZ9A^C8|X}L+8^4{nsFV$rlS& z5II-O#`|tzF$L%O}fx_m3e>&iGr&52X29rg>Re6w{2WlzPYsL2WH_N^kfqCo&jkWf8^ ztH2Ca86O})7&Pbv!!O1M5;0VgfgZ#Ord+8^OUI6};o*IS{g^785Y;n+1UX@SIb_;G~!j<%-1y74) zQb1II@K&>5B;?h!IcH9rs~NN+i?5S=+L_72T0K1`a%Q*ZI{Te`XZLm|Tm+C}uM;;8 ztvkvU_V3JG6uXNhUM%evB@6%V4X{7MFO5?pkIdZ9K{;DFlJmFSs89ZOG3( ztr(v9Ljio|0>8vUe8j_&tt{-11!987L@XMP@gzz|Cs`~U#b++?i$Ba;lgu;1luhym z>A01}L+BRLv@hWIg?NLNSsPf$=MQMYb2Jp^*5j<9zKhBa#^H7y#g4r|9S)nQ-So$Ee^y z5)kx2`H=jWB(7rO&c1bfo4EMV-9LExD364pMu>PM#-Ehm!ID{j(VpOrq{Z*C^RFxK zW&#IWuyt23LG~w=7eWFfiBaBuP%5O0u~LX1e2OJ={(>XP?Re5_=hrBljrOt^4oc=` zR`lha&|Av~SOVQU5@=-gdmQ|Mddb$#BJQv&AS3 zlW6k(+9t5w*0%ZFoHqF8P5b$%`YL#v3kAD>4_x)}va<*q#nGu9QZ-we8krtpX(xC@ zhJ*MP$%c1SKFSIO7K~`4ss%zP{{p{Wc*+v2F&FZ&$_@`fGcU=G>G|J@9DSyp|I`=r?*95=vBx{7E zqjxco*FiCu=+E%!zn%J9R(HBynygssPBD>ACP8MxWcH9BT~THWyE25=$Rsn_ z2){ea`eXf8(Fx>=#R|?OKQ6zUL|s+u)Hs?U3}{Sw1BCDSzSp0rZ&>hWXp9T~-ln{F z;n7J;R2AXsS?QxgikZNfs20no=}fqskX|wz5^NDiV+4eB3cRS8%@-qdB*O5Hfph%w zOC{xOzxDs*z_l-_e(GYUC0DTvF3BTL9kw#LgQ4mQWx^fKWhtl=HG{xFFg40f`ivh-6*Fdf=F^6z)v8$tFm*Anw zdqT|7>^++VI~SbCIG?5#hq;7vXAw>a&rV^1@LV!GT1=`jW0tf#B(g1Lvmsw+Tt$*| zOQkV~Fu}f}8ib5{IW(3nnW1&6qHsC`GMDD&Qg&=eMRv`)x+dWmD*=3@Q9>ss0SVr0 zZpn%vuHU0ZxM6fOSk?K{&*t41EqqY^cqX!cQb0P!TSuA%ZxXGo0t~llq;*{IaWOIi zH?Z<$1^SC7j*%P65Zg4gNWEYNTIAKK)i9rx1uKfYW+DqDy>YJG2@{z+>5MrAcM7N9 z1L(s#n8>`ofMz1g+lqqYG0p0xI>lcQFsh|vnJCW8U&D?VPK4ulc&;=%OE_+W7elbc z$~h8wXbFv_!L>fb>L*mYMq}kr={Ml0DwC^d$FwKuQt>IX+?M3=1(7Ea0wVcQ2(kJn zp)6*ZST>p!9Eg}e211{_*bqA8OQvs;Qr)7%-Nm!PObCr07RQc>hr!Un7T>j5e#bR0 z@^z0gch;7G<*`k4b%{1OuRqShLHrlsqq%Y4U^^e|h&S4I@=<61F4~kGT|0Ne_T4Ra zOW|l3$O#OJ^j^cACyRjc_<^HytTnd?_d zS5C{F)~(I8_xiO{f6xV+AWwj1ZZk}68lBN?%8le))P9E$0BJ!$sK4V**3u%@jGpo}Roz@U~R=G&N3JwQX3|H??!5qTlh&uW3a&iOx4N%MNQk!5mKf?ksUj!Nu zv^g|!Mx1<9c?a8x{{@^WC+-(~DRF29(HMW^8TK2&Zi1&O;7kHQ%kL$Em5L=3{O7-7 z$|`vkxlT{xDvM>R+YoQ`T&?U3-B+ZH>x^~MZ+#>58UwpSD1r*A5Zk4~&}4jjKtnl# zP)W2wKq(u*e z?)_uEh`dy3lE8&KpiyH11_;bKap=K9FOXaDh^lhNgHgZ>dJf$#8sXkaOx+;)BmS_a zwTApbuH3MUwY$s&HxGaMZsnWum6!68yDUm6E?@bRqw+V!ZvYNA54HnzaKkBPQ$8?r zxlLmZmDQ>Sx-=NQ%4)MM8dKI7y=n&mQ*Ke&w`v7;y1I{7FW{%aJBDlBp}d>h~coJs7zevveb&n$rlJQ4N<)fBOTEt@&MI^2r{Kn;XU z_9+9~Fj?d0!(%i;41WYaQF}pF31)~28`Q}F30Nm@gfSl)yr$2q;RMj`RpvT6FaYgd zJ_i@bplnsNR;P^ZB5T0V-+4I1iS{qR)LF?Bs`!KBceeMioiDpUDpkCOXSNzLJZfIE#rW3 z5O-X(GUXliFD?$PJb2vU@pH$yz+{W-vfBB<51TG9~)y3kII0uspug)Z);zEie5ij;LVfQ<{PJZVKGDl=_ zG)aPXxLiQ+K#{TtM@|%ABMOtzBFp7#9+r$D z7BkV@W@;fUq-ltF^=YP0!hVZHm>Eyp2NOj?BvgY1ey!^GEG-E)J_fnyMm8E57!+gh ztilZhUL^lHa}2Z>TCoI=5#e_RoIYOvGy`}BYU5uCh65YArM62k5<7vEqzFz(;VA7kLrSb3gpAGSd68Wlsu+#o&;(Wm>xzI$!= zaMSb5Gu&RHmwiPrAB;pW|6TtCli#I;q(#4D&TY)LZr`2v6V|kpM8t)hRdNyxWsq`V zyb;eZH_S?KrNTWOwP8MxP>nbHHo3OX>U}QwS9Wx0&kred;f-}K+^mL-s7Ttjh$+_2 zZ+aB4FD9PIJvwj?PXL;n&T_@9A`yaaKVSJ2b14ys+PzHIAV?55xIVU1NrOQ;cX&4= z=wNDTJxS9Vdb_X(ff&&FHTqxL zb&9ixIFvOKF#|n0aH<&4Y*+P(G@xT##VPOB)w<}6V<{WQp?=t6_8`CLB{r^jr8BJ|E%SnFIFK(vdQm*G zhk49BWP*XGV>Q@lZY%eW)JWJC9RYe@G5PviVb&&U71$`MiaNuqE;uvn1rNK;(g* z7vrnt9jwp3+i_3;IJG<5m*cCI9Sle(RjdLb(TVG86$^q4qj4sX_N92kbIg|tq{+AS zYxwj!2I1%k2uAUdF#C}S9Dj8QixZYBr@PCxUGVLqEALE^+M9u?9C4InC#UB43y(tl zP$M-z0}dB(`P{r=IrF&#E>g@CjH~gm%4C3^T_axzQcM$7MX~cLGK@OWXgZ$a$Dd;e zV3?Vx8W^g&H^@MV;VEbn$`#al7tgr+ZTke@2YgOC#UTl2`pLGgw^aWET{I*onrPG& zck|=RS==41Omxmfk~c} zGSyoNovT{PcZ2OhORN&@VJH94=@~1L&Mv?NjcYa+68|NpT#jR>hgN>#T&8?X{#)at z9%KpqYbnzsEnk0w$>%HQGnjj6Fxbkg@4X(KpaG?p8c-yo4MHa`$C=RS$0y#vXbMJ~ zMNRZZeiY@RJRK6?Mo#~d@mwz)(Ey2kvHakHzIFJ$yA@W zmv=nO(7gJ9pu?`?44qehjp1)^kop^#f~l5|Gx;yEm1-FA>KWtq26iO|7d<^>zehl| zF!gsq-~B$091dia{s=V<3g46Bp@+niP%%0VG?sb3FEvD_rzdZF=vw$0@Dy}8?Vz?n z2ZP|=fbUMzgU1(3_(!R7b2V~-XBt5L=QP9<#ue8ZHmH-ItiTm?f{S^QLPXPjn7!B{}HY+iugRN-tQYtl!NV_4ls%x*dR3(bZ2jcM35d=$8%lZ@fJ~4ooI%;qatg;#(ZF4#yM|uU!VR3%fQFhl|%;)n3p~~5W zGeho2#6T~u=*0-1(Utv1x@=l3Fm4Tu0EAxA$wHm{cQ8R@##yr5m&X#ybt-VhmV|A$ z2?4A#=1A4*rGjrMzHkP7n}*d(#Vw-gI%TsY{9%7Iz>nX=5}uq$B>D00uqf~kq$fpS z!%|J{ZM07l6Ogyz)F<2Ofg{Y4FvoCLzaehANqhr_@fjajkjb|`AO*1W1h5DiGhQ9(MgR=*syx}{{o3snBhd10rdo5dl z_o=4i2#LPHa@D2cd@jyEdFijr@JrQRyAk$4^jLXfFVE-06dVs_K8z5J&d!~-9WV;Z zLu{{q)K>07_!_XRfVAdAgurhcUteAmB~8C!iK;4HSGe;#ViIQJd%yGD09dL>0!`)=3NR$3ZoCkv6s(&$x6>!^zrq^&kXO6fY0~OeiAw;>DDA;<@Ko+7Y)BA$YUf z0BWs0^Q&L6aG1SZ{qY5KFFyrn;30~e+ziMM6OMobVbwuD>)ggdDp&%CJsb*$IQ<{y zfWP4R;rq)h7GasV>Vb+2W^~cx>9N~+FvI{v^1>h+39+0#3H_Ig&CSi534jk5nmr2m z%eFE@&m>-A9Q@(*ehn3wi2`bk1D359`8^$NcqZGmdWn1XK> z&3nWln3_QDqq$_3m-n+Ufvdu}@zf)pLboJSYGa1jwoc~o+WjDBn3VB+Z?YE(4Uj(J z2#dhu5{&Xt1QTWUoTpdxa&c(Sol_pN5TUOI zlpDcW+dErY;L*phI(mX=YTNJS)4@cH+kx3BPb(7(PNiS;=_>D4p0B0-RpM2h{5;7v z8E;DUo;ibXU&EcIJ0{^`OL{Uv_`}k%gzi|=mQ)kK8(E9JQ#%j`DcORNX<=XISc`EW z)Z8qzbHm`tt1y`=d8tYiI&qaWmMd!~LCw{myWYk2a?2Q| z@w`-xtv^Kkayv!u1X4D?jr+Si zjadQRyS?(0Cm+r8(j>d%V9N;-fM5BC=YP@1)BBCdtWw@Dt!-~yLs@p3GA+IB{M+(6 z=72Rl?=E_a0sf5K%WN@Q1g3VPxS&z>N`jOL4`d=EzA} zi^Fu8Q)T>Ft|&VZg>I+I%zf^li%>jYW%r1~y?aLKWkX1uL-ruN?1oJ@nXZG;cTVu? zcg1&1cW+W--$Bw5jUB?$HIHuUT?gs}pvCLpxoM`Rd4N%s0!F4kG%3yFo&*;)H&70t zYOj<$QmdDsi5c1m+Rt@o(m1s9N2fN<@YhCmUTvqZ;S?`trsB@@4#AOmWLA!hy<#jq zRN_%<-U0eLLvtP@Z)mlCv;mz=7~WT9HDxNPCVN_%4V|_whY_ph&0ebQQNcHL^8Hh{ zQN&KdlOd550XXqkI2lavK*AUEU<>eyUg7lzygcX)d*Xh?Z4OHH^{^y_JP|KPfRaA} z*%-}|FeyY6Jdy|{eKCq!@+O@mPF}4nnP&&|V4fNWF|Fw7#S8k&mJ#kob2vdbG1M-z zA!se0k>6oxhS%z>+fI-bT64*v)4A@tx`Tv`fSlk~L+_$ixIE>0D9%8YkXgr#phzra zG)VvPj&a*z%3brD|NAGWGyE6-g4nERUX8+26Io2J7Hn$Kv`Io{8Z~N6%b|i8vU;%$ z5oMG0rv!gO{)V(=GdsAMCB4Gw@p7*R3!3Dc53KQ?Dm6iJ8Gv&@BFA&N6oLxT*f`bN&*%;pRY+B;(h4?}#`z|{ljZ@X zv?)8d4t1c6t;o>tVFZ&f3(OtFL z(oy!S+{x#~uVH&uEk4Re(fz-&5#Nx#uN#(cO-0g@Dm3re)V~hS&|?*w#b1Nk#o6`Q zJ03=GxOnouho8vt<4RmY7q24dw_)R!+ns=u0lMLy&#ZJTz zY=9;d1ga!~AToE_k4VnbDuyi+E%4p1F%Y$3F;Gkm^3i{UHw%Fr=_tAcucRYls5%;GxlVp%3eJ`K zT3g6ug#oFV=9Kpbq=^QWbObC84|jMhQBRgP9%r6xw3touY_b?gI`}2ZI}Gfu{C?&A zEaU7=QVi=6Lx;l_YXS?fxHs;&`SxaBzvldS04`lms$S?L>7v-{3Ov*+G zbAD^wj_C578yC5R9aQh9ibSM5i8K6g1sTeqd8Fbw9!D4fTv7@}8DFkKC`U%dRnVjc zp=@o1PX;Inf>5p^O0F?6P;x_j*L~HiXZ9Y~TBtg*W~b>@@9He6;ww0Bow?TdPJXQR zC)gUD*WP;jN@T+OA%X;Qa&3g+Y8*0?~GWndS69;2_e@RhkxdleVIga zgF0yP^aL&ImyO$J&=fo?Qzr#`Ig*8ugwhnisr+Sm5u?nO)sg|FvXtp3n$056AH&wT zo=yir%@ML6N`*AiQ6)y-#GYuo_m+G;kM%fux&yq&-;)jWaSsIXo&8)~T6@oa$J}t@ z!=DbcKnRfm;*+&4e!{ky+BcGGRm)vA+wiWNr%%o-pMGSqqdWF@-A94C&p-4J-v`#= zJH~ycSE9h0S_8=!*hUAl``r*xdCZ=NB+L;)q#jV+y^yX@A)U6Gu++%yAb;ct%f(W8 zB#H>+tPt0^^1iM3<>Tu5tp)=zcBn$XRijW9&mg0aMsNkPRn+LINK)~HlfS;|`3thMNVyse_91Df*y(P!@Xf8v?CkQwc2;&t^h0UN$(>h+i~?*jM`+{xx>empZ?BJkYunr;~l%5L&EWdz{nnEJyBi!>+9F!To!ZZVWe0>&O#J2~U_W&KJ z;9GgcV#y$JRie610hJYSs~tD6W!M?mg~_bzZ#J2l@t5$chGH1#RQOql?+FZvl3i3)dF`C9V2G52H35D@5r#>aQvz7!dn$_o06DIHnaNiuS!o2VHnG;jOnK*V z33xE^0(<;oy8D0ZEW!uxomvDWFLK`!%>8-TsLGwgWW@ujKf?3zf0>X!01@{C;unoL za`9jy6c@1EBf%|vSTfpc7>#zKEBAreoyT(vlJb!ZJ5u zvD{mdQX&b{B}P&%V?Z@K;7EtF#_x*owW%8t-xU0t;s(!J4|fKgF<+Yd(qVv|`9J&| zu%ToWQaGSPfw-5CZeu@}8GRYUm)QApW(nix%`b~9mf9UZP1#jWiD#e4{)oeARlUQz zfOz}SHBV4_rVrst?E=PBp)Gul9=cB)YRcXPplpLA<4ea~o15-{eOmpwW;O`Gt7XKb z88puSHI8cy4Ob`mj5u**#Kj-2ysL>xor&e?xzbHH#(X z%NTxw-U2BU?R}l#^>~X_=n30Pk+1kaghwj)2F>IzBgafJg9=g&OC#BZ4L#hdVxBS}!Md*QZ(2Ok`CY&Psu?>}3P#Z>HN+n|n;%wNaOM=J`MjMV?sLjHv>g4Ovo`a!lxV^}^Q8N9RevSdYgZWeXZjf#XZ7 zt?HHt`6WY6C%NC?V&v%AWixfb=L0sPEl^|S#V549z>V|#bewe+Z9^1#3M1X=Wu(v% z>Y9{&T~=GI_Cw0o1a_-N`m2(sWRvQRo8=*^ULxFlV%5ezB9-9#4lo!_-7!0l+1xfe z0|f0O1ZqoTw%{>KawO{EP~QCp1xmbY^+AVnSezDtZ_q zipHY6-A-1{55cf&}$0CY+w_$#$ z$cl$pIEqh#<9iDFQZBe!8n}%;)WH&g800@tLcbR>;B6hO<2Hs&Uw(M`@jcvCyyGEu z+W>=y55Q|K60t-g${!kF0}nA+CStHs;(LD-jP4DkDCz%x%lh^sI3Ms7~=>Npi2;$A0%ort|_C>~^CiC75)?5Q@F- z7;~rmDp(fnQ%T61r{rx?B9+T)$QWJAXENa`E!;GrGm)-f8ufECMgQwbD?oj5DlmcXgh7Ee}2p8l(XJ&_k@fpM`{Uhw>xI2*;=nVbS7GAc-C%!6{<41$%g~ zJ8$j{@>GbGf^d$2<}^|4lcAz&MufXTJIOYqbub~=8{!2uP2+@-N8da{Pj!+~s<5_3 zxD?fl^0SCH=omV!mRc~Z7)Kh3Ek_#?+#QC%v4UXPn%4~dq4g}Q3`W#dzTpN+Qhugi z_2-pE%g>_KD&;RFQOxDwE19~Zf<0Ps=XERFc*x3vW~`D7zV@~0aEr!TYWo%K45zPHseizQ*A1!BMLYb5d3&LV8Nb=31z|jhhRl^99xecZw89>>*NEYDQP;QnFF;~9RE6lt>IRwc5 zN`{PF2^UgP{e^NZ^E#7O*zl3y-@yH$u!{U=SpIK`+i$0QJZ&k89kg}3+sPVn(~-50 zxcQR~HW(;%Qjw$9j*eEr;ccs46tKqci3cAP-$UrMd(hs;bA72nGEaPGYgGweqv=j2|h=4Yy?e8DtyW{rzN%7-DL zeu!`n9vCGd#M{X50ytL?nKbNha+{=s(fx(pgZ$zH44$hHgj?}7C=Qd-@ZRDPag2*x zC>KC(;}0w)h<-rLdxe-aKmWxRg0SB$fh`Ky6Z! zI7T(M)EQ~r)FJ@mj09YMLcT9kscX>B_j%aE&O>XdywR=O)~`j5 zb7=Y1dU1(NamH2Al+~vzsew>OsXfNGC)q8T&G%DO=tKMOxgX{?*aWn;(~BB#eN@lP zskdL(V}K^jPQ zDxoq{ScPh=pwtRi%dO~zRTPw4!T0#KUjBb^|1%j->Z7$+8%ZAEbE9;?2+;Az128ah z+4Bj>@R&COf}*BOFpTdLMplFl$jpt6#+K*^k%@^KT!?br&GWE(txKFkz1PL32wkCC zI{M+zkCN%{_;Emn;!&vA$ypBks#J!W^BzCLC>j8S=y(J4KBUujgDe^J=+>DRv=)qP z+xV{TV6u{7OJ1w2WxpdFNFe~qEWh(7tdJ=OkfJ?f4Rl%^{4-ZEq|KmI7{94qK-v~8 zk~2if>g2yfoPwS-K~GydJu-!~U0UwisgF!MB~$wj%XZjB!N*^?SWUeF2EBsanp`*0 zb{}8az^u;BsH4E0g=iVM-~So594*Z-?6DZkSSO7(VmeVZO(>UI_q8?l*`fyDCpUN!2;v}`Ef#z3|g}th-N#7xhaRB;5Ui2 z+q>2vQd!exHG)pX#bzk_3br%Sp+&<-kiCSzaRv6|93vcnD5VkBfcSRGb2PPr*i%(B zRfC$b{h={E7;kKWmD$K)n0wz)ZY(yg?w`396hQA?l}ga8?%%k5 ztb2&gh`P3F%BT$98%IK2TkM|`N!mgdZEdj!roNV3lltDJT8UQMzgam?&;tA1F=sc+BTAiX3XU;g+vtI%& z@NPrhV|J83!CbVxnl;5!wJ(7d*wcI%Hu@M>C?lfJ7O(|v6i0rN0KV1ot43dSIT+zw zM(&gKqgxL_-c>SeWns;u!Do5N%8=zQOewiCAB_&b{_S;s_U`iQO6UzkZY*DvTZ=kzX_7$*)f@lEi#4P95U8 z^m|gu7k2tQy#69YqNvGh5ub&OwS2?f%mdgMOes28b95(rv-I8{ zV~6rp@4a#QsPtp;w1#5+=A>2$VHS#2yb8Pc>>Mn1;iiNtBAZI*5c(p291iG|s_^2t zx!fMWiwq$KcSZ;P{pVf-Lh^2gXJoTs@Ai5egyh|#BYD*ix@dL?o{?Ed-mH$~JxEAi z^@DCQ{|`DAW4Hk$8H5p$vTSjgJw3SBa<=wVj1WM*LCu16+|U*E!>Tgg#bB#ya${oJ zyTzdDjXjNMRcl|j&XCnmjyuFwwc^sPNNa5)hFmB2$)}~$r;S+b(~D7kXC^}gEJk+$ zAPYqHWUTmKu2zCrK08udnNixwfo(`^Q%hBiv(b>7B_dPQ5MtDvEr$^xSeIui^6L@cOC6 zK6HI%e4Js2s}Xomqu?Oa8|Z#DkdzZs*BY4U7cVk8)O%Y4pd(eitM&(@M&6n=H3Po* zB3r3ki=*>J=1_fOC~>>pu@mVtJ%|SnP$FBnhC}`cN5ZYB2OeN#Q_#1Y2!i|)#CBx~ zqv%TnVS-mi(N>L;40B%;k0f}@!1l~`3gfo~EJT8qsg@Fu5=sezME(w`t-GaLMH{#D zwe|7#Qj2Ieb5nOy_qH(a6rG8#B6p;M#Z-o)5MU(Z<~dgenMuL%%bCFvKfLE*aip0K zcbDA21{XwcYPiq%vScf{5hYa!6e7he&!mb$6c$W5gBGWo@9b>x?TYi(gei{^^kK2I zi(s?Faa1#wK#_!;V|-f1`XYTu+|FacNH`Xu+}w$1GL}yA(el3R1f>_*=N#+cgFQ(x z)Qc}CvioMfOnMW6cmU5JZWU4E!FT{rolGhp&mqbb3{=BDt`fMxpI{HHsi{$M(Tg|( zYNZW0srB$ZJsI!=wB4UzYD@wZBhZrt(DwXv`g*C~GGrU^@!j6B_$YFirXoe8GzQQw zf+esxtU)WB{84k-oZ}n!+#)tL^S16DH^RxR!kof(=wG&*JDojY%77cSBhgFB5_CJb z+ZnMS&ZFN__Lc?T9~$q&$`6#r#Go15G1jdD%E$UfX1=UDjkT?o<@=Er`4cs{IFKbo zbb&NiS@2JTtf>m(V!YV*Hc6tRw+GSw1ERN=vJDLM^dg6mo^(P%n81P!-n4%7YW^^-LmEbYW8gpH$0Xcr2K z5owjHDjA_*V|;5mnyy#Y&KRwqW+m^zus$0eQAVdgq%)B`+F?hZgv-Irjf zdg=tlY4MsmQ;4V%!>4S>H9jzBdVf&Xao#%$!-gb`M)m?jsD zY?EIX3;wP=woEFDqCe;6Ub7z&oGReU5pl$w{35}6$Rda!^nHdAMSezEB*ilk6~IE$ zU!V_Fm~*LO41Rk9J+_^R9xEv{=mDX1P;iX8S=GqW37}k?+ojv zI$%2M4qI%YZth>xxxT59H%_2LgEQjtBbBgM%@<78D$U(Qd05~BN<`BbW3i`rq}N&kRh+&~ZjcUaW$V}PywSgs2YU+U-VpB#74wlk9(|yEY7Y0`du7oUMG?t%WJbzs5@^#C8Gxd$2yViuCWnGkOAyP1rJvW zAxy|eTG7a0@EpRL#( z=4)c>%Z=lFVggx{BN>012hy$ta=_5M3MpQO^64p5C(I!#+D_Re z^qtjr3(~t#pbTW&6Ua&n6FNI~v}Ug>&pLS33-y|rXe$m1kn46i0>i;W8jrc$jsfG=Hn(jbfSsy7Ldl26EMr7N$Z zx|@cwpk4G?{SpJ4ot^|N4oRo<+3AA(S~#Mw`k}`CsuYu%hfmjz0FT2Dud2}+rdQuD z{qQ7}*{pu4Sr5;=xzVDZU3I0w5OlqS>d{v`|BV`wKj&71%>q^=GOl7Fu5^%MIpR44 zVQBeeCyX~1|fK6FDJjBK*y-F zoQv~ZynwSx@x1y1Vn!~PHK2x;xYe8)YCpzHY8xOwv|7?%5wpsqk@UmIbH0?CFhqUL z>P^(=XLH-_J^=V_QL;)+va24Qaq?DAT^j|W}i-TFEw5E}BMl zuv3N?{4^6}vcg}QU2<`4+rxsbTv;O&KJ^quPgKV{$xm0ZNgQSv)hHFAl|jS2(33Ih zoUs-gy|s}+edg1wiE5AFRWNvUi8D-(+#gkTRb^4VbfZ|mZRZZ&X|kJ;IVRu>0!0tx zXv7zVKi`=%l}&tf$FXgXQF-@c`$zZk@=$Ilp5al-+YR$W0#YqlcZ>RN57c!(PfQem8>2_#`vvAHr@ z?HVy?#bPCWmYtukUQac0b-vEETq`nKRXWGy`zn*y&-|`dishXnW6YYT#+0vt1*g@x zD7xrZFuw}BnwC*lbzfKSUfq%j z&>~}SP3|D9NHt3!tjN!mj~Q$U)S#LzL4GAMwpMO|VC$^m9=P8m+g9X~KcFLxjv8}U ztgOp{WFo-tRH41aF!F7|-YQ!SDN0FCe@Km@)}y%aat4rqYQAZTUPDDcC3}_@T4Cgs zA!E?oW*fB-d_XQVN`*UIP4L7b_H@Ss*NNpY%&#^e2IS#0c8J2ao_pZCPebwgH-pEV znqEp6iwQ9SZLIm8u2E>2W&t&^YiIL1a?v&JX*_^{BP0tv2f5bg3<|nt99eBziz)6{ zhcYa!NCR!Qj!p+h0q+i4!i7HRxCK0G%zHRLP|otstZjhMPYZmiLsdOva6L_E^5Dc6 zT);(9@aH@{KQh`kPLYSB-T79YOBP6IJ|ZSsl3Pf}r-V+%0k#JmUKgKs0aB65kgOTB&Ggb4pXfUg_+N;ZC>j6(K$(1^m;MO~rbX!} zE70F*92GT(s|uO|)d$e?fa^ECQbCXVdhf0(KoDg9^~LMP&&(COs+!dz&@utxc&%=HRTb!nx)fH||{{D}?gFJS@StQNqk9EDJ^wdgOjX57fUe$aph zArU1ozLB3-Hr84Y-XajntCNUUoJwObP`gd8T`INvD(gtM$Dtj=s2Xwxxj0}%ff&B= z?I@HAm>*&Dce+~;0QFIw!w8{r*2K+xe2I>t&A=p=w7EJK8gJ5LduPSHv|%$E0d69NXV%vU3U=SI zaccv`xo$k*zK`RJYKIIS0prBu)nAXGh|}Kk?$H7_?Pjh_m_S6?bTI^f!`^Ld?|v4E zPz8zAXPV@$_>MoyBolj4IL(-3DU;GB>EwI2vb_fwa#s@1gVd?ZT{yFb7^8fWZ3#5m zTTm17I<0D^Mm3xeVdM@;QxX z?R1mRf%kT1Ku8M)oNQw5#FTU3`8!)Lp}dG>&Qp_k-I~lwSm$kk5Tq z@*)-vrpd8p_5~GHr(z;B=i^I)wKT(osW#AawNWuLphn;d15(HfyBcEJ7J>~ri>?xK za$kXS2DYO~P+rsQUw zATQ{g*0ho;yTeXlrSxiD@}d$MTMV8!eIJ7nNtA{K`(ks^8g~RU>&4~c_GFcZB{KNpr5mDIZBniPFL5^&X%cQ_Zecc z3{3iL@9Nt|1b&~6WfLR<2ns3-nTF}1#_a@e0X!9kx_5fUe1b1^f7F?o+WXj=uI+0n zziJ!nL;(-cqGE})_zDHa%-Jkzk#dqMFKQB`C`2f+s}}`A0EGkT)MqIFcht+IJkcTb z+c~VK7qomRF_0-Exd(Dt&iP%Vxicjb4am#e$Mara_BM&V2aywOzLR?#`$8@&UB=i* zA7Rh>6Jt}q*9mP>J~xvi2->5TBB(^@c`XkRE}xbL$P6s_osx*)kD9CxY#l-pQi(DV z#2>ZZ1-`)7gbG2v89H!t`$I5L)|Eyo0%$$ZW7X1mo~RE&8^S$rPx^>u96nbHNn|;| zo+`YvY)0rHRV&a_9DRPDtYf<#hB$c!ez8_Cd(lyH^-}h;TXhCTnTRG3Xmy0zHBZ|T@_D<#uYxdkUHY&ExeiR=}Rz;?T*Fxo%Jz2)vg z`^-Lq&#d@Q#b-R=GYjOxt|#-0=JYyUac5~(1PZs-Ed_P2o`Il5U|%l=N(6D~OO|q3 zZZCV(IzrW-Mv8+286H5!6M>*CP?quX=vama_qB{}gMk7<)qgqh3AiD!9V6Smrm2EA z8JW5?RBeee&}si%sIjU*sxrv}t;n~lyB5&OkZ`n8DHvGS*3GBM;HnBbnj2!iwk1mU zedeF_4oVPd4W!C>hNIeWWxr;4o>8%=+6PTG1kK-Ww4DIU9iHiGo}cv&>JCp`FF6yo z5Yh&@-631TNd!uBm< zQD=tua0US-(G1`BZ|t`>v$QWq*qUT2l0^o;n}5sxZ6C`-lW4B12ZB^xa2UV$g?@6`@RkCHU zVm(NB_t%)>fy4OwMp7o_U6)9q0PA!h;RF0`L|Zj692;tg-V=cMQPBt6;?KVn>I)iEsHVdve`^hziOfR)e9b0$wQ3ATn16$0ByM8s5-w z+<;x6SdY}uU;rlcRtS%+#w=(~+o}lQ)kbvZJpzG1D91ZL zdIhSoJ>A9cMyf`8)Fvp@2Q8g`H}b>d@fklZ`jpQ#;*I}(IxQI;qn^fUtR;-%+C%bo zm`0b&cB%P#Cf_Vr^AjwDN)$-dg+i_j>AHfb>t$EYU$9_^yb=XYs*gg|`G=P&69B1ttl4l|er= z!X?gU$e%@d9&pYKrBIFJP~pNjjWjQF#VS!^eNgnm02CvRhj-+?oxPvjzUdKe55R2YL@vBK`wCgFE;Wm$R%E zE3JnwHy>i-7qXbs?SLeQ&PXfJ4nJxGs*8V$pz3qJ#yU=Y6zjOq6x{?fz@l|mMXt$T zXSPn}v~>EdX>LjT%Gj!J|Ah68jrUFno)>JN*xS?B!XLhrr7iwWi;G*FopJ2A4Ou7D zyuVf5ou=v4OKNhOiDA?XY9qWDmH%8%d*2=Er4#Ufx%*P7-;Iu*_G{^b6CaX*qyZ#%F~@O5I_ zRfKf?Rm3WH`_)je2iira%VOd3|Cs)gR5tInjQM#!luJkQJd$_jY!Pmb^rX8B zMu5P)Q3GAp9+x@DZ9yk0Nb`s-=gNoqSZsH(T;hXs;sg*ppns`M%2!CSyte>_hqAo* z?YzC7A<>I9hgtXw$ykmL0HE_;aW<{aIjdG@)fedF?{IM3R-amy%*>P#+&Pqwqk=Tx zs`PQ@@R*FlBLi1wBMe`6+?;~r1J}3Oc_cRpb{dgXkbCE*N?AM$d4cn&VKjzO%M>YM zslu6ddXCpZ_Zl#v#b?pz1X=E@qrSG21@4h=2?p>$N#_za;sTG{^{Eqt!sNQ||u}>(so^`*_>nOvj zwgFR1D|>YrYs>CTKqI_JTzS1|8TW)eQ7`$M<B5kBIWCJdE=~A`=Ucf_JN7oC!UTocI+f3CM zu6UR^k#H3N&O|mB&*H%*7L7%b`kb$Pjd@Xd2ccAHpoi0L{=19djD+zAoJKy6g>%^q zf{;<~;w5QnO|8>*dSmT{N`ck9s^<*T%29x78|LsvicU1*8%U1DO(EO?l0@xr@38LT zHbNpWQ?4xzm-TkhND*19 zpkvl6CJj(UR!==S*wKn$15kkMS9bnsIrGnCw}aRAZB1HnD;M%z z*5m4G&Qyx=f#|9VoSMs3Pkcfi+B2F(|Ii7(S>Bj8Ti#bCJA(Uz4jy#DbQa;Rh&L5U z^Z4$e{?ST(m`;0lx0^eCEadUPNIa7WAe+oURLjL#GK%E#kf$|WBtoVZ9pBG>Ifp`gJKT8J(#`(a+?$O~v9*?IYNqR03NcefdKGF=t(_v+` zBo1yq)LrCMzTM~J-ImRp#TG8M>}dgL7Gmm{IEXYG%cR2%k2gLm__N~iiNnYFeHAVQ zhOA*Vr@@+xcxBvtZd}5?ApaJj z%*bL7f(`OrkL5~FN^{HN2<7kDF92wYdc!E(0Mo6?J_zRm_OX4}F0yGL3TV67wVO(w z5(lG*Xa-ghj)xN{y!Q(LpQsjN^~5Fw`>o=a0;ynH^``v2nv(&830Ke+aP#e7VKOSf zZT}HzFB8x?p$8$uHfqiJk(7z=nf^43u0POt3y`(CZfmqo~ci>Fwz$yXw92& z$B?^;vMU@_J}-HYW-1O@|3cPg#|E23XPS3p0nGwjTl>_O`@={2w(@PMN4BX$^(Zz5 zcSxrbex?lX%-}C4*FV`3OuiC@YlgSok0d)@1mbwzyxX*GEm@kj@7bx{fKe=H-C_C1 zlD*5->22pe=pVg4-s|hj#Cv()lMg=lWFPO1XZldEz}MTIM)`tvZ>Ouv&YeT;#a)6M z%FnK1`;e1QT_dESOmUF%+iS`XE{(-$;$1mtp$Xm-?{@imFiM6-d9wS#+xxEN@t!_s zZyckfX%z3Et8DMb?fo&tLd&2j!*l)VG7|X3dtH4!aq7Z}F1oM1{q}3SQA@$u9q-|Z zo?d4!DzGKW>HZwgm0Q%!*Lbg! zy1%yX_6NJ4q<76wsRpmBhq`Z1cjmgd+$`S?t+U|GLNB)77nai^N?or1cs(y5Mr2imCIi7+&!R-mF-)kqp z-WIta9lHk#_>A=sv`W)M=#VyxW*3qnDIdI$b=v~1NP3#@9trg2x%{3NnapZMz~wXU zciki_zp4)`s_zPeQ<6NS{(Zv~Y5(4G5~#H9IrOrTC9u zx$CAC;)`6I`@+R<2ySEz|LM&?xB_RFq79Hjpk!FNA!uJjLdrsg1E6_{Yi?cr4KKI0 zcXxD{DMLaK*%RKpYrAl|f_6UMHL|O}i9h4}_G)nrs=UbQH_6yV_LiDup_-i5xHawI zFMJwRjcA!?DYSDF!J^3vzhUWY9C-yn+VOO?+6n{f)k~Py?F>TGaV7%!WQva+VA;G{ zmCcW9yDxrLdD!nIPk0jNA`Fp6Qm99iy8noDiNT#s1OW37f;OO_;zWP**fvVo3V3rQrX z5Vo_u14Ix4W{)saDRfjv?Cx_8ixDg)vN3Dbho&AAm4WeXtFx`Uo1cF^YNaEGkoveG zTcC>B-rU&003QK(1CE1KsW?2Ml|#0x6@szcZEa>VQe-UXT14vD#gG3ZI;U= z)r#m;jz$|;X}m=0>}A<6u&~W#w~^Jzo(ZBl#{p#2Wd5xzXtO(EcCtlWxp1CmUt(Q7 z%;B;`kj{eew%Ifv9b)~Z;WVh#2==t!%*RZ~B^I_IHJjUM2_kA!UHR40ywx=H3#06- zuPng-FIFBz%L~Uzv4a%{*vS672ksI4r1-IGN9@9r4c#vg!Wz96jjYd@0U#ur{6 zUq&^HgH*FP0#^tm<$Wj;u%6)?H{@k_@li2^pF;iPc!cU72b12IhqQE7I>w?&q-u^4 z75)!@q5pjo&D%eDgtK2xNYT0QW_^!@(nZPOwDfN(%*ep>XgDAtUo(SSTV^<03}5ak z!^H8Yuni_KI^Ey`N?h$p($#TN(EkWiebYrpGAn(m#xxuBw5_uM=44N5s9kZj2Bfk4 z+S@ju-Em4)_I6){H+`)qJ}&9-cOCnVn%Jm}t$)y=^1#dj=~|7OM@?*0#@0dfdKajj zjSAXzQ#Sw*`ZO$zGGFqt!XXWOlby0n*5{Y;r|YF}g5@p)XdkxmZ!(z|UVgbCb3Ppx zQbFM_FOfN%u}`yQ`T9xQDb&nEG!XTbC)9tQeo?q}$r7u=xqR2;9GPXcAvqd8?@7ub zO%C-ctNA;A4t#>_Jgt+uPD?jE@i~u4sB#^0rXYZhj12av$X}v701ZP zVe%MyrzxaL{jsuB2TAG9D6AK;u1X1WdEAIbxgcV_VOphQqzd!j#tDmr!r=}YfzU+_ zLhYX%9GYZ=S>@6j!_JRHU}nf?qlNC0kSarQ3)lfSJ*Moae>}KtE8Ve!9bUDAuHCXU z^k56My1lJoD-BzbZX2fQXs<72p=mR#Gn=T%RL4wR^!u-qO`R}Kwt!u>VR%gvHSJMG z7hb1Fs&=8IU@cqUYHpyOzNR#Sis*tU7VmAxKU94|dx7j#eCuB{ZGCLL+bC8p$Zc4fC7xsL)-`p z2Ld5FG)g#;fj>lRM+s^(29WE8-~iI6d_E7YtstBbp#${dYt z)Q7ZRXCowYN$qCh1~kU)qK1TdzX*vd1j;+*qcVubiTO(5xm$Q9^Ndw_R7`UUe?&gX z9_`WJv&-~6 z)Wd8FHF(Uebj5?KS8P~GZ7q&wR})1xBbKS~)|3*QA(0mF=wX>&bBfEfR(o41^7j@} zLg+4G40AO+eunoGXMyyCmdY4xDT9#TC;yV3#S9KBJ%3EQ?Kb7z3(+Pc%0*^F)H6vV zBW7kvAw-Ef0TEO=QkSR;!2=Z!#sxOb8+FmUwS`%q1eA|S0q0TadVto!HxIyg@GYh} z3V}ZsIyr)&SE&~eXT%=VA!@)u@%rl*5Vr?(Uock9Nh=@?$Q^`P-iK`6{w#6gCa@{? zb2ddmu?VZt*9E`pn<&8(g*#h>E1?xlifN)R(r<$rJd3rej}#N)T$uMd0OBLT&nToG z3XUMWeS8XTXoELeJO9UuL2e9U{4k=;a6p5;-e5^Kh>?efjl9Ub$k$tN=nX^XV{u-D z_MRIQHm~yLJ01jhf%JPeI(l0LXC}Tf(iW%U%w%otoHG+>o&XJpP|cWUHlB&v+gN2O zj&(t4WUH$)F3=>+IbWAHl*twN2BW;4lTa=+n5gm35t z(8XDkO`OpVP=iW9M18HOKO3?pouD-9;zD+w#DJ>MMH+@JyZn2&wju+)dpWCz{9{Dw z!+UY(UirsJE~j=GK|apT(IX>cJhvv^7WY$aje0*(|Hs5XPa9?aOGxTJGK0Kjf)|B; z|76OOg{n`Du;U~2RZlkH4<~yxQ~ww04pp})>K8apM*AsoxNSZwjMiA*o;)=r;wEj8 zg1Y951=XdQ4{o{;6d%zQT zLG!pXig@NqzT2FfvJ=~PAIh%fo737C)sKBm{Q?iRr#d=Av@?{70AaKTgweZe5Si|? z_xfm`w>wbC4GsBT+feOW8>BU%+Eingn*OSI6+>l~RkoT23IpBC6TZ}c#JEH8XqX<-T9m^6z%wgq$-ei-8}Ai&;&OJQ zVAGiMVj0*nKA716kJpGuED#!)3J6x=zlYdl3Mr<&P!L=52Pc`Lg6X)E>QOVgeO63UnJfbG8cJ9i=f7> zy^#YF#-}Xjw4U>Q$?oWwMxNa$4FJ8nXo4borJ7mBM){JGp`bjTyI9_0EEg<*xl9MS z0>|Y7G!`nHJT6ogH;T+i0w2B5vr$F*k!q#2?2d}-7{zuJWmt6bnxC>G6`f`DD5em^ zY|u{soWu0!-m`Pz>138LDn!kN(D~YU5tv`@97)*%?53OwCwETDKa6>C70(Nru&Df| zpi4pQ7&t|>9+x@muR#EZxq^1Z3E(srXMe-r605AYFrmE;sVU1Lf{#nBN1aiY&GyP> znPFu^snTZ6QPg>p`iU!r-w$*AB91bVdQ^y8u_d!zjG01eU}n`s#u_*xPzJwC8u zcP9cp6hkC|ZfkdQfc}PMPas~?PoIA|$7m)^2BszzcH)g+;l%wD`_&cU^XW~_MGM%C z`G(9@#BOugT~2BR>8CYC7ar2iR?lVXr>I(fLs6;~wsL-YhO4639V=~?zpyIaF7-9w zG1RXx^%L4BnD!OQmJtvKcps=*{Q#p@AO{wch}GL_Md;pT4!V+b`A-ONWfeapP8%ZW z5m>TV_;I>*KLJtEJ^E#A2~nscLH9mQmUNNJ&m%nU4!hUR>vsU!2^qwQ1+7?CCN&4`Ge03uA(I4|t(|>&b==p*i0Wu-Z@gMqr$I@44 zYu#D*RN*mWU@&g0Qk-$GFzvT(-nR=KqkX1`I9L0O`dLy{x73WpHqe`2^bY(H+$ZtQJ76y76`bF> z2&HySNocY7`vV$IJ3)pmD~C`5iB61W!RF2Sn*R2w*QzLD%%IO0Hn^LU#G( z>_W;eeSYriNL{Kj3LNWU7?I020+~@=y!%FY>Y?0RAc`C>&3?p zS8XDwh3)rxD3EVX@Kx`eSxD~&lv{z8_a?D-y1N-_rytn=>!(Dy(#zN)%9`9Qb_6{yAzP9I zJw5&O$0_n`a{p7j9{6jRx|(5_8sgc9DjvR@qi643TD5?xhszYam*GvT8`iCX<>Ms* z6#U~ZvJuA~^KPo$i@D=s9~*UY#1rRr45x;{8!S4_D>CRFZf*!LNYYW{9&+OW2xpNr z$)&&1AzWpUzEC&z$+H~q!KW8XL}ZK#TFA1Tx`7i0=RsHASyi7qtGFaLa{O>rYE9_F z@lvrX^fj7qXN+Z+%Z%)Di2|xES9`&JFjU-uxzmEI$BzE(?$*AHiGlpglQ)omg@dW! zF8pokxMI{*f$)))Qyk2M+#ZOhpWwql?jkOiO`p7uctT|DmiyS+wY0XjZSmk*idMPG zRmL+4ieK3sC`lPZ)l=4m*d2;rO_z5dX(ryDaDpdPU~1mw9<8I8d{xe%MPV&d?MEf8(mC zn}Sxzpu+qZw|a-`H`N3vqW77b<@oF5 zcSxBY=4tI&cP5v{wfzmi2^(o=IuA3h>g3*Bfqnc=#n)&u zHTtNx%^To>-w})z{}HtqFbG^mn0ltt4jF|Bs0OHx6*O$4eZDSJppmNodyf)q$1LH7 zYJyq9q!{gSnFjj0y7~gN@`Tb>@cg{yDBKk=b@@PXwE^2fH%#PO(YK(n{Yp?Hp)%`8 z)VQz{p)T)8-x7^7!{8Mp`y_e+SxRLx3F)88KBB$boX5XCBK zXsBg0-=M*{J<3TtM6GCJy^N}7D40TAg9*piQihXc8qRaf47N!Hniy-2|HVit;7Ph* zE>uzt!3|-5{k@9QZt>f~)E@D5h7z>BgY5bdVf#os2t$J%!Hxjk`y{#HHqv6+;H($i zRrj&wyLqNqJp{G+|;^=Jar#o!yo_(Cy;=|!R-Ok5eb8uiUKg`?r;ex<1Hlafky!+Wes`i zF=9PH+QL9$glI>AjEoZWHyTTI#CeiRpw(@oZGN(@lBDh;9ZtW~?uG$_RP| zwd&Y-*2*_=3$Sp5n8eqe^0Em}BW+e2*r%ZB+hT1=f!FL3OuO&`6e+xXgVy5GmX%Di^{G_gErtim0e?B+UKE?%@D}cIC?a;%Zy|uV3 zHfoEtMob<2!Wy=&$+iylS!@ocovwkG3ezbm5-ce{f`ovjW@i-hnLnc+!pg1NL-3tF zSh?}+%Iy#^0k4cmMN}QZ&e-HQ|w0{69CzbO>j7VPu zjvx+4fLe%4LD=4@t4#`J$?s_y=txZ2ofT|nJd|t6V zym<2xD>Zvbi`U*9Y@vhSBHG7lh^v=)lhznI)5@$4a4WQeIO7X2mXK{(sL^WOe1jKy zr#Ovyq4}kKg^`&OH@Y`>a&EF(Jo;($#=^)vk z9DKy~FkPx%OtfE2d|v4uCGRrQ4JR;ayXnk3Q>qJwDlx9TsIWTAh9)e(i{*mDYBC&@ z?B7XK%F@ph8z2RAN-^y*b>0^9>K8ncHqBIZor{8lr_uQb z1nZ#o);{GV^TKzaF};8to`HVsBV#Z+rW>Dnj7|Id#N`w7l+0l9NjQNkg(p|ZuHz_U zXlOocs9_^Crdrk@(4l&J?U5sqi z{uwL0+lCe19Z2+eyXhUMx2@6zL=XeC(WnoxOsLNt_$NREU}k_ajSLF>OaoC8?oM`g zM`#oW0f-OU+i6P~aW{nPJ5BWED@p$)a{F`#262Kz@kO!cZ z%FG;@c51XwVt3TtOm4lcsI<~^&m$!x80rYq&Rt~Lqfp}f0YB`eT^V!ci;De$m1~#s zNZHCQ_I=c`lX!X3q8Ba&A(>)TB(Zq7YB$Jmv}Y!HZBTCpDC3<6iBNqHGx{*w+lU0FmuHc90Uww4b#1<=MaW}MdT1pZ)R{pt z|E#`r(h~9K!MjvY5K=pDa%uOE)}TL*K?|neNh5}yrfQjgP>i~Ayly>)W9Z*<(osFU zZe3B4Nz|P)@)mQY=RjJ>K`YYu*0mbx7SK&;b=AaYm76XnEe&gY4PCT3?HP#o(l$ma z!&SZOz{c{B^xaAIr9x)Y{%IlEWR2HY*HdeKjlDiewb{oB(ri(f8%4{x--mibKAv3W zO?Gr5x(8v5|Ii=*k!;@k=)kBz06fMXsUKQFpP0vuJs)=?x!YX~W~R++cR8r`_fH_5 zy|E7}qQR?da5x);&``3MWM*+^oPjtjj9)@+H>i2EdvOMz)MfFG_I1T@h}q+e*B;ri z1L5Q}gWD;|xbD!saI1C@<4*PAozw5J&ti|=%lFt8 z`F$k?9gh*{D?@79+$SMzFrB7O-NLt<9rSnREdAZjvfFJx+tavt75&*PNq+}Uxq!Ws zVnvuwviC31`j!8}Mi$Z{JCf{5&5k6ytFt4?w%J0|b`UPY)EBO@!kF~7!9hr0DXjS6 zRrouX8@ekJYoR=I#K5wlxRC0%_>D6an@als4OGeKQeMZTe6{cCB6Pz_<(`NP8;pC_XWUT_?TP{ z`JKVIwsW+`RHu@0Vhw1kl!q3QR8LpBJ4x^S1*zMytWW3(%Npxe(Rs5@wtVC^@5v_NWN6_JhuG@B+fw;(AjYUkIx8F2#7ZjkB5_?t6e<fek?S(scaS*r7%(8e%44t)ihOUwZUCPuj~V_ey>3Nb;b zIx%Y8^B~t{#BJ*CE2lkI&q2wg&HPxzVO$9E!SM{0cIqE+M}kEXzBpZ?DrEFV=qi1_ z&N%H%1p1(>%=_)hj+fLlwu^#@Qe(et*EjB_BrpzwJLU{Gy5jHnQ{;dIy24=KyaR=o6RjKlsRTY$N1v?|v!i~EoM2@4Jw;z| z@sEfj*&2b73VQOJULkvT4Lr#XQx+oHY6QxrGq+){#bCNf!xMeHbNxNbR?ru&CU5a< zrA}AON*}qs$eLXK1o=7)s84ENLr?sTtv1?fYwX}%yaVN+-DaN&`AcGO0`VX%T zJD>m2P8tf3-J1p;W{>hd%A7wJ@|~txQ<<}oKUb)e@~tc2+%lL{LH?}etd#dRLl*o_ zF{}r%bMx&E>|Ddd;7Gl5GxsR*?w$nFo}_AA*Rr7=b`u;0ZJsvd{=$7BoEg}C@jyHX z_s3lAux`{(sivcy>Lk0o+nf5=Ol7C5weoc-SaM256`>Ux)l_I>`$nOk?%&*=BWoH=<;wb7^9nc zcV?liZ3`6;%=x@d>P3Vu>ZPN=WP4))e7|7S7jp(_#Q;3})tT!QOPgH;j#?8opq>K+UG*|z3AJ`$4{MOu>K_^uC-L}b9$ORvNd zu=Sd|niEvJOGAaGYWW@W0YM|edV(=JBlAs|+*Ga z=x^2U5ogFB6SErcz%*|$k%%{d5&`tiQqs{z(vDD*!$eo!r28d&^j~q%B${075nO1p zu8&}-m)@;#*MD{=e4H%!HnBCWcQqwxQ^GZn?4`fJY{a^fDI~_I^R*=5;b&k#)`Nw) z@YpuaY~z40c{y{v;%IZbct)>1>O$B`{Y38bcJj{)ieR83!3p@O8x?L+`VqkACyBV?ZLuJY27z2ab%_xU{41PsJeH3# z23T~E!e{J9L5Z?E1@I8=rxNugVREk-75OSS&)Xp!bg#Y06e0P7g+e&f)81j1WGH29guTLWZP^Z+Usfy*VasN2ZfhU zYHI*~LBqTJII+i|tvP6>5*3-}a7B+dG+Mzw7}py*^i~s<6COawg~B)KM}E5VAtCo&_(%eB&&@DVWCEnP z*&-9NgpPDC@2-A9K!N};gF@zzt zW5w@Lk;_xAejNYBg_H(Xk-~ncxFg|CpzB&ajy#jLgwv??uzt$=n|siiLOJb3V3Z$= zmq_D4u5$HSGPDRm^Rzb+rpa(H?M~1KpC`>bR&HF*=yJBQp?MX3@Mhw+11Uxvsy7zI+SeXy0}xOqY>5ssJ{^CklE1h|Q<6+XJA++3aCqMy9&GFI z1*m-`Q8yozC2$1TvWweA1_B*$OyJ8oUnFSN4+kH4oE@R;*^1Fy8NIkzSp7S%?=e)% z9s~hvZS8}MUI6grwxSw2bLS#>bwUUnVY@)Y=#i&&b-MWB0>_D%L47^Qoj~*e!N><( zP#6RQ-W&%kneN<4Hf`FP+Rk;6oh_ScXwL=`25=vz$POMRO6$;Yaq}w95L%mR+C+U& zcr{9P)Q}!?q9IsMli$HdvznTHw8h_&9*hMnm+?@5aLoC4(#jJdT zAND$TfKD*LVf@=5*5HbdWATbF;x@IZzw0fq#rG*lPw*AaXa z0Lo9=YQ1(-7RNl*L-Yp>z;rqJvrzD;;sZ(1P`h&*LE1J2Mm*mL8~8@>%8g)k*&ukK z%zFLUS>B^B8#cTN1OeoqD}NX+0c`}i2_{qOx{F15?H*6g6&}%-^EwIdGcgwpC!MXORlR~ zbUm9x*`nV2x2~c$b^)?fi<)Y7p%0<5P^bf{7k%j|!v26XvjzCc7Ga3dAtfAYF{c-~ zmIq1zt5(mR!0|U-G57yEN#kE<0Nuz{{VVV^el!O2VGsqr-^gCSikKSgwXB9G>bYDl zr^iWGBndm83^Wq99C9gO30dhS{eQvgdrjM3?6LZQviE?QpPdbko21DpO-inJ3Kl-? z?CQTha{i`bMa6gxKWd7t(m{AQiFYd)x=P6PJTiq~EVpr-vSw&bZw5 zv0Mt1{y4UXobSBy`|SUttTwzZ4#s<*e3N|3kIA>v*S*SqOOezouj$YcJdI^vqR!@F z;^_q52TQa2-(tvriH!f-f6*hzWB;q10Dt?To^!9+|r(Sv<;EPC&})IfUfr??d|mLD4DO_FH@jI`m>IVfq=~dW1v>*`F3SU z^Q$b&wl0-0n!Lsk%GtIeyNbL1COvW_ua$wcDS3AUr#qy)P}C>051{VBuhlEbWlXpM zbyq{?@5_{H5J0h!Z)x+%&%u|PAaASl$x=93a(|(P+eW^P7TRn$a*cm4_+8zHJAREv z=iGSGsgYwnkbPS~F4L}r1AzCJr|=@Pe7*SZ0sa7m-0!TaWEB*bjf1l~cW(X%vJKYl zSRR9)>kP6N?Z-IFf8p7N^kdw6T>Ll(F8uZ}?yljwK6p;>hTN>> z9-y9>IO9VDF#T;uRyh&-s{och+M|3%ov(lg9c<_kkZZ!R??x$ccZ7m39jxib9|nsi zc+kOyZu}8O^{lX)f-fDc>Bb+%BSb*@{TA&!EVS&xL@J~oM9>`r$&KAemKz{OMGRbL z;Wk=7PMp&&&oRi3sg`Xa%2}O!7cs-^nZa8 zmr(P2NR!*r%o?D9kEy>>Sl7mI)I$HDoev+tzaZ)G6#i5{r`<(5H<;I=Jjw(T)tG7} z)mopDIaaKzPvDB&kGcY0vTFcI^@nj2MD?A;sU}fe-^(Q%giC%JGA@st*5xT4@@G`Y zMt1~70{>qK=uG)~8Pdl)kvEF>%TT@^>F1ZLPwVm&55X|`+c0S3;U(&8$mryZTPuD} z{6VC&A{5t|O!Dw9=wUip(#k8KBZde+*NSV1_MHla|8U>|^5gQ|-)FOtrBGQ}fv_Gb zc2}^$I zLXVq&{_>$;F*?kK+yjV|pm@2S7R{A{86c%??q189IRlY)CI{J$@oX|ZD2($@0`Eqo zkv<~yR-wsO6BClIPuj?Mm^x3LF>SJ!dO{oguuhBr>*ua=V)kF-Jobo|hnsYk{?rXluMFdyh)5az@Ld+uob$~}t#PX+WWIPNu#LN%n!2tPB zn+I3&E*RM)hliK~$he;FEv%=7s$9n58X|>t!D?y_bE0iJkKJ(xo0m_4R!W$Te$jrE zH?c9I{#v_G0XtF$sFf-hkrCoc#LSN2%n$ZOyQx;Y^g|iBT0c51rG;9_J&>c5^N28F z?F^qHbdg=Vbb!)IwwSV0CBg<0j=`B>Fd<#YgPUEEWRkx3Uam0OR(Y-%T)Tol2|9*a z=~j5rI(>Ej2fA{Wb`~jD|D;q@0BMI)GE1q0-1RtOk2oTYz~gu|*awloG5W{Q|yMWgw_v0E4RMesDKzZ`Lhrq#xoLGdhV7ao8 zU>wDm*j-DO+?7|$^(=B&`%-o#=sQcoCv-2+c$f4L6<(K~p$}kGAJcXzp*H-Jt=S0$ z8ni#CmhK9J{jrtr2fR;icHMi8#f`6o?>Q+!wA#8P?FI+WvzHoI|`#6K*AFOFT|NvDgTe>#J_$M z*Iui+&!JE=UWUud=|}ro`G!#Z1%n%(k}61ngEmN0+HPH_txcFrlw*Sh%%7zJRO#xnZ2>WmoPK?RH?hPPRC@wXSUMJ`hD7T!ZAmbIp7bW1 zVOj!+3ef|4y1|>vije>Xd|Lo?mSe^8v$!xLx0>%}Nz$N;BNNui7)SI<$C&PdE5l_Z z*NxrpfsdGPoxc1z>f5jfjdEA6ZRti z0L}rRW9YzPas+rCKS&NkG!$&-`Li32ki!EgLQ1n8Xur2hz~T55;4fv4IA zQS-@9eT>|>fz&-sd||v7M?vYd#?1(PgZjinLh6u){M5q?NW<7&#O%a**g#oJSMA1n z8g|1;5~dqBbq#TDrHzg*Go}+jf0JK#28(Vk$&|yZkX1}CL^{38WVO6W3p%n4p6c;Zc_bSCmm$@y)N2mnZ%UhhmF8}R ztixUbv2`R8xI2bw0NM}!K(> zheFtk;1ct9c!Bx8h?KOcZ%Pu!W1PfMD1vdgOyD^|-&M@OAqi9CIJT)^YBHxhAL-Tx zl`}1v7>$P+=^fV|CA+o%qij;kNJl)xnXJ_B{Q<%+#o}(Ne*b(D!^zxXr<*i1Xfn5e zCm&`Q1{DF@`*rGuY8P>WeRFq5FBPLY1|*%4@P&@y^Z?dQ z{WAk8evilmKnjK>&T|XokpNfb=(4@szl2u6>&0=D*iklEM+09wR}$_%r`>?HRL0j* z3tvlkuaxpCdL!pJ=tsW)Pqi~eZc=X|?J<36O}?SSfu)tbVIfcMv4`C;WOjTOG8*Ii1+a4wJM+z=*IHZwXfJiL?9GC(4AGJgJEL2N0X5!E@I z_st)H(t-z7b+#T|J7ewvg_H*jS8}I+#VEJf6!IMRsatiS(&Ov ziKR|=Dnyf^aHl6pZA0#1!Dka5P7Kjd(%oqdaY&A0^|)Js%SzOSYuIYaR=aC#wbTRl zqNInWJe}cClBPx?yVyhE|8VWJY^R=NYuEy8`#kMpB^nQ=ICFR*i)Cxj5{0z?iuN>d znIcVHE*Q1F(MT8F_$X0p8QFM0iI{qwuyVU25&SQjs!x-LG^?Wh5Wj*~0hDDMD*&rj zzN6l)JbFI)HY1M$oA^)a=aq2MokrKKp;nLEN*(p?I*$2X7p{vz9kF7l`jFiz>gw|L za^mLRU@zXSKHjIelNhQ8e1>i_>YzqU!6xLouqH2*ed@M|AtybZp>UGMH-!e-W*Fa| zLHj07m9YZGdn_1dAWnxC57wYaqti^?R+}6AWpG>>@mQ2E9D&XXYW~6#mKUurJY~^j z1Ue>ekk^lM$sKls#V#H0<1PSjsQhu_c4hYSWcJNOg*K{v6G3Kg1NqrP^0R})w~=UH z#Ol9BvEM>QcaY^nz-QJfqsZ%hjw}ZW7*U(EAy|EPakW+MF#VL}5e5;2h2P8w+~y*` zMf8NYQwY-cp(o^C6{#g>W@kAEMQwqRES7{@1jQ~qKsTg!p&Hn`>FzLb3-4r&jEBLW zf5wOM;ECCsE*6+r*aZ?JIF#Ddm!@!H;2)^K`@sdC<)SNseOv}dF}J$ zr5j1q8L;|n)Ms-#?VPx34zc>INd7~;HD5B2pvOVx9ZW={i2&^KB(Eg-;(Ty=s-GvI zf*DA6(%+7gp`k~RWfKhY*YS|fF( z4b9DZZjPR*19o`mJ!Y88{o-GzY<4*i^T=T+^djnM5b|Wwk2Z84s#&4oo=b!HZ$1l1 zj6GXHS`|;EV%v8!tmLAAfH6-sYR8Qix^$RgQIY{!9)OUFqemgzgMtj;&&JV z@F+G#T6=h*fzhnk9+!qDmWg_znpbdm9s121((JCrx-UmPjVW`sjC?M2`?6_MyGq_2 z&s*Kqa*NH z0zTsDHU#X(q%!mXVO!ycjK#p|fJ)6M`3WxuP6)Tt>8B2#1I`m#i@m-EE=`964h9r= zj`7rmaU3$?XtaaI0x@q0g>nDMCIpWwxg$C6-T-hZG>6vd?~CY2Hj>Wh>3=Y}ITPMQtADYLgPn|K(cD(X0}ii zz{#4hMnN_KTEdEae&NBJp%|<_j!%`(4#8Fnl2L;Z<+AgXo^^@rf^Bp7T=Z2Q8$$&! zW5F)jkB}aUn)w~5b&CZKk5GqQ1o@V*yTG1^4H0)hfZ{e^q%FH45+gmE=@4Jw@Bj89 zeF9=mA6^E+Ne-W9DeF_fWBh9UTu$bq=d9-Q?HKEkA%jqERqj;Mt4iQpl6zr5ZozRl zMW+enXshQa=6c6kJ>kgM%J&v*<%4@3K7a}cDI7&o65#2>jxFX*^niEIQnpZ^9U(4o ztM+To`Zdiv`E-;k(j_}rdIF?HbyP+a;k}aAbSn#9xy;$D$QVN_8P%NXQTm+X&WSJc zq`N|=C~nZd#8j_>`6Iv*sN*uornpmwPT|Rzb79cw#vvXhzv)q;?Net@%gV!+Wl3=P zcNET>eDe}Bt=ErH%lU_Ve-d2n#W~6%Z)Nsth_(87Zmr@$!?7*tP5Ot#4|!|nsGyY5NpBJU=Y&?{IDwI&U&>$*y)}Kl(0@I7Mp!u0bb1y#)#Pezr5hSN)yY~~ zYj0Y|T{>%eYlrOAWol~*x6oEFAA!K-6h@++t^JO#*gTNsFzeje*areJ5m_{1mwk;p zJGrW)OLXZYsM*=t*C!*3F-N-zF7n34&MKt!%O?8XuVJlKeT_8d_-=z6xv{URm2d1q zAKY+NXCorzb`VZ*PqTW=CCqRM0BcjuZc?@$5EsQ zR^cH>U=?7h5tz^Y+_UuEuX*9xX=b`$NTY)Ak)LmPQ%Wf#YLa$9Y zVpxX^b-Ja)1gTH}?oe5sALIGUhtu9P3(VZZZ!;&273}|uIsWDxt)I!pDLV7(%KB(SSL;UFv=vnZ z8-r#CZMAzFV`kb1R)E^UhAlvFg9@r>5WwkcW z1h#{2S2sLg=e#F9U)qIG=h844_0YFC2i`&G8&O2HMyvmjfY2XSmw%2#AEbTi0={f) z4x-uH4@oKi$==Fk+!G(gX1Lz=} zAn1CiJwbBSZ-^s-aD<5FJ^*wU%2~9b-tX|1gY0Qk|0Tz?V+!IZ`^yNYddtSh*d20_^DNzyb5XLv@dH znwXuvTZiHG2p5s!u?Yy}Lf3|Rpt#NQQ+mU$-eLX*Ea{gH9eRn;KGy5$M!I>wyioG) zf6h?Bg{OT{o(#OCiS(T4CopoS{g%AWFtM>DX1EzG*Ytoz&(qw>#tX>bE$h9DILBlQ4+d+UeEb;pKwc z5YM+o+c?x~>=&fI$Gn9{0=ZjAKTh!iIJ(5!oW;Eu;bG5N*)t2hG<^JciIhwu`QD@v z?&6;YM+A*o-Zxb%IM`w;WDA05W9SwlWMz;-u~(IQ-Ic3bmU4F_168`&-OL(=FVH|i zyYsL+$6%0(I4=)7Ll_I3?;TbEn!*~a&CO2w-bLik8;J+plblFTj}}T7=(>i6Ikh*q2FnLI|zaLdM<$ZRa14JQzZXoZyLz<&j;aeZxvKj{X9jmF=f|I|)9(GdHpYg)i z$WbioG<@Ge?E{!^ELH|I_}_jNkLN-krBcXo!5~%OaijvIdU}oYzkJAj1+<&vFptqv zr2iV_3}#_u%LoAR@Eb_xyD}rgSfKIBq-emtl{Zfo8X@7{)LnF6nllepd24)Q13y?T#* zTMymV9pBTujaoLXOV**Z(>7wWwmM86+P#)|Jvd(76i2o{>ZDLDU|HNRr>SRIUsx zO1Uz|<;o}(%5*_TVniIiJQ#91uY8qD!7_1djOl5DLIRgz>FEM?gh2!3>u?VKyNnNS z(%;6+@q6$jh;ZZI#BDG_+tvS2dSj_R&cRsdH{FnG?1heT0rC0$Z9YGp^B4jB@W!SD zt?gRnFxOI3ZEV$$35gpWD2qecw@F)@zpbr})|wrwx@u{$mhZff3;MxD)vh z;!b}ql(T;_Pl+2;7*bdVWH4m+d)tYFN&EC-7 zNEgP)#iy=ji24ggx6o4L4~ zgwxSlfQ_fSB*-CiFpolCqv7Zb4WHePsl>mjld#1+gF0JF27bGrrU z_))^ib%S0&nOI)v{8&yu5_(8YO%-Gvu0lJJIVI{$FEDzIjcmP$KCbU;91t}`jj~HT zn@g|CR1LKgEYBVIk7$5FA}>eWK)8GSl|g7}p~~UUzKz{u<-Lg8pgdZpDq$lW=o>1o_PI8;0He7<{jP8vl$QcyeEF;Fca**H z{L$B;**#7c)~7+awI-*DI?XLEa~NJfvJjs2d&%qO z=jXCZDZB9U%P)jyaZEv^QaHKw_>_!d-F$H2^8j9`G;_0(O7!`Ytu$frH(IRpj(b>Y*#=2_S@>mF}D1~)xL}z<-(u{4vfN34$cm(f>-^DpxS&ka|eLq!8+vO!3i8d zD2_7JOm;eHT^-rBnxH~47}#5pYwvKh@+>dZFh=IQE8>Z_L6PQ_yiut*6pKXZ;2^0R zobngs*>l(M9ey}v@n#prQ{jS+xI%Vbz}SnT@}$k~ak#`=(V3}!K~g`Qv<_6w)l(Wf zBQaj+7(gSu)fw}Ms)8sH5QzXrOwbt}YsB5%CE6d=84w*f!1d${LEHunPS!?r@{?E2 zb{WoGG1)jjv$EvN(*BO}Gyjic0^gOFo`cIq)Dxyo-i>4O8|A$KP;oYY!GigsQaIPz z@TuQqN9VFjuqx{1+hI{zR#eq4vG!PnR+Cv0>PX$j0KEA>OhDX88I@zWo9dX&iw+xr&}oJGT0Ndi8c$W$Ru?N zH*hN+Z6KBU;E3r8^|YnoF~8<}B~+^IX*bK_VSsQdLCU%)+crpp8A& zKr1$YTCF{ZKAy)vnCzk9BEXK4L)AxvCJY4&)oS%Yl+p{NfJ^oytx@pT>Y-z8rJ|k3 z8z9Q3orh-GbCwYjaCaa$}ei40qDJ|ZmIb` z%-WgkqIZ~jCSG~wJMTd1AUl%1a}i7gs#PMXd^$g4iRxDWw~}rk>5aS~=EnVu9%Ikm z%B~e8g~%oTn)bPC@1$0T*<)hV%uFF`j9z&Kpb_86~Ty|)2nH1X+Q z$gEU6QD+EM9vsZ+0bkcO$gb38uLEP3%k9G%;0QRtWrJcUXOcQBr#Ac@^-wNq&4oHm{Ep%a?jw;RC)PYJTL$GW0RX1y@0{Q_xG4Ez<3X)`I{HU%{4~D zGGz?+$~(%Y{S{6@?pe#;fMFN>4a%4qS&&Aadrcp(RBQ8Gai_lo$MD8HOOu}jq6Q?4 z%5pIWKst*V;GG=f^YUM6f|sQpT;5z=xxL`aEkN7Il_)Q;}^c>IXUlpJ&Dnh7? zVm6`D7sOs+O5k##j)oh#S)%KiA5_=24`jPG8SSFKN+eT=lJP}eWi2L;e|aDR$FF_; z0wo<6_08;QL>27xhWnOW$(ft9b5y9cD%4v1zj~&j*8ba)f4$)`+8S^4A>_x4g{JNF zK-Ko!c{J&qOD{!hRJ0Jv_lw(qbYLe=G;;=|Duvqi#LVLP%iyS(F*iFle#jP1u!<}C zL+0=VsKV!Im6MuR7C!HcNeysCf5^FI+1F0-S&c>|>fK0j3ZzqsG^5Xof?u|PnRfn= zAiv3#bkLvllXNKU21VJ{yGg|2Z?W3xUH6i9Clf_;@IP%%`qaH7Y42!pS?HR(iPK_s zn?v;aeqwc4oULx!`a|Mtwb*dr-o}vBVM+KpY3t7j5}l$reXrj@a2SVODZ1uK;!Fj) z6LEUzaRP?Hh(MRNN7|!w!Q;dh_jN~8bnBBOl0tnn5B>25l8kjmIwQ36r{p>9Rn9Q2 z>n#O*Yr1t*qHfESVWT!N7*(hof--XsFHg-1S(DBbqg`0kNPw2Xf_-_uxp_S7onpx- zlC?rfZzq&{R1B#k%e66X*h*h}sV>RKB}F3=l%X|?B?gT~gXJ`}J*8qf;vX!q^?U(v z)!P|#M!a;qj3iv?0O~oTbYI#Qp_d)O0rZdZ^BR_vo_8`e#p65-YOL6hg%rj0ZEVx9 zYbyej?|dL6d2rsv1n5hoh)YdXTgi)hS(1%>(yfq=Nedk>MFB6!(j*-pCrL;^Yl<2z z9suzJa)^QF-PRp&q~r@4SOluU^Qix6wO~5d5tkxiiZGQ3!C--Qy+y zj(%3N+Imkltp$x95dHWEALldjP24<@K3>Fck-88N+0Azm&q(bifb_N3Ft25bivQUH zMF46RQp;R!)WBN{N>eMiMko*W@l0XVf@$4CJPBI_YB(BhLCCEnGe7h9N&+$Kr;5J~ zR0JbR%e4YbW1js6IqZz+l@tKSc||K>S6@x9zM5$zuUt)Exmqn@ue?H|(MSwdXFM&A zr?g!hZBC2bX0y@r&eJYq=eIeE@l!&6&B>ujZekS%`0m0sCHFm6Harh=ps=saYSI3Q`dX5v9ys0oUCBT< z&D>h11bVDp%>mldVY2#KsO6$LbKxDii&gYj?cnc5M4n=-7_k)SMnnmdf>IPdw~LDE zEKb^v8VsyCjhHdGTsqTS_kEJY?d0+M#%g{B0k5$C`P**w(-b+Yd0Gq33*Bd3@TqZi zXU#BNbUdAKT#F$9RZzUJz#mjS&Mn5g&x612|X0MTJNh7=!A;Lclygmj=aq1%j}5YvntCv401NRLC&P>Y4y_dFS&SZ^6s4| zpLDF2kQaK$!>LETdm8Efh6BDwyXixnBo&G#SqkP_J7JsFryK0l>L!cp?_&3FqAPlq z1}bgzhDPAVIHwDbKEHYN=$pI@sP0c#PV7NJUt!9FRm?zCO~5L;M0j4%iSUrR`swrD z)jo~2^=WqAc})8>_ARd7wSNf=RWy|@1imo!uoE5%vIalW{eV~i_Gx@vsVIok=>gMG z2#znQpMp6S4S;^3mvxzV^6EqhXxB_8!0b>TKEPE%-l*i_(Q(dtq<%@m9qy=4zoUz% z!)9Xi!o{eGt%#E>1l1X#iWF@ZD`v&&+0P`jhe zjO47SJ?&3K>BgSUA-0vWt*t{1-N@0(Y|C6yboup?q=C?JqvH?=|)| zLJ$wUcH@)u7G$+X&20`Fb@Dem=}7IyW$b>+?r&IDT}$mwzd33H30Jx;9;N$tZaKuB zMd78jhnDQ5w_aWJ)>H4a_J(%lUp{Yq5$zKOjZpo$Sp$&&r0iNg&w`w&H2-st0oazc z8cy$4^%(bHf-m!9L9c1Mb}U%kK%pPJ;r#IS_#2?p=ce3hWAZ z!sh>y6uUeVzngWrcz0*Qsf5IiWp#C2WPFL#)h$C^Szb4CCc5kD=0{r_&WbmRF*OBW zi+c;o6m4G?T+4U@i4H_kkK|UXwhtMNmgXimr)Mf$de!g_?t_glSA=ow#~9Zo`SCHL znnBvDrzAj=EHn;{#4CR24dl$8=~@>B$|q(cqb6(z1xwtMLYg6F<_$Q|E-g-*&Y+!s z1oA$K^IiI09?Z__!%JanBhR>nmvQ6AI`GS7TDi;wGmd|OV1m5RHwng%T`s*lc((Mg z3vMg9D#mdR!>bI37SG;BMx;pDE+Zfoo>^?g6&QRa>IIq4D)!X`yg$a_&A0(OokD`9 z_HD$~*PLp=(O1Gq18cUp8tMPkKATPVDdZm1zj%qdJ9CcWNeb=L%B(IpmbEz(e+SjH zNUM9ZSk;2#z8_)4O8id+!5@XIVN!D&7R}|Ezax)nVTB`GUqLYL6-4jwar32%zLrKX zVelMbq6?qOahlLwj23O?T$e9S!wiD9SxSdNJdAt4!E|rX>1?~eZ-$|Dz z--=G25bIc2+s5L|BW*KQHeXTWe7oq!xACUDc1CY_y9xc>f;l4Z^5k396F3`;%hSFY zM^X6)Jd+dU;@u)H|3S^9-pNy#AxLRC?vu|Z5DUkqK-{kw;g7|@0>*-Dy z`BQ)O76yKn9+c)#3J-h zuabB)?c&t~pdGSSv&$Z(-*^>rMIXh<*RC-N(QxWf7~jzHvFR zIn5DHI|Wr1E|o~aN&j>?L9uMB6Xm**Kr)phg)2c>pBc27oMr|d@ImG0w~?SvuqewV ztGc8g6Id!gkXF~+#+M}KiF+cliqSEt7#U#mDdJ0Gb2uINWHwqO)fkHP848MhdqYn8 zCUoo|h>j7%_3}s$REj}f;=md5MyWT-VxcHKe4K>BkswU^D2sTJLbrK2dH!7zj6_4Y z##M-N9Ds-ge3yM4-}qBVeo%H&1>DJyu}Qw3K94XRcH>^sfXunXX??et!WO0R3q{+qI#eHun(i zDXmsPJy;jQ-fJ!)hfSj=;Zfoa#B{z;HDS{pC#L zSBf+Kv@{9a0^|W#eF{~EoZ6AoD~uLG&pMMJuyoo=2!9(;0aK}HdecG^Q9Qih@&x*BvYQcG0;Vru1raK5Bw{Cpm&=i9 z>O%cA!R8Q(0|SPd@Hauq{01{=jhG?SsKw>7aCyxi+8VVaT{P)Tvt)#-Z=tE2E`6#r zehFV<6hcZu__Y(#r8k!6fyBjZJvA6Mr_ov$frh{nwy6lS>*%^;IeU~__By%_&IyXS zvnE;sW`x^Zb~$%qT8b``S-8Ah+mG}f0XtQ%yaG6h3Gz#&O0CkHu?u>HZ2}a+y zMgfms0B#NOq4mWQwTiD=bh*%U$$66GM#riU3(g+11jJ%g*2or8?0F4y9N;1>RW11t(zr>%K@8 zEv0i%_eAo0jSLZZqGM{cV#LIRugVZrhH%oZRIeZ^|F1%6h2R(>NXX$G!|f@A)GQW! z7F;VV0go?p1)th0h*1+hnFI#G@QNWaiRe{c>{|uU6ESg|Pi?|7dhZLtvUfiP$?aJ`&@;N_GyAzkF+ZBW4CGXMsL_bD=?{r7^9rgD> zeF~;{WLb+WZ=E(?=Y%iEHJ%NgPY{o^Z(NpqaynrHZWP(z!r>=ICnnQ?7o6;J`pCq$)lZzBVw*1qXTt(0l!ND-AQ>n{n2kLD*aWV;ccNG_@|#OCOua(7icmFl zc9GWh-q#7gS)Mk7PgbhOdJoumDJ*it(z?Wv+$prk|U0L|!Yh zA1-GXLU$ss-_v8K0YB$n%yOJWyb*T@{4SOr9!=E!#N*ftbL817&vrh3(YGXEsrYx` zo%vzRfKx7r7?lr848C5Fw#g~bKh+q1C@X&9 zxSTG$2449|??HsH_>+(4-Z;*EezSA#H2K1l-GzF2vD5C|F`n{Y_-#w&aPs=3w?MBk z3)OkA$+nf#HT7I`$Yr^dms5K#FiQS?PWznV50*1jsNLy()_*sfI+|(ZT+(ih+o2YL zQ9s_Eq>(?9jvjLtYDkoz0`fm9kyK|CsI!h75x!3D3NGfI&k*hNOxbZ8FB1Z9a~|0% znx|Q(6M(e029)&=koEhBY0H{!;Bx$oz>XEEEv>;;a8NJJefDZO%bZ+I&fbVzIA|D2 z20Hn{cA#<(XiM>^YPP1uR4amR_iBxb25b=nY)t)fo~HxqWIMpA;wM;QMy5GhFyO;V zGSz_BN5Y^o@X?qTNh%y=e=DBydF^B8V0iROHvho-pV5`8H{8$eDduB(W%o-z+Cz74 z+Mjxu(c?L1p8*!+dBQ6kt4ws$&t7`smyEu|4)$;Fr@g7JbQ<`8i`7drHA*bn$)Zl0 zvL-BX3+*&@ZRlD@L$J|N@}!;rr-Mf0aqM#biL3w{@I*yay^32qn^PO;mc?6cVn0Nu zKMm}Vfw};A2&jwS?0a?Z)3kR>Uw;~zQ?XbSuCEs-ZczqVf26;YK0KUwrtUH7iP_qD z#!L-yIj!yPC~fJl-fP-OJz;x?%SD@89u)PKuYcl!7qBMy$va#50V`Wkv6$Ysr2202 zO6udd3nxvl9J%|kC3JN0*oxN}eT}{N(BqHL$M%mrnchW#*8l>8ns-${dEb6|?;|%p z^aJRzGqw8ZnZjm{$uJV|Apv`cw%OYMf9l=@&W@^H7u6$+v-E1tecqIwchvL51Qi>F zV}gjmi3E@|QwAWA5Hcs7&fIhFdEUEM?|JIIdmfTb(&>bdP9O;&Lzrwtg@{THD&TPn z4jk}Qzf-yQcfbFts_tDoO_M0Rd!GEjq;}P+RjXF5^&h_f_qk(kURd(T=7-z(!-YMM zI3MP&qxbAy9^wfP>W*CK;yG2zSDa?i7>Y@w{OJAY;(qF4W}=Hr@2{jTE);h)t=`T* z`)8(pRr{*a*ReahmtOSfd~r1jO+8zhYk1XumhVmM^zY=3hi~7r#L0bOP!xvvhGC*_ zN9ZY|fWbNfyUeWOg3}$#^Eszj^}5c@4Gyfn3}y?Hc9}_(BNO-9tDYKgqT{iqzugk>9?7g+r17rBVKbLKD7A zeIMI~f5X{yHb*EPQf!OFLi~{%*dGPE4OfP8?TvLq%^vbUp!gjnB%DKnB!D2-a;h_t5%<5WyN%a2ha7~ndec3 zF@OS^_o89KPn>JJ)5bnO_GKlXD1=erG+N1$ffy2x;EWA>_2eT8`65zgP}dSjhruPr@^KOMAp_M`sOJzo@NbNnaLW zCT_fOy+2bt;kHAo)h41rGZzx{1E*Wd`Y`FI{iz>*rkMoO zAtDcejYh;lG0wjW=Dk$a?j4X*p-8zkSWi)La4M%Gc^()9;F-|eJA0FR_+IsEEZ%MJ z1WM%0<)FKXve2%Yu5E%Zhhco(t1G|wfH=p`DA z^EF^rNJB6MJMjNgf^VT^a`zTI%(MfKt2>mMYcYJw%tcbgnFWDzMIS%7g(X6%2uQ@$ z4+>P{18D`mD;qoWe!*6Wb=#e*`PkRA9m<)15NxBU9m?(H#}U&@c6YY*AUTMm`$S*0 zdlO%ZOi!Sp5u^=>72c~>j8&U+rJ|zDgBv-Fx$>Wr0@ahic4jCMl!+m$ae7j``hPO$ z6m%3v{cx(B0Y5i%|YTd|bm!9N0aG2_wMS(otwb>K7krhbTB(o9Ryv z_p?kq6UEC>ubbG8D;Ztf3u&}M1Nh!)5m;kx(3G<`kX(`jYeVbCrl!rjX|q_{xtVuv z-q*ChmG5ua*V(y;ckU4foA&UgJsUeWw7^;U)oJB*@sgwzc>U^^z96{iVmHQ`5>QUR z0+hBOjFBRQM zpo|k4EPYZ8a%K~m^z!l@$ zQ_K~Y)=QwzWb5Y=12#BTUm)-g%HEb8X3dm~YfRR{EGA^%$@`Ch;erf?a&!imS1oZxcHb zwHbcZ3g!*EqrMcXA6oL*{P#b{YW6q|kYLB_ue_uaK*-n*is!#5zWz+}%Uu0%>sznn zRL+0i@XU4MDsU=0*4J$2`@h2y-iQlc7F@3u$h%#2gw=X?1hE;w(KWMy{7qdFS2wT`EZcmjC~wV#B79T@u`5#9v+SL_GiZ{YDZmOrxlz`Z>@2QcsE@rO{S?aI3sqX&Bk^93dL0Xq#@Sp z-011dI=EBj>E=DLHtFo-WI>ob<}~oSR5MmA^H#ckcn^}+N?%=MEBDX1ec^&F-0rmn zY$4vTrQY6*e8x4CV>}Ifkb(cf^N5q6hiND@?gZU(L#iRiZ`*s<3I7{BGT7G(p1p@~ z21#otd5fO(TZw(|cBr*{<-V1NYPsW}>v&)kgf-g5%1Hl#!^xNU9fAdJb}w_RqYzC- z{S7Sl_2z(%e+np@TUll@!;~=WY#0PqD;p(t)!O}L^&?)Q>77=adtYgd`jBaM;Y4%J z%&eJ`-NaRww~w7O-=9roqR?RW5y z%jt8*ITq=*cpLA2?!<}bx;g&ni@WfJ`khXYEg@<=&z)7_ZnUl?J{sm7q&!PhKf*Y02VMD+{&{sXL#OLX}= z_^Q|8l$SQ3J95AR;e;!w>&%}-qu>sUXyWq&j8>SM_ispRnE4J;cA2z2jzg!{(6KS)16*V@(~dju`KZ3dxICMEgUrQ-=Tl)ss0;X^W0b9y!oi-M6!w&3Cb+f{GY zZU)pCNi}QY{&wxl>X)_K1yX8y)g3}lMv%pv?s_1`uK-_2K1X^vhgY;6?$Db?mKWG<(c zeyP)k#w~cu52m#|wsP#-0AHl!fK#P})9*%3#vOD-p<=gegsnKZxOV}F)hf(2rFwjG z5~msFj0D%~WrDvH>}l~--_b+-*a$Mn>42kwUWFH*V$k@G#asAO&CEDG znM70`8uU-G2*kkSt^BDX%S4l@Sei$HW<#mpJx5qLCU6u|QTHR*S z_fI+5wKe!65qZG>A=5Y!iqxk9z8ULu9mSV8YU0K5-C2Ldmubwg3T?tKeEX(momN`0 zH+jQu1iFPY?n=4&4qsD^*b2I>m>UMTSFl-P?hE2IFl{n%xMO6`9-hwuAUISQCd%3I zePQt5$OU(5z}KmdLf9PuC7EETTpb^U=8?^N zxI4#c`ZpG;$dDf|>Y-8SZ0ZFsq3D$9D;kAV3NEXuOGBejUA>w@6r~2Lxir9c#luPO zcriL}tO4e)mF5?2zINbqM#oo=rRBdKH~oAlP@Y3m6|)r4iju2b7;@2wjm2{*p5D>l zG$?ppk0aTY{D3^GIV6=y8s(6u<^bp_?C9#8Ih2Vy zKTul7*hds;PzCN& z28KhmD8|D$W#VBCnR;@ru2zU8E;he#YtIUv+mUb1yLk?=T@Ve&5hMZ`Nj+!C(jONA19 zrSGMo@AAv4ufNj!Sz>r(+6R^@na}><%HLG;>hIRR6n=$kI6C$jp@goeySn-g{+pp6 z{37#bc~t$tamD-R>wj_C5Ffgu_v-i+vIRsilIj~uI^7n5@tbUlHxg&Oz9gXHGG;?Q z<`$&8w{E>#Y#roV2gT9BL9X7S-ok?I;dX!z3?cvs2!uNQJR6QWLFzaQu<~#u1RqX- zXii@tMktXNM^2T$_8KP2KaE|kvIQ`%bWQ zt{BC)OGPh#-If0aDL36pBxloHg!S!I!x3{8o>-`-Jg>6kJUm@ldpKgWS8-_SsNfKA zE7sJ4TB*&)Hvk9`j5HWpNHt`=Z3t&UtK6VzxSpw%>U_+~7-^T6a0E_Mm!Z;#h^di= zAdMlgh|B!ZA&_X|e^|AA3XaX0*ySp8KjO4$72_w+e;e%0#IkP?SfUK1RcG`y}0 z^Mp9IOf2ML;j(252^z5Mm^i`3iDSo3ApQi=KWvb+GZ^-QDdKx*Xb3h`h4!zC!Tf>d zBNTjG*Kik!6Jn?l-b;A+?1`ZsVq=|^N+KJTY^3c7(=MwVazFaCx?)X1RVhVDz;PO`hTo~CBHNpq8@R_x$n$;hhj z5`{>6ypZPYwoYe{hi|P}?_4+0fx{h#cJHRvcke#b0Za}$Jde124X*GeyQ8U0af>K)9!8U5;J!I{F2aAdbaW{ZyLg2KVl;WC zQ5siSvzj5_7emI+N@i>l9T&od5Ktrg)dMVr5;=bgI{`KX&?j7^T|_oPyiEsQO~!}} zL{_XVPAX=9xJ3DnPqV?TM+G(!qap2L^z}ZbPDj<@)R(qP3)ZGDatMvKV;LDGdHZQ< z-PrZeP5rfad=Y@+w^AV5l9|pic{UXcj41`APPx~U?vw;jZ zv^vzUA~>;kuXuRVUcPDX60vEM-q!86iz^wyISR)rv1da$x^_}+K&hOEoLA* zRuNgX`@9l zn@FdU{P~9&Kz1-xxJU((e!+tz%oJ)tNU7e=JU)BWm*c){v)IBfgvGO*^d~QN)iPf1LQpK3hHwH5{qJa!3!qsc-w<5WhdjROm33&DO;evsLdV zGDog1e;*R3YErunRKctj`Dco{U$iFAd7%rWt**{C1S3FwT>?kr4%9ObI_aEv?W> zAZ@2(7EK(K-y`9043c>?V6GvPm&~0;#P;k~uu$oDFq)n=He>8N%9m%cH!fyyI8Xy8 zZKYf&hs^BCBg~g}$K8ShH;O7^uKj~HP0>2EPUiE6a15%)aW_D>M^-Xu*MtBz#MSXM zDi1rLR&enfFoH@&B1R_qT+JRHXRdi*IWgDRh`9#Ua0bXQ4BXGUs%?lEtrk_g-1qW} z%MaiTY(;vN^axCI#LvXTZ21Fhi5$4o*se~Z_P$KR(-Nmr?W@V>C<#^6iKZqJKhaM+ zZz??%iVUX2niYg*@DeSCmP82u6MRd&wlBP!x9H9WRv6O{Sqkc32}HMih@Y8^l_Gg++W_^Xs9*LVlr@Y3ujs1k*Rtxim7)rl4tz+ z>PSbwPS=O)^gZ%iKOf?xsYE^n1S|9*!jhp65}UF?l09)80}xfiib1YQ+qTv7DJ{*B z&qp2|5c@4c5i=B!-#e-Nk~&>lDr04}rx7!JUm9=32FpC*cfMR+SNm7IsN?Xv&H7(^ z$^o8Sb&Ym8>u~mqjyC*Ry&T-MsR9+3>Q%sJW|Pj55`CZXG?r?(mzjX&kXom`fgAYzfC$lbj=t^fTLj-KY8>`D9t!%yfhhM!L&=sDbQgqN zvDJ_L0RZQZ!p4i5!#1RyQN&CpmakvGT<}d|YlmYH!b{9oOm?@!RZn#V(l#Cqu~;A( z0&oC7mI7)rEcm2QqK!keMRlliP~0V^+_~7Hg$=hf?g!x@aj^*}H6h?{kmix8O|C!CT=a{0 z3vN`b5+PRkkf*foD1iLhO(2zI-!r-EOa|lb>#VFX@POz!4tB*bX83Y_KQ5WHVP)-Qljrmr3;4I;4NxADK?^TXQYbT@qYs@-6@c zF21pN{x#s#ZDc}M*)juDzhvtQ+xC-4w{C`-Q)uP34eLn8^?dm{*2)L?=qZaOW0Ehbl9r3B=gdlZ4FGfz^^o72o#w8j>;BCOkDHl(1QW zR0DrLJN|^LObn&y-;MK>lxXQfhO=s39sjUMIdj#Jj2D(#&#LSF0IXbbA z5)XzMR@OgAqZEZg{*W5dxt(hjzyTB2 zP;F+chQ@VsYzX@z;V%=t?{vu+aaFw+4QqBHXRD@}Vs;e~dOf*@uF`j@|D}Dr-{{7~ zLqKNRKyaXii4NMvo1-^gLfbWBZh$eta_fe-Axm*c4DZU_%ikzJfHTHtHym9(k_d;@ zpy(yPb;z9b6cWRiw^9*HjFbu-W_Zfl8SQgZ=E=yX6}_FEIvPWFW}BLjg@!@(4Sg<6RQcgZ=yA5jQoC-BCbZ4R-1ppc$h%iL#W6d{|ox8r}fbS5@KXo!tGwI zJ5H2N3&Hv1lpuqRMQ-R)WY*GH1qKH8VwuHpvmO#_gjS12#%9BT!1REG2yQy=<&V!? z^czL|>Pe7E(4v69o;~MrAt-?c#zmXflbEGBBWlb^Z~!f>H~-QkNMSr;z_uo5JLDI?sy?`30RcP9n7$RVL_R@u3j)f1~P)$w_#QIJ`2ZD z!2n5&iSErZiYQ9JhV&+8m2?!Abn9{E0dRBy{yeh-6_U*`dF=iqlQg>gU~{F7jL zk&k8*{MlWA^C#29W|9r0Q35f4mt4^>_6D;LrlGmR*er|7`=E&Fs09K3mr&j0GHdQE z3*89<4Q^9LhhTQTPe-;-%R1$wM&VRcGfUvFvZ;2@gMUkR&)RdxAwYMl_EyGKOqSU!JK<-EN zL?vZu|INtDr%c%@Yn7V};UGi*jKMKtbu6S-7KE|>xy+4_ z_mDLw@=xdA0K#Q|!;7XUohV`7lCRta1+Y9fqZlOSC1t&g119?xol1}Wb#-*SY?h8$ zogZ8Y;V)AEDl40%ah8{#G%Hi(TQ8f8QZ|g5uolh8Xd2)K(@QsMzfAHX0z$zkfsn~4 z{*SmhbwiBIEbaYx#i)Og^c5?HtB$}*Y!>W@7##kZK0zEw+`Bvn$-mRvlscXnDn=Jvry5N%?oAP({e`z>jvMY^NQ5CWH z{CKO*7q57^x=`uay07V8!S5COI(qh9s$vuI~6IW zbA0!qV>=(Au*MS zrOG{H@yF5jN|Lxc%LPKGOs@xh(;XcfIVbub-|-Sk4!kD~x1&70c9IU02X$7DPpR|H zxyNYu9iPg0`ctR=|I;c_xlI(B*w_y{*um_f;DKg-wB}K9tei}AE-Aj=CapXPEB@GVvm#PJ-$uqg|qOv~)z< zle~~%;sq2Yv8GUCq!CwqosFtr09b?72kOHJ4~Xl~BY@E2yf?w7Wz?6IG7GiDcq_F< zO`a2S6?B`wTI)O~R0+aZ|8s1S(wu1W1i>sY9U^SO=4?|(kb@{7o9N&~Qp6G+p3de3 zAB8h!I2$O5KCAc{@pr>CL`>ea&v%KlnJ`XQm}yqy{BNIRKR#(*j4u5WB!ggbmgKy1 z$d?Blg26HPwIz)n9*#srm=Fc}JMhMLnTE^SVzvyKrSVKzGe#}xQw4ttt z@}p~gt>i6eSONh-BbOF*x0ajE9$xCNzqRelGewz<{JjZK3+!l zYMfk)Q2CvA-h}YfJltv?8+3JV6&ty@XZ`wnh>2^xR9W5EJvf+$;k9b=##B2DP-{fP zve#+Pp84>Uy+22?HEo(Yjm2XG4AHT<9fydS^wn#BG@ERf_0Fv|yxY^|Xt&8+QM=&Z z6(cK&Zm-5u(=0K-g#HMI`Lg4zy|t~a6+&A;8WVc=m200zBaNal)C?iNkEP^~B zsFNdfF9!=@s4c(&D~^C)b60cc76NhU%9&sM9kRq=XVboMG^# zqipG2Y|EDB)x@hh`$yNlO6K5!w!wbhkZlA*A9wg{q)mT^Sap~(f&JNjV45F%P|il} z&&S`v6bJE*Y3htl_%?`oW9C^+B!X_3OSPUjp~(p}73P<59HG1s!iVJoS#OeilAr_$ z@@gBi`2r0oJ5RcD0n|m|v$jrh{l+C`2}v@YSzKLax)cTbHw60s1^?RRElcbcbR`R% zUoyjA!RviRn}(V^lV8jFHQ>^JLG%BHf6<`zH#~`?4fFCV(83jH39Q~k=7k@0B@1iE z|IhYlOiJ*)8)JA70B(6LMT-wAjSig%5oAyX-*+tp>dTncP>z* zd|okG%z+KQMQvfZ9R5Nxyco#2xjG*uB{_G}F10BGc4*0JpU^%baS&y5Y6Js|Oypw1 z^2(f`C*(mECCr6n+NlMe?6`QHuSiXiL z@o>q>ul*iN`dw!$xKA4077w!D=TH8t_yecH9P5gV*5*`J&Y7T(M5f~0I4-3w;qd;R zW8#s?+g)Q@zhgVE8fG0G*?!6>;{!FG%Y63DOsi0_wLg63I%U-e1Me9b5oXdq%U0YE zipL1ic~HLP{2x0L&9luUV8Wx^Bp3o|?M11EFUkF2O!Yir}?-IaxnUBa8I*u zTiIgg-Ro-ZY&I3rvZ((bjwy!0rS1h|S_e}5Qf$ay^qG)A5 zdlsdLuBXWC8PXF{)n?~Uk9&{Ph;K(hW(OdzU2Q%5 zkzcV)p_l=C*Y5@UiK?g$~U<{jY+fD?(p(ix3fCuwsvBJ zdeaDMT0s4X$@*`Ym9uBFSAoj3)RWG5(<~=^ z^G#yTz)duW_TV-b;ah(v2?aHveWa{9_SIL}>{qGHk~-KoXP9@LF@w-p=T`@-egxSz zYp-z!JdCy875z8LP;oo`lbBrW;ccGkBU%7J638E=CU9& z=FGGjw)(`aKwxH4_=T2b-zyw@EczU`IiR9rZv`u(H``jwX87pi#8x3PK0dk#(41Dg zmjsmUEl{tq6<9=LmtC%1h^75PaoMjUDm&i>qTe~T_k;eOk5g%$rBl`poqu|AXa+X!T#n%2 zpAzZ^P+P5is+{0k{kt#!8-5^34VjOwFfaLq_`&t!%JS1sEIWqMQ1fK%!={-(`LLgF zI9t6{eZS%l!At`$1Lb$U312G2(;?s~6VTv&jiurcMFS)(9ZLIA+KnT=IHc3S!z9r@ z=5|=A=i|e?E6~;?>bW5^4BBc5sld+$`dR;kq-BLKg!n;XyeaAE6g^z@6goSTJQLGl zGx&KTBB{Pb)uxCQP}r3!RAjLA1cmZ+FUF{(lFkHeuzKj3pd-eV#L&oG)x-xai$x?q zsZn627f_ru?@LJY7qn&PCO-oxx+q)outAOk)}CS~zg-q>(MlNZsG87BARGDClRq{B z0}?J-fmHyv(AU)xXr_J--KNAneq5e2rFP9sDc#Q+$U>h=0vL1;2uZ{I7+>Tt7LyQN z(+Ek}6phDu?2cBxoJM4MYMq2p;?uPYh&xhbe_^F5W=B8%8#Pd#o{k zpi*YeECYG|JAlf!VXGWd#;#TnvaA#+udkE^zh8QJWv37E1*zWwoD_T{iAqTaQuqSB zNl#t?fBC?H0}lwk7azl1COiorxW+P}Tolff`bqVZ*q`QwAb*|55?^8A_5@Q zk9$EA>E|=xRR@Csz$h_?&U+HiM)fcl^1_KAe-jNQ{ZUWI3)+6^UJ?nCS9lTQ{wl9D+0*-AgI1Bm;rKqPXqubUpycu763-@ zpfl+>!v5I^k>lf>L@-8Wh0`u0?UKRYyz+xT3qD#NOv0?aK`dImV-Z(BepVW72B@fm zgg55kW(ct$5TxUXpoYN)7Cye_`;)i*S#ia4TYt+p$`48hZ^_Md04+nGt~G~> ze)`r*e%dG3$zQ{9s3HjikN#6OhkDZzn!TN|_I8dHrVip>^?b+-&G;W1y#4>bz)5~?=I>OeG)o5-#&Liyze@A`TW3U6cT zhVtPdnNgGm60`|wX$TUWBLr4IuOxSqLr!NxNcAa2{d^qv|%qJ4@bnn6|& zn5!EQcdN^La0o3go&0ch zdB=tzzFgij4Mz|gAV4QxtTdcos(cm^7pf@%%(zyeZG!9>I>l67(?YY=3^j_Pj+}IuVhO$dXas)NxPXX7VK__K86d3Kuc{)vo(gIf`67g|c z{R+w!$ig`y43hP%NDxk}3Dd^@W$ZHwg2j-J;V$9!2i*M5&u+Ut9N>P;rgk51_jRSx z?L1WghLxopEN$!d7g4{JPIciq!pUuq-1%qfV)ttL+PaM{>RUN_gFabFe+(A-k}Lyy zgFcnjh@#uZ=M7ZdkeT|#TQTychK-FOpCtK)lpbD^750eNh`5G0SyC~D?5g$6LJ>f> zrpw?C{H%(}&MM{IFub7Ilb%;w-WQj$ZW)(jG^((~|W*7l~2T{-q8B^^L=0V#?KVrQ|=eZ2>I8}g0f4Dnq!60AAU zl);u9XwP(YB>3@mmWAB}R>0fNd`ii8vW?lAz2ZQ5mbKPj=W5_9E1AdM5OiQ$$_Bf$ z1^(pk%VYi4eCbZ@6DC&nZb|E7JQx@%%N8aQbV zT5U?h9-MXxwH@XQB|k_16qd@4$(KqFjJ_5g$9A(wor#^Rw@yI&4Y-j&3ksz8)@uP) zok&|XAcE)T9ez~Kf%C4mxuGt^-bre_G8^K2%4N$2a7rOE<*68(OLw-vicV(G9 z@9h-bh^*ZV0q%G)*#;knu}phMt_wDkbi2*;7%^=Jcz3YaNc;;Rmk#-PZ>Xbz;5gPk z7z)GaZp z6OuhskDk2aV382`ykcNj;IP-I-kKw31jJSHSOH+Og%V(tQ8`Yb!5DJ<^s0g%Yi+$4SjUS9Rv{Xik` zR@u=V1A6Fohu8$hnt>gtRx)tO{aTfB^d^>bbcYKd70AW9Jq14R_iV@T>h6^SVvMUB zc2x0smokqd)=+@bo~JvOEAXQr6V?XH-zW@XBdB144-x7sd+P*(IiOw#Du8q>-GXT4 zYYW(`Pasg4ZUv2WDx3&JeY|G|`}YQxj--O{++zL~Z!o|Y%wkW>VhKRY@M$BA5Cn1a z%m!9J19a0Ubq_;k52&&@pY;Try@2^cz80`J`%z%tlIHbqvYFk?7xgDV9}P9;Od`pj zc#SQ1jRlh}h*(1GFO^6~`M-Cw9_U@B6X2BQd$ca43~OTn(;%n-3crOXP_1n8y3aeY z@OjC{M~vN5DW$^|VFC6~fQ+wdlC!iX` zTeAMNC&nA&jZt{F(@MNgN@SWVWwp6bQZ5qnS$-ctBLU?p^PQ*_Fm3CTC20>oe4xs6 zGmj5|KMvswYx*YyK!DQ;S3~=RLHGn@LY%38bSlP-zT_HULE_kR_#Y>FtFP(UsNqE- zi45r={R<0)P%h0uEYg}8`ixu3c+&&=o zcD3(@*g+KkfZ~=(tQFPsv-u3-+J-496=@qLy}rqc;96u~BXPCVUxFet>xfAw({oW9`9Wpn&A-8b*#Yn%mH`CI_O(V71);i9d)WRsV-V3g{l!&mZi03NO{Oy{Qru8>cyH{`Rvoo9}h}kC@@XL zTd;y)IXO20hrB@$b@9*M%z|NBCW*VGHHhf)jJ=A~+|jxCOIE#~v)30$E_9vCi`dU! ze*$Loz}UB_%oW0v;An+u0ktK2+`$otp>WF(9-2N#!IrFLz5tYj;9VF7fxgblB~`b3 zmmI_hts#&ez(ZL?Y2pQc(FH}6TI|XoN4yodgK;|eQ4IS97~Xd{atc{Zb2Zo3N9)Qp z?Z9z0p{Q%GfG|lmBA*Q+s&TFc|5R20tKor1 z7F-2lMQ|0Ov^%urdm>oMvo4l*XNU3AT^tz#LxnIeS~_c$tDeZ6VKV5cGYD4C zd`}vNOLdy2l^!QG^SKtSoa5R_qFOW@V{?%}mb`u=^V%Ci_6$Pjh*1>zq3^MQLr40L z3Gk5}sTo+!58cSJ_F#hy?>8j8MP830k%_Fk+16?DET*Y0RaFWr>?^9>yxOyNr{f5> zA1NLg%JHG>!R^Hru=y@hx~fBKcQ|;BV?(iVA8#Cr9~>y~{=xx!=SIFsTdq8!Ui8}o zBZ7~J->!c|yNJhN2@>gI3lrPx>%m|HXtu*dyRpc>|YS_A?^8h9LL{u~2#@7QfhbGA-2d-#=0 zS+mD2IrcARvzD+XkGqM250@~s+|=pezqpHaczQ)gmcRBedu<8p^z_hQe)%Bl$mV3g z;Sccmp^cg4Uw&9p9|0EMl;u|*XLq1DQd~r>wzNdRXU)Z1>^F}v)h3iypxXs1ZE%}N zBphksU%3k>5#=?V^y^0$pm298peefSb|(Hp2U>ML>}l$yWB;feJEi`;I68+PoX>W( zvrK|Ar#u~qkq%bvY!-Gp{7_?@1%<|#Ki!hyk!&IZyvblYTlXxhyZ1Ixzm5mIh;BCo zF$-;(U{|J{Co;f{YFm|JraO=Z4Bg9)&ROue%LM-x*0gGA0?RBnOyCqr`5Lgb(NQo^ zOM5vJ0tOhnaW#993Ym8utGgFk^~_sLbhl^tnHl#hTibzd3F2hlnoXrMQT|##%f|}- zn3MO-hCX1(1DP*M(Lz}f=dPR2>euaBc0};(#LA)Xx!AM5Bo_YZ=qY~hvv{MQP=BFp z{}xk^!B~2cfqOqgDCGGvxb6iTV&1=8S>KyhRq!mQiA$?C7n%I7Zh$K?Jo3nl*iiknSKwW{?A|!NZ-7bU* z(6HPlWmcau537(QSFxL-I>(!E@U~SES~0qJ(;OPPiE}8lb$A- zl4_BMPl0y?xC!XF%0#@6hrHe(IA4$-d-2O<$JRm#=rd)o^PEIc52@4{0kDlRb=5-R_K6TB?Ax0L?rI&K9_Z z;50@p)4pxHHpm^W<^-Uz*dBCZveTy~X$#wQrgMoWihwf*C@}9DHId2A=4m?W$wa+>XJIr{l z_aKvb4H*0*ZO$}mADv+z*i`utsL-PBj6c`Hb1j)vJj(}u!Un#Hh#-!&f}%K+j$$7M zwOOpq*XE3JXUyq~IJx6i*6<7qXFMrXJ7Br<1Dw)u6LUQIUL;`e(bg#boIOUhW7 zh41&>%GTV<{O*_?>Mc2cF_y~l!NY72+{*4^m`)06$vO6z{?7pMwrg zg^Ah#c!+uzs=EakXF#@@0@GW9f8h;OQxc|=M7Igq3C+4MTnuOvkWHBVwl-(G#B{vd zX?Ja=(*)5SFUKWdid<}h|r#NYJu7WSu+_kX0>Cq3PN`RI?-LbJ%j>jIXfd}0f ze-B>Cqo^D7rkr6Y$OXJnSH{o#ZN;rJhHl?l)6BhrhzlBfNl5i%Qhd0t_mFr1*uJ{^ zs`_CbTtCj(g=f;4+AxoHRk2tID%|pg!OM^wbBGrhRBd>R2yH~dA-=7Oi1*d$@X6gd zKNPv!vS}&6)affaL6VdIto{T({* zOJfQ`04f3|{|y^Ts$43@U+sJFf^O41N+#RV?n!e`#@6EV@TH5G&np~hFFy{pT~B)p zY7esM)^=1PR2^c&hu|s>DZZ?&6ef18<*X8G+BWs_C!PQWXJ-((zb?_003_mx1?<;C89vhen0S(l zCmSBCI>Iw;EgkMWcjcN}onF3Tcyafwg5N3@H>{}Q&P;QQ%MI1JmgbC;SFLDRENvUaod43l=b2rYV8!L8E92+PoZB?(Ms;4Q|MBxC>?DZPDTTa;`(7fwOk&mVX;&d3~;k8^HKkqY%VFDR}`EmD#pJf`bn4I!aVgEsT{p zWWHE1wBaG{DK^9$9suOR4Os_2`kkMyconw)q{T&7h;OZ2HebpsS~0JHjv8h+;>A>+ zEJ&beJpQAf42yN&iD&LZUa24Yj@DAZ8Ax@<{P7U+-CF5o=ReED?|_vnNfhZ{vF;kk zaH$P9MHlpfvI~r?zwSs##KhzmoIFRY1*{T6qzG)b?dATJz9^L33THs!xTFg(?kTWH zz-FuQUyU=*%q}<9$H22V=?_Rr;s-N3q3c~u8^TL9_Z!`}fzVQZ>tF*i4Ew}jcOH^= zxR2GX(s0BzxV0PR>PE4(!L^kHx=_nd){-j9n?jPr+S20khBRDrc1yUftWP-2+7ex8 z?GxGt8UdhRvvxrjr!uQIiAxwWUzy9Ai?x{=VlL84T6Kore$kid0K+?eH^IMF>R!FI z8d2+Q1}0N^fuYcnl+@`zEl6+SNQb@8+Yg)L{87~>$avR-Tz26|fCT<4ce7tEW4UZG zfpoy6VKC&Qkd@+J1NW$_+1DgE92yB4*WH&+(E({W{;v-(<3z))G1|g9Qac7RT!3Mt zeqI33&cRZVxP;XDp-~v&s~ya-nn49Q1Q!DMjCdr)9Uu)YfaN44akQy;G8N^A3aoI5 zq3|*ekheAq%vT0x3~HAKY|QOu>AJWSF4O-0OJ4%NKgYjc`YpKrvA+HNZ-0xW4hgRQ zP_K_?b4wN+3-F~IkbVidV;c@?`qLVf`MBMFnEE*5$=| zmiiU~8&g~^YZ<0$V07y1Ns)Z2%b_NN4^8g?6-8QmN5rSb)u;@{Rz4)pko!ipjH?fs zarIG{_f&?VM{i2GDUcl6pBe7l%?o{*ow1!fjf$Q2wmf_hib|Ly-)mM#S7Q+HW$!vPVRC_63&~Syz0Q^aJsc$O=(=t{fpvqG_-(v z)R~WT=iB(mv8Q%FC-`&XsVyVRc-|RlaJo3a+fm$j%f1yoOUaR6QNLv)VFpGg4S11_ zVV2ui(@H}q>c-b#&W>txG8mtT9Qz@2LbYpvgjor8EieC64GmyPv5PgLpib|f@^T2V z$T#p;H}DSk*X(dK^$t@}gSB%;RUF-0wf9ra^V_Be&yk8^)~l>D)>af9p#L}r%6fA- z3$r!8jdit~x%Of8!|YYfr+^gy?#*k*N%288)*wmoRmA(W)hakBAk8*gdsr!}JKg-2e!cM9s(W*;$vx3WQg7vQV2VL#0C(5{Sd)4LP|wr2Vr(Uulo_6Bgak z(=dQ3-h#p?(bF9r;63V3kU%(3c7P@7O2tskG%NLWf`ettz6q6C4x!nf96QF39s3Cs zXt}|-We{u`LY)w&yL)geQeD3vyHdIMVn~ITLpNt*wY1_YJsI}K8<4&h-&=DZUvuA$ zV$B+^9YUJPZuiwwRq#r-Z8h^bqs>{8yY7gzrTL*<>@c2P^rVZh8{e<~9qE>7e+Qly z7$8uD=IgO6f@vIn>#*_Otp>Tnf*#`)k~Z8!4a*icv3Zmxb_ z-OiFZ2)ThIX5Y5KDhlGdLLSgM-FD`m6k!z~1s{7WjF4tmBdnd^_(Evzb29}qL?otN z_R82jO3ez0P)B?eBnvY`CCyY&i!qUk0H4Kcj<859q>FTikV*qaLHw0hzPUpAEAEG0 zY71$%wwNENJzeMAwn3~!p*HJj=?QeYc`C$q*@vP79sK^iPl)@0xN07X>?-n9oOR}U zl09I?>S5yWw*Bicn2(wphWTABl^f@HsIB}kfIn4=v&9uckk=PtD{F3#tsdmty4H(r zjePai+rumSxj)WaDOcQy8+{AA*ID?u7a9Dn+2a`>?N??{Yz(i)Tw?VN_zaGijAbg$ zGoTl1D$SzH=fA;@9yM7kMk`sI${m57CWqRnO*|okkaFr!HX|8LqFng3)8loR2f#kE zMj^7aW?`2rIVH6^i-nE|{}0D6zf$-#kC8esKK8@vJxXW1Xr~Z}b|+l!Xb>*9`j7a4 z@{VvlLMc^EYupG9Y5yp+_x*=Z|8KJAUw&PEe;-%VT3o?a*(r*)P!sno-Lax-BQ~hU zn7fT%`37t5E)MMy{AbuZM=9fPea|#VI;Dd#NV22)IW$Nn^a_JCRxw+#f}cNg5nK3# z`PHL@z*;X9pL1&oE z7SpF@s(`_;7?1t7*O%PmofHYdlKsoEOO$vj4ax)lX89I&gJ37I&}5@|h-i;XX6xD} z<^vw?zIQJu-A33okZ%m@1am|U*CIfc7$%;hYJiz;yzQ>}67pLzC^-R;ZkX>r!CbqH z!o!k{`f;QB*vktW8M1tA?N3=4K(|DS&(v;K?!KJ;Qm{3@W2M>OrS)3A!nFaQ~-o4a=mVpXhywIidydi2IHcIe$wy8f)6hS^}M zdqC{L)JGe7oPY|>t5FDOe86}o^o7olYqM+*HI_HdfuVc>!*AvqfM~hC4B$i!xMcIV ze$Z-k!*-kUcWs&4YMM2t%clgkbi*P8Nzh3XZ9`kX8EPmIp21Nww1CnR6)FFr38hPm zCoc9%xILx{2umd5ehxE0_qfEs-IoROh|N&WF6?4bRP7YDO#BbF#MQ8I^gq7GHzZow ziD(ujg!Pwf+Cl1D(uEoyo7rcbfFn8{p6a^P5$MKZF_B*h&55$54M%hrhx|sV=2mt> z#%!O8c&7bHKq|{HP$d^#e`9j zm0>(HzA)>=8HUtvnNx)e#juHcfBQjDZ^3zn4w@87n@k7IV3qJ@N~F}(>;6lQ-n0(Y zfo!LGS2?TUT^bHqSq34WE$z;xECSv1Oa zb??g!36A2oV7Hg|^mhzq_wq#1*WpO=x-{B(KQJAVv*5GMwB`(I83n{X+Jp#^ppC@VugMF91JJ2ooaLeA#{!ZSb-?qPLuxT$9 z2Mu-(bW-LBKGHW4Azd%(7IDsJPVxy{*uuW^j<^2lPt`FphH_}YDZ_p7Xx0=cNxfF?a&WIfs?^M)mhv35Fd z$jHI#yQm@#7h%JYOJoVt_Fw#R>zP4uDUx78=h;AY5!d@ z59!*bd&RDX>{fn#`J?{;uV?GF2CU?I8I`%~=v#)l6YG()Y9wJsCpPV+ru`7XpnZ-- zY)%(ELZs1}Az2e?=k7njCK?ak;gOB_+pP$h+2T#H`WR0fXMN$GP-l?$1yJ$s;YAP3 ze;`Eb>^2=X>#Gg&<*r)-cq2E8TawH3yfz7XT`CN=7YQobTPy;3Ud(o<`jWge(G%}O zvrAch7!-0LzR)h3PT2V&&trk71%FyRp1d#1PZUMx!XiDe`dh``Lp69kD+Iqr+;WS! zM!KoB$sM^Gu*NZbD$&*$ANmQ4XVOW;#=(9XjC=XRQ1m_ga|Y$|1ZZ!NpbsW|yl)w6 zjl?1`e0i`vv;D~dD$IqGd-9g2t`U5NSY3}AKYKk>H)`KeEVapyzg8IY7|-&S5OKJ$Wjg^Sm(e(4K>L%UJZlwa}8+?Nr8 zCG#6t)=GooxFkpeduYNGHLaAl;ocr)NU!1_jP6NTIi^cFeZuS{T$t6we3)H&DeK;n zs}k#YnXcsPFTM0SaX=5dcQ^3g|GtD&LA&70bj6gbv_>d&DN78B`oXa!io4JdrCLvS zqyeP}X-~$R_48%bn{F33b74IDhQIBh_cL>3pWgJy-9tQ+&ZGfu$Q2^pAj@&&8X}<1 zX{y`i*ep0|tqM)O{6+1jit%bDeQj8kr;S~JowUpjzo29w;ci<8azDHA#_@r?;-@vYn)g?^ytLti2E6tqc7Q257O7)iG~Vm@=Zd#v_1vn@>M`V&AS z|3_CCb#^6Dp$UI0@9*mBFYe?=+V_r#2e}yV^*OpYOc#hUb5QRjfG^4!^YRNEL_O8w{Oa9q?G(d_tqx9wR2+* zrcl}P{x>gieevE*SDp_x?el=D8*f<(|Bg5g*6iVrBG9=6(_h9h%pHQjcDk!co~JhR z%mm}-y1h_mijsC$dsQ+%m9OUX7T8zf0)0M<44bij=6L3B0Q zo$cUDk0=dWT^qz&F4pEZbZ_Mje{%@thjmTu5FMcc=-Cd8%|?pSjJE3C#L(S(D{JmF zBX>sFuCtld*0L>KRrz%jWB7%auEsWGKJ&fq;fCG3ua9*!nSnhcBG}ZyoP+u^G=Pje5M@*1N>AaD#4>A+-_T8+1@<)rh!FMUv^%$KQmWcQMOs*gZwek z**>%6p_}E4DCzst;$gBvv0u@N-=?_ogcZ zzeIdp&fM33_R=3DWcOV&ohBh+m~F1F`YSDu`kDdJ=TDpf?hHA8!oa;E{!*@SufzJR zP@l@lg8!q~vA68T*G~m6>ZZ9w+>pGqI?lt(j@H@!Y9rGh3 zWIoYSLtM8ckGd2S1-^?lg?yagYu3E+ahwOQMbFh*yZ4-UMDO11w|$3-Of22|BY{ZS zAH_>%_iX6i|4ZJwLw4_J^*@y?)O&!8t*TLFr!6D5MdS3$vRlO2IVyQ_C>sB-W}Dp|(UVn<-bz_F?>P{%DQR>id;i3#iLvKNuIm zJ{Z6UWQNK#yk#A}e+KV=Xp&@5w%8!@{&A;)o44ReTIEPd&j?S@}bU<{D$@;l;+&-=}mnmAs zb*3P?Y&?-&i(K1J#}+7H?PwvI-jiVC$pm5qT@QObe#9z?cl1}@w{mEa1hdF^-~Js4Q_YHWQsoq`~_OZDRqHM@v(i9m(r-3mc9MNbRqR$-@T~ zOl;h-wF-<$*qb6wdutPqHW!_ptvubzhGTo$d-{1#%^utSFg4cE(oXVPt)0$dbCfRG z7~3vo$W9x^Sjn1JjWy#m6Z>`!?u$}m9nrR8ODB(X*xQ;~xUYp(g*V&kYj}PC=Ju)> z_qDR7R=YFOO#1|M`Wkc2Zjuqv19JVM+nHaS6p*VWwyOtHDd*Eky*QTn3oVqNaG z22q1(g4A-T5e;qbF0SDUcVmOIW&#b|-`3rg!?L}LbYH;#;Y-7}q4R=JIsz34wFTuL zXcfu(C9;T7O$X@MJ{x@}R0v}g(o)6Q1ILT>36~5ND=|a*FI}i{A2xdtP zff$EPK)@DYkPXJWElaX?jb=3as-xLg&1ke+vMlcwHpa$mCIQ463{7!J!_v@1Bs3qS zSO4$oTb^_7y|Y;nI3?}xLrr1s+-=T1_dW0Oyw8*Sv7gWVSR9f2v7aaUz+mh2wMvCP zP)LoSwIpo@awJQRpY~lL^=g^*aSRKVj0SNtVT?R>EVvoCmDSwO%X5I|SnKtPpln9% z!D+Nu=(*=|0JxKTTqv2zZ^ECyIGh(N{wA2TI*DRE`>ez3FV(PHOQPXQaqv9lWCshYG@Ich$S!K`m@X|!R|q5cgE zp-&Mt55=)$`h?0mNlXS2R@~6<-X_3jB|G4dbnjpaoI%^zpgG+}pH?O$&Vze{d^Wj3 zIh(vzF5SlN-DQ1*dV!}3$LVk6t4Juud;8J-9&?B6^f$^?By0;>z357M;NDHQ!RSfy zR%tPgmcOZ<34&zjN~duG?UL|q#?_2rLGqSa345q#^;ivk`6hDHO{Au#eKiL(S#`%m zi`sb)da?V#62LLFPt=Ujn_ea_zf8u)dM0?gQlex(1xL!UnOo35N0drDZ5%foAI@ee z4#GvVhIj)LTvGp2cQ<)GWcq|I4Ihm*m%#=FgoUVl0L+etu8i`W6b$h{?T$l0*ay(Q zfnELw$F%sa>urq&1H}Wx!vLejFdOD+e^!tI!(%9(;o{M^udb=#hv7kt)%0kOzIZ?W z0E;KOc=SCJV`F?kaKW0fcJ0w?`%S?q$57VC8?y#Jv9Mbx+xMdC6z_@w>;2?2WytWg7fJ%hYV*w-Fx z2GlMVfCn+adQG8LP^pHy!a|5nuxniiiVL63ZKrFmA=Z}Wjs{q&+j#J0jM_o(6GL32p}?7|id$m^rmc9NVUU7wND%$eGsKv#XC@;x z8CiV>Ebrv&2L$(2@+Ppfi}fR4nsmf4`AG1H%gn(;0XDpmAq1A=zy}s z8;vCD4cWa8>T`hlAwX~L=k;S{)g)<&G+Uf4)Y+13>W9;{{uZLNXw#)t$4PW zYuY|#ay3Xe-97CIp$A}V54O`MfLRFGoj_?&7jG&G(^c|(0^0aE-^XQZ2TR`b${G@I z*j%nKt$bq6M}@vY-a4rL>zUxO{h8zREii3rlEa!X@p@!%eQvi0^l1*}igZQk9?;4M zv(}z=sQ3iit=@LJ4(_MVVzqtf87fEa&o5eE&OjbNW!0BR!$8?J#_t=+fOqE5pl2H$6gcm?GwEU^tti z+0?K%WuQ-Ak0|w@xi{4VekI|7BT^K!bLE&(|Ov0EI7&bS3%r7-8M^WJidagC-XMDCa2`k@Mt>i1KoG zN$z`+;RNukN>0$E_2?8t8vOk_ZCw4b=`*2=#(5qDM4`65p}|UbEGJ>B*JJ^Gh07Fh zCF$yy;m;AatTYwJZK;}cgEtk;$CYXl0Z#^TADl`kYw?T|f;)s7n8r?##HwI=o{DeH z`L9kL1jh+mbG|P4SQmoO?9}Ghf(1aEk)q2fKVz%aE~jyQD(Vxkn#XFnt4`=w!uu^~ev(3*A9;f7=`G9iee?=6m2u2F4udd|N3-fEF zyb}|&ErA?|a@kdcJzw-mF2_gVvs7?DcPmOxS4)%KM~<^sC{uM<^bXh?k1Uu`pXxdZ;HpP3k|9Uk72R{A~dy_6Gv= z$Pog8o;X(hsw2d!cAqHtT09;xEL>!EL5aY(LhmHX%|9WIfF0cT6x@=;>T(LVO8FQJ zOko=o4&tsvC(;@+eTK+4-wBr~)&-txdEO*Rgd+)-M8e}^P?9Ncp#r=QmSh#I@ji&) z9)!O%g>A6RD)BQ9%5sTxY*nd-m9xac?(^XK^a|qfy3rY&6F+>7C!>Hf=bWOaZMmy? z3Fg!Sv?%3@3Y|8Ud4JBQ+3&P(paM3%FqqHZOJUn<{(kU|F2rh6Qk$q!6@^jMsgf|c zbu=pX+f}p$H~D(!K6kuTY$$f+#uwp`7KO^?U6c@;v_IKKXQT^eZuQ zA^(fNbMbO=hK^k0Ie11&nMUL*-C$_+7Q(gUuSte5fvx~CI=(7YiY zbHvQQ^>jyqDLQn5pu-J~^I*pg`p}3rw3vjj=}vZpEpBvhka}yKtClm6)rGevs3n9^m3|$U^xm`qLl%BuaS(+k*#sQH zTxzKmV9Ei0z~%A^vC#FU&g)*xwfGQnBfEE#!NDD=oeZegUB;oU^konjbfB#kP&KsG zqHAdxT9(1>y{m>~P05x(6HQ%C{058B;HB+8A&+Z$mw?$HqV4pCXGu%eGQvhFdj&^* zPYU?_-hhuDUJE5a;xXDgpi6(#jk?^DrpWWk%OnsC1pQ$e-$P{cb5cB-_C>+fxd1eZ zQJc?XMXk1MdLYQE2yOZOeA z=Caya-71qugI9AmNxo<*Kam&cXFHO2*YQHJP<)oA-x_k4prdk4F|^qfc6?!P~GPG!kE zQeUXg7Hy*w7m`T3yA8O-2RD%M8;;-g>PEW%QIhEH4fQzb2dhc1Bi`z6r9QAq+d12z8hQ_sDSr6E7hv7ChbuU^e7)`-zGjPVQR{{9?3z6D)Y#S1!H zHs|4T7ns&(vzppjp2j*d+ihvLv`F4Er|o6)=e#;MN^||Vvgxzo;Msk4Fc(s+;vEqi z4cVb8@1hZKx}*KJuLVRjD|K#i-N|^d_R+c>v;+KLoKY`oiJ5#HlBRqeHQc+{iU(cG zsIT21h|yu)2DLtIi#rm@SNuv1gnoT&m1)5Y?|x!|$*IUd@x z=MbY9UQs_T{!aBP5#wGqeV!CeJNqENW{S2t9Y%VkaviZ7+^w9S7qPO6nLmk_5`j}- ztgmM~jiudvM$jrp+uU{&bz000ld!~Df%g`vku*y=A{H7m^}5nAnu&$_K(A~bXzgi; z(w`5Io>068(o@I<#oNJVUq{}08QNn0K3G)?;J~o4-@K9x9!C9Q=4z=}gY9kkIugx> z`nU*DOz*Q=ByMk`x2CndmfDeZ%-Ct#-WQId?>Cu>rt`TPHGN=W=BP7C?di6V$w7^F zcPse3QqBGCLk_yAnHbziJQ--Hy~hS+tp6Ax-@S>LReW;N+~-Rr=WIm0wn7%AbvI5QOeexL z8R_lh+V?8#3T#*J9%^G#AL-4{&T=}*>^$2ad- z#)0Mz!mhgMrmNuFEh)try)Q^qLtHBqOp~BL0tIae6p8l}fM~UFpG!~K2U|+^IrTi} zh`YuZ{X3*9w-Zy$(+4u2Pst@wK1C2X`2&7>_|W*{>D^HC0jUID>^;?wuRKVr4pr>^ z9-~Xy{f0I5^t}Hd2+FUP279*0cQC38w{Ef3wl~sEyGe2&w9UDVb{xBBY`GblLeN0> z(2cve7pDAWn;y#}VmBgumq&QrSPk1_v91hYQpyO5#t(g4vLVj5^EkiL6vvL=hjl3q z>6P!}jV^m1aYc75p5gp`DvNC`YieS(z>h_tIY!ycRia2bNv6LjsFahOqYi##X8?2U zpCF^~Pozt0Z0fco(E&D7#&b~efc*dBhLyi1NcTFRQ#@Al7(?A;e4GS>(9UKw(gm?4 z`29arZXmBdjYQQ11S%>>V}=;xhA?OXk(lI#f=*~YbQ&F2kfL97f%FIYOPm6^o;BJ~ z_VIXd%p@GU2_j#LYYnz;w~Yzxq%)#3cxwz66RQEMAYe-j_C)E{k&>_YoUuo7q;zG@xU34V23FZ$?|If%wRcf4?Q+!AMIY-hLr>QGS z6W)Bh=Y5L3jDSGt$>P~gqFx5+N)(dx-1yI*(XYD)`^0xQ*e&{>w}kp{|MKl*yDvWQ0x$w?Lj_APeBx z{x4-%&jlA{pofW!ucUsK?lotqDchVty%OWvz?!Wl12r2nZRom?zk%AtM?CHvW;>|X z_payMRGB&XwvTdQkd}!(vov+iAvAc*dRkzv-FP~X^FrqM3cgEf>)S+=kxHKs!5mdYv!U+VTq;wS{^nUCYd4pADl` zfwe8v*hAW~^$94FRI&#iV3o)%BX3bsj(UZ!KwIaGVMGbAsP0~Jj(PushmViXd2uX$ zUw~d+IE)78APaN`yfkeh9=lJ7VWG_lEe~#cmn90CO4arG=p%hV9Z^e{-Ay}^hDa;# z8MXN!>S0O~Q@SnM!sR443f3Y8fQurP3SL91OV;-pC<9|x@mOVm;(>uMmTo^l`m)Jk z9(d-_z-oNQAyiDhiI=V4dso^&K75otRoY!zIeh>AYIs?Jra&|O;l8TZc>}6^_J<$p zDh%f{u6vgFbD*H$CHvHX^mOKaQMA`Ul&cLB0{94I8(rf$V`2m`14VV z%VtMsNZWvQJGYT)M}^L@v&CB5)=V#ONjupNJ7+)KY+mB1rS7&ikKk!;3%Yx$W4rmM zTq0?Rx7>__}tez++Jyz<=O%=Y^MCK~VOpJRpbM3C+B)Z*3#&2@X zr=l$^?o7!$H@tqKTNH3h>PS9p{V4>SHIMbFO7hfGnAjQK$e?wDR$^Nv9C>v6fIl3K z5va{oK1HxuZ@oml4mCU+h|O9bW|_Q%tFRVltJmky#6}DF|;N1@UQ&M<6dOuP;YW@DlMR zRnZNP!yGizP%F7-2QjKi9xbQx#1_)(CT4YqfCXY30s2ywJiC$Hv60-2Rz2u$crDa} z^Q1hGekS*9AzF5Yr-GnQ^u!M0Zbi*N?2z)NqJbpMzC@k{yle>q9es_`3@SQz+3ltF z?l}T8oC`Kt22<~Tq!WHhp4~vQHCwy^5hnx<$`$P3QA0O?;!msIH)^# z)E|Abf!wj1JiD8`gq}g%RxnAqmWU-tTSDZ?9VD%$upH@Do-w+63kmlLu@UURf3(L! z4L6fJHt^dxdQ@Kpv}F|`9D5E=q*HE{Oj&0?Pm`UTIyuFtUOOybRE`zDnYBa=X{uNi zTS-Pqw#fkGO-|n^bw-_#e*$jF>FRXQ4&@xu>FNY@in5iv*F6G3%voV0j|6w`V}~iY zsqsfUWE@p=95S#CWv3PeTU-J)1@3GxBa8D)&+C?qV6&LR;}F*ft#Cy5#lTs4#a3M=8Hq{YQ>}EpP!Nc!u`sP z^o{G8Qg;3I^!n?SGIsqN)E2iVA(mp}IXm#pBWD)}ON}vM>Ne94&xdg7hab`pKVN(!1#IVoH zzjx|8hh1XZ$TvWG$-`)-9M=uH#;BwQjK|5lc|l?R8W$TT{tBz+@FSM<`#*!-%BGtz zywSX&0tr1z=}|kUgR%LXPI~XXCMBS<%DFJAIW;^joHIM_$heYdT_&~XKZ16%u8dQxIYr1k3UWhtv}xU98N8<|NKpl ze70yJzeK(kb__zX>Ix9I0$0c9Rs&Jauv~QsQ$B}T_ob-|<+E_$m)Hj%F!@WA9kA|g z8>geA1mrq>fa_b5AlJ3f#zs;-wyJ*xIvR#K5U88QRqJj`a5B>~dF56*seoT=zFu?E zC2CRjO2renxE33gP5b2=utEy+gW}tQ(uoE&Je*IjY**{(4<5rqCA0XqqgV`)fZxhL zEb&Jx1W-V23XJKc@kuc^3m{J1fO+_9JbItc)sXaK-oWz!~1Q$8&Os|w`BV*WVh0%p#2&nSQ4zVSx9{jdUWTn}p? zm$5hApwVa~7J-yWT8LDqZGl!KgB@m@74_5e&j%tEZ~yt+3cwot`q%P>;SaCkQ>#Qf zPY54btpInH$ph2NBv+T)=W$a|#e>t5`W=3uNUioR8FO_`))sB=wt!j+PD!;u&NU@k zdZ1e3s;^}m;IRvC!f8BVJz27+e*2~{?MWs2In#_<=EULM$!HHxbgJHg8#Fd?kTIe! z=jet5E5h{qN|aB8f@TT*`D&)8TK7O_-D^@gLtXM0$-&ZhQP>UV4s(f)NVl%a}WgjmSZwv^gzu8UF!BjMnfNqBj@D>Wrc*(v63ySkX8IA7QK%lJFCPZu;Z8#^cF7^^v z7dNf^?fDWzCYRrnb~$ZiAVt7OCXA86)zpy*r6YK#d|YoK;9k)@p7SBFYzNpcMm5*} z!w<>m=wSvX=AL#Krn_)Q?dVv7O;*lEV~xe)GO~79@^jF~6gEPsLgt&3mL4aEoBboF z7wd^6Avbz98hMh*u;4<28^d~BAWFkYu-TGqkF>*#V{e|W`szYSqjeT#wpcun0znr< zE2DVrVSIpmhqPo%KS1i$!ceyeeaiQEpOcwcfW8OtCD6gE8}l&IWqV@swUSoh6K9Oa zQh_9g#Y8R4%!!$J_^6}2xxsH~;Q)AS#COc>un1g>86pmtWtdc8y_l4%B~2AWlm~5g zv)2*=*ac*bXfm5b*PXaE0jM7*I?A4(auRP-G|8FALfu$jwVUL>dm8U%vX4Q{S=@Nc z7C;>vzs2IvemG~-Rc}uH;o|HV4fTgd#V7?*X!I68vuw{UxlJ-dy))HJr<7|*s(G*j z9ag-1WE&gmP7cryRD5VxtW&ZFch2T;j!x>c|kSd zJap1MNkRj0q^>|GEdL#0A8^I!9z5AkCA4^#a!9iL2&F|{ra>();smXW#$WN{BuNDl zVZ@^Hm*g*zMJI{!CFM()o-PX_6}eyjvNSNj_B0RB<^iz#HPhN!0!RtTAf0^@3727kj_?CO;Cj;}X9L`|3gGDNY;qz6OWANe-+b z5og>N3({C1j8q>oHt?rZPf}Y>UC=u8*!?!>LSb**A&$h|7JFn!+zT2oJNhMiTT^Hh z{omCG1ltGa|KV6QDslgwVLK*4-MDlZ3nLg_p|k#}O$ zMUF2+r%dvxa$Q7b#tzRxdmE>wK(kQ#F5>Xw)se4Yr@V+LnU+nLS4ud=EC%9)9 z6EY{KSO?sbW@nVxv|Wd6;78hRUNhv}Lzbi?#pooiE;K@l0wN?`@C6zbqM;-WCA}%= zvx$4^l3$iQwvH}WjMg1kJN{rlwFbT2=wC?3vti_SA7=+wv3mhm^75B^rkY7kdq<}y z4g=>+aoh3u!Gn)8+Q+gk(Dub#-VUpWKG?Fhx^4~ktL6W#yd$lzThV?WqxZ2DJ?ls5 z#tkJw=bqi9Zg2ae?6KMMf%mnqs9VohHqCr#z=n;Gdhe2y|0VA!&GW#cRwcTgUY08W ztKKsY-XYN?$6y_1*Tb1zJI6Zn6>0uQ1mS7~2)RHw9E4ZH6FpP96Fi=u1LZsg1b4?1 za8bXj(+Nh$`SXbj-R4;70VGz@kz8gir?{3wM{=NK4l}9hu+4lWAtBofY1^)_kA}TL z5rQu(3>>SGw-AnQIc{kg)K@)jmzTPH&Mr@g;=Ww~H;2O zidDj`d)!VqtJDP%MUG5(TGMV2alyDg08_a1(;$5hhQk3g;zc@xZu-%D5&={j640j& z{!L7OMZ)1BPnXl@qFy8e;EwS`R=NILtMjk%6u~zbmY^4D;&4|a6r>ojFi0BVu$0LM zr>~bn$z&9-Mq%$S=jbU~DnZ?@T&#S%WNymemMK_yXKKiS+@H;M*OWi_d@v3gW>%gglL$5!uiAEng@F!dVmY$!HY3map**TGzD{)N*jQ! zdNhnwx0dZF1C!`?BnZ%P$pbFn6lD=l5N)n!0?Z$=cbc8(urcA)cf$sY5YiQZXt09x z193!I3K<2^pj<@ut&zOGPB&PPpD8EGw=8IH@uI_pGO(iaj+ROzGDKXU691VYC<+P~ zdk{Ku@SMJtB+qaJ7IfwO6bE#oFXplf$z>n#?v*YygK%%_+R9IgO7NrJz2UXXMU0}) z<_bLrK3Oy-6|+Yw{4&!OpA-b(I%*#z&Z3X#q&}ePb;M$VQC6>WYG44Jj2x~)w}{pH zt>R747bBGnA7pAv*%BTo@z*lvS~lfoPpn`zV5>SCsu_0!F?38$O6=pe|Md?H#J_J} z#lFE4e_&;rf8X6e>b7!)t^1yY9MXKA0hB9mp1V}8Tq;r?Y7PD|_VC)^{j>5;qacfA ztav)ZlknXC&|0`oS{n~)k>mypxYkLv1{uC05C*UFBobKZ3ih~Q@%Jvm%YDDrBB-V} zMM_EwI0_KmM=%CQ$wkS z9M{;#jtgN@`OKM%=1j-nGACyb+|(yiIj4XBxY@JxiYHcijAuc4 z4y(NOpipQ$OC>ld__3#55tb_#alY46$(l88NTm|h*CV<>1s)Qt0k;;O+NP#j{7aLt zKu?1ZuIQxOy@O3bKpIt*iz?AQCoISjGJO_IZf&q>(7T;>sxlY(6LM65dQ4P-Va|Qh z&d?R4f$f8X+XFP%C2l}jD}PYaPj|+$5VdzL5QT(s3^pNu0AT? zwTKMZGlE9rTdS2@mFvGt+a0Z7&Y;Q<-dZEy+W9*WF=Pe?u$tX5X2GYi9zgY-(2d`8Wqu}9z`=Jq?OtkdLAI&|+F;#=tcyZk zPY@^IYt>^f1o};??MngL8^+V>2aBya+ZN^=Vo9Ra%)yhU$crJwAh;WVE)eq z`g`-);U}hVo4y@q6^xQV3WJx@2!D3rLb87?q6WXs%Tqpf7kJwE8lso3L>eg+<;kL8 z!iRhi_I~FbGV?hlD=M)1q9jUkE;|O!?y>R&_}cvh=ho02)#Ez9C62{6&WJBA>b5aX zyI+RRtja%nHk%)%UG;2QS{P^3GQ;_^1$h-%koYT0X4JBfo>LM(A*}v{c>X77m=p|! zW}EUuA+lc%7pjghVr$Dejp815h_d3Q7&XL;O`nj`tlO2a(MKOZGA9C|VA|Q?vVinc z;i@W_O)j9hl)t-$fK|nX^64d)5NM%*5wC+|+wC1NFx2NK=v2o42Yp^Q;+I&g8xUr$ znezVo$h(DOp%{G;$<0tK42nl8FPE1KRhJAOJ`<`?N}1)t3$02SICnGtG^z-Jq&;A! zfEQTIUJ59buZ_`!Que_Ga+#HO_4#{)S?Wj8AmyVeZxpE@>NRy5JB?KSa^i#ECgd^- zS{1TdM2oHaAnN(-c4f-ifXbuxgBC2M4pGG?GXM5>A?M1Uno^r+!EIiWC8h1oZK? z8k(96b?DkMc|{YJr#qUU{X0iTcMkBz4lg<}%Km-&@1=jamSl$$J2?UHaUaZ0VmEv|(H7iYyFvqv)zkL)`Ae2fuND#Zb(?#QbFYBfib zssB0aZWWVr9$tq0H{6Xi9DjlXbmuK;k6Zn8=Zm~`NO(hP{{}7!S2vU2PmLVm zmT<8k^@Hx03mGH*gL+3e5VNsKie5>M+)08i66*+w8WYm~ZY%u-7wg1*!PJ8W9uk-J z5Wkv41rF7vh^Q|89zEqr_NfAjOGTPC$LtNUR4BqnG6>k6Dv${ozVW}F!ycN2^i z?Tp$C@ATGZwV?jcKd1v*Ee_^<5+G%2=WYOUmR=XVR9Q632);PW2wo~LBHpl1h}-w~ z^z>sk<7~J`oVxfu8^`8xJ#>bP3i6~T&J=zHI~PIfKj_?ysI|hPfLv4XdNs7Ra_+N7 zc{4$4AL!y3XriJ~dh8e;3Qh*4pd|gbpYa}Qiv{|J?M?$%@2MH%1pRO|2BHJ$EIskd zf=f>_QawM1BPxa(sV|Ivb2@+bR(pe~l@^a2rs4<^E~iejU9$|*2;9|ErnjljKFmqM zRIYID!=5SW-Lh)Mbt`KT<<_nJEh89&z&9khOhKq!WIuRk#S6385BbY7NJfy(=810% zeV?%hm};)m6|d$4HN)L&4W1$3nIo2NGMe0W$Kuu@ir@U)UH;pPg%nNGSl>LWp zN7UJ*(XHTNi}#mprnHjxvd=zp;3sr&Px?UY09D%LRw7#=CLTEGn{e%>p3&yMtwFjfwAQ}?vCtA_ z2~nO=ehlea5K?(p`B#TG(qP?S>oyNP=-S`8$4^hn|EYV+lNGwR%vs!lOQ_&kYC+dt zQSn*r#9yw*C#7U3Vri_57Q5BbNxdLU#-Lq}m``MPbeHyBzFGf}j3rWC)Q=@=E!VUn zUq`T^+mEFU_paGUl?YHryBi*7$Duv&_S;B;jYFmj^mQmG@eXyL)8ldaK%z(R7Py!L zz1xBvPJivxCO524r+dhv%xNEj7alb&QOe^aH|z=d3kP%cGl6EiNBP1kQD=*lqY>>d zNROq@*6*NCw7zP6gV8tGtBEK2Xnzz!BJfV3q0Mp!oXeCi2?#9)KdulCguyK;|BX!06zYz+y>xj4L83Uw z4a|ZS|FQljVH*ki z)ul`!fc_dEsM|mrdurob7_R`+M8^(Yzyc|rnpp+ z`XpDax+k9pRnw`woO+kkuu4ewXHL_gT@>cvd?U%HH$>t4y-=xDaI(2MU? z&hlS8Ne6a}j_&HGZ?1mN@$&(yw7epD{%zgQfAnKI7#R!>chUEkJu}~-e4efgtV?Xn zQsn}9os>29v~{=A%OAY`+WO0=H{IFIX?Uid_{F~*q9-ntGMnP-{Hy78Rm&H*e2*G3 zrmVT2`sFj8`~B}9qmR%0wN%9(OsyNF!J+6-Vt~GN_wP*qHb5U(zqA>!*#BP=i(U?% zfs#~nk=2pUdODY-F8V?{=)mQ39)5Y_oJaFWdt5q_&+A~GYyM>wUpG#WDr9WbHROc0 zLY*M!kjW=jaDZA$!dZ~sQ3+(YkWO*_yy)KLvxerpEz}5*GyX4}05xBvv!qQ&hywk| zO;tob2yF%en%Zs-wxdU1Cdr=OICl{7eLw59CEMxIn@H3eR7oTI^psOkk+=^_VH2_@ ztU|9ykLS~qlSM>@^8lXcbax^J5rSM!s51ogJuf8_a=L293R zwSWnTjNbv?c%=8NkL%<{I0LSTb$h&s!bPD<|B?)k08?h#yeMj%;?ALonNw#DI4?*IQ!_O_e(*bw0NRkC6CifN(p}) z2{C_=WcCbZ$M>SuQY%W=c-+bVe3X9B%6 zMM717ATm|HPxdT-)%@5VdTdYfwT;Kn4j|u2HXi$7YWW6=W)INha(+-1-|nI0DI|sTROt)Dz1BM5Y#{ur)U|KGtV3Hfs6?oM3K5 z*(A6Z!IOF=7VoX2ym6^Z$nXe~$ir1wa)tOT2pj^zF#WxJE{P_5X(XGJ^W_Ul!XOQg zR!}YC6NcX`FJ=G9cd7kKz~A(2h8kz_lNaYB_zl1kEtoTdK91+4mCUjGA3r6A|I_SO zF*nY-Ouh|eGNua9>SVA*dbN#8tJKmaE;8CkFsZUdO+`Fa0=Zig*dr6pxFbPV z9+5gyM*LBu*=X)CQ&7<;Uj?s+d@D>@9Guf23Bm4RYuF0j)JLkMZgU2IG?U7tx>NKL zUcwv4Ps0rt;OkE?^1ia2G#gA?;mv7`ivScp-1Rt>A9xw;OtGwsvojfD?JlZ3a5HHi z*xFswMK6)J6Z^KtY#kTlCGrwfdD%#RKO%U98EQN)FwcnYIi~o8d{U~no3}o)x;@Gw|RS$H=Ux5~VmV3?QZ8kSxYqoFR=K`ym+VZ9n zs_G$5xDy^WmG1{#3zkami_iA`ZjpH_XrGx`kT>+GSLHRy<;sejPC9~3?Q9%fqF<3S zA*;-k3btY%6?pXWg&ee=aUxCdhjW2s2Q-}n+{bdR0BnwK15y8y*M=naZ3Kw|G5l*h z=P%+7s#5Ctny4;CVil{_eFeT00cfB`dFnE!EnMm{CqCe9RRcf1%wefiKd8$bKi!{x z%Twehj2yjdXU7e=OYlU`J{EtZ0*48LQRfK=WDKEdp!${A!rxB$1l}#Z$_T8F=8a-!z9$Y< zn&ci($V4KzUv&^aYhOW*0m{V5HdWy0?3m61{O6SX0oH0vtRf#5S;unRbVcdrJQufK z#jD^LRP0{t@B;7_Lr!uspLVWLy)W#jS|J6qov%@_^ZHDmm!Ik5qTHQZ$xrCBxUhyQ z8pu^?nlpK&SVLJ>=AGKpuenc6fviL{lyZ}Y_lY-w{NWF|EE>8_^bGbyf#Znsb0s7e z3ZAH-BtOObtHSR^((4rOPpA!^;{6@L-biMtvqf>jT`gX|l%MO-i`-K>_Z4i>PbmMK zn>z3p6q{w-Ego>SNG=yx>uJnmEE)wC3%lR}WB20CGuUwo4{@G2kMON0F%GO`OWEYC z3Bz(61y2!(xtPR`W)F>l3}2Q?9KQl5axdGN?H{G>+ig499{R$~aAT95ysWucUW{sI zCt7J~YoaNPo&uevx-s08Xr;&3OHOB(C5HZR>!52WPgOJI8npJI1<=xkT&=QLS*%)A ztT9^ACx%U(!|}M<>F3dfj6~`5wwyAQU7WjCo9e;9*EpKI;p+W5k&vo>SxKS zQg7>N4z7%Pf=)WLfgJvT1f!9VSOB7)5FXnGV8!?D~=;vulIq*@P9b@QZ#uiD~T_Tgk6q^J>>i8Na^ zfx@u1LL-rn6j7QYo=xRG{_o7vTR!ONo>h8@D23|(FT7a zvvzwe?QbEyZJ_>vMmx3@FGIbArCr^&1a0Xj>-M%ByMh}OZCZ6uFs>w zbm=&mK5+mGDJmevzkt_7igH27TDhm!={$utU~0fq2!@H3E6Huoqevj=3$@J=C%N`o z!YTZUm`_$u@V#f-ylx$_+T0z?MA>(^61aArn93J){5W|J7;@Gf=B=siayw8wIJHne zh=V`{or1W9M-Owti{i}{le|^EY}x^eqC$%~mt|EV%0GN+D}A9^x|LO~GTaGG#;6?# zAGwn^u|?5mN;|=Y1lq-Q*_yGoz0?#k2GDkie$hLmK+F;2jsDmd4G;E!oo4TRYSjyj zzQCS3nEf&M1YeZcdzeGv?s%LI4Dc~4X7a#L9zJ|Jn~|9B?cJ7uz{_!VcqLm(S)H}M ztqpMzjD*c5QrzERrCh!1OKW=&AU@@53YOdR<@0%=x4fLdVw9a4e=(j3QF-oj z&GR3N^7yP@l&tbt&*XU(nA-D1Kn@c)i7hc!vW1i0{TSPRRf@-kgRL#7g)b3jg)9Q| zF&RgW7>4W7t!nsZVhf;%3OxhEb)?>IXh5s%^jhWfD!ow!kw3-@I*C*+-q_rKV-x?T zx@0Gm?*9_Kd)~NDw7eI~tXq9t1{5Z6_z($$QXa`1OSr@1w9@rA5j@6(ypY2JV0H8D zH>!Jhq9E-I_f&;7VArjEo0A(rD$6!owzi_%3Y9he=VK&w8=@6Jk)kf%Qb6dW9mTWK z+JYtTTvqA=8u>FRPIKVtz{gxvy7{EoJnP;@*sLNQnGAo#u$cwfD;x;d1t%%qjLTs2 ztb22f&FSOv1}S05dQ&kp5(fKhXI}Mn(gf*pF`hfLX@-3)uA_0c(?|FPBN*)eC1_$`C`>-VDz6%{(JbEe}`VTGxE)MXE5dOD7(A&zMZS- z{g)6BtyuwGj5rd$co6lxV`RKzw{tIS%{ zZ-NS2WJiiBzmR{y+ol&l#FIU`>A(XK8kv0Zu@_-%V~ha8abKi@KH%82maX9FITTX( zTB$7H^%B0!6_@@O5e|~gOZOp(A&|s^vqz+5c>tk%O~@xVNhPVibFaAKTt?4jSG>*s zfM@=`#5@Eny^Wlux1qOr*h3$7l5l5G^g=paZmMN)!K)(l&jJ~f5PIurmK^>{CT>%z#xOA^FDAmt{e-t$#30=Dm6|5xyZwqopp5MiiZ?zhImMFdO8PXp2Z_6V3 z`NCnMoHJpR&+UGl2D7~p5RuDCj;uY2%#t8vpJJqZSxI)f_$5|qm)wjvn|^PNb5_J(!&pvp`o4KyD%FrDm7#XY>JH#YQ;qU(z^t@a-LK> zO&Pom1~XOu=3>&+UT3etYjh$XM@{Mr1af6a9PFIFLh9&kN+Ob5%{Faft9ea>-TKyz z2Wn`_o@KBl2gcrdg-?AyVH2Bnt!A{5HCZ~^sKY`WNupe*OiB+ugt^5nrwDB;^wmaw zossQqG&b0S`uneOe}Q3hmYRDSg8crzbU9IQ=f(ao;x)Xf1@SPKi{8XGcQiI)WWam2 zHaEH!;$vh3tEu7iESrYwc6#Wu4g&Bc$~0Kp@eiXvU<7pVX6~XHQmO< zxjN$57=m54t6_9=klqm??k=>2p%fRwKa9S}kWxbR9DvTWB?TsPBhHk>)DuZ=8P+JA2PsXn)`hA$81JnQX z=cgoB09S{_`j{&bokNngKQAXam_o2zyq1~V_6Qw*WPD&dqubed^R@@*@B^6&b_1P{ zSD_r41DNEPFUk=)z}I5izZg!`JV9NkcR1tpoej@k!{!62>2(6hr(6o?WWeEfa5Phe zZm=tIYZM$#7WGF#M5A1W{v&tLD*%BXJ^SW6d#Dpa@+hX%++$eGuH{cm{^B=DG#K+E z(TlraQv`R!Iln(iF6cpM+l-8ibF(mDO|{XZHb7+nOPr2#NLpf)(3x%Wf z=qn`EJCHzvYbV>bl~>TtBdn&Qw$VVpzmzy!Zb2ivNDp3hrv>Z8Soij!EPZ$fUo^ZP zVrZx}30x296bvW$HG++v%ZzDTw0~{kCGgs`VVHWcI4&X&Zy`g5?pi)eXS+@u5-MPu zI!+q19ou<}5-^F%S}EY=0&eul)sRFey*v5u$@B{{7}~ZpG>Cg@P#Ld>hj%cl=`KZQ zOj2>7jQuNlasvHG0sMo+&)@S^Wdb8V7l;V*Dep<_)kmLwbr&68eYg%}7EiMy<3opO zCea%K)}l|uqc(&K?KhD)2qnWEP-M;JxAKwy8uS`BHyi3vVz1SI&t{HjH^)5P)HzJP z3p3I{JRN4c0QT$dJ+NELhSxmJUZm4!8Rx8I@(Q8_sgzx8M|)2#?eZnt2G-O5O}+=~ ztLe>Gk$03c0NL*3oC}L8h|_KBa-&FHk7`=|xbIN^E;_I`+1ur(J==HmU_F1EkyXlF z($Qzh1*oyyK_FjnzVtTEv3@1s^|nrDh%TBW&M-9WRH(OLnq~Lhd3}8iILur=D9V02 zzt*Z@-BezwPE)Ddj!a%WkxF#aa1Y0C< z`6?)~ydLyC`>@!6`2ilgJ4_o!e)K9kRUgI9@wEr&foESi@GqjSqrIFnsKscn>^LmK z+3Z7=+&Is#s=tL;A)xZXiR8{!MG*AYyIRD$`X@DP6pl9!-HU6%&=9Q7J5_3kfy=^0 z*UiS6$`Xm!6ZO+^KT$g+w&v~yTh*fxGahE*qC7Cbm7zp2tdg_W2)T%(=SC`PySS8@!&_!n3xxg@T8# ze1#Dh{n(rqq(wMQ8}%J%PPM=6^=2gzqlqj@vP!>YPM#T3*!JvG2$TjddHldWw-d^6pC z35jpo)3!5C-ybEr61}@^yJ+3}#JX!^??85`umIg%Y2D||ggX3jyfS-~z9mm|lXF>~%E$cmWC! zjrSsX!joUBJ1&4)UiyPOvF0sARcGtYX{hq0Qwc->oZ1WuR@Q)tjw$C8zqLyMw7i6U zzw*|_2o#hr@El9=Yf(tetiG<;1$AGtI9>Sy`x#{c#H(E(dZc4`G>5(^$=^d{gQVHl z@vjQ>>&Q)@Bzc29e|X!A;G54ScHj`GcZ}}r`WJ6Id_AMrv)fkI-@@Cq^(YU^yP7h> zf!l6*e&rhoKJa?L+!U{|)ujeqshqh$Vx}Yye4fDl9?@+L9|QddR^cod5QH{>*Z!pX ziKGa_38hSMeG95+nddZ$CUpKSAcb-cnt8{lP7=2=#TOKJ|W*PMPU}> zs#!#?ae;C!@wniHcJi(gKdNIZj_`ifbR7Afc)Ak>f^Bu=ZJz4-h4KyQ7IycO@Dj9k zmzUTRce7h4yXBs{Z|2>nzHRjh+hY0wE356X(x8tdoWTxw z@J*|U%VP3aBGerq##p0BAK)!kt2$NrHfPB2^)_@H?bPihXjKhD->rFqOMi8vTQ%q> zJ#jYHo}eB-u|_BK;Nhb|?Nya|Z&8atq%;<-_vbMc(5I84Yg{pS22{Fwm z8ybYr0!Az*uiy_=9izLSR3EDP+@HMrMU2=o=>oP$oqH)WLRcBLH_vhi6n|ZIa{`I! zA2IR=<*GjieIfMMPe>Y7#jG?I72L!A;LSHb$O|9h@ieuyHQ`n1XQYX>^>W_YFqvKg z>~66o(Q&G(8wiK*}bdI$jsoh5hx<_IdNtip=^h1)> zZ(&v+RqjNc%;ykR))uh?8vtoJBO+@a=mB7vEY= zdR-~OfPj;0Y^3w@japMYw|owtBWj=CtTyU$gE*QgfgAG)9GvE zmx|i|)z|Y>DjPiakNG84elBRy?KM7*2mt^1sL|BqhEOK}3E{3N^{0b_-~&beH;A0|DG)0uPnH&TOKU&WOt0s?w zV>C&yPGy7i(tQMSkSrRZFFi&$oFU?+&(e#i+(D5?cj6Ij#A_uIwNjj-%r~V zV3FQ%u;oeiV{{i02K=%=Lhr{)6jW}P%yX%2$dd>?Tki{kQek;G5y ze8uHBc`7k+lp|MP6b9}tcK<`xN)8bJTO!}9{6f0ia?|zfvfMlKOZeogA&1Sa<0>mp}Dq`^3DdHkG+JjdXe}WuO`a=+}zTppZwRH*ZO_kKY5?8DM>e&Z>P$ES70@$ z`CfM&X=|&2)uQE7Sg+ljCGMj`r8T;u+L~~?R+KE^#cu*k`LlciMIBrW7OK~XaqFtd z4mg2bC^~{-*&mCAqF(x3)1e=6f-957V#1X9e|TcqB7CNw9{S01gP1ZgHyW{agIE~{ z?tp7pH%@#3pJ;lIM`5e@%5nms?s4|$qrB7a_=-oDa>d3KD|lgh>7y%-oA4FSP_Le)Onmd|cBra1=Fea-G*)Wir=T%x6|W0WB{( zBwvPf$w1dYPao~bIEP|`G}05xB-1n$4u>K^3W>DHRz@#pH{8H3r_<{WN#S51;*C&G z(wPJcxjXFWLUym&vNg>sJ`u2bcTqDq70f>B>_pMXNm>0?Q*)BKf)0G94xw&Hy)Xe` z4;`?uEzM@?^6<|zXo*;&&;)ixJHtUbkQ_0w1`q&;T$olVkVd5Z(02@pC1ZFbJz$|> zPZXbNG#*KEi4WYD3%8tNV*{wSU0gS3T!Z>)&-C9)8a&zsc(f7lXtJJk4Ay5LRj!AU zaCPDADrKt?P1@CHGHdS%gC^+_67u2?FQRf%e?pq0T^QXigl(6Pkp5UE8yTSTUq4K) zR$8PJ;JAJbtk?=r1t-d-=h@@C5|2=S!kci#=|y0@z{?O4-Oi9pwu5sSTWlU{0NsiY zJih69C}RH2`%>-%IGOU-%DIyE-6x&xBAm^oo(*&Kix>uD$d(QF&~Q&S)&r(J3`1&g zTLw$}BW$-&99D-x{_<*)amNhqcIs+3*xSP(=zfH_z4+6IK_&L9Ln``W+w{yze6^sg7A2cSqnD51b9NZ04m$3$Zev3Nu*PO z>5xAUAJh!qn+F6-m>?nKdMchECn9`?kpb?HAWNhN>Q$hjFc_L)AFxr+L%_!$5PHn= zS(ne9!GuHm6NJ^zlddlEZDjZRVle#`xy!RvNM5&bg*>fZw^f&pE_7ABH1CsXo;6w@J=~xV+WrH6iYG`Hfm1KMo|aNF6D_)fMPH)q>v7{OpoPeO&dxKbs$k~i%lZnPz%3~y#O8|SF6 zC4g9*fr2a6P5{hO@XOJZ9HQ7$bR{b-$}tI@2VW$s&@GK-X@`R}H<@dAn}`!EDFJ$H zn1li$G@GM%tPrpedquV161OGs#{Phj{cLY~bd-*bq3`-2Zf*)Cbbb7gwJA>9PRB|! zdn}LApP7gJ)I4;wR$wBIW8D||;qrW98(xAi5i#tBU2t}orUFVzjzrQip};rc$UJ)s z&Z*yukw5|mt!Lo@9_*-Gm1rMfZUi1jiTn^)a@|AL0V9xKaDjEY-wp6~1~@QdHbyBe z#Hc_HO3RZ^3X5+fY|HBUJ31)RHvylY8Z)hlmKc4g z=kEAjLf*>P0-MicH$Rn+D{+Yd+8FE(r9w0nPKA>Q>`X8qTN>|2reI29I|1@wq0S)9 z%(7F{{U$oIq(|b<^px9p@QMv8RE3j%P0vG+RJ4VqIo+TObb}AKKWTf08`LM`4@2}Q ze;40FRty}_h3EN%qf{nu8BE^z!_`HiK1U+1fZscZZc=+eI08yf^3R4qDj%0hs`e^} zpeFjd3<1t~X7|W8x@}~41{KQU$5_QyHg4WbH*aoS$>G+;Uhjv~D^ahypRHZX?x$=6 zQKpq%(qZ+sCOT-MqtDwNr$2j!$Vjsv##`RyLxu+G86lbog_B?1LM2-RQe++N=Ye+d~8V{c;$&LX&faPtMYbe+3S)m%bc z{i6JTOR-2;Byo%C>CBAYHz&ObX3b`7_L>s(&O-zdBS7l3xbn`Lvj)@+cPh0cv}Icp z2237dnEoS!!5ws`T#Kc&M-`AXWU-E74K=W4IFkVf!0vZGNKBTNpe;jh1g%4veD3s8 zXju~Cvr&2JaKhby{sMKp8+;5vI0~ zGXdU;t^_|7m2baLn)QoWPZ0xe!mMA+dJ5&QOs8>fx%WnCwNg&nD{fg=(L>k8QSjES z5_fZTbCl-=s6i1P1d*pnR8>AFps!kKn!9iX@s&AG0G1T3;HD?Qiwa-kD4ERqgPUKb(Y{jA|Y%^Red0|Cqf~Uy8;J*l0EC>`2h|9JiMlTL8@ z&p=)X|MS0l6vSiuqoXiB_aJg+)j3D;6OQL(dUHL5(;vghx#uw_NSkXOauoPy`~*Jc z&0llOBeYA-J%&7;f>g1F1ep~j0+=SeB6YnhM4+qh7+>=^>HR}n>E*Xb& zsre)v607?wGxKt#t5eRZU74XnRG#>R_X@ve0#}%XLo~Bc_xa&36;A_xDlJeb$DZ&D z7v>S_xRug(HGljxPzF4&Jl9h$6TRsZY`+n$Ibj#sUYl9VA()M>Mq3LAh`bQ3LyB(` zYHTW1y*mixEN^B_vq(Gm5}KNb&j&HMPWrvZlAXF)$cTt%_13D6`xw1~k*X?mNr22$ z=yq`qQ`w1%DeAG^ViUfxgIZSkCeR(WAYWi#9(m!&5w7d8ef$2*1TS|@p9yRJFxyof zTupyjPqsOFpd5vwfTa}#$;yJO$@`b^#;f9qTfy7V904SFmCpf$p4?-fs2ioLCvIVD z*3cjP5IQY@=D8@A#!g74y#7-G9Ye_scp|8+jb`_Gha2f|LttH79RM%_g|dD<`M1)` zC=&%{_j}-b`l@_onnw2R9NiBeWd7U{a^YOV1J>snup_{e0TUoRscO9rtNUW|w@F>3 zb(o{8eoX4oJ|FMmRm6BK-HDF3Q!dz1mlh`i5IIA1hS{n94jI zbd~Z|!V4VQlbOUt)t8iSTqS>%c)BgIVt|zT{Z2+6W_#;WYv>~=mV+Ku{2tteiZjBq z)oVrLqGP3cmTz!--TqXtn;u4OJb7U6=))W~r{9YPirLJ_HF#q=7EoRHaF6<{I4MGu z>4n0iiX}qbQnYi~B?F*@YY*9Z;wq#jt`K3)BPsMy@#3c zn}Wh?F|P+d0!byX=QZW-O=Xr>)cI79o#J|mu)887rJlD8cqmw_cVBz$T}B^cM;gc0 z(D7Ad{2&=SaAfE)M&D(x@uaUNEO8&J8mc)!s}GXuRitK3RU?cH(!#GH9FTW1Y1K2t zFjU)JBk03aGO>fJo?vv0ZST$w(Sy&Ry+t~B7s)nm?-&zW$ts)Ns)K+m)v($QLnB>v z7caUU`iZn7yDPg#$Ug~c-7fk|Fir$rAc*lp_y`$bkWa-KJ`Ic46>TXk z@Q(PzEFw+7&vyo0$f2hk=xPKTMZ4W(b|97R5lQz-w3%XhJcWvsCycDPIf_n1R62WN z=~R@4gOJz4AV&1Hj-P(I63G$ZaCzrQ(VK<@LUETR5FGDfpHIIz|86TE^8!T$Uiw1v z6^{Nb3YuZwm7FzR>@|z^`j))|C_@rV56>snOIN zrk9W0-g0{jy|v}m=8MOuA#BM4Lm|H>UyZcahJz1^l8E|d?^$(-i%IX{uP_9$Al3uc zFJyK+qfG;K0Sa1ak>bWUcmzf$E2y!l{Ms=IFLZztb00)D2nQ> zc6oUX5`}y<#XcPD?}-d?dA47Pyf|x1autDJ*Ss+BLO9u4H zVS4e{tG=z)@J!@pCvkS|=U#4mJntsQmgd35_do^{6pm>gwpioqK}*5RDA=_Y4PU zs4E}_auspN_eyoarUB0g?F``*Dv5d44Q!bovPO_A2I2s|60ZJ6m=ChY=cdlamiC6c zbP0w$6oSYR&2VzXPH^{tls`i#^MwP5~ z*rKhoLl99ZS#q(6mF$i!MP@ONCDiUk4a;#aYWAPsi)_Mz$^9w)h1HxeY$-PGjqf6n zE6n_ydn;!@Euo`L2();(1F3&d5wj~$VgtQ+?9ihjNNwiY}g-k4mB zt_?!BlQ-L`dM>e8Lac^i7FPS{VA8vIV_ieAZGaaZwhs=WRwtV1#J*USzC8WEq*&V1 z*Ab_tSS!>g>D}KY3cSzam4+jku7roAe6IAHmr=r6N$jRzYqyQ2?R~*`H+}LgBI6Xg zpQZ+v3QqA$Ugg3miodxDKF49G+;Ik39sRRmQ^N*EfrZ!A)V-jg_fo$Nbv-1|I>MrP ztGc;%ODA4;e2FQ|d17;nJF{eDXXaJU8R;VZTzq3CW3X^)&cmnje&}rB^y%jcfIn*D z?p*Orad)nQ9H{`4DPh&6E2MmzUjBP6Uwu%WauhGml(f3rlxVIJbIS-cGtPvJy4pKh z@Kw29*(=Z2?lNj!0BL`x4`@mH%B8#`SuY08cEl8%3x6aKrZ>LNTcY(MAfQOfrQJxW z#)6RdaM1&s$bnrX7KMO@W3~@a3Gt(eDCPl^l%2AfwFr6KcDsi(P z0|9GvwhvC1%?ExON6>|KBm_=49-YhGChF;69@UQ#X4c&WLn&RHw@EI3P_ z(Ow|nuH^LYrxL(vR8($N1(GixHcHisSzy z?@hqtsLpiZ{4<>7n?RJA3^hq+GIs)G0~40mkbr?e%r17D#dzNrS&}8S)RKDNm(%;c zXlpH&Y{~nA!I;%yiAaFpgdq;eFeEc1CX?I&Zt3~U_kaHPRCTq~vT$rd=Faon@dUf8 zPF0<%I(5#syx;qZ&7lJLOar0d0yqkmR~A9co@ht zZa$=6Li>=l8u0f;jf0p}Yjz$Q7p!-hCionEA{fB=E|~7}yr+V)MR%&$3SLt;|H-X! zNqr^}b($Ki;DiB-x3Y^v_=ImM^}#6?C=q?|wTPm=>d*H*bbJOyO7VOaeH^V@^qz^J zpZ?q?4Dd>k$<}S0aTpNi0`Aq_HO#v^1(sB;ADF@HSx{y6$Od8e%+@C5xAkxkhDJFz zrJUsgb!noS>JJ}z8QLgTgbF`k5omZ3QmO#JXKGYF*~D4kDgAStSTUF?bbQs&3XS9q zU+??!(-J?7Qi)hPg}BSv#~4ICgJ4d7bO{JwLr$NEu7Q5L$La9eg?2T&{4!S9=vmpg z83-Zeb2i*mYtSz{})>vP*FDrm0YMx6qFSX+*&s#Gwr%!tBkzGIHuj7~*xiG%F`_}I-Bfb_R zuGEH+KZ;JlJ*Xlxf>DpbHq&Ob#%H2_*1H6)^LO>ZsgJ@c)+{+SUdjGf?a&bbzn7nt zpkEI=PB)C|^eJ2ZI`~B|VVhc4TfqcSrik!(>ekm^R+FVM+4ar9`N)H7+- zjOr*4q|H%)2XNN5{ypO5=xnnqtZPs>xacva{6C1BpGhfP5z#n($XA{p#<TnL%Fgg(?&;>PqK(xtD$aeYu${P!3@=o z3si87N^+_axQbdPeKG^ZU%PCG=x(D1`jIa4li<`9e@!gfI#9?lj7#~{=N zS$6e@8xrX-M8`I;rAyfSBdE4jH^V1$fU@Pte70mM+c3sF32b(gy4fQlJUB_YO&sZd zBr9MWJi1g+f0f&a&51ZY`>d$B+qi+1FBEs9bwaepNyHCODWcRNjg$yF1>G22vV6w! zW^@XhMQiOsSOx$n5Up{N1P;ZIa`!fD6wGnBB)C+>8O!&GPAz>@v@V}fxenhH&WZlw z?r_A*w}_wUCzOlC&zGxymR^>$?OK}}1iS%=T&0R~t8oi8SR+aRL<|PHsEbfGhzU!B zObS0<&+4sW!@^Wm(cDiy2qO&&NkFT87gQ>kC*X}};b(ni2Ve^a&$wN*e;w^(Sp>_T zd=javYz*QXI6V?=MB;a0$w1gRVRa zHZhx(9$Kg>4uL^p#XhVc2Arm`0luvGzVwv@7W~SUdNY`wDE8{j<*@Prg_q41p%$bT z=4sO3qSAZ;TsJ)~uW{d(dGGnA!RBq8<|yBg`uiEbiSfp4Q>mSPd>4y*qwbK4va?yF zvuNu^sd*%ecs5zzX8r+NUuS{VL#ae27WJ}j!>|>hfw9SHS+mWNesOST)RUx+5;Kgf z?^??t9ray}V~>HcZB`w#`@HsugQ9i-#iNu)9V&)&)k^|N-JFVZrR|x( z5Umb22re|xCQoap1`H6&spbO^){@v$eFiQMGW9IVAUq(!ReSWLCO=}j1gjw9F0*SN znc@Z?)!pDa5rR<{8Q~Bk4k8$z%db)9U`fXc9yEB{+?~{)?dp#WK_!<<(WWj#zm42o zzP>;|ZQ|_p1ya-z^@Rfz4kJvOrbG_ zP}FZ7Kn-TB;K|UU-vI!dO_#EiOG37^NWqb4p)f#*z!Y71G7tC)p%DVpS>BqY=DaOq z9VA!Yl0;E$L(m*;DNw6YbIJ-q6i*ygqrMFz>oEa7FVOxbH>h`%S;|FN9Q{sJv?}64 zo*x;Pi*)KAfw_f=s>fuNgLg|vo+ETBR$HuHLJk-8xaE`ymS1c}T-6L>iL!DnRzz&p z*&jixiKPJtVskmEf|>kr`6VIcE$s0{!pwKQSD}9Qfr=f0)xOq3Gbc>iDk4^!gkS*{ zGJS%b=$=)pZuee~SbQZ$S*x4rxIUo$D2)8k{%4;XqLel7!VHf_T|A#m<>&$o}aFK>E6A9eJaydGQ<4SAD8A~HS<=py^X|8h-k0&Mb%<> zE>_-E9UJwAaq|;%5T=>(3~DCsAD8$Fi?Fy|cB{vN*M`fXP$v#;g4S);-2!_5r9ak=4c~{PnvD03mAFKw0xvi$DQYT0tu z-H_e@1}{)-YG80>KC9~h@Y&R9M{EVzbbAk!pfXekV7@$G#1iEzC+ zTsUI-k0Mi(2n%4w!UxfO3YBxd;($P6SQNz`pI9IgMQYAW=pEGeHVdO!O@BT3sV{)DaCWT*2OITu@Hoh{7o2j9 z#H@SD{X+L~0TN3wKh$c9Eq$wUODVBA+K727lgcHt3EJ0xAU7OG&MFSTIN@zUNilHnAMAdQN=ocDlbpMi3yZOb^tv^kvELb-!|dy4~v zTAD}{$vM1P49UpE-$?1azt@r>OWNqSTj)I046ohIs>#d7YgxjR5~2#}P%;W4DTI!0 z#A1qipz5Pxu3Op7uTDGs_^j!NFOa)X7%_2FMEo@d{tn*hu{sF#NCS^OKK86iC7_mW zYg7WrJ8>k7#(#=ppinpzqMkyoAGLZX;(jRx{YrqB%Vw23W}}!rC|G=yJCr(@xOCdY z7{ru2(kV|30g553*tWl*iY| znThX6!MHDqSNO5s5Qh^1W~m30EBDwAqXgR>bBAd`a`_|9r$zRCp+cwZ*q?r5=fX&x zmM5q?SubG}gJd$LD+DzW30K5lC^eH)C-8WyY+nx9fXP`^b9L%O>y++Qs0Y`o+MI$= zPlR@l>d`Y{CS#UYjRN(m8j~uvR{XmZ8?T#Ekt+h$x>-D0iFaWmW?r#c4qyOO4_V=| zRu66A7mMk4TDz~D)DBzZixI$_R>)ShRGqjO`gqmnuRia41JjF>s+{L%RaL+#A5`_x zXG~h9)X4v@Gz94A5KAUemG2@{UfaOC*o19O&`;)8mm8I7HV#(DrXhyS>h-Q3w~I1zVUi=8i)XqV8Fy{0$Vh^dVfGx6{6?nu)*! zN##n%a$V#3H?s}#)e%I5)!clY2$hSx&ZaGm2aUx4b9x{fG5#^{H?*g_j zygNQ7qU&c*-}e=Euy@Na--dlS`@Z_&hr8+JgG_ngCaHD>h+)*~%5p8H{Fg^r!JqSG zy|mjLHwzKIpx?B-omO|=XTMGG^xT(O-9^iL{6(;h@Jzh0yFhyiN0KjdXl8c>i$Ube z01=-f$+IDrqGk_7bak#GEmCIVPG@rkCiXG(1zV5F(jl}1rdpU`X;<&!UTROa#+zf* zxSY9dJ{33H8SfM{^mb$oh7x@#daO6u%TYsg%XN}#sHF#GQw_YO-PJ@rfxciLr#{{n z>H)dQt@78TW^b#f9hyo;mr($P9Z4gKY5SZ*?yX)*XIM5DSLsjlK3FT%cz`{dl!87E z01;|bRTg#KUX6lM0`e{4wvbu~)fO}csAi%O=3F|I_6Z(tchnJdkndsZbq(JG63Zv5 zx#v#DmxIFr->=n;hHjq+2v@+>1Awbv)G!T*jJNT^oR9pS?Y~I;30ha8RA5GV zF6A8Wv?=(|^ci>&R+3q#(dbpU1}OgWx}9|Q^~~i6n?OgEb``_PG(GklD?G#^jufiC z2no&Vxf-y2)6>jbuw>w4!OlcEg1$I?EE;4RA(WfaxX#DN#p5ojwE%!7Iz?>(e+yq= zm5SdL%+L?lT5aG2k`b;tY}%Tg?_&6@LPsqVh~flhU&052rxL9ht)x?WBA4wL^%=o5ZX6qabE zg?OA=BDmZJ4{#j))ct&v4GZC^P22QXK{@vD+B_YkId5Jiw4gyWw{2qz4pC#q>PI#8 z4rRbdyWCwM9rL{ z5cW&i5gdT6qmAk*<(U}OSmdW*nlZ-zc%whV;8ecSpyP0~>rXL5v`(tMo^=|H+lT*&1nZzW}n2 zP6dTD&BjqSHpz`v@4k^o}@+@0hi|!%z>x6ubDjp=#KUYkDxo@$4145%yRV!-8;rTv}rw`_Au)u?A z1_+9nH=-g>f#^d_<$?Q|g2I!q%HB-J-(^UNmXkJR$_b=FUm9nzSUi$|$#cUz>7rQ- z-m?L~XWo)F6ajb7LeJrc1%~4X$i?4Y%eHUSQ*BloWPGx;aXXuJ5rdf_sO)8Q{OPAz zG#1fH7yNNw47?S%47AE3ZVq-w5m17vHdfzvkF$l3GaIsR*&OAvNK4u1@r7*sCDv8! zPisAQbeZ&yU{_ZtDyNJ{I zH%rb;X9x-!hI-3dQMrgCTl_8F7DBPUsmKY?=R_E3m*L0=xD~~bTT+AhKG3;@@~#xU zZ?8l|apXGhdgprbSu$o^6X4BwI7OK~v3;Uo4o8mk_ak*V!ut)XC<)s8*SXh`-;%Zh zGLhj)ai!#*=$>+WEjBuU^`Y8^!Ikc|-_tgqJZ9zvvv?LoGv0h0lu_{_8+al6lHmJ9 z5^JY?8&sC|j6F5-1M#Y!YS_Dx_H5j;y0Dp|;CDxc$h>*;>eU-*)rJQfYX!g110$=) zs556N*a~#{1I*?&h3#o_qyxo7jvhV6mK zrUgK%LK$bCwY)|!U21E9NY_W8R1K5}MpK%OfE;KG@44>)YZx#NnCJ$$9rqn3&B%9Zgj%BI1bJq~c5karq1hMs) z&El}d$dGQ10m#$Do4p1H)r2R$EJCA6U=2h>2v`P}2#wnO7Ev)g%T+8vQy` z>g0_uIsJKDU;f}y>ChpjJjU6fMNH@$*Gw#shorin-tgoVDy@po=%HW$9My1^zlV7s z?La?JrxuRIs8|HN17w9_17H?~^=(GLRRkjw-Kka{`T>1uZ50_fRcHYPFxr^H(y2e1 zI(=J2FmNU#Nak;hyn2Jzk2%rRkU~Q+-r-Q;FOea8s zabf`oR?dJcrRG^VB*EN|dvIzli2l(LKLQwA>#;4Oc6|*KWF6R+w^X))L$T@;5iHEf zsDL9Je6(^0=&%;XxJqCr)jDK!-52#w>G}?}>tOH@oJokTlCc*pz%30BAs#dm0)YGK zj|91RtTdt$0r{tk`PH>8V8Xi?YGFHEqvjFN+^Nfs#Zr0d>-BB|^%~(}IJgwTsc~{d zr_by`9hSc>pi>VYZ$UCG&_d0g8lMk|v z0?>EI;}643f-TPDkc@ARH42Sc<-8fFv8I0ipI2U#R@;`k7js(7mnBwZ$riOau)LP; zdT!Y(oL=G2JyLpsVp&_-23k7t{2;@n)`W@MuTq*5&#!*#8cu73C|LRVO>a@i{wAx<&D^_qXJA( zbIbx-eZN7FzA0cEL!p~s8~eO`_ZfVrwV)Kv@tjw6@eyBRSf2$_5-Yr3b@9Nh%Uf*( z1xS%sXwBZL)*EScVQ8h?V@6o;qa0bj>`Bi>#z4T`;9k@WL4`SO${n83Ow=V`EkzTl zaKcB&R)HrusxlRNd>#?1Ud7x#XV?c;`9wGskI~pL+c?ZTu|Ntr6S>$XZER#?jlfNY zon8>ryPW}dl;-YbUZ?>GdnO)@gKqiQUQ{Qf0^kRWxdTp*po5+`GI6=Yo7!8O{j}K4 zP-iCGqQqk?GFa*XYJXVxCXEoc+9-Ke>|DD)PP^mm$&1X&XRo@7uDVM3tofoRDZZP< z_pjZ#!b5Ak?7?>n@@LH(61zTF7MPA`PFNM0geFNKocgey$=?e)8;@!KqbRtOeEeF;Dz=wF6eG8|2U)-9NH z#b8!0_p#^!rX-bP(qjph4yByP6u1{Ne{*X)Oy)*z=ocR1P)`Z<$|sG20snxzL_NFO z9$N<4h3Dq&x|##1FmLUWduW-bb#c#Ha<}?h#7y*&$9|n`l46-6k3n~O1$)fRGJ%-I z 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

$38w0b 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 Date: Tue, 9 Sep 2025 21:10:15 +0200 Subject: [PATCH 03/14] Implemented code editor window Includes live compilation of expression code.preset-editor Also fixed some issues in the editor control and made Milkdrop expressions case-insensitive. --- src/gui/preset_editor/CMakeLists.txt | 1 + .../preset_editor/CodeContextInformation.cpp | 7 +- src/gui/preset_editor/CodeEditorTab.cpp | 92 ++++++++++++++++++- src/gui/preset_editor/CodeEditorTab.h | 24 ++++- src/gui/preset_editor/CodeEditorWindow.cpp | 53 ++++++++++- src/gui/preset_editor/CodeEditorWindow.h | 16 +++- src/gui/preset_editor/PresetEditorGUI.cpp | 47 +++++++--- src/gui/preset_editor/PresetEditorGUI.h | 10 +- .../imgui_color_text_editor/TextEditor.cpp | 72 ++++++++------- src/notifications/CMakeLists.txt | 8 +- 10 files changed, 271 insertions(+), 59 deletions(-) diff --git a/src/gui/preset_editor/CMakeLists.txt b/src/gui/preset_editor/CMakeLists.txt index bfcdc30..a191f3c 100644 --- a/src/gui/preset_editor/CMakeLists.txt +++ b/src/gui/preset_editor/CMakeLists.txt @@ -20,6 +20,7 @@ add_library(PresetEditor STATIC target_link_libraries(PresetEditor PUBLIC + projectM::Eval ImGUIColorTextEditor ImGui Poco::Util diff --git a/src/gui/preset_editor/CodeContextInformation.cpp b/src/gui/preset_editor/CodeContextInformation.cpp index e696def..6e2115b 100644 --- a/src/gui/preset_editor/CodeContextInformation.cpp +++ b/src/gui/preset_editor/CodeContextInformation.cpp @@ -926,8 +926,13 @@ const TextEditor::LanguageDefinition& CodeContextInformation::GetLanguageDefinit TextEditor::LanguageDefinition CodeContextInformation::PopulateLanguageDefinitionForType(ExpressionCodeTypes type) { - auto definition = TextEditor::LanguageDefinition::MilkdropExpression(); + + if (type == ExpressionCodeTypes::WarpShader || type == ExpressionCodeTypes::CompositeShader) + { + definition = TextEditor::LanguageDefinition::HLSL(); + } + for (const auto& sourceIdentifier : GetIdentifierList(type)) { TextEditor::Identifier id; diff --git a/src/gui/preset_editor/CodeEditorTab.cpp b/src/gui/preset_editor/CodeEditorTab.cpp index 16399eb..d6df750 100644 --- a/src/gui/preset_editor/CodeEditorTab.cpp +++ b/src/gui/preset_editor/CodeEditorTab.cpp @@ -1,13 +1,103 @@ #include "CodeEditorTab.h" +#include "CodeContextInformation.h" + +#include + +void projectm_eval_memory_host_lock_mutex() +{ +} +void projectm_eval_memory_host_unlock_mutex() +{ +} + namespace Editor { -CodeEditorTab::CodeEditorTab() +CodeEditorTab::CodeEditorTab(ExpressionCodeTypes type, std::string& code, int index) + : _code(code) { + _tabTitle = CodeContextInformation::GetContextName(type, index); + _textEditor.SetLanguageDefinition(CodeContextInformation::GetLanguageDefinition(type)); + _textEditor.SetText(code); + + if (type != ExpressionCodeTypes::WarpShader && type != ExpressionCodeTypes::CompositeShader) + { + _compileTextContext = projectm_eval_context_create(nullptr, nullptr); + CheckCodeSyntax(code); + } } CodeEditorTab::~CodeEditorTab() { + if (_compileTextContext) + { + projectm_eval_context_destroy(_compileTextContext); + _compileTextContext = nullptr; + } +} + +bool CodeEditorTab::Draw() +{ + ImGui::PushID(_tabTitle.c_str()); + + bool tabVisible = ImGui::BeginTabItem(_tabTitle.c_str(), &_documentOpen, _nextRenderingFlags); + + if (tabVisible) + { + _textEditor.Render((_tabTitle + "##EditorControl").c_str()); + + if (_textEditor.IsTextChanged()) + { + std::string changedText = _textEditor.GetText(); + + // The text editor always leaves a newline if empty, check & clear if that's the case. + if (changedText.size() == 1 && changedText.at(0) == '\n') + { + changedText = ""; + } + + CheckCodeSyntax(changedText); + + _code = changedText; + } + + ImGui::EndTabItem(); + } + + ImGui::PopID(); + + _nextRenderingFlags = ImGuiTabItemFlags_None; + + return _documentOpen; +} + +void CodeEditorTab::SetSelected() +{ + _nextRenderingFlags |= ImGuiTabItemFlags_SetSelected; +} + +std::string CodeEditorTab::Title() const +{ + return _tabTitle; +} + +void CodeEditorTab::CheckCodeSyntax(const std::string& codeToCheck) +{ + if (_compileTextContext) + { + auto* code = projectm_eval_code_compile(_compileTextContext, codeToCheck.c_str()); + if (code) + { + _textEditor.SetErrorMarkers({}); + projectm_eval_code_destroy(code); + } + else + { + int line{}; + std::string errorMessage = projectm_eval_get_error(_compileTextContext, &line, nullptr); + _textEditor.SetErrorMarkers({{line, errorMessage}}); + } + } } } // namespace Editor diff --git a/src/gui/preset_editor/CodeEditorTab.h b/src/gui/preset_editor/CodeEditorTab.h index 1451f48..5bd2e8b 100644 --- a/src/gui/preset_editor/CodeEditorTab.h +++ b/src/gui/preset_editor/CodeEditorTab.h @@ -1,15 +1,37 @@ #pragma once +#include "ExpressionCodeTypes.h" +#include "TextEditor.h" + +#include + namespace Editor { class CodeEditorTab { public: - CodeEditorTab(); + CodeEditorTab() = delete; + + explicit CodeEditorTab(ExpressionCodeTypes type, std::string& code, int index); virtual ~CodeEditorTab(); + bool Draw(); + + void SetSelected(); + + std::string Title() const; + private: + void CheckCodeSyntax(const std::string& codeToCheck); + + ImGuiTabItemFlags _nextRenderingFlags{}; //!< Additional flags to set when rendering the tab next time, e.g. activate it. + std::string _tabTitle; //!< The title of the tab. + std::string& _code; //!< A reference to the preset code. + TextEditor _textEditor; //!< The expression/shader code editor control. + bool _documentOpen{true}; //!< This flag holds the opened/closed state of the document, e.g. becomes false if the user closes the tab. + + projectm_eval_context* _compileTextContext{nullptr}; //!< A projectm-eval context for test-compiling the editor code. }; } // namespace Editor diff --git a/src/gui/preset_editor/CodeEditorWindow.cpp b/src/gui/preset_editor/CodeEditorWindow.cpp index b6e8003..251aa5e 100644 --- a/src/gui/preset_editor/CodeEditorWindow.cpp +++ b/src/gui/preset_editor/CodeEditorWindow.cpp @@ -1,13 +1,62 @@ #include "CodeEditorWindow.h" +#include "CodeContextInformation.h" + +#include "imgui.h" + namespace Editor { -CodeEditorWindow::CodeEditorWindow() +void CodeEditorWindow::Draw() { + if (!_visible) + { + return; + } + + ImGui::PushID("CodeEditorWindow"); + + if (ImGui::BeginChild("CodeEditorChild", ImVec2(0, 500), ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeY, ImGuiWindowFlags_None)) + { + if (ImGui::BeginTabBar("OpenCodeTabs", ImGuiTabBarFlags_AutoSelectNewTabs)) + { + for (auto it = begin(_codeEditorTabs); it != end(_codeEditorTabs);) + { + if (it->Draw()) + { + ++it; + continue; + } + + it = _codeEditorTabs.erase(it); + } + + ImGui::EndTabBar(); + } + } + ImGui::EndChild(); + + if (_codeEditorTabs.empty()) + { + _visible = false; + } + + ImGui::PopID(); } -CodeEditorWindow::~CodeEditorWindow() +void CodeEditorWindow::OpenCodeInTab(ExpressionCodeTypes type, std::string& code, int index) { + std::string newTabTitle = CodeContextInformation::GetContextName(type, index); + for (auto& tab : _codeEditorTabs) + { + if (tab.Title() == newTabTitle) + { + tab.SetSelected(); + return; + } + } + + _codeEditorTabs.emplace_back(type, code, index); + _visible = true; } } // namespace Editor diff --git a/src/gui/preset_editor/CodeEditorWindow.h b/src/gui/preset_editor/CodeEditorWindow.h index e2078b3..04c3ebb 100644 --- a/src/gui/preset_editor/CodeEditorWindow.h +++ b/src/gui/preset_editor/CodeEditorWindow.h @@ -1,15 +1,27 @@ #pragma once +#include "CodeEditorTab.h" +#include "ExpressionCodeTypes.h" + +#include +#include + namespace Editor { class CodeEditorWindow { public: - CodeEditorWindow(); + CodeEditorWindow() = default; + + virtual ~CodeEditorWindow() = default; + + void Draw(); - virtual ~CodeEditorWindow(); + void OpenCodeInTab(ExpressionCodeTypes type, std::string& code, int index); private: + bool _visible{false}; + std::list _codeEditorTabs; }; } // namespace Editor diff --git a/src/gui/preset_editor/PresetEditorGUI.cpp b/src/gui/preset_editor/PresetEditorGUI.cpp index d2e0fb7..675758d 100644 --- a/src/gui/preset_editor/PresetEditorGUI.cpp +++ b/src/gui/preset_editor/PresetEditorGUI.cpp @@ -71,6 +71,8 @@ bool PresetEditorGUI::Draw() return false; } + HandleGlobalEditorKeys(); + _menu.Draw(); const ImGuiViewport* viewport = ImGui::GetMainViewport(); @@ -81,6 +83,10 @@ bool PresetEditorGUI::Draw() if (ImGui::Begin("Preset Editor", &_visible, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoBackground)) { DrawLeftSideBar(); + + ImGui::SameLine(); + + _codeEditorWindow.Draw(); } ImGui::End(); @@ -110,6 +116,32 @@ void PresetEditorGUI::UpdatePresetPreview() } } +void PresetEditorGUI::HandleGlobalEditorKeys() +{ + ImGuiIO& io = ImGui::GetIO(); + auto shift = io.KeyShift; + auto ctrl = io.ConfigMacOSXBehaviors ? io.KeySuper : io.KeyCtrl; + auto alt = io.ConfigMacOSXBehaviors ? io.KeyCtrl : io.KeyAlt; + + // Reload edited preset - Alt+Enter or F5 + if ((io.KeysData[ImGuiKey_Enter - ImGuiKey_NamedKey_BEGIN].Down && alt) || io.KeysData[ImGuiKey_F5 - ImGuiKey_NamedKey_BEGIN].Down) + { + UpdatePresetPreview(); + } + + // Save preset - Ctrl+S + if (io.KeysData[ImGuiKey_S - ImGuiKey_NamedKey_BEGIN].Down && ctrl) + { + + } + + // New preset - Ctrl+N + if (io.KeysData[ImGuiKey_N - ImGuiKey_NamedKey_BEGIN].Down && ctrl) + { + Show(""); + } +} + void PresetEditorGUI::TakeProjectMControl() { // Detach playlist, seize control over projectM @@ -132,8 +164,7 @@ void PresetEditorGUI::ReleaseProjectMControl() void PresetEditorGUI::EditCode(ExpressionCodeTypes type, std::string& code, int index) { - _textEditor.SetLanguageDefinition(CodeContextInformation::GetLanguageDefinition(type)); - _textEditor.SetText(code); + _codeEditorWindow.OpenCodeInTab(type, code, index); } unsigned long PresetEditorGUI::GetLoC(const std::string& code) @@ -188,18 +219,6 @@ void PresetEditorGUI::DrawLeftSideBar() ImGui::EndChild(); - ImGui::SameLine(); - - ImGui::SetNextWindowBgAlpha(0.5f); - ImGui::BeginChild("CodeEditor", ImVec2(0, 500), ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeY, window_flags); - - ImGui::PushID("CodeEditor"); - _textEditor.Render("Code Editor"); - ImGui::PopID(); - - ImGui::EndChild(); - - ImGui::PopStyleVar(); } diff --git a/src/gui/preset_editor/PresetEditorGUI.h b/src/gui/preset_editor/PresetEditorGUI.h index 658ae89..7df9d8b 100644 --- a/src/gui/preset_editor/PresetEditorGUI.h +++ b/src/gui/preset_editor/PresetEditorGUI.h @@ -1,10 +1,10 @@ #pragma once -#include "EditorPreset.h" -#include "PresetFile.h" -#include "TextEditor.h" +#include "CodeEditorWindow.h" #include "EditorMenu.h" +#include "EditorPreset.h" #include "ExpressionCodeTypes.h" +#include "PresetFile.h" #include @@ -45,6 +45,7 @@ class PresetEditorGUI void UpdatePresetPreview(); private: + void HandleGlobalEditorKeys(); void TakeProjectMControl(); void ReleaseProjectMControl(); @@ -83,7 +84,8 @@ class PresetEditorGUI PresetFile _presetFile; //!< The raw preset data. EditorPreset _editorPreset; //!< The preset data in a parsed, strongly-typed container. - TextEditor _textEditor; //!< The expression/shader code editor. + CodeEditorWindow _codeEditorWindow; //!< The code editor window. + }; } // namespace Editor diff --git a/src/gui/preset_editor/imgui_color_text_editor/TextEditor.cpp b/src/gui/preset_editor/imgui_color_text_editor/TextEditor.cpp index d644160..c692b46 100644 --- a/src/gui/preset_editor/imgui_color_text_editor/TextEditor.cpp +++ b/src/gui/preset_editor/imgui_color_text_editor/TextEditor.cpp @@ -10,6 +10,8 @@ #define IMGUI_DEFINE_MATH_OPERATORS #include "imgui.h" // for imGui::GetCurrentWindow() +#include + // TODO // - multiline comments vs single-line: latter is blocking start of a ML @@ -1059,11 +1061,15 @@ void TextEditor::Render() } // Draw a tooltip on known identifiers/preprocessor symbols - if (ImGui::IsMousePosValid()) + if (ImGui::IsWindowHovered() && ImGui::IsMousePosValid()) { auto id = GetWordAt(ScreenPosToCoordinates(ImGui::GetMousePos())); if (!id.empty()) { + if (!mLanguageDefinition.mCaseSensitive) + { + std::transform(id.begin(), id.end(), id.begin(), ::tolower); + } auto it = mLanguageDefinition.mIdentifiers.find(id); if (it != mLanguageDefinition.mIdentifiers.end()) { @@ -2194,7 +2200,7 @@ void TextEditor::ColorizeRange(int aFromLine, int aToLine) // todo : allmost all language definitions use lower case to specify keywords, so shouldn't this use ::tolower ? if (!mLanguageDefinition.mCaseSensitive) - std::transform(id.begin(), id.end(), id.begin(), ::toupper); + std::transform(id.begin(), id.end(), id.begin(), ::tolower); if (!line[first - bufferBegin].mPreprocessor) { @@ -2962,7 +2968,7 @@ const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::CPlusPlus( const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::MilkdropExpression() { static bool inited = false; - static LanguageDefinition langDef; + static LanguageDefinition milkdropLangDef; if (!inited) { static const char* const nseel2Keywords[] = { @@ -2971,17 +2977,17 @@ const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::MilkdropEx "_if", "_and", "_or", "_not", "_equal", "_noteq", "_below", "_above", "_beleq", "_aboeq", "_set", "_add", "_sub", "_mul", "_div", "_mod", "_mulop", "_divop", "_orop", "_andop", "_subop", "_modop", "_powop", "_neg", "_gmem"}; for (auto& k : nseel2Keywords) - langDef.mKeywords.insert(k); + milkdropLangDef.mKeywords.insert(k); static const char* const identifiers[] = {}; for (auto& k : identifiers) { Identifier id; id.mDeclaration = "Internal function"; - langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + milkdropLangDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); } - langDef.mTokenize = [](const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end, PaletteIndex& paletteIndex) -> bool { + milkdropLangDef.mTokenize = [](const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end, PaletteIndex& paletteIndex) -> bool { paletteIndex = PaletteIndex::Max; while (in_begin < in_end && isascii(*in_begin) && isblank(*in_begin)) @@ -3005,18 +3011,18 @@ const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::MilkdropEx return paletteIndex != PaletteIndex::Max; }; - langDef.mCommentStart = "/*"; - langDef.mCommentEnd = "*/"; - langDef.mSingleLineComment = "//"; + milkdropLangDef.mCommentStart = "/*"; + milkdropLangDef.mCommentEnd = "*/"; + milkdropLangDef.mSingleLineComment = "//"; - langDef.mCaseSensitive = true; - langDef.mAutoIndentation = true; + milkdropLangDef.mCaseSensitive = false; + milkdropLangDef.mAutoIndentation = true; - langDef.mName = "Milkdrop Expression"; + milkdropLangDef.mName = "Milkdrop Expression"; inited = true; } - return langDef; + return milkdropLangDef; } const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::HLSL() @@ -3379,31 +3385,31 @@ const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::SQL() if (!inited) { static const char* const keywords[] = { - "ADD", "EXCEPT", "PERCENT", "ALL", "EXEC", "PLAN", "ALTER", "EXECUTE", "PRECISION", "AND", "EXISTS", "PRIMARY", "ANY", "EXIT", "PRINT", "AS", "FETCH", "PROC", "ASC", "FILE", "PROCEDURE", - "AUTHORIZATION", "FILLFACTOR", "PUBLIC", "BACKUP", "FOR", "RAISERROR", "BEGIN", "FOREIGN", "READ", "BETWEEN", "FREETEXT", "READTEXT", "BREAK", "FREETEXTTABLE", "RECONFIGURE", - "BROWSE", "FROM", "REFERENCES", "BULK", "FULL", "REPLICATION", "BY", "FUNCTION", "RESTORE", "CASCADE", "GOTO", "RESTRICT", "CASE", "GRANT", "RETURN", "CHECK", "GROUP", "REVOKE", - "CHECKPOINT", "HAVING", "RIGHT", "CLOSE", "HOLDLOCK", "ROLLBACK", "CLUSTERED", "IDENTITY", "ROWCOUNT", "COALESCE", "IDENTITY_INSERT", "ROWGUIDCOL", "COLLATE", "IDENTITYCOL", "RULE", - "COLUMN", "IF", "SAVE", "COMMIT", "IN", "SCHEMA", "COMPUTE", "INDEX", "SELECT", "CONSTRAINT", "INNER", "SESSION_USER", "CONTAINS", "INSERT", "SET", "CONTAINSTABLE", "INTERSECT", "SETUSER", - "CONTINUE", "INTO", "SHUTDOWN", "CONVERT", "IS", "SOME", "CREATE", "JOIN", "STATISTICS", "CROSS", "KEY", "SYSTEM_USER", "CURRENT", "KILL", "TABLE", "CURRENT_DATE", "LEFT", "TEXTSIZE", - "CURRENT_TIME", "LIKE", "THEN", "CURRENT_TIMESTAMP", "LINENO", "TO", "CURRENT_USER", "LOAD", "TOP", "CURSOR", "NATIONAL", "TRAN", "DATABASE", "NOCHECK", "TRANSACTION", - "DBCC", "NONCLUSTERED", "TRIGGER", "DEALLOCATE", "NOT", "TRUNCATE", "DECLARE", "NULL", "TSEQUAL", "DEFAULT", "NULLIF", "UNION", "DELETE", "OF", "UNIQUE", "DENY", "OFF", "UPDATE", - "DESC", "OFFSETS", "UPDATETEXT", "DISK", "ON", "USE", "DISTINCT", "OPEN", "USER", "DISTRIBUTED", "OPENDATASOURCE", "VALUES", "DOUBLE", "OPENQUERY", "VARYING", "DROP", "OPENROWSET", "VIEW", - "DUMMY", "OPENXML", "WAITFOR", "DUMP", "OPTION", "WHEN", "ELSE", "OR", "WHERE", "END", "ORDER", "WHILE", "ERRLVL", "OUTER", "WITH", "ESCAPE", "OVER", "WRITETEXT"}; + "add", "except", "percent", "all", "exec", "plan", "alter", "execute", "precision", "and", "exists", "primary", "any", "exit", "print", "as", "fetch", "proc", "asc", "file", "procedure", + "authorization", "fillfactor", "public", "backup", "for", "raiserror", "begin", "foreign", "read", "between", "freetext", "readtext", "break", "freetexttable", "reconfigure", + "browse", "from", "references", "bulk", "full", "replication", "by", "function", "restore", "cascade", "goto", "restrict", "case", "grant", "return", "check", "group", "revoke", + "checkpoint", "having", "right", "close", "holdlock", "rollback", "clustered", "identity", "rowcount", "coalesce", "identity_insert", "rowguidcol", "collate", "identitycol", "rule", + "column", "if", "save", "commit", "in", "schema", "compute", "index", "select", "constraint", "inner", "session_user", "contains", "insert", "set", "containstable", "intersect", "setuser", + "continue", "into", "shutdown", "convert", "is", "some", "create", "join", "statistics", "cross", "key", "system_user", "current", "kill", "table", "current_date", "left", "textsize", + "current_time", "like", "then", "current_timestamp", "lineno", "to", "current_user", "load", "top", "cursor", "national", "tran", "database", "nocheck", "transaction", + "dbcc", "nonclustered", "trigger", "deallocate", "not", "truncate", "declare", "null", "tsequal", "default", "nullif", "union", "delete", "of", "unique", "deny", "off", "update", + "desc", "offsets", "updatetext", "disk", "on", "use", "distinct", "open", "user", "distributed", "opendatasource", "values", "double", "openquery", "varying", "drop", "openrowset", "view", + "dummy", "openxml", "waitfor", "dump", "option", "when", "else", "or", "where", "end", "order", "while", "errlvl", "outer", "with", "escape", "over", "writetext"}; for (auto& k : keywords) langDef.mKeywords.insert(k); static const char* const identifiers[] = { - "ABS", "ACOS", "ADD_MONTHS", "ASCII", "ASCIISTR", "ASIN", "ATAN", "ATAN2", "AVG", "BFILENAME", "BIN_TO_NUM", "BITAND", "CARDINALITY", "CASE", "CAST", "CEIL", - "CHARTOROWID", "CHR", "COALESCE", "COMPOSE", "CONCAT", "CONVERT", "CORR", "COS", "COSH", "COUNT", "COVAR_POP", "COVAR_SAMP", "CUME_DIST", "CURRENT_DATE", - "CURRENT_TIMESTAMP", "DBTIMEZONE", "DECODE", "DECOMPOSE", "DENSE_RANK", "DUMP", "EMPTY_BLOB", "EMPTY_CLOB", "EXP", "EXTRACT", "FIRST_VALUE", "FLOOR", "FROM_TZ", "GREATEST", - "GROUP_ID", "HEXTORAW", "INITCAP", "INSTR", "INSTR2", "INSTR4", "INSTRB", "INSTRC", "LAG", "LAST_DAY", "LAST_VALUE", "LEAD", "LEAST", "LENGTH", "LENGTH2", "LENGTH4", - "LENGTHB", "LENGTHC", "LISTAGG", "LN", "LNNVL", "LOCALTIMESTAMP", "LOG", "LOWER", "LPAD", "LTRIM", "MAX", "MEDIAN", "MIN", "MOD", "MONTHS_BETWEEN", "NANVL", "NCHR", - "NEW_TIME", "NEXT_DAY", "NTH_VALUE", "NULLIF", "NUMTODSINTERVAL", "NUMTOYMINTERVAL", "NVL", "NVL2", "POWER", "RANK", "RAWTOHEX", "REGEXP_COUNT", "REGEXP_INSTR", - "REGEXP_REPLACE", "REGEXP_SUBSTR", "REMAINDER", "REPLACE", "ROUND", "ROWNUM", "RPAD", "RTRIM", "SESSIONTIMEZONE", "SIGN", "SIN", "SINH", - "SOUNDEX", "SQRT", "STDDEV", "SUBSTR", "SUM", "SYS_CONTEXT", "SYSDATE", "SYSTIMESTAMP", "TAN", "TANH", "TO_CHAR", "TO_CLOB", "TO_DATE", "TO_DSINTERVAL", "TO_LOB", - "TO_MULTI_BYTE", "TO_NCLOB", "TO_NUMBER", "TO_SINGLE_BYTE", "TO_TIMESTAMP", "TO_TIMESTAMP_TZ", "TO_YMINTERVAL", "TRANSLATE", "TRIM", "TRUNC", "TZ_OFFSET", "UID", "UPPER", - "USER", "USERENV", "VAR_POP", "VAR_SAMP", "VARIANCE", "VSIZE "}; + "abs", "acos", "add_months", "ascii", "asciistr", "asin", "atan", "atan2", "avg", "bfilename", "bin_to_num", "bitand", "cardinality", "case", "cast", "ceil", + "chartorowid", "chr", "coalesce", "compose", "concat", "convert", "corr", "cos", "cosh", "count", "covar_pop", "covar_samp", "cume_dist", "current_date", + "current_timestamp", "dbtimezone", "decode", "decompose", "dense_rank", "dump", "empty_blob", "empty_clob", "exp", "extract", "first_value", "floor", "from_tz", "greatest", + "group_id", "hextoraw", "initcap", "instr", "instr2", "instr4", "instrb", "instrc", "lag", "last_day", "last_value", "lead", "least", "length", "length2", "length4", + "lengthb", "lengthc", "listagg", "ln", "lnnvl", "localtimestamp", "log", "lower", "lpad", "ltrim", "max", "median", "min", "mod", "months_between", "nanvl", "nchr", + "new_time", "next_day", "nth_value", "nullif", "numtodsinterval", "numtoyminterval", "nvl", "nvl2", "power", "rank", "rawtohex", "regexp_count", "regexp_instr", + "regexp_replace", "regexp_substr", "remainder", "replace", "round", "rownum", "rpad", "rtrim", "sessiontimezone", "sign", "sin", "sinh", + "soundex", "sqrt", "stddev", "substr", "sum", "sys_context", "sysdate", "systimestamp", "tan", "tanh", "to_char", "to_clob", "to_date", "to_dsinterval", "to_lob", + "to_multi_byte", "to_nclob", "to_number", "to_single_byte", "to_timestamp", "to_timestamp_tz", "to_yminterval", "translate", "trim", "trunc", "tz_offset", "uid", "upper", + "user", "userenv", "var_pop", "var_samp", "variance", "vsize "}; for (auto& k : identifiers) { Identifier id; diff --git a/src/notifications/CMakeLists.txt b/src/notifications/CMakeLists.txt index 582f24c..3cf8e90 100644 --- a/src/notifications/CMakeLists.txt +++ b/src/notifications/CMakeLists.txt @@ -1,7 +1,13 @@ add_library(ProjectMSDL-Notifications STATIC DisplayToastNotification.cpp DisplayToastNotification.h - QuitNotification.cpp QuitNotification.h PlaybackControlNotification.cpp PlaybackControlNotification.h UpdateWindowTitleNotification.cpp UpdateWindowTitleNotification.h) + PlaybackControlNotification.cpp + PlaybackControlNotification.h + QuitNotification.cpp + QuitNotification.h + UpdateWindowTitleNotification.cpp + UpdateWindowTitleNotification.h + ) target_include_directories(ProjectMSDL-Notifications PRIVATE From 62c74188ef7075b8f5f394ecaf1aa4f9419d7b5f Mon Sep 17 00:00:00 2001 From: Kai Blaschke Date: Thu, 11 Sep 2025 00:22:21 +0200 Subject: [PATCH 04/14] Fix line coloration bug in Redo method. In a multi-line change, the last line wasn't colorized when applying a redo operation. --- src/gui/preset_editor/imgui_color_text_editor/TextEditor.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui/preset_editor/imgui_color_text_editor/TextEditor.cpp b/src/gui/preset_editor/imgui_color_text_editor/TextEditor.cpp index c692b46..6afb5eb 100644 --- a/src/gui/preset_editor/imgui_color_text_editor/TextEditor.cpp +++ b/src/gui/preset_editor/imgui_color_text_editor/TextEditor.cpp @@ -2484,14 +2484,14 @@ void TextEditor::UndoRecord::Redo(TextEditor* aEditor) if (!mRemoved.empty()) { aEditor->DeleteRange(mRemovedStart, mRemovedEnd); - aEditor->Colorize(mRemovedStart.mLine - 1, mRemovedEnd.mLine - mRemovedStart.mLine + 1); + aEditor->Colorize(mRemovedStart.mLine - 1, mRemovedEnd.mLine - mRemovedStart.mLine + 2); } if (!mAdded.empty()) { auto start = mAddedStart; aEditor->InsertTextAt(start, mAdded.c_str()); - aEditor->Colorize(mAddedStart.mLine - 1, mAddedEnd.mLine - mAddedStart.mLine + 1); + aEditor->Colorize(mAddedStart.mLine - 1, mAddedEnd.mLine - mAddedStart.mLine + 2); } aEditor->mState = mAfter; From 10b478e1f1394e9589092fe411cf52d27f567659 Mon Sep 17 00:00:00 2001 From: Kai Blaschke Date: Thu, 11 Sep 2025 00:30:37 +0200 Subject: [PATCH 05/14] Add (un)indentation via tab key to text editor Users can now indent and unindent lines using the tab key: - If no selection is active, pressing tab only inserts a tab or n spaces at the cursor position - With no selection, pressing shift+tab unindents the current line one step - If a selection is active, tab indents and shift+tab unindents all lines touched by the selection (with at least one character selected at the last line) - Unindented lines with less than the tab size characters of whitespace will be fully unindented - Lines with mixed indentation will be unindented using the proper number of spaces per tab if a tab char needs to be broken down (e.g. unindenting SSTSS with 4 characters will become SSSS) The editor can be configured to insert tab (\t, 0x09) characters or mTabSize number of spaces. --- .../imgui_color_text_editor/TextEditor.cpp | 209 +++++++++++++++++- .../imgui_color_text_editor/TextEditor.h | 9 + 2 files changed, 217 insertions(+), 1 deletion(-) diff --git a/src/gui/preset_editor/imgui_color_text_editor/TextEditor.cpp b/src/gui/preset_editor/imgui_color_text_editor/TextEditor.cpp index 6afb5eb..f325294 100644 --- a/src/gui/preset_editor/imgui_color_text_editor/TextEditor.cpp +++ b/src/gui/preset_editor/imgui_color_text_editor/TextEditor.cpp @@ -552,6 +552,208 @@ bool TextEditor::IsOnWordBoundary(const Coordinates& aAt) const return isspace(line[cindex].mChar) != isspace(line[cindex - 1].mChar); } +void TextEditor::IndentSelection(bool aIndent) +{ + std::string tabString = mTabsAsSpaces ? std::string(mTabSize, ' ') : "\t"; + + UndoRecord u; + + u.mBefore = mState; + + auto start = mState.mSelectionStart; + auto end = mState.mSelectionEnd; + + if (start.mLine == end.mLine) + { + // Just tab without selection inserts tab/spaces at cursor position + if (aIndent && mState.mSelectionStart.mColumn == end.mColumn) + { + u.mAdded = tabString; + u.mAddedStart = start; + u.mAddedEnd = Coordinates(start.mLine, start.mColumn + tabString.size()); + + InsertText(tabString); + } + else + { + // Otherwise, (un)indent the whole line + IndentLine(start.mLine, aIndent, tabString, u); + } + } + else + { + // Multiple lines - indent or unindent each (if possible) + // Selection start/end columns are ignored, each "touched" line counts + + // Undo the change of the whole text block + auto undoStart = Coordinates(start.mLine, 0); + auto undoEnd = Coordinates(end.mLine, GetLineCharacterCount(end.mLine)); + + u.mRemoved = GetText(undoStart, undoEnd); + u.mRemovedStart = undoStart; + u.mRemovedEnd = undoEnd; + + UndoRecord dummyUndo; + for (int line = start.mLine; line <= end.mLine; line++) + { + // If the last line is only selected at the beginning, ignore it. + if (line != end.mLine || end.mColumn > 0) + { + IndentLine(line, aIndent, tabString, dummyUndo); + } + } + + undoStart = Coordinates(mState.mSelectionStart.mLine, 0); + undoEnd = Coordinates(mState.mSelectionEnd.mLine, GetLineCharacterCount(mState.mSelectionEnd.mLine)); + + u.mAdded = GetText(undoStart, undoEnd); + u.mAddedStart = undoStart; + u.mAddedEnd = undoEnd; + } + + u.mAfter = mState; + + AddUndo(u); +} + +int TextEditor::IndentLine(int aIndex, bool aIndent, const std::string& aIndentChars, UndoRecord& aUndoRecord) +{ + Coordinates lineStart(aIndex, 0); + Coordinates lineEnd(aIndex, GetLineMaxColumn(aIndex)); + auto text = GetText(lineStart, lineEnd); + + if (!aIndent && text.empty()) + { + return 0; + } + + auto selectionStart = mState.mSelectionStart; + auto selectionEnd = mState.mSelectionEnd; + + int lastWhitespace = 0; + int totalIndentation = 0; + while (lastWhitespace < text.size() && (text.at(lastWhitespace) == ' ' || text.at(lastWhitespace) == '\t')) + { + totalIndentation += text.at(lastWhitespace) == ' ' ? 1 : mTabSize; + lastWhitespace++; + } + + if (aIndent) + { + Coordinates insertCoordinates(aIndex, lastWhitespace); + + InsertTextAt(insertCoordinates, aIndentChars.c_str()); + + if (selectionStart.mLine == aIndex && selectionStart.mColumn >= lastWhitespace - 1) + { + selectionStart.mColumn += aIndentChars.size(); + } + if (selectionEnd.mLine == aIndex && selectionEnd.mColumn >= lastWhitespace - 1) + { + selectionEnd.mColumn += aIndentChars.size(); + } + SetSelection(selectionStart, selectionEnd); + auto pos = GetCursorPosition(); + if (pos.mLine == aIndex && pos.mColumn >= lastWhitespace) + { + pos.mColumn += aIndentChars.size(); + SetCursorPosition(pos); + } + + aUndoRecord.mAdded = aIndentChars; + aUndoRecord.mAddedStart = insertCoordinates; + aUndoRecord.mAddedEnd = Coordinates(insertCoordinates.mLine, insertCoordinates.mColumn + aIndentChars.size()); + + return aIndentChars.size(); + } + + // Easy case: Up to one tab stop - just remove all indentation. + if (totalIndentation <= mTabSize) + { + Coordinates removeEnd(aIndex, lastWhitespace); + + aUndoRecord.mRemoved = GetText(lineStart, removeEnd); + aUndoRecord.mRemovedStart = lineStart; + aUndoRecord.mRemovedEnd = removeEnd; + + auto pos = GetCursorPosition(); + + DeleteRange(lineStart, removeEnd); + + if (selectionStart.mLine == aIndex && selectionStart.mColumn >= removeEnd.mColumn) + { + selectionStart.mColumn -= lastWhitespace; + } + if (selectionEnd.mLine == aIndex && selectionEnd.mColumn >= removeEnd.mColumn) + { + selectionEnd.mColumn -= lastWhitespace; + } + SetSelection(selectionStart, selectionEnd); + if (pos.mLine == aIndex && pos.mColumn >= removeEnd.mColumn) + { + pos.mColumn -= lastWhitespace; + SetCursorPosition(pos); + } + else if (pos.mLine == aIndex && pos.mColumn < lastWhitespace - 1) + { + pos.mColumn = 0; + SetCursorPosition(pos); + } + + return -lastWhitespace; + } + + // Read backwards, gather as many tabs/spaces as needed. + int removeIndentation = 0; + int removeStartPos = lastWhitespace; + for (; removeStartPos > 0 && removeIndentation < mTabSize; removeStartPos--) + { + removeIndentation += text.at(removeStartPos - 1) == ' ' ? 1 : mTabSize; + } + + Coordinates removeStartCoords(aIndex, removeStartPos); + Coordinates removeEndCoords(aIndex, lastWhitespace); + + aUndoRecord.mRemoved = GetText(removeStartCoords, removeEndCoords); + aUndoRecord.mRemovedStart = removeStartCoords; + aUndoRecord.mRemovedEnd = removeEndCoords; + + DeleteRange(removeStartCoords, Coordinates(aIndex, lastWhitespace)); + + int indentChars = removeStartPos - lastWhitespace; + + // Insert spaces if removed size was larger than necessary (e.g. mixed spaces and tabs) + if (removeIndentation > mTabSize) + { + std::string fillString(removeIndentation - mTabSize, ' '); + InsertTextAt(removeStartCoords, fillString.c_str()); + + aUndoRecord.mAdded = fillString; + aUndoRecord.mAddedStart = removeStartCoords; + aUndoRecord.mAddedEnd = Coordinates(removeStartCoords.mLine, removeStartCoords.mColumn + fillString.size()); + + indentChars += fillString.size(); + } + + if (selectionStart.mLine == aIndex && selectionStart.mColumn >= lastWhitespace) + { + selectionStart.mColumn += indentChars; + } + if (selectionEnd.mLine == aIndex && selectionEnd.mColumn >= lastWhitespace) + { + selectionEnd.mColumn += indentChars; + } + SetSelection(selectionStart, selectionEnd); + auto pos = GetCursorPosition(); + if (pos.mLine == aIndex && pos.mColumn >= lastWhitespace) + { + pos.mColumn += indentChars; + SetCursorPosition(pos); + } + + return indentChars; +} + void TextEditor::RemoveLine(int aStart, int aEnd) { assert(!mReadOnly); @@ -740,7 +942,7 @@ void TextEditor::HandleKeyboardInputs() else if (!IsReadOnly() && !ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Enter)) EnterCharacter('\n', false); else if (!IsReadOnly() && !ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_Tab)) - EnterCharacter('\t', shift); + IndentSelection(!shift); if (!IsReadOnly() && !io.InputQueueCharacters.empty()) { @@ -1442,6 +1644,11 @@ void TextEditor::SetTabSize(int aValue) mTabSize = std::max(0, std::min(32, aValue)); } +void TextEditor::SetTabsAsSpaces(bool aValue) +{ + mTabsAsSpaces = aValue; +} + void TextEditor::InsertText(const std::string& aValue) { InsertText(aValue.c_str()); diff --git a/src/gui/preset_editor/imgui_color_text_editor/TextEditor.h b/src/gui/preset_editor/imgui_color_text_editor/TextEditor.h index 1b6aada..0787905 100644 --- a/src/gui/preset_editor/imgui_color_text_editor/TextEditor.h +++ b/src/gui/preset_editor/imgui_color_text_editor/TextEditor.h @@ -307,6 +307,12 @@ class TextEditor return mTabSize; } + void SetTabsAsSpaces(bool aValue); + inline bool GetTabsAsSpaces() const + { + return mTabsAsSpaces; + } + void InsertText(const std::string& aValue); void InsertText(const char* aValue); @@ -411,6 +417,8 @@ class TextEditor int GetLineCharacterCount(int aLine) const; int GetLineMaxColumn(int aLine) const; bool IsOnWordBoundary(const Coordinates& aAt) const; + void IndentSelection(bool aIndent); + int IndentLine(int aIndex, bool aIndent, const std::string& aIndentChars, UndoRecord& aUndoRecord); void RemoveLine(int aStart, int aEnd); void RemoveLine(int aIndex); Line& InsertLine(int aIndex); @@ -432,6 +440,7 @@ class TextEditor int mUndoIndex{0}; int mTabSize{4}; + bool mTabsAsSpaces{false}; bool mOverwrite{false}; bool mReadOnly{false}; bool mWithinRender{false}; From 42f3e17efdd0e6a2ea8e21df1edbb136c68ac538 Mon Sep 17 00:00:00 2001 From: Kai Blaschke Date: Thu, 11 Sep 2025 00:31:26 +0200 Subject: [PATCH 06/14] Add more icons to the main UI dialogs/menus --- src/gui/AboutWindow.cpp | 7 ++++--- src/gui/FileChooser.cpp | 10 ++++++---- src/gui/HelpWindow.cpp | 4 +++- src/gui/MainMenu.cpp | 6 +++--- src/gui/SettingsWindow.cpp | 15 ++++++++------- 5 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/gui/AboutWindow.cpp b/src/gui/AboutWindow.cpp index b9d1d8d..bc9138e 100644 --- a/src/gui/AboutWindow.cpp +++ b/src/gui/AboutWindow.cpp @@ -1,5 +1,6 @@ #include "AboutWindow.h" +#include "IconsFontAwesome7.h" #include "ProjectMGUI.h" #include "SystemBrowser.h" @@ -30,7 +31,7 @@ void AboutWindow::Draw() } ImGui::SetNextWindowSize(ImVec2(750, 600), ImGuiCond_FirstUseEver); - if (ImGui::Begin("About the projectM SDL Frontend###About", &_visible, ImGuiWindowFlags_NoCollapse)) + if (ImGui::Begin(ICON_FA_INFO " About the projectM SDL Frontend###About", &_visible, ImGuiWindowFlags_NoCollapse)) { _gui.PushToastFont(); ImGui::TextUnformatted("projectM SDL Frontend"); @@ -46,9 +47,9 @@ void AboutWindow::Draw() ImGui::TextWrapped("The projectM SDL frontend is open-source software licensed under the GNU General Public License, version 3."); ImGui::Dummy({.0f, 10.0f}); ImGui::TextWrapped("Get the source code on GitHub or report an issue with the SDL frontend:"); - if (ImGui::SmallButton("https://github.com/projectM-visualizer/frontend-sdl2")) + if (ImGui::SmallButton(ICON_FA_ARROW_UP_RIGHT_FROM_SQUARE " https://github.com/projectM-visualizer/frontend-sdl-cpp")) { - SystemBrowser::OpenURL("https://github.com/projectM-visualizer/frontend-sdl2"); + SystemBrowser::OpenURL("https://github.com/projectM-visualizer/frontend-sdl-cpp"); } ImGui::Dummy({.0f, 10.0f}); if (ImGui::CollapsingHeader("Open-Source Software Used in this Application")) diff --git a/src/gui/FileChooser.cpp b/src/gui/FileChooser.cpp index dd1956e..497f5fa 100644 --- a/src/gui/FileChooser.cpp +++ b/src/gui/FileChooser.cpp @@ -1,5 +1,7 @@ #include "FileChooser.h" +#include "IconsFontAwesome7.h" + #include "imgui.h" #include @@ -143,7 +145,7 @@ bool FileChooser::Draw() } ImGui::PushStyleColor(ImGuiCol_Button, 0xFF000080); - if (ImGui::Button("Cancel")) + if (ImGui::Button(ICON_FA_BAN " Cancel")) { _selectedFiles.clear(); fileSelected = true; @@ -151,7 +153,7 @@ bool FileChooser::Draw() } ImGui::PopStyleColor(); ImGui::SameLine(); - if (ImGui::Button("Select")) + if (ImGui::Button(ICON_FA_CHECK " Select")) { for (auto index : _selectedFileIndices) { @@ -194,7 +196,7 @@ void FileChooser::DrawNavButtons() std::vector roots; Poco::Path::listRoots(roots); - if (ImGui::Button("Up")) + if (ImGui::Button(ICON_FA_CARET_UP " Up")) { ChangeDirectory(_currentDir.parent()); poco_debug_f1(_logger, "Going one dir up: %s", _currentDir.toString()); @@ -202,7 +204,7 @@ void FileChooser::DrawNavButtons() ImGui::SameLine(); - if (ImGui::Button("Home")) + if (ImGui::Button(ICON_FA_HOUSE " Home")) { ChangeDirectory(Poco::Path::home()); poco_debug_f1(_logger, "Going to user's home dir: %s", _currentDir.toString()); diff --git a/src/gui/HelpWindow.cpp b/src/gui/HelpWindow.cpp index 16b8db8..4311d37 100644 --- a/src/gui/HelpWindow.cpp +++ b/src/gui/HelpWindow.cpp @@ -1,5 +1,7 @@ #include "HelpWindow.h" +#include "IconsFontAwesome7.h" + #include "imgui.h" #include @@ -32,7 +34,7 @@ void HelpWindow::Draw() constexpr ImGuiTableFlags tableFlags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg; ImGui::SetNextWindowSize(ImVec2(1000, 600), ImGuiCond_FirstUseEver); - if (ImGui::Begin("Quick Help###Help", &_visible, windowFlags)) + if (ImGui::Begin(ICON_FA_CIRCLE_QUESTION " Quick Help###Help", &_visible, windowFlags)) { if (ImGui::BeginTabBar("Help Topics", tabBarFlags)) { diff --git a/src/gui/MainMenu.cpp b/src/gui/MainMenu.cpp index e6b0a19..e116526 100644 --- a/src/gui/MainMenu.cpp +++ b/src/gui/MainMenu.cpp @@ -70,7 +70,7 @@ void MainMenu::DrawFileMenu() } if (ImGui::MenuItem(ICON_FA_FOLDER_OPEN " Select Preset From Disk...", "Ctrl+l")) { - _presetChooser.Title("Select a Preset for Editing"); + _presetChooser.Title(ICON_FA_FILE_IMPORT " Select a Preset for Editing"); _presetChooser.Show(); } if (ImGui::MenuItem(ICON_FA_SQUARE_PLUS " Create New Preset", "Ctrl+Shift+n")) @@ -194,14 +194,14 @@ void MainMenu::DrawHelpMenu() { if (ImGui::BeginMenu("Help")) { - if (ImGui::MenuItem("Quick Help...")) + if (ImGui::MenuItem(ICON_FA_CIRCLE_QUESTION " Quick Help...")) { _gui.ShowHelpWindow(); } ImGui::Separator(); - if (ImGui::MenuItem("About projectM...")) + if (ImGui::MenuItem(ICON_FA_INFO " About projectM...")) { _gui.ShowAboutWindow(); } diff --git a/src/gui/SettingsWindow.cpp b/src/gui/SettingsWindow.cpp index fa3844d..09b8f5e 100644 --- a/src/gui/SettingsWindow.cpp +++ b/src/gui/SettingsWindow.cpp @@ -4,6 +4,7 @@ #include "ProjectMSDLApplication.h" #include "SDLRenderingWindow.h" +#include "IconsFontAwesome7.h" #include "ProjectMGUI.h" #include "notifications/DisplayToastNotification.h" @@ -38,7 +39,7 @@ void SettingsWindow::Draw() constexpr ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoCollapse; constexpr ImGuiTabBarFlags tabBarFlags = ImGuiTabBarFlags_None; - std::string windowId = "Settings"; + std::string windowId = ICON_FA_GEAR " Settings"; if (_changed) { windowId.append(" [CHANGED - NOT SAVED]"); @@ -84,7 +85,7 @@ void SettingsWindow::DrawProjectMSettingsTab() ImGui::TableSetupColumn("##desc", ImGuiTableColumnFlags_WidthFixed, .0f); ImGui::TableSetupColumn("##setting", ImGuiTableColumnFlags_WidthStretch, .0f); ImGui::TableSetupColumn("##choose", ImGuiTableColumnFlags_WidthFixed, 100.0f); - ImGui::TableSetupColumn("##reset", ImGuiTableColumnFlags_WidthFixed, 50.0f); + ImGui::TableSetupColumn("##reset", ImGuiTableColumnFlags_WidthFixed, 100.0f); ImGui::TableSetupColumn("##override", ImGuiTableColumnFlags_WidthFixed, .0f); ImGui::TableNextRow(); @@ -168,7 +169,7 @@ void SettingsWindow::DrawWindowSettingsTab() ImGui::TableSetupColumn("##desc", ImGuiTableColumnFlags_WidthFixed, .0f); ImGui::TableSetupColumn("##setting", ImGuiTableColumnFlags_WidthStretch, .0f); ImGui::TableSetupColumn("##choose", ImGuiTableColumnFlags_WidthFixed, 100.0f); - ImGui::TableSetupColumn("##reset", ImGuiTableColumnFlags_WidthFixed, 50.0f); + ImGui::TableSetupColumn("##reset", ImGuiTableColumnFlags_WidthFixed, 100.0f); ImGui::TableSetupColumn("##override", ImGuiTableColumnFlags_WidthFixed, .0f); ImGui::TableNextRow(); @@ -270,7 +271,7 @@ void SettingsWindow::DrawAudioSettingsTab() { ImGui::TableSetupColumn("##desc", ImGuiTableColumnFlags_WidthFixed, .0f); ImGui::TableSetupColumn("##setting", ImGuiTableColumnFlags_WidthStretch, .0f); - ImGui::TableSetupColumn("##choose", ImGuiTableColumnFlags_WidthFixed, 50.0f); + ImGui::TableSetupColumn("##choose", ImGuiTableColumnFlags_WidthFixed, 100.0f); ImGui::TableSetupColumn("##reset", ImGuiTableColumnFlags_WidthFixed, 100.0f); ImGui::TableSetupColumn("##override", ImGuiTableColumnFlags_WidthFixed, .0f); @@ -322,7 +323,7 @@ void SettingsWindow::DrawHelpTab() const void SettingsWindow::SaveButton() { - if (ImGui::Button("Save Settings")) + if (ImGui::Button(ICON_FA_FLOPPY_DISK " Save Settings")) { try { @@ -384,7 +385,7 @@ void SettingsWindow::PathSetting(const std::string& property) if (ImGui::Button("...")) { _pathChooser.CurrentDirectory(path); - _pathChooser.Title("Select directory"); + _pathChooser.Title(ICON_FA_FOLDER_OPEN " Select directory"); _pathChooser.Context(property); _pathChooser.Show(); } @@ -605,7 +606,7 @@ bool SettingsWindow::ResetButton(const std::string& property1, const std::string bool pressed{false}; ImGui::PushID(std::string(property1 + property2 + "_ResetButton").c_str()); - if (ImGui::Button("Reset")) + if (ImGui::Button(ICON_FA_ERASER " Reset")) { _userConfiguration->remove(property1); if (!property2.empty()) From a9b4b6c66840e767d9373a22a239016d142838ff Mon Sep 17 00:00:00 2001 From: Kai Blaschke Date: Thu, 11 Sep 2025 00:32:12 +0200 Subject: [PATCH 07/14] Add more Milkdrop-specific shader identifiers --- .../preset_editor/CodeContextInformation.cpp | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/gui/preset_editor/CodeContextInformation.cpp b/src/gui/preset_editor/CodeContextInformation.cpp index 6e2115b..d0c0ea5 100644 --- a/src/gui/preset_editor/CodeContextInformation.cpp +++ b/src/gui/preset_editor/CodeContextInformation.cpp @@ -642,6 +642,17 @@ std::vector> CodeContextInformation::GetIden {ExpressionCodeTypes::WarpShader, {{"shader_body", ICON_FA_PAPER_PLANE " Shader entry point.\nWill be replaced with the appropriate function declaration at runtime."}, // {"ret", ICON_FA_CUBE " float3\n" ICON_FA_RIGHT_FROM_BRACKET " Shader output RGB color."}, // + {"M_PI", ICON_FA_CUBE " float\nPi constant (3.14159265359)"}, // + {"M_PI_2", ICON_FA_CUBE " float\n2*Pi constant (6.28318530718)"}, // + {"M_INV_PI_2", ICON_FA_CUBE " float\n1/Pi constant (0.159154943091895)"}, // + {"GetMain", ICON_FA_CUBES " float3 GetMain(float2 uv)\nSample main texture at uv, equivalent to:\ntex2D(sampler_main, uv).xyz"}, // + {"GetPixel", ICON_FA_CUBES " float3 GetMain(float2 uv)\nSample main texture at uv, equivalent to:\ntex2D(sampler_main, uv).xyz"}, // + {"GetBlur1", ICON_FA_CUBES " float3 GetBlur1(float2 uv)\nSample blur1 texture at uv with range bias applied, equivalent to:\ntex2D(sampler_blur1, uv).xyz * blur1_min + blur1_max"}, // + {"GetBlur2", ICON_FA_CUBES " float3 GetBlur2(float2 uv)\nSample blur2 texture at uv with range bias applied, equivalent to:\ntex2D(sampler_blur2, uv).xyz * blur2_min + blur2_max"}, // + {"GetBlur3", ICON_FA_CUBES " float3 GetBlur3(float2 uv)\nSample blur3 texture at uv with range bias applied, equivalent to:\ntex2D(sampler_blur3, uv).xyz * blur3_min + blur3_max"}, // + {"lum", ICON_FA_CUBES " float lum(float3 color)\nCalculates the luminosity of the given color equivalent to:\ndot(color, float3(0.32, 0.49, 0.29)"}, // + {"tex2d", ICON_FA_CUBE " float4\nLower-case alias for tex2D"}, // + {"tex3d", ICON_FA_CUBE " float4\nLower-case alias for tex3D"}, // {"uv", ICON_FA_CUBE " float2\nWarped UV coordinates (approx. 0..1)."}, // {"uv_orig", ICON_FA_CUBE " float2\nOriginal, unwarped UV coordinates (0..1)."}, // {"rad", ICON_FA_CUBE " float\nRadius of the current pixel from center of screen (0..1)."}, // @@ -774,6 +785,17 @@ std::vector> CodeContextInformation::GetIden {ExpressionCodeTypes::CompositeShader, {{"shader_body", ICON_FA_PAPER_PLANE " Shader entry point.\nWill be replaced with the appropriate function declaration at runtime."}, // {"ret", ICON_FA_CUBE " float3\n" ICON_FA_RIGHT_FROM_BRACKET " Shader output RGB color."}, // + {"M_PI", ICON_FA_CUBE " float\nPi constant (3.14159265359)"}, // + {"M_PI_2", ICON_FA_CUBE " float\n2*Pi constant (6.28318530718)"}, // + {"M_INV_PI_2", ICON_FA_CUBE " float\n1/Pi constant (0.159154943091895)"}, // + {"GetMain", ICON_FA_CUBES " float3 GetMain(float2 uv)\nSample main texture at uv, equivalent to:\ntex2D(sampler_main, uv).xyz"}, // + {"GetPixel", ICON_FA_CUBES " float3 GetMain(float2 uv)\nSample main texture at uv, equivalent to:\ntex2D(sampler_main, uv).xyz"}, // + {"GetBlur1", ICON_FA_CUBES " float3 GetBlur1(float2 uv)\nSample blur1 texture at uv with range bias applied, equivalent to:\ntex2D(sampler_blur1, uv).xyz * blur1_min + blur1_max"}, // + {"GetBlur2", ICON_FA_CUBES " float3 GetBlur2(float2 uv)\nSample blur2 texture at uv with range bias applied, equivalent to:\ntex2D(sampler_blur2, uv).xyz * blur2_min + blur2_max"}, // + {"GetBlur3", ICON_FA_CUBES " float3 GetBlur3(float2 uv)\nSample blur3 texture at uv with range bias applied, equivalent to:\ntex2D(sampler_blur3, uv).xyz * blur3_min + blur3_max"}, // + {"lum", ICON_FA_CUBES " float lum(float3 color)\nCalculates the luminosity of the given color equivalent to:\ndot(color, float3(0.32, 0.49, 0.29)"}, // + {"tex2d", ICON_FA_CUBE " float4\nLower-case alias for tex2D"}, // + {"tex3d", ICON_FA_CUBE " float4\nLower-case alias for tex3D"}, // {"uv", ICON_FA_CUBE " float2\nUnwarped UV coordinates (0..1)."}, // {"hue_shader", ICON_FA_CUBE " float3\nA color that varies across the screen (the old 'hue shader' effect from MilkDrop 1)."}, // {"rad", ICON_FA_CUBE " float\nRadius of the current pixel from center of screen (0..1)."}, // From 61630eeff76f220785d3431ba44d935766c3b2a9 Mon Sep 17 00:00:00 2001 From: Kai Blaschke Date: Thu, 11 Sep 2025 00:52:34 +0200 Subject: [PATCH 08/14] Reduce includes by using unique_ptr and fwd declarations --- src/RenderLoop.cpp | 1 + src/gui/ProjectMGUI.cpp | 35 +++++++++++++++++------ src/gui/ProjectMGUI.h | 32 +++++++++++++-------- src/gui/preset_editor/EditorMenu.cpp | 2 ++ src/gui/preset_editor/PresetEditorGUI.cpp | 13 +++++++-- src/gui/preset_editor/PresetEditorGUI.h | 8 ++++-- 6 files changed, 64 insertions(+), 27 deletions(-) diff --git a/src/RenderLoop.cpp b/src/RenderLoop.cpp index 7d428e0..71c2ad8 100644 --- a/src/RenderLoop.cpp +++ b/src/RenderLoop.cpp @@ -4,6 +4,7 @@ #include "gui/ProjectMGUI.h" +#include #include #include diff --git a/src/gui/ProjectMGUI.cpp b/src/gui/ProjectMGUI.cpp index 8d9d33a..0085714 100644 --- a/src/gui/ProjectMGUI.cpp +++ b/src/gui/ProjectMGUI.cpp @@ -3,10 +3,16 @@ #include "ProjectMWrapper.h" #include "SDLRenderingWindow.h" +#include "AboutWindow.h" #include "AnonymousProFont.h" #include "FontAwesomeIconsRegular7.h" #include "FontAwesomeIconsSolid7.h" +#include "HelpWindow.h" #include "LiberationSansFont.h" +#include "MainMenu.h" +#include "PresetEditorGUI.h" +#include "SettingsWindow.h" +#include "ToastMessage.h" #include "imgui.h" #include "imgui_impl_opengl3.h" @@ -18,6 +24,17 @@ #include +ProjectMGUI::ProjectMGUI() + : _mainMenu(std::make_unique(*this)) + , _presetEditorGUI(std::make_unique(*this)) + , _settingsWindow(std::make_unique(*this)) + , _aboutWindow(std::make_unique(*this)) + , _helpWindow(std::make_unique()) +{ +} + +ProjectMGUI::~ProjectMGUI() = default; + const char* ProjectMGUI::name() const { return "Preset Selection GUI"; @@ -171,12 +188,12 @@ void ProjectMGUI::Draw() if (_visible) { - if (!_presetEditorGUI.Draw()) + if (!_presetEditorGUI->Draw()) { - _mainMenu.Draw(); - _settingsWindow.Draw(); - _aboutWindow.Draw(); - _helpWindow.Draw(); + _mainMenu->Draw(); + _settingsWindow->Draw(); + _aboutWindow->Draw(); + _helpWindow->Draw(); } } @@ -223,22 +240,22 @@ void ProjectMGUI::PopFont() void ProjectMGUI::ShowPresetEditor(const std::string& presetFileName) { - _presetEditorGUI.Show(presetFileName); + _presetEditorGUI->Show(presetFileName); } void ProjectMGUI::ShowSettingsWindow() { - _settingsWindow.Show(); + _settingsWindow->Show(); } void ProjectMGUI::ShowAboutWindow() { - _aboutWindow.Show(); + _aboutWindow->Show(); } void ProjectMGUI::ShowHelpWindow() { - _helpWindow.Show(); + _helpWindow->Show(); } float ProjectMGUI::GetScalingFactor() diff --git a/src/gui/ProjectMGUI.h b/src/gui/ProjectMGUI.h index 19a7d9c..f209957 100644 --- a/src/gui/ProjectMGUI.h +++ b/src/gui/ProjectMGUI.h @@ -1,12 +1,5 @@ #pragma once -#include "AboutWindow.h" -#include "HelpWindow.h" -#include "MainMenu.h" -#include "PresetEditorGUI.h" -#include "SettingsWindow.h" -#include "ToastMessage.h" - #include "notifications/DisplayToastNotification.h" #include @@ -16,13 +9,28 @@ #include +#include + +namespace Editor { +class PresetEditorGUI; +} + struct ImFont; class ProjectMWrapper; class SDLRenderingWindow; +class MainMenu; +class SettingsWindow; +class AboutWindow; +class HelpWindow; +class ToastMessage; class ProjectMGUI : public Poco::Util::Subsystem { public: + ProjectMGUI(); + + ~ProjectMGUI() override; + const char* name() const override; void initialize(Poco::Util::Application& app) override; @@ -146,11 +154,11 @@ class ProjectMGUI : public Poco::Util::Subsystem float _userScalingFactor{1.0f}; //!< The user-defined UI scaling factor. float _textScalingFactor{0.0f}; //!< The text scaling factor. - MainMenu _mainMenu{*this}; - Editor::PresetEditorGUI _presetEditorGUI{*this}; //!< The preset editor GUI. - SettingsWindow _settingsWindow{*this}; //!< The settings window. - AboutWindow _aboutWindow{*this}; //!< The about window. - HelpWindow _helpWindow; //!< Help window with shortcuts and tips. + std::unique_ptr _mainMenu; + std::unique_ptr _presetEditorGUI; //!< The preset editor GUI. + std::unique_ptr _settingsWindow; //!< The settings window. + std::unique_ptr _aboutWindow; //!< The about window. + std::unique_ptr _helpWindow; //!< Help window with shortcuts and tips. std::unique_ptr _toast; //!< Current toast to be displayed. diff --git a/src/gui/preset_editor/EditorMenu.cpp b/src/gui/preset_editor/EditorMenu.cpp index 7004794..913855b 100644 --- a/src/gui/preset_editor/EditorMenu.cpp +++ b/src/gui/preset_editor/EditorMenu.cpp @@ -7,6 +7,8 @@ #include "notifications/QuitNotification.h" +#include "imgui.h" + #include namespace Editor { diff --git a/src/gui/preset_editor/PresetEditorGUI.cpp b/src/gui/preset_editor/PresetEditorGUI.cpp index 675758d..88be2dc 100644 --- a/src/gui/preset_editor/PresetEditorGUI.cpp +++ b/src/gui/preset_editor/PresetEditorGUI.cpp @@ -1,8 +1,8 @@ #include "PresetEditorGUI.h" #include "CodeContextInformation.h" +#include "CodeEditorWindow.h" #include "IconsFontAwesome7.h" -#include "ProjectMGUI.h" #include "ProjectMSDLApplication.h" #include "ProjectMWrapper.h" @@ -27,6 +27,10 @@ PresetEditorGUI::PresetEditorGUI(ProjectMGUI& gui) { } +PresetEditorGUI::~PresetEditorGUI() +{ +} + void PresetEditorGUI::Show(const std::string& presetFile) { if (presetFile.empty()) @@ -55,6 +59,7 @@ void PresetEditorGUI::Show(const std::string& presetFile) } _editorPreset.FromParsedFile(_presetFile); + _codeEditorWindow = std::make_unique(); _visible = true; } @@ -86,7 +91,7 @@ bool PresetEditorGUI::Draw() ImGui::SameLine(); - _codeEditorWindow.Draw(); + _codeEditorWindow->Draw(); } ImGui::End(); @@ -100,6 +105,8 @@ bool PresetEditorGUI::Draw() _visible = false; _wantClose = false; + + _codeEditorWindow.reset(); } return true; @@ -164,7 +171,7 @@ void PresetEditorGUI::ReleaseProjectMControl() void PresetEditorGUI::EditCode(ExpressionCodeTypes type, std::string& code, int index) { - _codeEditorWindow.OpenCodeInTab(type, code, index); + _codeEditorWindow->OpenCodeInTab(type, code, index); } unsigned long PresetEditorGUI::GetLoC(const std::string& code) diff --git a/src/gui/preset_editor/PresetEditorGUI.h b/src/gui/preset_editor/PresetEditorGUI.h index 7df9d8b..31b3305 100644 --- a/src/gui/preset_editor/PresetEditorGUI.h +++ b/src/gui/preset_editor/PresetEditorGUI.h @@ -1,11 +1,11 @@ #pragma once -#include "CodeEditorWindow.h" #include "EditorMenu.h" #include "EditorPreset.h" #include "ExpressionCodeTypes.h" #include "PresetFile.h" +#include #include class ProjectMGUI; @@ -14,12 +14,14 @@ class ProjectMWrapper; namespace Editor { +class CodeEditorWindow; + class PresetEditorGUI { public: explicit PresetEditorGUI(ProjectMGUI& gui); - ~PresetEditorGUI() = default; + ~PresetEditorGUI(); /** * @brief Displays the preset editor screen and associated windows. @@ -84,7 +86,7 @@ class PresetEditorGUI PresetFile _presetFile; //!< The raw preset data. EditorPreset _editorPreset; //!< The preset data in a parsed, strongly-typed container. - CodeEditorWindow _codeEditorWindow; //!< The code editor window. + std::unique_ptr _codeEditorWindow; //!< The code editor window. }; From 0131b4ec805ab0bc13769b6e376f82f2f904b1e1 Mon Sep 17 00:00:00 2001 From: Kai Blaschke Date: Thu, 11 Sep 2025 00:53:27 +0200 Subject: [PATCH 09/14] Fix some editor window behaviors --- src/gui/preset_editor/PresetEditorGUI.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/gui/preset_editor/PresetEditorGUI.cpp b/src/gui/preset_editor/PresetEditorGUI.cpp index 88be2dc..6d99535 100644 --- a/src/gui/preset_editor/PresetEditorGUI.cpp +++ b/src/gui/preset_editor/PresetEditorGUI.cpp @@ -85,7 +85,7 @@ bool PresetEditorGUI::Draw() ImGui::SetNextWindowSize(viewport->WorkSize); ImGui::PushStyleColor(ImGuiCol_TitleBgActive, IM_COL32(0xd9, 0x1e, 0x18, 0xff)); - if (ImGui::Begin("Preset Editor", &_visible, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoBackground)) + if (ImGui::Begin("Preset Editor", &_visible, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoBackground)) { DrawLeftSideBar(); @@ -109,7 +109,7 @@ bool PresetEditorGUI::Draw() _codeEditorWindow.reset(); } - return true; + return _visible; } void PresetEditorGUI::UpdatePresetPreview() @@ -131,19 +131,19 @@ void PresetEditorGUI::HandleGlobalEditorKeys() auto alt = io.ConfigMacOSXBehaviors ? io.KeyCtrl : io.KeyAlt; // Reload edited preset - Alt+Enter or F5 - if ((io.KeysData[ImGuiKey_Enter - ImGuiKey_NamedKey_BEGIN].Down && alt) || io.KeysData[ImGuiKey_F5 - ImGuiKey_NamedKey_BEGIN].Down) + if (ImGui::IsKeyPressed(ImGuiKey_F5, false) || alt && ImGui::IsKeyPressed(ImGuiKey_Enter, false)) { UpdatePresetPreview(); } // Save preset - Ctrl+S - if (io.KeysData[ImGuiKey_S - ImGuiKey_NamedKey_BEGIN].Down && ctrl) + if (ctrl && ImGui::IsKeyPressed(ImGuiKey_S, false)) { } // New preset - Ctrl+N - if (io.KeysData[ImGuiKey_N - ImGuiKey_NamedKey_BEGIN].Down && ctrl) + if (ctrl && ImGui::IsKeyPressed(ImGuiKey_N, false)) { Show(""); } From 365a7438ddbd04c782dcd3dc7b88d3a9c48dc557 Mon Sep 17 00:00:00 2001 From: Kai Blaschke Date: Thu, 11 Sep 2025 00:56:21 +0200 Subject: [PATCH 10/14] Make code editor child window collapsible Toggles drawing of the whole child as interfering with the user-resizable window makes ImGui forget the previously set height when expanding it again. --- src/gui/preset_editor/CodeEditorWindow.cpp | 45 +++++++++++++++++----- src/gui/preset_editor/CodeEditorWindow.h | 6 ++- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/gui/preset_editor/CodeEditorWindow.cpp b/src/gui/preset_editor/CodeEditorWindow.cpp index 251aa5e..dd0278f 100644 --- a/src/gui/preset_editor/CodeEditorWindow.cpp +++ b/src/gui/preset_editor/CodeEditorWindow.cpp @@ -1,6 +1,7 @@ #include "CodeEditorWindow.h" #include "CodeContextInformation.h" +#include "IconsFontAwesome7.h" #include "imgui.h" @@ -15,25 +16,42 @@ void CodeEditorWindow::Draw() ImGui::PushID("CodeEditorWindow"); - if (ImGui::BeginChild("CodeEditorChild", ImVec2(0, 500), ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeY, ImGuiWindowFlags_None)) + if (ImGui::Button(_collapsed ? ICON_FA_CARET_RIGHT "##CollapseButton" : ICON_FA_CARET_DOWN "##CollapseButton")) { - if (ImGui::BeginTabBar("OpenCodeTabs", ImGuiTabBarFlags_AutoSelectNewTabs)) + _collapsed = !_collapsed; + } + + ImGui::SameLine(); + + if (!_collapsed) + { + ImGui::SetNextWindowSize(ImVec2(0, 500), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowBgAlpha(_collapsed ? 0.0f : 0.5f); + if (ImGui::BeginChild("CodeEditorChild", ImVec2(0, 500), ImGuiChildFlags_ResizeY, ImGuiWindowFlags_None)) { - for (auto it = begin(_codeEditorTabs); it != end(_codeEditorTabs);) + if (ImGui::BeginTabBar("OpenCodeTabs", ImGuiTabBarFlags_AutoSelectNewTabs)) { - if (it->Draw()) + for (auto it = begin(_codeEditorTabs); it != end(_codeEditorTabs);) { - ++it; - continue; - } + if (it->Draw()) + { + ++it; + continue; + } - it = _codeEditorTabs.erase(it); + it = _codeEditorTabs.erase(it); + } } ImGui::EndTabBar(); } + ImGui::EndChild(); + } + else + { + ImGui::SameLine(); + ImGui::TextDisabled("%s", ("(" + std::to_string(_codeEditorTabs.size()) + " Tabs Open)").c_str()); } - ImGui::EndChild(); if (_codeEditorTabs.empty()) { @@ -45,6 +63,9 @@ void CodeEditorWindow::Draw() void CodeEditorWindow::OpenCodeInTab(ExpressionCodeTypes type, std::string& code, int index) { + // Fold out the editor if the user opens a new tab or focuses an existing one. + _collapsed = false; + std::string newTabTitle = CodeContextInformation::GetContextName(type, index); for (auto& tab : _codeEditorTabs) { @@ -55,8 +76,12 @@ void CodeEditorWindow::OpenCodeInTab(ExpressionCodeTypes type, std::string& code } } - _codeEditorTabs.emplace_back(type, code, index); + CodeEditorTab newCodeEditorTab(type, code, index); + newCodeEditorTab.SetSelected(); + _codeEditorTabs.push_back(std::move(newCodeEditorTab)); + _visible = true; } + } // namespace Editor diff --git a/src/gui/preset_editor/CodeEditorWindow.h b/src/gui/preset_editor/CodeEditorWindow.h index 04c3ebb..497a6ad 100644 --- a/src/gui/preset_editor/CodeEditorWindow.h +++ b/src/gui/preset_editor/CodeEditorWindow.h @@ -20,8 +20,10 @@ class CodeEditorWindow void OpenCodeInTab(ExpressionCodeTypes type, std::string& code, int index); private: - bool _visible{false}; - std::list _codeEditorTabs; + bool _visible{false}; //!< Determines if the code editor window is visible. + bool _collapsed{false}; //!< If true, the window is displayed collapsed, e.g. the tab contents aren't rendered. + + std::list _codeEditorTabs; //!< Currently opened editor tabs. }; } // namespace Editor From b984c549f53a56b2b5afd461519438762a985d9a Mon Sep 17 00:00:00 2001 From: Kai Blaschke Date: Thu, 11 Sep 2025 00:57:33 +0200 Subject: [PATCH 11/14] Add color to code tabs and a toolbar for each editor --- src/gui/preset_editor/CodeEditorTab.cpp | 142 +++++++++++++++++++++--- src/gui/preset_editor/CodeEditorTab.h | 24 +++- 2 files changed, 146 insertions(+), 20 deletions(-) diff --git a/src/gui/preset_editor/CodeEditorTab.cpp b/src/gui/preset_editor/CodeEditorTab.cpp index d6df750..e6dac75 100644 --- a/src/gui/preset_editor/CodeEditorTab.cpp +++ b/src/gui/preset_editor/CodeEditorTab.cpp @@ -1,6 +1,7 @@ #include "CodeEditorTab.h" #include "CodeContextInformation.h" +#include "IconsFontAwesome7.h" #include @@ -13,42 +14,90 @@ void projectm_eval_memory_host_unlock_mutex() namespace Editor { +using CodeDestructor = void (*)(projectm_eval_code*); +using CodeHandle = std::unique_ptr; + CodeEditorTab::CodeEditorTab(ExpressionCodeTypes type, std::string& code, int index) : _code(code) + , _textEditor(std::make_unique()) { _tabTitle = CodeContextInformation::GetContextName(type, index); - _textEditor.SetLanguageDefinition(CodeContextInformation::GetLanguageDefinition(type)); - _textEditor.SetText(code); + _textEditor->SetTabsAsSpaces(true); + _textEditor->SetTabSize(4); + _textEditor->SetLanguageDefinition(CodeContextInformation::GetLanguageDefinition(type)); + _textEditor->SetText(code); if (type != ExpressionCodeTypes::WarpShader && type != ExpressionCodeTypes::CompositeShader) { - _compileTextContext = projectm_eval_context_create(nullptr, nullptr); + _compileTextContext.reset(projectm_eval_context_create(nullptr, nullptr)); CheckCodeSyntax(code); } -} -CodeEditorTab::~CodeEditorTab() -{ - if (_compileTextContext) + switch (type) { - projectm_eval_context_destroy(_compileTextContext); - _compileTextContext = nullptr; + case ExpressionCodeTypes::PerFrameInit: + case ExpressionCodeTypes::PerFrame: + case ExpressionCodeTypes::PerVertex: + _tabHoverColor = {0xe7 / 256.f, 0x4c / 256.f, 0x3c / 256.f, 1.0f}; + break; + + case ExpressionCodeTypes::CustomWaveInit: + case ExpressionCodeTypes::CustomWavePerFrame: + case ExpressionCodeTypes::CustomWavePerPoint: + _tabHoverColor = {0x29 / 256.f, 0x80 / 256.f, 0xb9 / 256.f, 1.0f}; + break; + + case ExpressionCodeTypes::CustomShapeInit: + case ExpressionCodeTypes::CustomShapePerFrame: + _tabHoverColor = {0x8e / 256.f, 0x44 / 256.f, 0xad / 256.f, 1.0f}; + break; + + case ExpressionCodeTypes::WarpShader: + _tabHoverColor = {0x27 / 256.f, 0xae / 256.f, 0x60 / 256.f, 1.0f}; + break; + + case ExpressionCodeTypes::CompositeShader: + _tabHoverColor = {0xf3 / 256.f, 0x9c / 256.f, 0x12 / 256.f, 1.0f}; + break; } + + _tabActiveColor = {_tabHoverColor.x * 0.8f, _tabHoverColor.y * 0.8f, _tabHoverColor.z * 0.8f, _tabHoverColor.w}; + _tabColor = {_tabHoverColor.x * 0.5f, _tabHoverColor.y * 0.5f, _tabHoverColor.z * 0.5f, _tabHoverColor.w * 0.75f}; +} + +CodeEditorTab& CodeEditorTab::operator=(CodeEditorTab&& other) noexcept +{ + _nextRenderingFlags = other._nextRenderingFlags; + _tabTitle = std::move(other._tabTitle); + _tabColor = other._tabColor; + _tabActiveColor = other._tabActiveColor; + _tabHoverColor = other._tabHoverColor; + _code = other._code; + _textEditor = std::move(other._textEditor); + _documentOpen = other._documentOpen; + _compileTextContext = std::move(other._compileTextContext); + + return *this; } bool CodeEditorTab::Draw() { ImGui::PushID(_tabTitle.c_str()); + ImGui::PushStyleColor(ImGuiCol_Tab, _tabColor); + ImGui::PushStyleColor(ImGuiCol_TabActive, _tabActiveColor); + ImGui::PushStyleColor(ImGuiCol_TabHovered, _tabHoverColor); bool tabVisible = ImGui::BeginTabItem(_tabTitle.c_str(), &_documentOpen, _nextRenderingFlags); if (tabVisible) { - _textEditor.Render((_tabTitle + "##EditorControl").c_str()); + bool textChangedByToolBar = DrawToolBar(); - if (_textEditor.IsTextChanged()) + _textEditor->Render((_tabTitle + "##EditorControl").c_str()); + + if (_textEditor->IsTextChanged() || textChangedByToolBar) { - std::string changedText = _textEditor.GetText(); + std::string changedText = _textEditor->GetText(); // The text editor always leaves a newline if empty, check & clear if that's the case. if (changedText.size() == 1 && changedText.at(0) == '\n') @@ -64,6 +113,10 @@ bool CodeEditorTab::Draw() ImGui::EndTabItem(); } + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopID(); _nextRenderingFlags = ImGuiTabItemFlags_None; @@ -81,21 +134,76 @@ std::string CodeEditorTab::Title() const return _tabTitle; } +bool CodeEditorTab::DrawToolBar() +{ + bool textChanged = false; + + ImGui::BeginDisabled(!_textEditor->HasSelection()); + if (ImGui::Button(ICON_FA_COPY " Copy##ToolBar")) + { + _textEditor->Copy(); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_SCISSORS " Cut##ToolBar")) + { + _textEditor->Cut(); + textChanged= true; + } + ImGui::EndDisabled(); + + ImGui::BeginDisabled(strlen(ImGui::GetClipboardText()) == 0); + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_PASTE " Paste##ToolBar")) + { + _textEditor->Paste(); + textChanged= true; + } + ImGui::EndDisabled(); + + ImGui::SameLine(); + ImGui::Separator(); + + ImGui::BeginDisabled(!_textEditor->CanUndo()); + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_ARROW_ROTATE_LEFT " Undo##ToolBar")) + { + _textEditor->Undo(); + textChanged= true; + } + ImGui::EndDisabled(); + + ImGui::BeginDisabled(!_textEditor->CanRedo()); + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_ARROW_ROTATE_RIGHT " Redo##ToolBar")) + { + _textEditor->Redo(); + textChanged= true; + } + ImGui::EndDisabled(); + + ImGui::SameLine(); + if (ImGui::Checkbox("Whitespace##ToolBar", &_showWhitespace)) + { + _textEditor->SetShowWhitespaces(_showWhitespace); + } + + return textChanged; +} + void CodeEditorTab::CheckCodeSyntax(const std::string& codeToCheck) { if (_compileTextContext) { - auto* code = projectm_eval_code_compile(_compileTextContext, codeToCheck.c_str()); + auto code = CodeHandle(projectm_eval_code_compile(_compileTextContext.get(), codeToCheck.c_str()), projectm_eval_code_destroy); if (code) { - _textEditor.SetErrorMarkers({}); - projectm_eval_code_destroy(code); + _textEditor->SetErrorMarkers({}); } else { int line{}; - std::string errorMessage = projectm_eval_get_error(_compileTextContext, &line, nullptr); - _textEditor.SetErrorMarkers({{line, errorMessage}}); + std::string errorMessage = projectm_eval_get_error(_compileTextContext.get(), &line, nullptr); + _textEditor->SetErrorMarkers({{line, errorMessage}}); } } } diff --git a/src/gui/preset_editor/CodeEditorTab.h b/src/gui/preset_editor/CodeEditorTab.h index 5bd2e8b..c996404 100644 --- a/src/gui/preset_editor/CodeEditorTab.h +++ b/src/gui/preset_editor/CodeEditorTab.h @@ -14,7 +14,12 @@ class CodeEditorTab explicit CodeEditorTab(ExpressionCodeTypes type, std::string& code, int index); - virtual ~CodeEditorTab(); + virtual ~CodeEditorTab() = default; + + CodeEditorTab(const CodeEditorTab& other) = delete; + CodeEditorTab(CodeEditorTab&& other) noexcept = default; + CodeEditorTab& operator=(const CodeEditorTab& other) = delete; + CodeEditorTab& operator=(CodeEditorTab&& other) noexcept; bool Draw(); @@ -23,15 +28,28 @@ class CodeEditorTab std::string Title() const; private: + using ContextDestructor = void(*)(projectm_eval_context*); + using ContextHandle = std::unique_ptr; + + /** + * Draws the editor toolbar for this tab. + * @return true if an action was executed which changed the editor contents, false if not. + */ + bool DrawToolBar(); + void CheckCodeSyntax(const std::string& codeToCheck); ImGuiTabItemFlags _nextRenderingFlags{}; //!< Additional flags to set when rendering the tab next time, e.g. activate it. std::string _tabTitle; //!< The title of the tab. + ImVec4 _tabColor{0.0f, 0.0f, 1.0f, 1.0f}; //!< The background color of the inactive tab. + ImVec4 _tabActiveColor{0.0f, 0.0f, 1.0f, 1.0f}; //!< The background color of the active tab. + ImVec4 _tabHoverColor{0.0f, 0.0f, 1.0f, 1.0f}; //!< The background color of the hovered tab. std::string& _code; //!< A reference to the preset code. - TextEditor _textEditor; //!< The expression/shader code editor control. + bool _showWhitespace{true}; //!< If true, the editor will display whitespace. + std::unique_ptr _textEditor; //!< The expression/shader code editor control. bool _documentOpen{true}; //!< This flag holds the opened/closed state of the document, e.g. becomes false if the user closes the tab. - projectm_eval_context* _compileTextContext{nullptr}; //!< A projectm-eval context for test-compiling the editor code. + ContextHandle _compileTextContext{nullptr, projectm_eval_context_destroy}; //!< A projectm-eval context for test-compiling the editor code. }; } // namespace Editor From e66e1acb0900591ceb73fc1e60bb1cbbf23acf3a Mon Sep 17 00:00:00 2001 From: Kai Blaschke Date: Thu, 11 Sep 2025 21:51:40 +0200 Subject: [PATCH 12/14] Enable shift+click selection in text editor --- .../imgui_color_text_editor/TextEditor.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/gui/preset_editor/imgui_color_text_editor/TextEditor.cpp b/src/gui/preset_editor/imgui_color_text_editor/TextEditor.cpp index f325294..8cc5fb5 100644 --- a/src/gui/preset_editor/imgui_color_text_editor/TextEditor.cpp +++ b/src/gui/preset_editor/imgui_color_text_editor/TextEditor.cpp @@ -966,7 +966,7 @@ void TextEditor::HandleMouseInputs() if (ImGui::IsWindowHovered()) { - if (!shift && !alt) + if (!alt) { auto click = ImGui::IsMouseClicked(0); auto doubleClick = ImGui::IsMouseDoubleClicked(0); @@ -1013,7 +1013,13 @@ void TextEditor::HandleMouseInputs() */ else if (click) { - mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos()); + if (shift) + { + mInteractiveStart = mState.mSelectionStart; + mState.mCursorPosition = mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos()); + } + else + mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos()); if (ctrl) mSelectionMode = SelectionMode::Word; else @@ -3186,14 +3192,6 @@ const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::MilkdropEx for (auto& k : nseel2Keywords) milkdropLangDef.mKeywords.insert(k); - static const char* const identifiers[] = {}; - for (auto& k : identifiers) - { - Identifier id; - id.mDeclaration = "Internal function"; - milkdropLangDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); - } - milkdropLangDef.mTokenize = [](const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end, PaletteIndex& paletteIndex) -> bool { paletteIndex = PaletteIndex::Max; From 4610e9c5bd783371fbf1ea7b9365319c518fc9c2 Mon Sep 17 00:00:00 2001 From: Kai Blaschke Date: Sat, 4 Oct 2025 11:57:30 +0200 Subject: [PATCH 13/14] Build projectm-eval in GitHub workflows --- .github/workflows/buildcheck.yaml | 36 +++++++++++++++---------------- vcpkg.json | 1 + 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/.github/workflows/buildcheck.yaml b/.github/workflows/buildcheck.yaml index cbf58fe..495d531 100644 --- a/.github/workflows/buildcheck.yaml +++ b/.github/workflows/buildcheck.yaml @@ -64,6 +64,13 @@ jobs: cmake --build cmake-build-libprojectm --parallel cmake --install "${{ github.workspace }}/cmake-build-libprojectm" + - name: Build/Install projectm-eval + run: | + mkdir cmake-build-projectm-eval + cmake -G Ninja -S projectm/vendor/projectm-eval -B cmake-build-projectm-eval -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/install-projectm-eval + cmake --build cmake-build-projectm-eval --parallel + cmake --install "${{ github.workspace }}/cmake-build-projectm-eval" + - name: Checkout frontend-sdl2 Sources uses: actions/checkout@v4 with: @@ -75,7 +82,7 @@ jobs: mkdir cmake-build-frontend-sdl2 cmake -G Ninja -S frontend-sdl2 -B cmake-build-frontend-sdl2 \ -DCMAKE_BUILD_TYPE=Release \ - "-DCMAKE_PREFIX_PATH=${GITHUB_WORKSPACE}/install-libprojectm;${GITHUB_WORKSPACE}/install-poco" \ + "-DCMAKE_PREFIX_PATH=${GITHUB_WORKSPACE}/install-libprojectm;${{ github.workspace }}/install-projectm-eval;${GITHUB_WORKSPACE}/install-poco" \ "-DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/install-frontend-sdl2" cmake --build cmake-build-frontend-sdl2 --parallel @@ -126,20 +133,6 @@ jobs: setapikey "${{ secrets.VCPKG_PACKAGES_TOKEN }}" ` -Source "${{ env.FEED_URL }}" - - name: Checkout libprojectM Sources - uses: actions/checkout@v4 - with: - repository: projectM-visualizer/projectm - path: projectm - submodules: recursive - - - name: Build/Install libprojectM - run: | - mkdir cmake-build-libprojectm - cmake -G "Visual Studio 17 2022" -A "X64" -S "${{ github.workspace }}/projectm" -B "${{ github.workspace }}/cmake-build-libprojectm" -DCMAKE_TOOLCHAIN_FILE="${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows-static -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/install-libprojectm" -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreaded$<$:Debug>" -DCMAKE_VERBOSE_MAKEFILE=YES -DBUILD_SHARED_LIBS=OFF -DBUILD_TESTING=NO - cmake --build "${{ github.workspace }}/cmake-build-libprojectm" --config Release --parallel - cmake --install "${{ github.workspace }}/cmake-build-libprojectm" --config Release - - name: Checkout projectMSDL Sources uses: actions/checkout@v4 with: @@ -149,7 +142,7 @@ jobs: - name: Build projectMSDL run: | mkdir cmake-build-frontend-sdl2 - cmake -G "Visual Studio 17 2022" -A "X64" -S "${{ github.workspace }}/frontend-sdl2" -B "${{ github.workspace }}/cmake-build-frontend-sdl2" -DCMAKE_TOOLCHAIN_FILE="${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows-static -DCMAKE_PREFIX_PATH="${{ github.workspace }}/install-libprojectm" -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/install-frontend-sdl2" -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreaded$<$:Debug>" -DCMAKE_VERBOSE_MAKEFILE=YES -DSDL2_LINKAGE=static -DBUILD_TESTING=YES + cmake -G "Visual Studio 17 2022" -A "X64" -S "${{ github.workspace }}/frontend-sdl2" -B "${{ github.workspace }}/cmake-build-frontend-sdl2" -DCMAKE_TOOLCHAIN_FILE="${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows-static -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/install-frontend-sdl2" -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreaded$<$:Debug>" -DCMAKE_VERBOSE_MAKEFILE=YES -DSDL2_LINKAGE=static -DBUILD_TESTING=YES cmake --build "${{ github.workspace }}/cmake-build-frontend-sdl2" --parallel --config Release - name: Package projectMSDL @@ -181,10 +174,17 @@ jobs: - name: Build/Install libprojectM run: | mkdir cmake-build-libprojectm - cmake -G Ninja -S "${{ github.workspace }}/projectm" -B "${{ github.workspace }}/cmake-build-libprojectm" -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/install-libprojectm" + cmake -G Ninja -S "${{ github.workspace }}/projectm" -B "${{ github.workspace }}/cmake-build-libprojectm" -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/install-libprojectm;" cmake --build "${{ github.workspace }}/cmake-build-libprojectm" --parallel cmake --install "${{ github.workspace }}/cmake-build-libprojectm" + - name: Build/Install projectm-eval + run: | + mkdir cmake-build-projectm-eval + cmake -G Ninja -S "${{ github.workspace }}/projectm/vendor/projectm-eval" -B "${{ github.workspace }}/cmake-build-projectm-eval" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/install-projectm-eval" + cmake --build "${{ github.workspace }}/cmake-build-projectm-eval" --parallel + cmake --install "${{ github.workspace }}/cmake-build-projectm-eval" + - name: Checkout projectMSDL Sources uses: actions/checkout@v4 with: @@ -194,7 +194,7 @@ jobs: - name: Build projectMSDL run: | mkdir cmake-build-frontend-sdl2 - cmake -G Ninja -S "${{ github.workspace }}/frontend-sdl2" -B "${{ github.workspace }}/cmake-build-frontend-sdl2" -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH="${{ github.workspace }}/install-libprojectm" -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/install-frontend-sdl2" + cmake -G Ninja -S "${{ github.workspace }}/frontend-sdl2" -B "${{ github.workspace }}/cmake-build-frontend-sdl2" -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH="${{ github.workspace }}/install-libprojectm;${{ github.workspace }}/install-projectm-eval" -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/install-frontend-sdl2" cmake --build "${{ github.workspace }}/cmake-build-frontend-sdl2" --parallel - name: Package projectMSDL diff --git a/vcpkg.json b/vcpkg.json index 38f5627..cf19f06 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,6 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", "dependencies": [ + "expat", "glew", "sdl2", { From 9110ec744a6adbddf7b858cd376cb7c1d787f8c3 Mon Sep 17 00:00:00 2001 From: Kai Blaschke Date: Sat, 25 Oct 2025 17:29:30 +0200 Subject: [PATCH 14/14] Temp: Copy preset to clipboard --- src/gui/preset_editor/EditorMenu.cpp | 5 +++++ src/gui/preset_editor/PresetEditorGUI.cpp | 7 +++++++ src/gui/preset_editor/PresetEditorGUI.h | 5 +++++ 3 files changed, 17 insertions(+) diff --git a/src/gui/preset_editor/EditorMenu.cpp b/src/gui/preset_editor/EditorMenu.cpp index 913855b..9f8969f 100644 --- a/src/gui/preset_editor/EditorMenu.cpp +++ b/src/gui/preset_editor/EditorMenu.cpp @@ -59,6 +59,11 @@ void EditorMenu::DrawFileMenu() { } + if (ImGui::MenuItem(ICON_FA_COPY " Copy Preset to Clipboard")) + { + _presetEditorGUI.CopyToClipboard(); + } + ImGui::Separator(); if (ImGui::MenuItem(ICON_FA_CIRCLE_XMARK " Exit Preset Editor")) diff --git a/src/gui/preset_editor/PresetEditorGUI.cpp b/src/gui/preset_editor/PresetEditorGUI.cpp index 6d99535..7b96f3c 100644 --- a/src/gui/preset_editor/PresetEditorGUI.cpp +++ b/src/gui/preset_editor/PresetEditorGUI.cpp @@ -10,6 +10,7 @@ #include "notifications/DisplayToastNotification.h" #include "notifications/UpdateWindowTitleNotification.h" +#include #include #include @@ -123,6 +124,12 @@ void PresetEditorGUI::UpdatePresetPreview() } } +void PresetEditorGUI::CopyToClipboard() +{ + _editorPreset.ToParsedFile(_presetFile); + SDL_SetClipboardText(_presetFile.AsString().c_str()); +} + void PresetEditorGUI::HandleGlobalEditorKeys() { ImGuiIO& io = ImGui::GetIO(); diff --git a/src/gui/preset_editor/PresetEditorGUI.h b/src/gui/preset_editor/PresetEditorGUI.h index 31b3305..3ce5cfc 100644 --- a/src/gui/preset_editor/PresetEditorGUI.h +++ b/src/gui/preset_editor/PresetEditorGUI.h @@ -46,6 +46,11 @@ class PresetEditorGUI */ void UpdatePresetPreview(); + /** + * @brief Reloads the rendered preview with the current changes. + */ + void CopyToClipboard(); + private: void HandleGlobalEditorKeys(); void TakeProjectMControl();