├── .clang-format ├── .clang-tidy ├── .github ├── FUNDING.yml └── workflows │ └── build_and_run_tests.yml ├── .gitignore ├── .gitmodules ├── .vscode └── settings.json ├── CMakeLists.txt ├── Design Decisions.md ├── LICENSE ├── README.md ├── build └── imgui.ini ├── generate_flags_checkbox.py ├── generated └── checkboxes_for_all_flags.inl ├── imgui.ini ├── include └── imgui_gradient │ └── imgui_gradient.hpp ├── src ├── ColorRGBA.hpp ├── Flags.hpp ├── Gradient.cpp ├── Gradient.hpp ├── GradientWidget.cpp ├── GradientWidget.hpp ├── HoverChecker.cpp ├── HoverChecker.hpp ├── Interpolation.hpp ├── Mark.hpp ├── MarkId.hpp ├── RelativePosition.cpp ├── RelativePosition.hpp ├── Settings.hpp ├── Utils.hpp ├── WrapMode.hpp ├── color_conversions.cpp ├── color_conversions.hpp ├── extra_widgets.cpp ├── extra_widgets.hpp ├── imgui_draw.cpp ├── imgui_draw.hpp ├── imgui_internal.hpp ├── internal.hpp ├── maybe_disabled.cpp └── maybe_disabled.hpp └── tests ├── CMakeLists.txt └── tests.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: Google 3 | AccessModifierOffset: '-4' 4 | AlignAfterOpenBracket: BlockIndent 5 | AlignConsecutiveMacros: 'true' 6 | AlignConsecutiveAssignments: 'true' 7 | AlignConsecutiveDeclarations: 'true' 8 | AlignEscapedNewlines: Left 9 | AlignOperands: 'true' 10 | AlignTrailingComments: 'true' 11 | AllowAllArgumentsOnNextLine: 'true' 12 | AllowAllConstructorInitializersOnNextLine: 'true' 13 | AllowAllParametersOfDeclarationOnNextLine: 'true' 14 | AllowShortBlocksOnASingleLine: 'false' 15 | AllowShortCaseLabelsOnASingleLine: 'true' 16 | AllowShortFunctionsOnASingleLine: Inline 17 | AllowShortIfStatementsOnASingleLine: Never 18 | AllowShortLambdasOnASingleLine: Inline 19 | AllowShortLoopsOnASingleLine: 'false' 20 | AlwaysBreakAfterReturnType: None 21 | AlwaysBreakBeforeMultilineStrings: 'true' 22 | AlwaysBreakTemplateDeclarations: 'Yes' 23 | BinPackArguments: 'true' 24 | BinPackParameters: 'true' 25 | BreakBeforeBinaryOperators: NonAssignment 26 | BreakBeforeBraces: Custom 27 | BraceWrapping: 28 | AfterCaseLabel: true 29 | AfterClass: false 30 | AfterControlStatement: true 31 | AfterEnum: false 32 | AfterFunction: true 33 | AfterNamespace: false 34 | AfterObjCDeclaration: true 35 | AfterStruct: false 36 | AfterUnion: false 37 | AfterExternBlock: true 38 | BeforeCatch: true 39 | BeforeElse: true 40 | BeforeLambdaBody: false 41 | IndentBraces: false 42 | SplitEmptyFunction: false 43 | SplitEmptyRecord: false 44 | SplitEmptyNamespace: true 45 | BreakBeforeTernaryOperators: 'true' 46 | BreakConstructorInitializers: BeforeComma 47 | BreakInheritanceList: BeforeComma 48 | BreakStringLiterals: 'true' 49 | ColumnLimit: '0' 50 | CompactNamespaces: 'true' 51 | ConstructorInitializerAllOnOneLineOrOnePerLine: 'true' 52 | ConstructorInitializerIndentWidth: '4' 53 | ContinuationIndentWidth: '4' 54 | Cpp11BracedListStyle: 'true' 55 | DerivePointerAlignment: 'false' 56 | DisableFormat: 'false' 57 | ExperimentalAutoDetectBinPacking: 'false' 58 | FixNamespaceComments: 'true' 59 | IncludeBlocks: Merge 60 | IndentCaseLabels: 'false' 61 | IndentPPDirectives: None 62 | IndentWidth: '4' 63 | IndentWrappedFunctionNames: 'true' 64 | KeepEmptyLinesAtTheStartOfBlocks: 'false' 65 | Language: Cpp 66 | MaxEmptyLinesToKeep: '1' 67 | NamespaceIndentation: None 68 | PointerAlignment: Left 69 | ReflowComments: 'true' 70 | SortIncludes: 'true' 71 | SortUsingDeclarations: 'false' 72 | SpaceAfterCStyleCast: 'false' 73 | SpaceAfterLogicalNot: 'false' 74 | SpaceAfterTemplateKeyword: 'false' 75 | SpaceBeforeAssignmentOperators: 'true' 76 | SpaceBeforeCpp11BracedList: 'false' 77 | SpaceBeforeCtorInitializerColon: 'true' 78 | SpaceBeforeInheritanceColon: 'true' 79 | SpaceBeforeParens: ControlStatements 80 | SpaceBeforeRangeBasedForLoopColon: 'true' 81 | SpaceInEmptyParentheses: 'false' 82 | SpacesBeforeTrailingComments: '1' 83 | SpacesInAngles: 'false' 84 | SpacesInCStyleCastParentheses: 'false' 85 | SpacesInContainerLiterals: 'false' 86 | SpacesInParentheses: 'false' 87 | SpacesInSquareBrackets: 'false' 88 | Standard: Auto 89 | TabWidth: '4' 90 | UseTab: Never -------------------------------------------------------------------------------- /.clang-tidy: -------------------------------------------------------------------------------- 1 | --- 2 | Checks: ' 3 | *, 4 | -readability-convert-member-functions-to-static, 5 | -misc-non-private-member-variables-in-classes, 6 | -*braces-around-statements 7 | -readability-implicit-bool-conversion 8 | -readability-identifier-length, 9 | -cppcoreguidelines-avoid-magic-numbers, 10 | -readability-magic-numbers, 11 | -readability-redundant-access-specifiers, 12 | -readability-redundant-string-init, 13 | -misc-use-anonymous-namespace, 14 | -cppcoreguidelines-pro-type-union-access, 15 | -cppcoreguidelines-pro-bounds-array-to-pointer-decay, -hicpp-no-array-decay, 16 | -hicpp-signed-bitwise, 17 | -hicpp-named-parameter, -readability-named-parameter, 18 | -misc-no-recursion, 19 | -performance-no-automatic-move, 20 | -cppcoreguidelines-pro-type-vararg, -hicpp-vararg, 21 | -google-runtime-references, 22 | -fuchsia-default-arguments-*, 23 | -fuchsia-overloaded-operator, 24 | -fuchsia-trailing-return, 25 | -hicpp-uppercase-literal-suffix, -readability-uppercase-literal-suffix, 26 | -abseil-*, 27 | -altera-*, 28 | -android-*, 29 | -boost-*, 30 | -linuxkernel-*, 31 | -llvm-*, 32 | -llvmlibc-*, 33 | -mpi-*, 34 | -objc-*, 35 | -openmp-*, 36 | -zircon-*, 37 | ' -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [Coollab-Art] 2 | patreon: Coollab 3 | -------------------------------------------------------------------------------- /.github/workflows/build_and_run_tests.yml: -------------------------------------------------------------------------------- 1 | name: Build and run tests 2 | 3 | on: 4 | push: 5 | branches: [main, mixbox] 6 | pull_request: 7 | branches: [main, mixbox] 8 | 9 | env: 10 | cmake_configure_args: -D WARNINGS_AS_ERRORS_FOR_IMGUI_GRADIENT=ON 11 | cmakelists_folder: tests 12 | cmake_target: imgui_gradient-tests 13 | 14 | jobs: 15 | build-and-run-tests: 16 | name: ${{matrix.config.name}} ${{matrix.build_type}} 17 | runs-on: ${{matrix.config.os}} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | config: 22 | - { 23 | name: Windows MSVC, 24 | os: windows-latest, 25 | cmake_configure_args: -D CMAKE_C_COMPILER=cl CMAKE_CXX_COMPILER=cl, 26 | } 27 | - { 28 | name: Windows Clang, 29 | os: windows-latest, 30 | cmake_configure_args: -D CMAKE_C_COMPILER=clang -D CMAKE_CXX_COMPILER=clang, 31 | } 32 | - { 33 | name: Windows GCC, 34 | os: windows-latest, 35 | cmake_configure_args: -D CMAKE_C_COMPILER=gcc -D CMAKE_CXX_COMPILER=g++, 36 | } 37 | - { 38 | name: Linux Clang, 39 | os: ubuntu-latest, 40 | cmake_configure_args: -D CMAKE_C_COMPILER=clang -D CMAKE_CXX_COMPILER=clang++, 41 | } 42 | - { 43 | name: Linux GCC, 44 | os: ubuntu-latest, 45 | cmake_configure_args: -D CMAKE_C_COMPILER=gcc -D CMAKE_CXX_COMPILER=g++, 46 | } 47 | - { 48 | name: MacOS Clang, 49 | os: macos-latest, 50 | cmake_configure_args: -D CMAKE_C_COMPILER=clang -D CMAKE_CXX_COMPILER=clang++, 51 | } 52 | build_type: 53 | - Debug 54 | - Release 55 | 56 | steps: 57 | - uses: actions/checkout@v3 58 | with: 59 | submodules: recursive 60 | 61 | - name: Set up MSVC # NOTE: required to find cl.exe and clang.exe when using the Ninja generator. And we need to use Ninja in order for ccache to be able to cache stuff on Windows. 62 | if: runner.os == 'Windows' && matrix.config.name != 'Windows GCC' 63 | uses: ilammy/msvc-dev-cmd@v1.13.0 64 | 65 | - name: Install Linux dependencies 66 | if: runner.os == 'Linux' 67 | run: sudo apt-get install -y libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev mesa-common-dev 68 | 69 | - name: ccache 70 | uses: hendrikmuhs/ccache-action@main 71 | with: 72 | key: ${{matrix.config.name}}-${{matrix.build_type}} 73 | 74 | - name: Build 75 | uses: lukka/run-cmake@v3 76 | with: 77 | cmakeListsOrSettingsJson: CMakeListsTxtAdvanced 78 | cmakeListsTxtPath: ${{github.workspace}}/${{env.cmakelists_folder}}/CMakeLists.txt 79 | cmakeAppendedArgs: ${{env.cmake_configure_args}} -G Ninja -D CMAKE_BUILD_TYPE=${{matrix.build_type}} ${{matrix.config.cmake_configure_args}} -D CMAKE_C_COMPILER_LAUNCHER=ccache -D CMAKE_CXX_COMPILER_LAUNCHER=ccache 80 | buildWithCMakeArgs: --config ${{matrix.build_type}} --target ${{env.cmake_target}} 81 | cmakeBuildType: ${{matrix.build_type}} 82 | buildDirectory: ${{github.workspace}}/build 83 | 84 | - name: Run 85 | run: ${{github.workspace}}/build/${{env.cmake_target}} -nogpu 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tests/build 2 | build/* 3 | !build/imgui.ini 4 | .vs 5 | .vscode/* 6 | !.vscode/settings.json 7 | 8 | .DS_Store 9 | __pycache__ 10 | .cache 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tooling"] 2 | path = tooling 3 | url = https://github.com/CoolLibs/tooling 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmake.sourceDirectory": "${workspaceFolder}/tests" 3 | } -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.8) 2 | 3 | set(WARNINGS_AS_ERRORS_FOR_IMGUI_GRADIENT OFF CACHE BOOL "ON iff you want to treat warnings as errors") 4 | 5 | add_library(imgui_gradient) 6 | add_library(imgui_gradient::imgui_gradient ALIAS imgui_gradient) 7 | target_compile_features(imgui_gradient PUBLIC cxx_std_11) 8 | 9 | # ---Add source files--- 10 | if(WARNINGS_AS_ERRORS_FOR_IMGUI_GRADIENT) 11 | target_include_directories(imgui_gradient PUBLIC include) 12 | else() 13 | target_include_directories(imgui_gradient SYSTEM PUBLIC include) 14 | endif() 15 | 16 | file(GLOB_RECURSE SRC_FILES CONFIGURE_DEPENDS src/*.cpp) 17 | target_sources(imgui_gradient PRIVATE ${SRC_FILES}) 18 | 19 | # Set warning level 20 | if(MSVC) 21 | target_compile_options(imgui_gradient PRIVATE /W4) 22 | else() 23 | target_compile_options(imgui_gradient PRIVATE -Wall -Wextra -Wpedantic -pedantic-errors -Wconversion -Wsign-conversion) 24 | endif() 25 | 26 | # Maybe enable warnings as errors 27 | if(WARNINGS_AS_ERRORS_FOR_IMGUI_GRADIENT) 28 | if(MSVC) 29 | target_compile_options(imgui_gradient PRIVATE /WX) 30 | else() 31 | target_compile_options(imgui_gradient PRIVATE -Werror) 32 | endif() 33 | endif() -------------------------------------------------------------------------------- /Design Decisions.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coollab-Art/imgui_gradient/df1b9eca61e0b11414fba52ff9f2fbeb315b9259/Design Decisions.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Copyright (c) 2022 Coollab-Art 4 | 5 | Permission is hereby granted, free of charge, to any person or organization 6 | obtaining a copy of the software and accompanying documentation covered by 7 | this license (the "Software") to use, reproduce, display, distribute, 8 | execute, and transmit the Software, and to prepare derivative works of the 9 | Software, and to permit third-parties to whom the Software is furnished to 10 | do so, all subject to the following: 11 | 12 | The copyright notices in the Software and this entire statement, including 13 | the above license grant, this restriction and the following disclaimer, 14 | must be included in all copies of the Software, in whole or in part, and 15 | all derivative works of the Software, unless such copies or derivative 16 | works are solely in the form of machine-executable object code generated by 17 | a source language processor. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 22 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 23 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 24 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # imgui_gradient 2 | 3 | [Dear ImGui](https://github.com/ocornut/imgui) extension that adds a *gradient* widget. 4 | 5 | ![Animation](https://user-images.githubusercontent.com/45451201/218996538-5f5f80d2-3cbe-4f5c-8c26-a107970bd97b.gif) 6 | 7 | ## Table of Contents 8 | 9 | - [Compatibility](#compatibility) 10 | - [Basic usage](#basic-usage) 11 | - [Tutorial](#tutorial) 12 | - [Including](#including) 13 | - [Sampling the gradient](#sampling-the-gradient) 14 | - [Interpolation](#interpolation) 15 | - [Settings](#settings) 16 | - [Color randomization](#color-randomization) 17 | - [For maintainers](#for-maintainers) 18 | - [Running the tests](#running-the-tests) 19 | 20 | 21 | ## Compatibility 22 | 23 | This library is tested and compiles with C++11 on: 24 | - [x] Windows 25 | - [x] Clang 26 | - [x] MSVC 27 | - [x] Linux 28 | - [x] Clang 29 | - [x] GCC 30 | - [x] MacOS 31 | - [x] Clang 32 | 33 | ## Basic usage 34 | 35 | The whole state of the widget is stored in a `ImGG::GradientWidget` variable. You need to create and store one in order to have access to a gradient. You can then use the `widget()` method to render the ImGui widget: 36 | ```cpp 37 | ImGG::GradientWidget gradient_widget{}; 38 | gradient_widget.widget("My Gradient"); 39 | ``` 40 | 41 | In order to access the gradient data (without the GUI state), you can get the `ImGG::Gradient` with `gradient_widget.gradient()`. 42 | 43 | ## Tutorial 44 | 45 | ### Including 46 | 47 | To add this library to your project, simply add these three lines to your *CMakeLists.txt* and replace `folder/containing/imgui` with the path to the folder containing the *imgui* headers: 48 | ```cmake 49 | add_subdirectory(path/to/imgui_gradient) 50 | target_include_directories(imgui_gradient SYSTEM PRIVATE folder/containing/imgui) 51 | target_link_libraries(${PROJECT_NAME} PRIVATE imgui_gradient::imgui_gradient) 52 | ``` 53 | 54 | Then include it as: 55 | ```cpp 56 | #include 57 | ``` 58 | 59 | ### Sampling the gradient 60 | 61 | ```cpp 62 | const ColorRGBA color = widget.gradient().at({0.5f}); 63 | ``` 64 | 65 | Note that you should use values between `0.f` and `1.f` when sampling the gradient. If you know you are gonna use values outside that range, specify a `WrapMode` to indicate how the values should be mapped back to the [0, 1] range: 66 | 67 | ```cpp 68 | const ColorRGBA color = widget.gradient().at({0.5f, WrapMode::Clamp}); 69 | ``` 70 | 71 | Our wrap modes mimic the OpenGL ones: 72 | 73 | - `ImGG::WrapMode::Clamp`: If the value is bigger than 1, maps it to 1. If it smaller than 0, maps it to 0. 74 | - `ImGG::WrapMode::Repeat`: Maps the number line to a bunch of copies of [0, 1] stuck together. Conceptually equivalent to adding or subtracting 1 to the position until it is in the [0, 1] range. 75 | - `ImGG::WrapMode::MirrorRepeat`: Like `ImGG::WrapMode::Repeat` except that every other range is flipped. 76 | 77 | To create a widget for the wrap mode, you can use: 78 | ```cpp 79 | ImGG::wrap_mode_widget("Wrap Mode", &wrap_mode); 80 | ``` 81 | 82 | ### Interpolation 83 | 84 | Controls how the colors are interpolated between two marks. 85 | 86 | - `ImGG::Interpolation::Linear`: Linear interpolation. 87 | - `ImGG::Interpolation::Constant`: Constant color between two marks (uses the color of the mark on the right). 88 | 89 | To create a widget that changes the interpolation mode, use: 90 | ```cpp 91 | ImGG::interpolation_mode_widget("Interpolation Mode", &widget.gradient().interpolation_mode()); 92 | ``` 93 | 94 | > NB: Our linear interpolation is done in the [Oklab](https://bottosson.github.io/posts/oklab/) color space, which gives more accurate results. We also use premultiplied alpha during the interpolation for the same reason. 95 | > BUT the colors we output are all in sRGB space with straight alpha, for ease of use. 96 | 97 | ### Settings 98 | 99 | The `ImGG::GradientWidget::widget()` method can also take settings that control its behaviour: 100 | ```cpp 101 | ImGG::Settings settings{}; 102 | 103 | settings.gradient_width = 500.f; 104 | settings.gradient_height = 40.f; 105 | settings.horizontal_margin = 10.f; 106 | 107 | /// Distance under the gradient bar to delete a mark by dragging it down. 108 | /// This behaviour can also be disabled with the Flag::NoDragDowntoDelete. 109 | settings.distance_to_delete_mark_by_dragging_down = 80.f; 110 | 111 | settings.flags = ImGG::Flag::None; 112 | 113 | settings.color_edit_flags = ImGuiColorEditFlags_None; 114 | 115 | /// Controls how the new mark color is chosen. 116 | /// If true, the new mark color will be a random color, 117 | /// otherwise it will be the one that the gradient already has at the new mark position. 118 | settings.should_use_a_random_color_for_the_new_marks = false; 119 | 120 | gradient_widget.widget("My Gradient", settings); 121 | ``` 122 | 123 | Here are all the available flags: 124 | 125 | - `ImGG::Flag::None`: All options are enabled. 126 | - `ImGG::Flag::NoTooltip`: No tooltip when hovering a widget. 127 | - `ImGG::Flag::NoResetButton`: No button to reset the gradient to its default value. 128 | - `ImGG::Flag::NoLabel`: No name for the widget. 129 | - `ImGG::Flag::NoAddButton`: No "+" button to add a mark. 130 | - `ImGG::Flag::NoRemoveButton`: No "-" button to remove a mark. 131 | - `ImGG::Flag::NoPositionSlider`: No slider widget to chose a precise position for the selected mark. 132 | - `ImGG::Flag::NoColorEdit`: No color edit widget for the selected mark. 133 | - `ImGG::Flag::NoDragDownToDelete`: Don't delete a mark when dragging it down. 134 | - `ImGG::Flag::NoBorder`: No border around the gradient widget. 135 | - `ImGG::Flag::NoAddAndRemoveButtons`: No "+" and "-" buttons. 136 | - `ImGG::Flag::NoMarkOptions`: No widgets for the selected mark. 137 | 138 | ### Color randomization 139 | 140 | `Settings::should_use_a_random_color_for_the_new_marks` allows you to randomize the colors of the marks. 141 | To create a widget for that setting you can use: 142 | ```cpp 143 | ImGG::random_mode_widget("Use a random color for the new marks", &settings.should_use_a_random_color_for_the_new_marks); 144 | ``` 145 | 146 | By default we use our own internal `std::default_random_engine`, but you can provide your own rng function that we will use instead. It can be any function that returns a number between `0.f` and `1.f`. 147 | 148 | ```cpp 149 | const auto your_custom_rng = []() { 150 | static auto rng = std::default_random_engine{std::random_device{}()}; 151 | static auto distribution = std::uniform_real_distribution{0.f, 1.f}; 152 | return distribution(rng); 153 | }; 154 | gradient_widget.widget("My Gradient", your_custom_rng, settings); 155 | ``` 156 | 157 | > *NB:* If all calls to `gradient_widget.widget(...)` provide a rng function, we will not create our `std::default_random_engine` at all. 158 | 159 | ## For maintainers 160 | 161 | ### Running the tests 162 | 163 | Simply use "tests/CMakeLists.txt" to generate a project, then run it.
164 | If you are using VSCode and the CMake extension, this project already contains a *.vscode/settings.json* that will use the right CMakeLists.txt automatically. 165 | -------------------------------------------------------------------------------- /build/imgui.ini: -------------------------------------------------------------------------------- 1 | [Window][MyMainDockSpace] 2 | Pos=0,0 3 | Size=1920,1001 4 | Collapsed=0 5 | 6 | [Window][imgui_gradient tests] 7 | Pos=0,0 8 | Size=1368,480 9 | Collapsed=0 10 | DockId=0x00000007,0 11 | 12 | [Window][Debug##Default] 13 | Pos=60,60 14 | Size=400,400 15 | Collapsed=0 16 | 17 | [Window][Flags] 18 | Pos=0,482 19 | Size=581,519 20 | Collapsed=0 21 | DockId=0x00000003,0 22 | 23 | [Window][Programmatic Actions] 24 | Pos=583,482 25 | Size=754,257 26 | Collapsed=0 27 | DockId=0x0000000B,0 28 | 29 | [Window][Options] 30 | Pos=583,741 31 | Size=754,260 32 | Collapsed=0 33 | DockId=0x0000000C,0 34 | 35 | [Window][Framerate] 36 | Pos=1370,0 37 | Size=550,58 38 | Collapsed=0 39 | DockId=0x00000009,0 40 | 41 | [Window][Dear ImGui Demo] 42 | Pos=1370,60 43 | Size=550,420 44 | Collapsed=0 45 | DockId=0x0000000A,0 46 | 47 | [Window][Settings] 48 | Pos=1339,482 49 | Size=581,519 50 | Collapsed=0 51 | DockId=0x00000006,0 52 | 53 | [Docking][Data] 54 | DockSpace ID=0xF3CABE56 Window=0x74B75B81 Pos=0,29 Size=1920,1001 Split=Y Selected=0x8AC27BA7 55 | DockNode ID=0x00000001 Parent=0xF3CABE56 SizeRef=1920,480 Split=X Selected=0x8AC27BA7 56 | DockNode ID=0x00000007 Parent=0x00000001 SizeRef=1368,278 CentralNode=1 Selected=0x8AC27BA7 57 | DockNode ID=0x00000008 Parent=0x00000001 SizeRef=550,278 Split=Y Selected=0xE87781F4 58 | DockNode ID=0x00000009 Parent=0x00000008 SizeRef=550,58 Selected=0xD38DAB5E 59 | DockNode ID=0x0000000A Parent=0x00000008 SizeRef=550,420 Selected=0xE87781F4 60 | DockNode ID=0x00000002 Parent=0xF3CABE56 SizeRef=1920,519 Split=X Selected=0xE205E13F 61 | DockNode ID=0x00000003 Parent=0x00000002 SizeRef=581,399 Selected=0xE205E13F 62 | DockNode ID=0x00000004 Parent=0x00000002 SizeRef=1337,399 Split=X Selected=0xCDE67257 63 | DockNode ID=0x00000005 Parent=0x00000004 SizeRef=754,519 Split=Y Selected=0xCDE67257 64 | DockNode ID=0x0000000B Parent=0x00000005 SizeRef=754,257 Selected=0xCDE67257 65 | DockNode ID=0x0000000C Parent=0x00000005 SizeRef=754,260 Selected=0xE365E069 66 | DockNode ID=0x00000006 Parent=0x00000004 SizeRef=581,519 Selected=0x54723243 67 | 68 | -------------------------------------------------------------------------------- /generate_flags_checkbox.py: -------------------------------------------------------------------------------- 1 | # ---HOW TO--- 2 | # Modify `all_flags()` and `includes()`, then run this script. 3 | # ------------ 4 | 5 | 6 | def all_flags(): 7 | return { 8 | "ImGG::Flag::NoTooltip": ["isNoTooltip"], 9 | "ImGG::Flag::NoResetButton": ["isNoResetButton"], 10 | "ImGG::Flag::NoLabel": ["isNoLabel"], 11 | "ImGG::Flag::NoAddButton": ["isNoAddButton"], 12 | "ImGG::Flag::NoRemoveButton": ["isNoRemoveButton"], 13 | "ImGG::Flag::NoPositionSlider": ["isNoPositionSlider"], 14 | "ImGG::Flag::NoColorEdit": ["isNoColorEdit"], 15 | "ImGG::Flag::NoDragDownToDelete": ["isNoDragDownToDelete"], 16 | "ImGG::Flag::NoBorder": ["isNoBorder"], 17 | "ImGG::Flag::NoAddAndRemoveButtons": ["isNoAddAndRemoveButtons"], 18 | "ImGG::Flag::NoMarkOptions": ["isNoMarkOptions"], 19 | } 20 | 21 | 22 | def checkboxes_for_all_flags(): 23 | out = f""" 24 | #include 25 | #include "../src/Flags.hpp" 26 | 27 | auto checkboxes_for_all_flags() -> ImGG::Flags 28 | {{ 29 | ImGG::Flags options = ImGG::Flag::None; 30 | 31 | """ 32 | for key, values in all_flags().items(): 33 | for value in values: 34 | out += f""" 35 | static auto {value} = false; 36 | ImGui::Checkbox("{key}", &{value}); 37 | if ({value}) 38 | {{ 39 | options|={key}; 40 | }} 41 | """ 42 | out += f""" 43 | return options; 44 | }}""" 45 | return out 46 | 47 | 48 | if __name__ == '__main__': 49 | from tooling.generate_files import generate 50 | generate( 51 | folder="generated", 52 | files=[ 53 | checkboxes_for_all_flags, 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /generated/checkboxes_for_all_flags.inl: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------------- 2 | * This file was automatically generated by a Python script. 3 | * PLEASE DON'T EDIT IT DIRECTLY, your changes would be overwritten the next time the script is run. 4 | * Instead, go to "generate_flags_checkbox.py" and edit the "checkboxes_for_all_flags" function there. 5 | * ----------------------------------------------------------------------------- 6 | */ 7 | 8 | #include 9 | #include "../src/Flags.hpp" 10 | 11 | auto checkboxes_for_all_flags() -> ImGG::Flags 12 | { 13 | ImGG::Flags options = ImGG::Flag::None; 14 | 15 | static auto isNoTooltip = false; 16 | ImGui::Checkbox("ImGG::Flag::NoTooltip", &isNoTooltip); 17 | if (isNoTooltip) 18 | { 19 | options |= ImGG::Flag::NoTooltip; 20 | } 21 | 22 | static auto isNoResetButton = false; 23 | ImGui::Checkbox("ImGG::Flag::NoResetButton", &isNoResetButton); 24 | if (isNoResetButton) 25 | { 26 | options |= ImGG::Flag::NoResetButton; 27 | } 28 | 29 | static auto isNoLabel = false; 30 | ImGui::Checkbox("ImGG::Flag::NoLabel", &isNoLabel); 31 | if (isNoLabel) 32 | { 33 | options |= ImGG::Flag::NoLabel; 34 | } 35 | 36 | static auto isNoAddButton = false; 37 | ImGui::Checkbox("ImGG::Flag::NoAddButton", &isNoAddButton); 38 | if (isNoAddButton) 39 | { 40 | options |= ImGG::Flag::NoAddButton; 41 | } 42 | 43 | static auto isNoRemoveButton = false; 44 | ImGui::Checkbox("ImGG::Flag::NoRemoveButton", &isNoRemoveButton); 45 | if (isNoRemoveButton) 46 | { 47 | options |= ImGG::Flag::NoRemoveButton; 48 | } 49 | 50 | static auto isNoPositionSlider = false; 51 | ImGui::Checkbox("ImGG::Flag::NoPositionSlider", &isNoPositionSlider); 52 | if (isNoPositionSlider) 53 | { 54 | options |= ImGG::Flag::NoPositionSlider; 55 | } 56 | 57 | static auto isNoColorEdit = false; 58 | ImGui::Checkbox("ImGG::Flag::NoColorEdit", &isNoColorEdit); 59 | if (isNoColorEdit) 60 | { 61 | options |= ImGG::Flag::NoColorEdit; 62 | } 63 | 64 | static auto isNoDragDownToDelete = false; 65 | ImGui::Checkbox("ImGG::Flag::NoDragDownToDelete", &isNoDragDownToDelete); 66 | if (isNoDragDownToDelete) 67 | { 68 | options |= ImGG::Flag::NoDragDownToDelete; 69 | } 70 | 71 | static auto isNoBorder = false; 72 | ImGui::Checkbox("ImGG::Flag::NoBorder", &isNoBorder); 73 | if (isNoBorder) 74 | { 75 | options |= ImGG::Flag::NoBorder; 76 | } 77 | 78 | static auto isNoAddAndRemoveButtons = false; 79 | ImGui::Checkbox("ImGG::Flag::NoAddAndRemoveButtons", &isNoAddAndRemoveButtons); 80 | if (isNoAddAndRemoveButtons) 81 | { 82 | options |= ImGG::Flag::NoAddAndRemoveButtons; 83 | } 84 | 85 | static auto isNoMarkOptions = false; 86 | ImGui::Checkbox("ImGG::Flag::NoMarkOptions", &isNoMarkOptions); 87 | if (isNoMarkOptions) 88 | { 89 | options |= ImGG::Flag::NoMarkOptions; 90 | } 91 | 92 | return options; 93 | } -------------------------------------------------------------------------------- /imgui.ini: -------------------------------------------------------------------------------- 1 | [Window][MyMainDockSpace] 2 | Pos=0,0 3 | Size=1920,991 4 | Collapsed=0 5 | 6 | [Window][Gradient Editor] 7 | Size=1920,590 8 | Collapsed=0 9 | DockId=0x00000001,0 10 | 11 | [Window][Debug##Default] 12 | Pos=60,60 13 | Size=400,400 14 | Collapsed=0 15 | 16 | [Window][Tests] 17 | Pos=0,592 18 | Size=1920,399 19 | Collapsed=0 20 | DockId=0x00000002,0 21 | 22 | [Window][imgui_gradient tests] 23 | Pos=0,0 24 | Size=1920,464 25 | Collapsed=0 26 | DockId=0x00000001,0 27 | 28 | [Window][Options] 29 | Pos=1713,716 30 | Size=207,275 31 | Collapsed=0 32 | DockId=0x0000000A,0 33 | 34 | [Window][Programmatic Actions] 35 | Pos=542,466 36 | Size=1169,525 37 | Collapsed=0 38 | DockId=0x00000007,0 39 | 40 | [Window][Flags] 41 | Pos=0,466 42 | Size=540,525 43 | Collapsed=0 44 | DockId=0x00000005,0 45 | 46 | [Window][Framerate] 47 | Pos=1713,466 48 | Size=207,248 49 | Collapsed=0 50 | DockId=0x00000009,0 51 | 52 | [Docking][Data] 53 | DockSpace ID=0xF3CABE56 Window=0x74B75B81 Pos=0,29 Size=1920,991 Split=Y Selected=0x1DEBD970 54 | DockNode ID=0x00000003 Parent=0xF3CABE56 SizeRef=1920,464 Split=Y 55 | DockNode ID=0x00000001 Parent=0x00000003 SizeRef=1920,590 CentralNode=1 Selected=0x8AC27BA7 56 | DockNode ID=0x00000002 Parent=0x00000003 SizeRef=1920,399 Selected=0x0A7F6D31 57 | DockNode ID=0x00000004 Parent=0xF3CABE56 SizeRef=1920,525 Split=X Selected=0xE205E13F 58 | DockNode ID=0x00000005 Parent=0x00000004 SizeRef=540,399 Selected=0xE205E13F 59 | DockNode ID=0x00000006 Parent=0x00000004 SizeRef=1378,399 Split=X Selected=0xCDE67257 60 | DockNode ID=0x00000007 Parent=0x00000006 SizeRef=1169,399 Selected=0xCDE67257 61 | DockNode ID=0x00000008 Parent=0x00000006 SizeRef=207,399 Split=Y Selected=0xD38DAB5E 62 | DockNode ID=0x00000009 Parent=0x00000008 SizeRef=207,248 Selected=0xD38DAB5E 63 | DockNode ID=0x0000000A Parent=0x00000008 SizeRef=207,275 Selected=0xE365E069 64 | 65 | -------------------------------------------------------------------------------- /include/imgui_gradient/imgui_gradient.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../src/GradientWidget.hpp" 4 | #include "../src/extra_widgets.hpp" 5 | -------------------------------------------------------------------------------- /src/ColorRGBA.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define IMGUI_DEFINE_MATH_OPERATORS 4 | #include // Include ImVec4 5 | 6 | namespace ImGG { 7 | 8 | /// sRGB, Straight Alpha 9 | using ColorRGBA = ImVec4; 10 | 11 | } // namespace ImGG -------------------------------------------------------------------------------- /src/Flags.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace ImGG { 4 | 5 | using Flags = int; 6 | 7 | namespace Flag { 8 | 9 | enum ImGuiGradientFlag { 10 | None = 0, // All options are enabled 11 | NoTooltip = 1 << 1, // No tooltip when hovering a widget 12 | NoResetButton = 1 << 2, // No button to reset the gradient to its default value. 13 | NoLabel = 1 << 3, // No name for the widget 14 | NoAddButton = 1 << 5, // No "+" button to add a mark 15 | NoRemoveButton = 1 << 6, // No "-" button to remove a mark 16 | NoPositionSlider = 1 << 7, // No slider widget to chose a precise position for the selected mark 17 | NoColorEdit = 1 << 8, // No color edit widget for the selected mark 18 | NoDragDownToDelete = 1 << 11, // Don't delete a mark when dragging it down 19 | NoBorder = 1 << 12, // No border around the gradient widget 20 | NoAddAndRemoveButtons = NoAddButton | NoRemoveButton, // No "+" and "-" buttons 21 | NoMarkOptions = NoColorEdit | NoPositionSlider, // No widgets for the selected mark 22 | }; 23 | 24 | } // namespace Flag 25 | } // namespace ImGG 26 | -------------------------------------------------------------------------------- /src/Gradient.cpp: -------------------------------------------------------------------------------- 1 | #include "Gradient.hpp" 2 | #include "color_conversions.hpp" 3 | #include "imgui_internal.hpp" 4 | 5 | namespace ImGG { 6 | 7 | void Gradient::sort_marks() 8 | { 9 | _marks.sort([](const Mark& a, const Mark& b) { return a.position < b.position; }); 10 | } 11 | 12 | auto Gradient::find(MarkId id) const -> const Mark* 13 | { 14 | return id.find(*this); 15 | } 16 | auto Gradient::find(MarkId id) -> Mark* 17 | { 18 | return id.find(*this); 19 | } 20 | 21 | auto Gradient::find_iterator(MarkId id) const -> std::list::const_iterator 22 | { 23 | return id.find_iterator(*this); 24 | } 25 | 26 | auto Gradient::find_iterator(MarkId id) -> std::list::iterator 27 | { 28 | return id.find_iterator(*this); 29 | } 30 | 31 | auto Gradient::is_empty() const -> bool 32 | { 33 | return _marks.empty(); 34 | } 35 | 36 | auto Gradient::add_mark(const Mark& mark) -> MarkId 37 | { 38 | _marks.push_back(mark); 39 | const auto id = MarkId{_marks.back()}; 40 | sort_marks(); 41 | return id; 42 | } 43 | 44 | void Gradient::remove_mark(MarkId mark) 45 | { 46 | const auto* const ptr = find(mark); 47 | if (ptr) 48 | { 49 | _marks.remove(*ptr); 50 | } 51 | } 52 | 53 | void Gradient::clear() 54 | { 55 | _marks.clear(); 56 | } 57 | 58 | void Gradient::set_mark_position(const MarkId mark, const RelativePosition position) 59 | { 60 | auto* const ptr = find(mark); 61 | if (ptr) 62 | { 63 | ptr->position = position; 64 | sort_marks(); 65 | } 66 | } 67 | 68 | void Gradient::set_mark_color(const MarkId mark, const ColorRGBA color) 69 | { 70 | auto* const ptr = find(mark); 71 | if (ptr) 72 | { 73 | ptr->color = color; 74 | } 75 | } 76 | 77 | auto Gradient::interpolation_mode() const -> Interpolation 78 | { 79 | return _interpolation_mode; 80 | } 81 | 82 | auto Gradient::interpolation_mode() -> Interpolation& 83 | { 84 | return _interpolation_mode; 85 | } 86 | 87 | void Gradient::distribute_marks_evenly() 88 | { 89 | if (_marks.empty()) 90 | return; 91 | 92 | if (_marks.size() == 1) 93 | { 94 | _marks.begin()->position.set(0.5f); 95 | return; 96 | } 97 | 98 | const float f = 1.f / static_cast(_marks.size() - (_interpolation_mode == Interpolation::Constant ? 0 : 1)); 99 | 100 | float i = _interpolation_mode == Interpolation::Constant ? 1.f : 0.f; 101 | for (auto& mark : _marks) 102 | { 103 | mark.position.set(f * i); 104 | i += 1.f; 105 | } 106 | } 107 | 108 | auto Gradient::get_marks() const -> const std::list& 109 | { 110 | return _marks; 111 | } 112 | 113 | struct SurroundingMarks { 114 | SurroundingMarks(const Mark* lower, const Mark* upper) // We need to explicitly define the constructor in order to compile with MacOS Clang in C++ 11 115 | : lower{lower} 116 | , upper{upper} 117 | {} 118 | const Mark* lower{nullptr}; 119 | const Mark* upper{nullptr}; 120 | }; 121 | 122 | /// Returns the marks positioned just before and after `position`, or nullptr if there is none. 123 | static auto get_marks_surrounding(const RelativePosition position, const std::list& marks) -> SurroundingMarks 124 | { 125 | const Mark* lower{nullptr}; 126 | const Mark* upper{nullptr}; 127 | for (const Mark& mark : marks) 128 | { 129 | if (mark.position >= position && (!upper || mark.position < upper->position)) 130 | { 131 | upper = &mark; 132 | } 133 | if (mark.position <= position && (!lower || mark.position > lower->position)) 134 | { 135 | lower = &mark; 136 | } 137 | } 138 | return SurroundingMarks{lower, upper}; 139 | } 140 | 141 | static auto interpolate(const Mark& lower, const Mark& upper, const RelativePosition position, Interpolation interpolation_mode) -> ColorRGBA 142 | { 143 | switch (interpolation_mode) 144 | { 145 | case Interpolation::Linear: 146 | { 147 | const float mix_factor = (position.get() - lower.position.get()) 148 | / (upper.position.get() - lower.position.get()); 149 | // Do the interpolation in Lab space with premultiplied alpha because it looks much better. 150 | return internal::sRGB_Straight_from_Oklab_Premultiplied(ImLerp( 151 | internal::Oklab_Premultiplied_from_sRGB_Straight(lower.color), 152 | internal::Oklab_Premultiplied_from_sRGB_Straight(upper.color), 153 | mix_factor 154 | )); 155 | } 156 | 157 | case Interpolation::Constant: 158 | { 159 | return upper.color; 160 | } 161 | 162 | default: 163 | assert(false && "[ImGuiGradient::interpolate] Invalid enum value"); 164 | return {-1.f, -1.f, -1.f, -1.f}; 165 | } 166 | } 167 | 168 | auto Gradient::at(const RelativePosition position) const -> ColorRGBA 169 | { 170 | const auto surrounding_marks = get_marks_surrounding(position, _marks); 171 | const Mark* const lower{surrounding_marks.lower}; 172 | const Mark* const upper{surrounding_marks.upper}; 173 | 174 | if (!lower && !upper) 175 | { 176 | return ColorRGBA{0.f, 0.f, 0.f, 1.f}; 177 | } 178 | else if (upper && !lower) 179 | { 180 | return upper->color; 181 | } 182 | else if (!upper && lower) 183 | { 184 | return lower->color; 185 | } 186 | else if (upper == lower) 187 | { 188 | return upper->color; 189 | } 190 | else 191 | { 192 | return interpolate(*lower, *upper, position, _interpolation_mode); 193 | } 194 | } 195 | 196 | } // namespace ImGG 197 | -------------------------------------------------------------------------------- /src/Gradient.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "Interpolation.hpp" 5 | #include "MarkId.hpp" 6 | 7 | namespace ImGG { 8 | 9 | class Gradient { 10 | public: 11 | Gradient() = default; 12 | explicit Gradient(const std::list& marks) 13 | : _marks{marks} 14 | {} 15 | 16 | /// Returns the color at the given position in the gradient. 17 | /// 0.f corresponds to the beginning of the gradient and 1.f to the end. 18 | auto at(RelativePosition) const -> ColorRGBA; 19 | 20 | auto find(MarkId) const -> const Mark*; 21 | auto find(MarkId) -> Mark*; 22 | auto find_iterator(MarkId id) const -> std::list::const_iterator; 23 | auto find_iterator(MarkId id) -> std::list::iterator; 24 | auto contains(MarkId id) const -> bool { return find(id); } 25 | auto is_empty() const -> bool; 26 | 27 | auto add_mark(const Mark&) -> MarkId; 28 | void remove_mark(MarkId); 29 | void clear(); 30 | void set_mark_position(MarkId, RelativePosition); 31 | void set_mark_color(MarkId, ColorRGBA); 32 | auto interpolation_mode() const -> Interpolation; 33 | auto interpolation_mode() -> Interpolation&; 34 | 35 | void distribute_marks_evenly(); 36 | 37 | auto get_marks() const -> const std::list&; 38 | 39 | friend auto operator==(const Gradient& a, const Gradient& b) -> bool { return a._marks == b._marks; } 40 | 41 | private: 42 | void sort_marks(); 43 | 44 | private: 45 | std::list _marks{ 46 | // We use a std::list instead of a std::vector because it doesn't invalidate our iterators when adding, removing or sorting the marks. 47 | Mark{RelativePosition{0.f}, ColorRGBA{0.f, 0.f, 0.f, 1.f}}, 48 | Mark{RelativePosition{1.f}, ColorRGBA{1.f, 1.f, 1.f, 1.f}}, 49 | }; 50 | /// Controls how the colors are interpolated between two marks. 51 | Interpolation _interpolation_mode{Interpolation::Linear}; 52 | 53 | friend class MarkId; 54 | }; 55 | 56 | } // namespace ImGG -------------------------------------------------------------------------------- /src/GradientWidget.cpp: -------------------------------------------------------------------------------- 1 | #include "GradientWidget.hpp" 2 | #include 3 | #include 4 | #include 5 | #include "Settings.hpp" 6 | #include "imgui_draw.hpp" 7 | #include "internal.hpp" 8 | #include "maybe_disabled.hpp" 9 | 10 | namespace ImGG { 11 | 12 | static auto new_mark_id(const Gradient& new_gradient, const Gradient& old_gradient, MarkId old_mark_id) -> MarkId 13 | { 14 | const auto old_iterator = old_gradient.find_iterator(old_mark_id); 15 | if (old_iterator != old_gradient.get_marks().end()) 16 | { 17 | const auto dist = std::distance( 18 | old_gradient.get_marks().begin(), 19 | old_iterator 20 | ); 21 | return MarkId{ 22 | std::next( 23 | new_gradient.get_marks().begin(), 24 | dist 25 | ) 26 | }; 27 | } 28 | else 29 | { 30 | return MarkId{}; 31 | } 32 | } 33 | 34 | GradientWidget::GradientWidget(const GradientWidget& widget) 35 | : _gradient{widget._gradient} 36 | , _selected_mark{new_mark_id(_gradient, widget._gradient, widget._selected_mark)} 37 | , _dragged_mark{new_mark_id(_gradient, widget._gradient, widget._dragged_mark)} 38 | , _mark_to_hide{new_mark_id(_gradient, widget._gradient, widget._mark_to_hide)} 39 | , _hover_checker{widget._hover_checker} 40 | {} 41 | 42 | static auto random_color(RandomNumberGenerator rng) -> ColorRGBA 43 | { 44 | return ColorRGBA{rng(), rng(), rng(), 1.f}; 45 | } 46 | 47 | void GradientWidget::add_mark_with_chosen_mode(const RelativePosition relative_pos, RandomNumberGenerator rng, bool add_a_random_color) 48 | { 49 | const auto mark = Mark{ 50 | relative_pos, 51 | add_a_random_color 52 | ? random_color(rng) 53 | : _gradient.at(relative_pos) 54 | }; 55 | _selected_mark = _gradient.add_mark(mark); 56 | } 57 | 58 | static auto delete_button(const bool disable, const char* reason_for_disabling, const bool should_show_tooltip, Settings const& settings) -> bool 59 | { 60 | bool b = false; 61 | maybe_disabled(disable, reason_for_disabling, [&]() { 62 | b |= settings.minus_button_widget(); 63 | }); 64 | if (!disable && should_show_tooltip) 65 | ImGui::SetItemTooltip("%s", "Removes the selected mark.\nYou can also middle click on it,\nor drag it down."); 66 | return b; 67 | } 68 | 69 | static auto add_button(const bool should_show_tooltip, Settings const& settings) -> bool 70 | { 71 | bool const b = settings.plus_button_widget(); 72 | if (should_show_tooltip) 73 | ImGui::SetItemTooltip("%s", "Add a mark here\nor click on the gradient to choose its position."); 74 | return b; 75 | } 76 | 77 | static auto color_button( 78 | Mark& selected_mark, 79 | const bool should_show_tooltip, 80 | const ImGuiColorEditFlags flags = 0 81 | ) -> bool 82 | { 83 | return ImGui::ColorEdit4( 84 | "##colorpicker1", 85 | reinterpret_cast(&selected_mark.color), 86 | flags | ImGuiColorEditFlags_NoInputs | (should_show_tooltip ? 0 : ImGuiColorEditFlags_NoTooltip) 87 | ); 88 | } 89 | 90 | static auto open_color_picker_popup( 91 | Mark& selected_mark, 92 | const float popup_size, 93 | const bool should_show_tooltip, 94 | const ImGuiColorEditFlags flags = 0 95 | ) -> bool 96 | { 97 | if (ImGui::BeginPopup("SelectedMarkColorPicker")) 98 | { 99 | ImGui::SetNextItemWidth(popup_size); 100 | const bool modified = ImGui::ColorPicker4( 101 | "##colorpicker2", 102 | reinterpret_cast(&selected_mark.color), 103 | flags | (should_show_tooltip ? 0 : ImGuiColorEditFlags_NoTooltip) 104 | ); 105 | ImGui::EndPopup(); 106 | return modified; 107 | } 108 | else 109 | { 110 | return false; 111 | } 112 | } 113 | 114 | static void draw_gradient_bar( 115 | Gradient& gradient, 116 | const ImVec2 gradient_bar_position, 117 | const ImVec2 gradient_size 118 | ) 119 | { 120 | ImDrawList& draw_list = *ImGui::GetWindowDrawList(); 121 | draw_border({ 122 | gradient_bar_position, 123 | gradient_bar_position + gradient_size, 124 | }); 125 | if (!gradient.is_empty()) 126 | { 127 | draw_gradient( 128 | draw_list, 129 | gradient, 130 | gradient_bar_position, 131 | gradient_size 132 | ); 133 | } 134 | ImGui::SetCursorScreenPos( 135 | gradient_bar_position + ImVec2{0.f, gradient_size.y} 136 | ); 137 | ImGui::Dummy({0.f, 0.f}); // Needed by ImGui after a SetCursorScreenPos 138 | } 139 | 140 | static auto handle_interactions_with_hovered_mark( 141 | MarkId& dragged_mark, 142 | MarkId& selected_mark, 143 | MarkId& mark_to_delete, 144 | MarkId hovered_mark 145 | ) -> bool 146 | { 147 | bool interacted{false}; 148 | if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) 149 | { 150 | dragged_mark = hovered_mark; 151 | selected_mark = hovered_mark; 152 | interacted = true; 153 | } 154 | if (ImGui::IsMouseDoubleClicked(ImGuiPopupFlags_MouseButtonLeft)) 155 | { 156 | ImGui::OpenPopup("SelectedMarkColorPicker"); 157 | selected_mark = hovered_mark; 158 | dragged_mark.reset(); 159 | interacted = true; 160 | } 161 | if (ImGui::IsMouseReleased(ImGuiPopupFlags_MouseButtonMiddle)) 162 | { 163 | mark_to_delete = hovered_mark; // When we middle click to delete a non selected mark it is impossible to remove this mark in the loop 164 | interacted = true; 165 | } 166 | return interacted; 167 | } 168 | 169 | auto GradientWidget::draw_gradient_marks( 170 | MarkId& mark_to_delete, 171 | const ImVec2 gradient_bar_position, 172 | const ImVec2 gradient_size, 173 | Settings const& settings 174 | ) -> internal::draw_gradient_marks_Result 175 | { 176 | auto res = internal::draw_gradient_marks_Result{}; 177 | ImDrawList& draw_list = *ImGui::GetWindowDrawList(); 178 | for (const Mark& mark : _gradient.get_marks()) 179 | { 180 | MarkId current_mark_id{mark}; 181 | if (_mark_to_hide != current_mark_id) 182 | { 183 | ImGui::PushID(current_mark_id.as_imgui_id()); 184 | draw_marks( 185 | draw_list, 186 | gradient_bar_position + ImVec2{mark.position.get(), 1.f} * gradient_size, 187 | ImGui::ColorConvertFloat4ToU32(mark.color), 188 | gradient_size.y, 189 | _selected_mark == current_mark_id, 190 | settings 191 | ); 192 | if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) 193 | { 194 | res.hitbox_is_hovered = true; 195 | res.selected_mark_changed = handle_interactions_with_hovered_mark( 196 | _dragged_mark, 197 | _selected_mark, 198 | mark_to_delete, 199 | current_mark_id 200 | ); 201 | } 202 | ImGui::PopID(); 203 | } 204 | } 205 | static constexpr float space_between_gradient_bar_and_options = 20.f; 206 | ImGui::SetCursorScreenPos( 207 | gradient_bar_position + ImVec2{0.f, gradient_size.y + space_between_gradient_bar_and_options} 208 | ); 209 | ImGui::Dummy({0.f, 0.f}); // Needed by ImGui after a SetCursorScreenPos 210 | return res; 211 | } 212 | 213 | static auto position_where_to_add_next_mark(const Gradient& gradient) -> float 214 | { 215 | if (gradient.is_empty()) 216 | { 217 | return 0.f; 218 | } 219 | else if (gradient.get_marks().size() == 1) 220 | { 221 | const auto first_position_mark = gradient.get_marks().front().position.get(); 222 | return first_position_mark > 0.5f 223 | ? 0.f 224 | : 1.f; 225 | } 226 | else 227 | { 228 | // Find where is the bigger space between two marks 229 | // to return the position between these two marks. 230 | const auto first_mark_iterator{gradient.get_marks().begin()}; 231 | const auto last_mark_iterator{std::prev(gradient.get_marks().end())}; 232 | auto max_value_mark_position{0.f}; 233 | auto max_value_between_two_marks{first_mark_iterator->position.get()}; 234 | for (auto mark_iterator = first_mark_iterator; mark_iterator != last_mark_iterator; ++mark_iterator) 235 | { 236 | const Mark& mark{*mark_iterator}; 237 | const auto mark_next_iterator{std::next(mark_iterator)->position.get()}; 238 | const auto mark_position{mark.position.get()}; 239 | if (max_value_between_two_marks < abs(mark_next_iterator - mark_position)) 240 | { 241 | max_value_mark_position = mark_position; 242 | max_value_between_two_marks = abs(mark_next_iterator - max_value_mark_position); 243 | } 244 | } 245 | const auto last_mark_position{last_mark_iterator->position.get()}; 246 | if (max_value_between_two_marks < abs(1.f - last_mark_position)) 247 | { 248 | max_value_mark_position = last_mark_position; 249 | max_value_between_two_marks = abs(1.f - max_value_mark_position); 250 | } 251 | return max_value_mark_position + max_value_between_two_marks / 2.f; 252 | } 253 | } 254 | 255 | auto GradientWidget::mouse_dragging_interactions( 256 | const ImVec2 gradient_bar_position, 257 | const ImVec2 gradient_size, 258 | const Settings& settings 259 | ) -> bool 260 | { 261 | if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) 262 | { 263 | _dragged_mark.reset(); 264 | } 265 | bool is_dragging = false; 266 | auto* const drag_mark_id = gradient().find(_dragged_mark); 267 | if (ImGui::IsMouseDragging(ImGuiMouseButton_Left) && drag_mark_id) 268 | { 269 | const auto map{ImClamp((ImGui::GetIO().MousePos.x - gradient_bar_position.x) / gradient_size.x, 0.f, 1.f)}; 270 | if (drag_mark_id->position.get() != map) 271 | { 272 | gradient().set_mark_position(_dragged_mark, RelativePosition{map}); 273 | is_dragging = true; 274 | } 275 | if (!(settings.flags & Flag::NoDragDownToDelete)) 276 | { // hide dragging mark when mouse under gradient bar 277 | const auto diffY{ImGui::GetIO().MousePos.y - gradient_bar_position.y - gradient_size.y}; 278 | if (diffY >= settings.distance_to_delete_mark_by_dragging_down) 279 | { 280 | _mark_to_hide = _dragged_mark; 281 | } 282 | // do not hide it anymore when mouse on gradient bar 283 | if (_gradient.contains(_mark_to_hide) && diffY <= settings.distance_to_delete_mark_by_dragging_down) 284 | { 285 | _mark_to_hide.reset(); 286 | } 287 | } 288 | } 289 | return is_dragging; 290 | } 291 | 292 | static auto next_selected_mark(const std::list& gradient, MarkId mark) -> MarkId 293 | { 294 | assert(!gradient.empty()); 295 | if (gradient.size() == 1) 296 | { 297 | return MarkId{}; 298 | } 299 | else if (mark == MarkId{gradient.front()}) 300 | { 301 | return MarkId{gradient.back()}; 302 | } 303 | else 304 | { 305 | return MarkId{gradient.front()}; 306 | }; 307 | } 308 | 309 | static auto compute_number_of_lines_under_bar(Settings const& settings) -> float 310 | { 311 | float res{0.f}; 312 | 313 | if (!(settings.flags & Flag::NoResetButton)) 314 | { 315 | res += 1.f; 316 | } 317 | 318 | if (!(settings.flags & Flag::NoAddButton) 319 | || !(settings.flags & Flag::NoRemoveButton) 320 | || !(settings.flags & Flag::NoPositionSlider) 321 | || !(settings.flags & Flag::NoColorEdit)) 322 | { 323 | res += 1.f; 324 | } 325 | 326 | return res; 327 | } 328 | 329 | static auto compute_border_rect(const char* label, Settings const& settings, ImVec2 gradient_bar_position, ImVec2 gradient_size) -> ImRect 330 | { 331 | const float space_over_bar = !(settings.flags & Flag::NoLabel) 332 | ? ImGui::CalcTextSize(label).y + ImGui::GetStyle().ItemSpacing.y * 3.f 333 | : ImGui::GetStyle().ItemSpacing.y * 2.f; 334 | 335 | const float number_of_lines_under_bar = compute_number_of_lines_under_bar(settings); 336 | static constexpr float space_between_gradient_bar_and_options = 20.f; 337 | const float space_under_bar = (internal::line_height() + ImGui::GetStyle().ItemSpacing.y * 2.f) * number_of_lines_under_bar + space_between_gradient_bar_and_options; 338 | 339 | return ImRect{ 340 | gradient_bar_position - ImVec2{settings.horizontal_margin + 4.f, space_over_bar}, 341 | gradient_bar_position + gradient_size + ImVec2{settings.horizontal_margin + 4.f, space_under_bar}, 342 | }; 343 | } 344 | 345 | auto GradientWidget::widget( 346 | const char* label, 347 | RandomNumberGenerator rng, 348 | const Settings& settings 349 | ) -> bool 350 | { 351 | auto modified{false}; 352 | 353 | ImGui::PushID(label); 354 | ImGui::BeginGroup(); 355 | if (!(settings.flags & Flag::NoLabel)) 356 | { 357 | ImGui::TextUnformatted(label); 358 | ImGui::Dummy(ImVec2{0.f, 1.5f}); 359 | } 360 | 361 | const auto gradient_bar_position = ImVec2{internal::gradient_position(settings.horizontal_margin)}; 362 | const auto gradient_size = ImVec2{ 363 | std::max( // To avoid a width equal to zero and library crash the minimum width value is 1.f 364 | 1.f, 365 | std::min( // When the window is smaller than gradient width we compute a relative width 366 | ImGui::GetContentRegionAvail().x - settings.horizontal_margin * 2.f, 367 | settings.gradient_width * ImGui::GetFontSize() / 20.f 368 | ) 369 | ), 370 | settings.gradient_height * ImGui::GetFontSize() / 20.f 371 | }; 372 | 373 | ImGui::BeginGroup(); 374 | ImGui::InvisibleButton("gradient_editor", gradient_size); 375 | draw_gradient_bar(_gradient, gradient_bar_position, gradient_size); 376 | 377 | const auto wants_to_add_mark{ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)}; // We need to declare it before drawing the marks because we want to 378 | // test if the mouse is hovering the gradient bar not the marks. 379 | MarkId mark_to_delete{}; 380 | const auto res = draw_gradient_marks( // We declare it here because even if we cannot add a mark we need to draw gradient marks. 381 | mark_to_delete, 382 | gradient_bar_position, 383 | gradient_size, 384 | settings 385 | ); 386 | const auto mark_hitbox_is_hovered = res.hitbox_is_hovered; 387 | modified |= res.selected_mark_changed; 388 | 389 | if (wants_to_add_mark && !mark_hitbox_is_hovered) 390 | { 391 | const auto position{(ImGui::GetIO().MousePos.x - gradient_bar_position.x) / gradient_size.x}; 392 | add_mark_with_chosen_mode({position, WrapMode::Clamp}, rng, settings.should_use_a_random_color_for_the_new_marks); 393 | _dragged_mark.reset(); 394 | modified = true; 395 | // ImGui::OpenPopup("SelectedMarkColorPicker"); 396 | } 397 | 398 | modified |= mouse_dragging_interactions(gradient_bar_position, gradient_size, settings); 399 | if (!(settings.flags & Flag::NoDragDownToDelete)) 400 | { 401 | // If mouse released and there is still a mark hidden, then it becomes a mark to delete 402 | if (_gradient.contains(_mark_to_hide) && !ImGui::IsMouseDown(ImGuiMouseButton_Left)) 403 | { 404 | mark_to_delete = _mark_to_hide; 405 | } 406 | } 407 | // Remove mark_to_delete if it exists 408 | if (_gradient.contains(mark_to_delete)) 409 | { 410 | if (_gradient.contains(_selected_mark) && _selected_mark == mark_to_delete) 411 | { 412 | _selected_mark = next_selected_mark(_gradient.get_marks(), _selected_mark); 413 | } 414 | gradient().remove_mark(mark_to_delete); 415 | modified = true; 416 | } 417 | ImGui::EndGroup(); 418 | const auto is_there_a_tooltip{!(settings.flags & Flag::NoTooltip)}; 419 | const auto is_there_remove_button{!(settings.flags & Flag::NoRemoveButton)}; 420 | { // "Remove" button 421 | 422 | const auto window_is_hovered{ImGui::IsWindowHovered( 423 | ImGuiHoveredFlags_ChildWindows 424 | | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem 425 | )}; 426 | 427 | const auto delete_button_pressed = is_there_remove_button 428 | ? delete_button(!_gradient.contains(_selected_mark), "There is no mark selected", is_there_a_tooltip, settings) 429 | : false; 430 | 431 | const auto delete_key_pressed = window_is_hovered 432 | && !ImGui::GetIO().WantTextInput 433 | && (ImGui::IsKeyPressed(ImGuiKey_Delete) || ImGui::IsKeyPressed(ImGuiKey_Backspace)); 434 | 435 | const auto wants_to_delete = delete_button_pressed || delete_key_pressed; 436 | if (wants_to_delete && _gradient.contains(_selected_mark)) 437 | { 438 | const MarkId new_selected_mark = next_selected_mark(_gradient.get_marks(), _selected_mark); 439 | gradient().remove_mark(_selected_mark); 440 | _selected_mark = new_selected_mark; 441 | modified = true; 442 | } 443 | } 444 | 445 | const auto is_there_add_button{!(settings.flags & Flag::NoAddButton)}; 446 | if (is_there_add_button) 447 | { 448 | if (is_there_remove_button) 449 | { 450 | ImGui::SameLine(); 451 | } 452 | if (add_button(is_there_a_tooltip, settings)) 453 | { 454 | const auto position = RelativePosition{position_where_to_add_next_mark(_gradient), WrapMode::Clamp}; 455 | add_mark_with_chosen_mode(position, rng, settings.should_use_a_random_color_for_the_new_marks); 456 | modified = true; 457 | } 458 | } 459 | 460 | bool force_dont_deselect_mark = false; 461 | 462 | const auto selected_mark = gradient().find(_selected_mark); 463 | if (selected_mark) 464 | { 465 | const auto is_there_color_edit{!(settings.flags & Flag::NoColorEdit)}; 466 | if (is_there_color_edit) 467 | { 468 | if (is_there_remove_button || is_there_add_button) 469 | { 470 | ImGui::SameLine(); 471 | } 472 | modified |= color_button(*selected_mark, is_there_a_tooltip, settings.color_edit_flags); 473 | force_dont_deselect_mark = ImGui::IsItemActive(); // The color popup can go outside the border, but we don't want to deselect the mark when we click on it 474 | } 475 | 476 | if (!(settings.flags & Flag::NoPositionSlider)) 477 | { 478 | if ((is_there_remove_button || is_there_add_button || is_there_color_edit)) 479 | { 480 | ImGui::SameLine(); 481 | } 482 | auto position = selected_mark->position; // Make a copy, we can't modify the position directly because we need to pass through set_mark_position() because it has some invariants to presere (sorting the marks) 483 | if (position.imgui_widget("##3", gradient_size.x * 0.25f)) 484 | { 485 | _dragged_mark.reset(); 486 | gradient().set_mark_position(_selected_mark, position); 487 | modified = true; 488 | } 489 | } 490 | } 491 | 492 | if (!(settings.flags & Flag::NoResetButton)) 493 | { 494 | if (ImGui::Button("Reset")) 495 | { 496 | _gradient = {}; 497 | modified = true; 498 | } 499 | } 500 | 501 | if (selected_mark) // Optimization, we don't need to even check if the popup was opened if there is no selected mark 502 | { 503 | const auto picker_popup_size{internal::line_height() * 12.f}; 504 | modified |= open_color_picker_popup( 505 | *selected_mark, 506 | picker_popup_size, 507 | is_there_a_tooltip, 508 | settings.color_edit_flags 509 | ); 510 | } 511 | 512 | { // Border 513 | const ImRect border_rect = compute_border_rect(label, settings, gradient_bar_position, gradient_size); 514 | 515 | // Draw border 516 | if (!(settings.flags & Flag::NoBorder)) 517 | draw_border(border_rect); 518 | 519 | // Deselect mark if we click outside the border 520 | { 521 | // Check if bounding box hovered 522 | ImGui::ItemAdd(border_rect, ImGui::GetID("gradient border")); 523 | _hover_checker.update(); 524 | 525 | // Check if one of the widgets is active 526 | if (ImGui::IsPopupOpen("SelectedMarkColorPicker") 527 | || force_dont_deselect_mark) 528 | { 529 | _hover_checker.force_consider_hovered(); 530 | } 531 | 532 | // Deselect mark if clicking while not hovered 533 | if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !_hover_checker.is_item_hovered()) 534 | _selected_mark = {}; 535 | } 536 | } 537 | 538 | ImGui::PopID(); 539 | ImGui::EndGroup(); 540 | ImGui::SetCursorScreenPos( 541 | internal::gradient_position(0.f) 542 | + ImVec2{ 543 | 0.f, 544 | !(settings.flags & Flag::NoLabel) 545 | ? ImGui::GetStyle().ItemSpacing.y * 2.f 546 | : ImGui::GetStyle().ItemSpacing.y * 3.f, 547 | } 548 | ); 549 | ImGui::Dummy({0.f, 0.f}); // Needed by ImGui after a SetCursorScreenPos 550 | ImGuiContext& g = *GImGui; 551 | 552 | if (modified) 553 | { 554 | ImGui::MarkItemEdited(g.ActiveId); 555 | } 556 | return modified; 557 | } 558 | 559 | static auto default_rng() -> float 560 | { 561 | static auto rng = std::default_random_engine{std::random_device{}()}; 562 | static auto distribution = std::uniform_real_distribution{0.f, 1.f}; 563 | return distribution(rng); 564 | } 565 | 566 | auto GradientWidget::widget(const char* label, const Settings& settings) -> bool 567 | { 568 | return widget(label, &default_rng, settings); 569 | } 570 | }; // namespace ImGG 571 | -------------------------------------------------------------------------------- /src/GradientWidget.hpp: -------------------------------------------------------------------------------- 1 | // Inspired by https://gist.github.com/galloscript/8a5d179e432e062550972afcd1ecf112 2 | 3 | #pragma once 4 | 5 | #include 6 | #include "Gradient.hpp" 7 | #include "HoverChecker.hpp" 8 | #include "MarkId.hpp" 9 | #include "Settings.hpp" 10 | 11 | namespace ImGG { 12 | 13 | /// A function that returns a random number between 0.f and 1.f whenever it is called. 14 | using RandomNumberGenerator = std::function; 15 | 16 | namespace internal { 17 | struct draw_gradient_marks_Result { 18 | bool hitbox_is_hovered{false}; 19 | bool selected_mark_changed{false}; 20 | }; 21 | } // namespace internal 22 | 23 | class GradientWidget { 24 | public: 25 | GradientWidget() = default; 26 | explicit GradientWidget(const std::list& marks) 27 | : _gradient{marks} 28 | {} 29 | 30 | GradientWidget(const GradientWidget&); 31 | 32 | GradientWidget& operator=(const GradientWidget& widget) 33 | { 34 | *this = GradientWidget{widget}; // Construct and then move-assign 35 | return *this; 36 | } 37 | 38 | GradientWidget(GradientWidget&&) noexcept = default; 39 | GradientWidget& operator=(GradientWidget&&) = default; 40 | 41 | friend auto operator==(const GradientWidget& a, const GradientWidget& b) -> bool { return a.gradient() == b.gradient(); } 42 | 43 | auto gradient() const -> const Gradient& { return _gradient; } 44 | auto gradient() -> Gradient& { return _gradient; } 45 | 46 | auto widget( 47 | const char* label, 48 | const Settings& settings = {} 49 | ) -> bool; 50 | 51 | /// If `_should_use_a_random_color_for_the_new_marks` is true when adding a new mark, 52 | /// `rng` is used to generate the random color of the new mark. It can be any function that returns a number between 0.f and 1.f. 53 | /// There is an overload that doesn't need `rng` and uses a `std::default_random_engine` internally. 54 | auto widget( 55 | const char* label, 56 | RandomNumberGenerator rng, 57 | const Settings& settings = {} 58 | ) -> bool; 59 | 60 | private: 61 | void add_mark_with_chosen_mode(RelativePosition relative_pos, RandomNumberGenerator rng, bool add_a_random_color); 62 | 63 | auto draw_gradient_marks( 64 | MarkId& mark_to_delete, 65 | ImVec2 gradient_bar_pos, 66 | ImVec2 size, 67 | Settings const& settings 68 | ) -> internal::draw_gradient_marks_Result; 69 | 70 | auto mouse_dragging_interactions( 71 | ImVec2 gradient_bar_position, 72 | ImVec2 gradient_size, 73 | const Settings& settings 74 | ) -> bool; 75 | 76 | private: 77 | Gradient _gradient{}; 78 | MarkId _selected_mark{}; 79 | MarkId _dragged_mark{}; 80 | MarkId _mark_to_hide{}; 81 | 82 | internal::HoverChecker _hover_checker{}; 83 | }; 84 | 85 | } // namespace ImGG -------------------------------------------------------------------------------- /src/HoverChecker.cpp: -------------------------------------------------------------------------------- 1 | #include "HoverChecker.hpp" 2 | #include 3 | #include 4 | 5 | namespace ImGG { namespace internal { 6 | 7 | void HoverChecker::update() 8 | { 9 | if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) 10 | _frame_since_not_hovered = 0; 11 | else 12 | _frame_since_not_hovered = std::min(_frame_since_not_hovered + 1, 3); 13 | } 14 | 15 | void HoverChecker::force_consider_hovered() 16 | { 17 | _frame_since_not_hovered = 0; 18 | } 19 | 20 | auto HoverChecker::is_item_hovered() const -> bool 21 | { 22 | return _frame_since_not_hovered < 3; 23 | } 24 | 25 | }} // namespace ImGG::internal -------------------------------------------------------------------------------- /src/HoverChecker.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace ImGG { namespace internal { 4 | 5 | class HoverChecker { 6 | public: 7 | void update(); 8 | void force_consider_hovered(); 9 | auto is_item_hovered() const -> bool; 10 | 11 | private: 12 | int _frame_since_not_hovered = 0; 13 | }; 14 | 15 | }} // namespace ImGG::internal 16 | -------------------------------------------------------------------------------- /src/Interpolation.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // Includes size_t 4 | 5 | namespace ImGG { 6 | 7 | /// Controls how the colors are interpolated between two marks. 8 | enum class Interpolation : size_t { // We use size_t so that we can use the WrapMode to index into an array 9 | /// Linear interpolation between two marks. 10 | Linear, 11 | /// Constant color between two marks: it uses the color of the mark on the right. 12 | Constant, 13 | }; 14 | 15 | } // namespace ImGG 16 | -------------------------------------------------------------------------------- /src/Mark.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ColorRGBA.hpp" 4 | #include "RelativePosition.hpp" 5 | 6 | namespace ImGG { 7 | 8 | struct Mark { 9 | RelativePosition position; 10 | ColorRGBA color; 11 | 12 | Mark( // We need to explicitly define the constructor in order to compile with MacOS Clang in C++ 11 13 | RelativePosition position = RelativePosition{0.f}, 14 | ColorRGBA color = {0.f, 0.f, 0.f, 1.f} 15 | ) 16 | : position{position} 17 | , color{color} 18 | {} 19 | 20 | friend auto operator==(const Mark& a, const Mark& b) -> bool 21 | { 22 | return a.position == b.position 23 | && a.color == b.color; 24 | }; 25 | }; 26 | 27 | } // namespace ImGG -------------------------------------------------------------------------------- /src/MarkId.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "Mark.hpp" 6 | 7 | namespace ImGG { 8 | 9 | namespace internal { 10 | // Thanks to https://stackoverflow.com/a/34143224 11 | template 12 | struct transfer_const_ptr { 13 | using type = typename std::remove_const::type*; 14 | }; 15 | template 16 | struct transfer_const_ptr { 17 | using type = const U*; 18 | }; 19 | 20 | template 21 | struct transfer_const_iterator { 22 | using type = typename std::remove_const::type::iterator; 23 | }; 24 | 25 | template 26 | struct transfer_const_iterator { 27 | using type = typename U::const_iterator; 28 | }; 29 | 30 | } // namespace internal 31 | 32 | /// Used to identify a Mark. 33 | class MarkId { 34 | public: 35 | MarkId() = default; 36 | explicit MarkId(const Mark* ptr) 37 | : _ptr{ptr} 38 | {} 39 | explicit MarkId(const Mark& ref) 40 | : _ptr{&ref} 41 | {} 42 | explicit MarkId(const std::list::const_iterator iterator) 43 | : _ptr{&*iterator} 44 | {} 45 | 46 | void reset() { _ptr = nullptr; } 47 | 48 | friend auto operator==(const MarkId& a, const MarkId& b) -> bool { return a._ptr == b._ptr; }; 49 | friend auto operator!=(const MarkId& a, const MarkId& b) -> bool { return !(a == b); }; 50 | 51 | auto as_imgui_id() const -> void const* { return _ptr; } 52 | 53 | private: 54 | /// If it is not in the list returns an invalid iterator 55 | template 56 | auto find_iterator(GradientT&& gradient) const -> typename internal::transfer_const_iterator>::type // Returns a `const std::list::iterator>` if GradientT is const and a mutable `std::list::iterator>` otherwise. 57 | { 58 | return std::find_if(gradient._marks.begin(), gradient._marks.end(), [&](const Mark& mark) { 59 | return &mark == _ptr; 60 | }); 61 | } 62 | 63 | template 64 | auto find(GradientT&& gradient) const -> typename internal::transfer_const_ptr::type // Returns a `const Mark*` if GradientT is const and a mutable `Mark*` otherwise. 65 | { 66 | const auto it = find_iterator(gradient); 67 | return it != gradient._marks.end() 68 | ? &*it 69 | : nullptr; 70 | } 71 | 72 | friend class Gradient; 73 | 74 | private: 75 | const Mark* _ptr{}; 76 | }; 77 | 78 | } // namespace ImGG -------------------------------------------------------------------------------- /src/RelativePosition.cpp: -------------------------------------------------------------------------------- 1 | #include "RelativePosition.hpp" 2 | #include "Utils.hpp" 3 | 4 | namespace ImGG { 5 | 6 | static auto make_relative_position(float position, WrapMode wrap_mode) -> RelativePosition 7 | { 8 | return [&] { 9 | switch (wrap_mode) 10 | { 11 | case WrapMode::Clamp: 12 | { 13 | return internal::clamp_position(position); 14 | } 15 | case WrapMode::Repeat: 16 | { 17 | return internal::repeat_position(position); 18 | } 19 | case WrapMode::MirrorRepeat: 20 | { 21 | return internal::mirror_repeat_position(position); 22 | } 23 | default: 24 | assert(false && "[ImGuiGradient::make_relative_position] Invalid enum value"); 25 | return RelativePosition{0.f}; 26 | } 27 | }(); 28 | } 29 | 30 | RelativePosition::RelativePosition(float position, WrapMode wrap_mode) 31 | : RelativePosition{make_relative_position(position, wrap_mode).get()} 32 | {} 33 | 34 | auto RelativePosition::imgui_widget(const char* label, const float width) -> bool 35 | { 36 | ImGui::SetNextItemWidth(width); 37 | return ImGui::DragFloat( 38 | label, 39 | &_value, 40 | 0.0001f, /* speed */ 41 | 0.f, 1.f, /* min and max */ 42 | "%.4f", /* precision */ 43 | ImGuiSliderFlags_AlwaysClamp 44 | ); 45 | } 46 | 47 | } // namespace ImGG -------------------------------------------------------------------------------- /src/RelativePosition.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "WrapMode.hpp" 6 | 7 | namespace ImGG { 8 | 9 | /// Represents a number between 0 and 1. 10 | class RelativePosition { 11 | public: 12 | /// `position` must be between 0 and 1. 13 | explicit RelativePosition(float position) 14 | : _value{position} 15 | { 16 | assert_invariants(); 17 | } 18 | 19 | RelativePosition(float position, WrapMode wrap_mode); 20 | 21 | /// Returns a number between 0 and 1. 22 | auto get() const -> float { return _value; } 23 | 24 | /// `position` must be between 0 and 1. 25 | void set(float position) 26 | { 27 | _value = position; 28 | assert_invariants(); 29 | } 30 | 31 | auto imgui_widget(const char* label, float width) -> bool; 32 | 33 | friend auto operator<(const RelativePosition& a, const RelativePosition& b) -> bool { return a.get() < b.get(); } 34 | friend auto operator>(const RelativePosition& a, const RelativePosition& b) -> bool { return a.get() > b.get(); } 35 | friend auto operator<=(const RelativePosition& a, const RelativePosition& b) -> bool { return a.get() <= b.get(); } 36 | friend auto operator>=(const RelativePosition& a, const RelativePosition& b) -> bool { return a.get() >= b.get(); } 37 | friend auto operator==(const RelativePosition& a, const RelativePosition& b) -> bool { return a.get() == b.get(); } 38 | friend auto operator!=(const RelativePosition& a, const RelativePosition& b) -> bool { return !(a == b); } 39 | 40 | private: 41 | void assert_invariants() const 42 | { 43 | assert(0.f <= _value && _value <= 1.f && "RelativePosition value should be between 0.f and 1.f"); 44 | } 45 | 46 | private: 47 | float _value{0.f}; 48 | }; 49 | 50 | } // namespace ImGG -------------------------------------------------------------------------------- /src/Settings.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "Flags.hpp" 6 | #include "internal.hpp" 7 | 8 | namespace ImGG { 9 | 10 | inline auto fully_opaque(ImU32 col) -> ImU32 11 | { 12 | return col | 0xFF000000; 13 | } 14 | 15 | struct Settings { 16 | float gradient_width{500.f}; 17 | float gradient_height{40.f}; // Must be strictly positive 18 | float horizontal_margin{10.f}; 19 | 20 | /// Distance under the gradient bar to delete a mark by dragging it down. 21 | /// This behaviour can also be disabled with the Flag::NoDragDowntoDelete. 22 | float distance_to_delete_mark_by_dragging_down{80.f}; 23 | 24 | ImGG::Flags flags{ImGG::Flag::None}; 25 | 26 | ImGuiColorEditFlags color_edit_flags{ImGuiColorEditFlags_None}; 27 | 28 | /// Controls how the new mark color is chosen. 29 | /// If true, the new mark color will be a random color, 30 | /// otherwise it will be the one that the gradient already has at the new mark position. 31 | bool should_use_a_random_color_for_the_new_marks{false}; 32 | 33 | std::function plus_button_widget = []() { 34 | return ImGui::Button("+", internal::button_size()); 35 | }; 36 | std::function minus_button_widget = []() { 37 | return ImGui::Button("-", internal::button_size()); 38 | }; 39 | 40 | ImU32 mark_color = fully_opaque(ImGui::GetColorU32(ImGuiCol_Button)); 41 | ImU32 mark_hovered_color = fully_opaque(ImGui::GetColorU32(ImGuiCol_ButtonHovered)); 42 | ImU32 mark_selected_color = fully_opaque(ImGui::GetColorU32(ImGuiCol_ButtonActive)); 43 | ImU32 mark_selected_hovered_color = fully_opaque(ImGui::GetColorU32(ImGuiCol_ButtonActive)); 44 | }; 45 | 46 | } // namespace ImGG -------------------------------------------------------------------------------- /src/Utils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "RelativePosition.hpp" 4 | 5 | // https://registry.khronos.org/OpenGL/specs/gl/glspec46.core.pdf page 260 6 | 7 | namespace ImGG { 8 | 9 | namespace internal { 10 | 11 | inline auto clamp_position(float position) -> RelativePosition 12 | { 13 | return RelativePosition{std::fmin(std::fmax(position, 0.f), 1.f)}; 14 | } 15 | 16 | /// Always returns a number between 0.f and 1.f, even if x is negative. 17 | inline auto fract(float x) -> float 18 | { 19 | return x - std::floor(x); 20 | } 21 | 22 | inline auto modulo(float x, float mod) -> float 23 | { 24 | return fract(x / mod) * mod; 25 | } 26 | 27 | inline auto repeat_position(float position) -> RelativePosition 28 | { 29 | return RelativePosition{fract(position)}; 30 | } 31 | 32 | // Applies a mirror transform on position given around 0.f, then repeat it 33 | inline auto mirror_repeat_position(float position) -> RelativePosition 34 | { 35 | return RelativePosition{1.f - (std::abs(modulo(position, 2.f) - 1.f))}; 36 | } 37 | 38 | }} // namespace ImGG::internal -------------------------------------------------------------------------------- /src/WrapMode.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include // Includes size_t 4 | 5 | namespace ImGG { 6 | 7 | /// Controls how a position that is outside of the [0, 1] range is mapped back into that range. 8 | /// This is like the OpenGL texture wrap modes; see https://learnopengl.com/Getting-started/Textures section "Texture Wrapping". 9 | enum class WrapMode : size_t { // We use size_t so that we can use the WrapMode to index into an array 10 | /// If it is bigger than 1, maps to 1. If it smaller than 0, maps to 0. 11 | Clamp, 12 | /// Maps the number line to a bunch of copies of [0, 1] stuck together. 13 | /// Conceptually equivalent to adding or subtracting 1 to the position until it is in the [0, 1] range. 14 | /// 1.2 maps to 0.2, 1.8 maps to 0.8, -0.2 maps to 0.8, -0.8 maps to 0.2 15 | Repeat, 16 | /// Like `Repeat` except that every other range is flipped. 17 | /// 1.2 maps to 0.8, 1.8 maps to 0.2, -0.2 maps to 0.2, -0.8 maps to 0.8, -1.2 maps to 0.8 18 | MirrorRepeat, 19 | }; 20 | 21 | } // namespace ImGG -------------------------------------------------------------------------------- /src/color_conversions.cpp: -------------------------------------------------------------------------------- 1 | #include "color_conversions.hpp" 2 | #include 3 | #define IMGUI_DEFINE_MATH_OPERATORS 4 | #include "imgui_internal.hpp" 5 | 6 | namespace ImGG { namespace internal { 7 | 8 | namespace { 9 | struct Vec3 { 10 | float x; 11 | float y; 12 | float z; 13 | }; 14 | } // namespace 15 | 16 | static auto clamp(float x, float min, float max) -> float 17 | { 18 | return x < min 19 | ? min 20 | : x > max 21 | ? max 22 | : x; 23 | } 24 | 25 | static auto saturate(Vec3 const& v) -> Vec3 26 | { 27 | return { 28 | clamp(v.x, 0.f, 1.f), 29 | clamp(v.y, 0.f, 1.f), 30 | clamp(v.z, 0.f, 1.f), 31 | }; 32 | } 33 | 34 | // Start of [Block1] 35 | // From https://entropymine.com/imageworsener/srgbformula/ 36 | 37 | static auto LinearRGB_from_sRGB_impl(float x) -> float 38 | { 39 | return x < 0.04045f 40 | ? x / 12.92f 41 | : std::pow((x + 0.055f) / 1.055f, 2.4f); 42 | } 43 | 44 | static auto sRGB_from_LinearRGB_impl(float x) -> float 45 | { 46 | return x < 0.0031308f 47 | ? x * 12.92f 48 | : 1.055f * std::pow(x, 1.f / 2.4f) - 0.055f; 49 | } 50 | 51 | static auto LinearRGB_from_sRGB(Vec3 srgb) -> Vec3 52 | { 53 | srgb = saturate(srgb); 54 | return { 55 | LinearRGB_from_sRGB_impl(srgb.x), 56 | LinearRGB_from_sRGB_impl(srgb.y), 57 | LinearRGB_from_sRGB_impl(srgb.z), 58 | }; 59 | } 60 | 61 | static auto sRGB_from_LinearRGB(Vec3 rgb) -> Vec3 62 | { 63 | rgb = saturate(rgb); 64 | return { 65 | sRGB_from_LinearRGB_impl(rgb.x), 66 | sRGB_from_LinearRGB_impl(rgb.y), 67 | sRGB_from_LinearRGB_impl(rgb.z), 68 | }; 69 | } 70 | // End of [Block1] 71 | 72 | // Start of [Block2]// From https://bottosson.github.io/posts/oklab/ 73 | 74 | static auto Oklab_from_LinearRGB(Vec3 c) -> Vec3 75 | { 76 | float l = 0.4122214708f * c.x + 0.5363325363f * c.y + 0.0514459929f * c.z; 77 | float m = 0.2119034982f * c.x + 0.6806995451f * c.y + 0.1073969566f * c.z; 78 | float s = 0.0883024619f * c.x + 0.2817188376f * c.y + 0.6299787005f * c.z; 79 | 80 | float l_ = cbrtf(l); 81 | float m_ = cbrtf(m); 82 | float s_ = cbrtf(s); 83 | 84 | return { 85 | 0.2104542553f * l_ + 0.7936177850f * m_ - 0.0040720468f * s_, 86 | 1.9779984951f * l_ - 2.4285922050f * m_ + 0.4505937099f * s_, 87 | 0.0259040371f * l_ + 0.7827717662f * m_ - 0.8086757660f * s_, 88 | }; 89 | } 90 | 91 | static auto LinearRGB_from_Oklab(Vec3 c) -> Vec3 92 | { 93 | float l_ = c.x + 0.3963377774f * c.y + 0.2158037573f * c.z; 94 | float m_ = c.x - 0.1055613458f * c.y - 0.0638541728f * c.z; 95 | float s_ = c.x - 0.0894841775f * c.y - 1.2914855480f * c.z; 96 | 97 | float l = l_ * l_ * l_; 98 | float m = m_ * m_ * m_; 99 | float s = s_ * s_ * s_; 100 | 101 | return { 102 | +4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s, 103 | -1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s, 104 | -0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s, 105 | }; 106 | } 107 | 108 | // End of [Block2] 109 | 110 | auto Oklab_Premultiplied_from_sRGB_Straight(ColorRGBA const& col) -> ImVec4 111 | { 112 | auto const lab = Oklab_from_LinearRGB(LinearRGB_from_sRGB({col.x, col.y, col.z})); 113 | return { 114 | lab.x * col.w, 115 | lab.y * col.w, 116 | lab.z * col.w, 117 | col.w, 118 | }; 119 | } 120 | 121 | auto sRGB_Straight_from_Oklab_Premultiplied(ImVec4 const& col) -> ColorRGBA 122 | { 123 | auto const srgb = sRGB_from_LinearRGB(LinearRGB_from_Oklab({col.x / col.w, col.y / col.w, col.z / col.w})); 124 | return { 125 | srgb.x, 126 | srgb.y, 127 | srgb.z, 128 | col.w, 129 | }; 130 | } 131 | 132 | }} // namespace ImGG::internal -------------------------------------------------------------------------------- /src/color_conversions.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ColorRGBA.hpp" 3 | 4 | namespace ImGG { namespace internal { 5 | 6 | auto Oklab_Premultiplied_from_sRGB_Straight(ColorRGBA const&) -> ImVec4; 7 | auto sRGB_Straight_from_Oklab_Premultiplied(ImVec4 const&) -> ColorRGBA; 8 | 9 | }} // namespace ImGG::internal -------------------------------------------------------------------------------- /src/extra_widgets.cpp: -------------------------------------------------------------------------------- 1 | #include "extra_widgets.hpp" 2 | #include 3 | #include 4 | #include "imgui_internal.hpp" 5 | 6 | namespace ImGG { 7 | 8 | template 9 | static auto selector_with_tooltip( 10 | const char* label, 11 | size_t* current_item_index, 12 | const std::array& items, 13 | const char* longuest_item_name, // Use the longuest word to choose the selector's size 14 | const std::array& tooltips, 15 | const bool should_show_tooltip 16 | ) -> bool 17 | { 18 | const auto width{ 19 | ImGui::CalcTextSize(longuest_item_name).x 20 | + ImGui::GetFrameHeightWithSpacing() 21 | + ImGui::GetStyle().FramePadding.x * 2.f}; 22 | ImGui::SetNextItemWidth(width); 23 | 24 | auto modified{false}; 25 | if (ImGui::BeginCombo(label, items[*current_item_index])) 26 | { 27 | for (size_t n = 0; n < items.size(); n++) 28 | { 29 | const bool is_selected{(*current_item_index == n)}; 30 | if (ImGui::Selectable(items[n], is_selected)) 31 | { 32 | *current_item_index = n; 33 | modified = true; 34 | } 35 | 36 | if (is_selected) 37 | { 38 | ImGui::SetItemDefaultFocus(); 39 | } 40 | if (should_show_tooltip) 41 | { 42 | ImGui::SetItemTooltip("%s", tooltips[n]); 43 | } 44 | } 45 | ImGui::EndCombo(); 46 | } 47 | return modified; 48 | } 49 | 50 | auto random_mode_widget( 51 | const char* label, 52 | bool* should_use_a_random_color_for_the_new_marks, 53 | const bool should_show_tooltip 54 | ) -> bool 55 | { 56 | const bool modified = ImGui::Checkbox( 57 | label, 58 | should_use_a_random_color_for_the_new_marks 59 | ); 60 | if (should_show_tooltip) 61 | { 62 | ImGui::SetItemTooltip("%s", "The new marks will use a random color instead of keeping the one the gradient had at that position."); 63 | } 64 | return modified; 65 | } 66 | 67 | auto wrap_mode_widget(const char* label, WrapMode* wrap_mode, const bool should_show_tooltip) -> bool 68 | { 69 | static constexpr std::array items = { 70 | "Clamp", 71 | "Repeat", 72 | "Mirror Repeat", 73 | }; 74 | static constexpr std::array tooltips = { 75 | "If the position gets bigger than 1, maps it to 1. If it gets smaller than 0, maps it to 0.", 76 | "Maps the number line to a bunch of copies of [0, 1] stuck together.", 77 | "Like `Repeat` except that every other range is flipped.", 78 | }; 79 | 80 | return selector_with_tooltip( 81 | label, 82 | reinterpret_cast(wrap_mode), 83 | items, 84 | "Mirror Repeat", 85 | tooltips, 86 | should_show_tooltip 87 | ); 88 | } 89 | 90 | auto interpolation_mode_widget(const char* label, Interpolation* interpolation_mode, const bool should_show_tooltip) -> bool 91 | { 92 | static constexpr std::array items = { 93 | "Linear", 94 | "Constant", 95 | }; 96 | static constexpr std::array tooltips = { 97 | "Linear interpolation between two marks", 98 | "Constant color between two marks", 99 | }; 100 | 101 | return selector_with_tooltip( 102 | label, 103 | reinterpret_cast(interpolation_mode), 104 | items, 105 | "Constant", 106 | tooltips, 107 | should_show_tooltip 108 | ); 109 | } 110 | } // namespace ImGG -------------------------------------------------------------------------------- /src/extra_widgets.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Interpolation.hpp" 4 | #include "WrapMode.hpp" 5 | 6 | namespace ImGG { 7 | 8 | auto random_mode_widget( 9 | const char* label, 10 | bool* should_use_a_random_color_for_the_new_marks, 11 | bool should_show_tooltip = true 12 | ) -> bool; 13 | 14 | auto wrap_mode_widget( 15 | const char* label, 16 | WrapMode* wrap_mode, 17 | bool should_show_tooltip = true 18 | ) -> bool; 19 | 20 | auto interpolation_mode_widget( 21 | const char* label, 22 | Interpolation* interpolation_mode, 23 | bool should_show_tooltip = true 24 | ) -> bool; 25 | 26 | } // namespace ImGG -------------------------------------------------------------------------------- /src/imgui_draw.cpp: -------------------------------------------------------------------------------- 1 | #include "Gradient.hpp" 2 | #include "Interpolation.hpp" 3 | #include "Settings.hpp" 4 | #include "color_conversions.hpp" 5 | #include "internal.hpp" 6 | 7 | namespace ImGG { 8 | 9 | static void draw_uniform_square( 10 | ImDrawList& draw_list, 11 | const ImVec2 top_left_corner, 12 | const ImVec2 bottom_right_corner, 13 | const ImU32& color 14 | ) 15 | { 16 | static constexpr auto rounding{1.f}; 17 | draw_list.AddRectFilled( 18 | top_left_corner, 19 | bottom_right_corner, 20 | color, 21 | rounding 22 | ); 23 | } 24 | 25 | static void draw_naive_gradient_between_two_colors( 26 | ImDrawList& draw_list, 27 | ImVec2 const top_left_corner, 28 | ImVec2 const bottom_right_corner, 29 | ImU32 const& color_left, ImU32 const& color_right 30 | ) 31 | { 32 | draw_list.AddRectFilledMultiColor( 33 | top_left_corner, 34 | bottom_right_corner, 35 | color_left, color_right, color_right, color_left 36 | ); 37 | } 38 | 39 | // We draw two "naive" gradients to reduce the visual artifacts. 40 | // The problem is that ImGui does its color interpolation in sRGB space which doesn't look the best. 41 | // We use Lab for better-looking results. 42 | static void draw_gradient_between_two_colors( 43 | ImDrawList& draw_list, 44 | ImVec2 const top_left_corner, 45 | ImVec2 const bottom_right_corner, 46 | ImVec4 const& color_left, ImVec4 const& color_right 47 | ) 48 | { 49 | auto const color_middle = internal::sRGB_Straight_from_Oklab_Premultiplied( 50 | ( 51 | internal::Oklab_Premultiplied_from_sRGB_Straight(color_left) 52 | + internal::Oklab_Premultiplied_from_sRGB_Straight(color_right) 53 | ) 54 | * ImVec4{0.5f, 0.5f, 0.5f, 0.5f} 55 | ); 56 | auto const color_left_as_ImU32 = ImGui::ColorConvertFloat4ToU32(color_left); 57 | auto const color_middle_as_ImU32 = ImGui::ColorConvertFloat4ToU32(color_middle); 58 | auto const color_right_as_ImU32 = ImGui::ColorConvertFloat4ToU32(color_right); 59 | 60 | auto const middle_x = (top_left_corner.x + bottom_right_corner.x) * 0.5f; 61 | auto const bottom_middle_corner = ImVec2{middle_x, bottom_right_corner.y}; 62 | auto const top_middle_corner = ImVec2{middle_x, top_left_corner.y}; 63 | 64 | draw_naive_gradient_between_two_colors(draw_list, top_left_corner, bottom_middle_corner, color_left_as_ImU32, color_middle_as_ImU32); 65 | draw_naive_gradient_between_two_colors(draw_list, top_middle_corner, bottom_right_corner, color_middle_as_ImU32, color_right_as_ImU32); 66 | } 67 | 68 | void draw_gradient( 69 | ImDrawList& draw_list, 70 | const Gradient& gradient, 71 | const ImVec2 gradient_position, 72 | const ImVec2 size 73 | ) 74 | { 75 | assert(!gradient.is_empty()); 76 | float current_starting_x = gradient_position.x; 77 | for (auto mark_iterator = gradient.get_marks().begin(); mark_iterator != gradient.get_marks().end(); ++mark_iterator) // We need to use iterators because we need to access the previous element from time to time 78 | { 79 | const Mark& mark = *mark_iterator; 80 | 81 | const auto color_right = mark.color; 82 | 83 | const auto from{current_starting_x}; 84 | const auto to{gradient_position.x + mark.position.get() * size.x}; 85 | if (mark.position.get() != 0.f) 86 | { 87 | if (gradient.interpolation_mode() == Interpolation::Linear) 88 | { 89 | const auto color_left = (mark_iterator != gradient.get_marks().begin()) 90 | ? std::prev(mark_iterator)->color 91 | : color_right; 92 | draw_gradient_between_two_colors( 93 | draw_list, 94 | ImVec2{from - 0.55f, gradient_position.y}, // We extend the rectangle by 0.55 on each side, otherwise there can be small gaps that appear between rectangles 95 | ImVec2{to + 0.55f, gradient_position.y + size.y}, // (for some gradient width and some mark positions) (see https://github.com/Coollab-Art/imgui_gradient/issues/5) 96 | color_left, color_right 97 | ); 98 | } 99 | else if (gradient.interpolation_mode() == Interpolation::Constant) 100 | { 101 | draw_uniform_square( 102 | draw_list, 103 | ImVec2{from - 0.55f, gradient_position.y}, // We extend the rectangle by 0.55 on each side, otherwise there can be small gaps that appear between rectangles 104 | ImVec2{to + 0.55f, gradient_position.y + size.y}, // (for some gradient width and some mark positions) (see https://github.com/Coollab-Art/imgui_gradient/issues/5) 105 | ImGui::ColorConvertFloat4ToU32(color_right) 106 | ); 107 | } 108 | else 109 | { 110 | assert(false && "Unknown Interpolation enum value."); 111 | } 112 | } 113 | current_starting_x = to; 114 | } 115 | // If the last element is not at the end position, extend its color to the end position 116 | if (gradient.get_marks().back().position.get() != 1.f) 117 | { 118 | draw_uniform_square( 119 | draw_list, 120 | ImVec2{current_starting_x, gradient_position.y}, 121 | ImVec2{gradient_position.x + size.x, gradient_position.y + size.y}, 122 | ImGui::ColorConvertFloat4ToU32(gradient.get_marks().back().color) 123 | ); 124 | } 125 | } 126 | 127 | static auto mark_invisible_button( 128 | const ImVec2 position_to_draw_mark, 129 | const float mark_square_size, 130 | const float gradient_height 131 | ) -> bool 132 | { 133 | ImGui::SetCursorScreenPos(position_to_draw_mark - ImVec2{mark_square_size * 1.5f, gradient_height}); 134 | const auto button_size = ImVec2{ 135 | mark_square_size * 3.f, 136 | gradient_height + mark_square_size * 2.f 137 | }; 138 | ImGui::InvisibleButton("mark", button_size, ImGuiButtonFlags_MouseButtonMiddle | ImGuiButtonFlags_MouseButtonLeft); 139 | return ImGui::IsItemHovered(); 140 | } 141 | 142 | static void draw_mark( 143 | ImDrawList& draw_list, 144 | const ImVec2 position_to_draw_mark, 145 | const float mark_square_size, 146 | ImU32 mark_color 147 | ) 148 | { 149 | const auto mark_top_triangle = ImVec2{0.f, -mark_square_size}; 150 | const auto mark_bottom_triangle = ImVec2{mark_square_size, 0.f}; 151 | draw_list.AddTriangleFilled( 152 | position_to_draw_mark + mark_top_triangle, 153 | position_to_draw_mark - mark_bottom_triangle, 154 | position_to_draw_mark + mark_bottom_triangle, 155 | mark_color 156 | ); 157 | 158 | const auto mark_top_left_corner = 159 | ImVec2{-mark_square_size - 1.f, 0.f}; 160 | const auto mark_bottom_right_corner = 161 | ImVec2{ 162 | mark_square_size + 1.f, 163 | 2.f * mark_square_size 164 | }; 165 | draw_uniform_square( 166 | draw_list, 167 | position_to_draw_mark + mark_top_left_corner, 168 | position_to_draw_mark + mark_bottom_right_corner, 169 | mark_color 170 | ); 171 | 172 | static constexpr auto offset_between_mark_square_and_mark_square_inside = ImVec2{1.f, 1.f}; 173 | draw_uniform_square( 174 | draw_list, 175 | position_to_draw_mark + mark_top_left_corner + offset_between_mark_square_and_mark_square_inside, 176 | position_to_draw_mark + mark_bottom_right_corner - offset_between_mark_square_and_mark_square_inside, 177 | mark_color 178 | ); 179 | } 180 | 181 | void draw_marks( 182 | ImDrawList& draw_list, 183 | const ImVec2 position_to_draw_mark, 184 | const ImU32 mark_color, 185 | const float gradient_height, 186 | const bool mark_is_selected, 187 | Settings const& settings 188 | ) 189 | { 190 | float const mark_square_size = 0.3f * ImGui::GetFontSize(); 191 | 192 | bool const is_hovered = mark_invisible_button(position_to_draw_mark, mark_square_size, gradient_height); 193 | 194 | draw_mark( 195 | draw_list, 196 | position_to_draw_mark, 197 | mark_square_size, 198 | is_hovered 199 | ? (mark_is_selected 200 | ? settings.mark_selected_hovered_color 201 | : settings.mark_hovered_color) 202 | : (mark_is_selected 203 | ? settings.mark_selected_color 204 | : settings.mark_color) 205 | ); 206 | 207 | auto const square_size{0.15f * ImGui::GetFontSize()}; 208 | auto const mark_top_left_corner = ImVec2{-square_size, square_size}; 209 | auto const mark_bottom_right_corner = ImVec2{square_size, square_size * 3}; 210 | draw_uniform_square( 211 | draw_list, 212 | position_to_draw_mark + mark_top_left_corner, 213 | position_to_draw_mark + mark_bottom_right_corner, 214 | mark_color 215 | ); 216 | } 217 | 218 | } // namespace ImGG -------------------------------------------------------------------------------- /src/imgui_draw.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Gradient.hpp" 4 | #include "Interpolation.hpp" 5 | #include "Settings.hpp" 6 | #include "internal.hpp" 7 | #include 8 | 9 | namespace ImGG { 10 | 11 | inline void draw_border(ImRect border_rect) 12 | { 13 | static constexpr float rounding{1.f}; 14 | static constexpr float thickness{2.f}; 15 | ImGui::GetWindowDrawList()->AddRect(border_rect.GetTL(), border_rect.GetBR(), internal::border_color(), rounding, ImDrawFlags_None, thickness); 16 | } 17 | 18 | void draw_gradient( 19 | ImDrawList& draw_list, 20 | const Gradient& gradient, 21 | ImVec2 gradient_position, 22 | ImVec2 size 23 | ); 24 | 25 | void draw_marks( 26 | ImDrawList& draw_list, 27 | ImVec2 mark_position, 28 | ImU32 mark_color, 29 | float gradient_height, 30 | bool mark_is_selected, 31 | Settings const& settings 32 | ); 33 | 34 | } // namespace ImGG -------------------------------------------------------------------------------- /src/imgui_internal.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #if defined(__GNUC__) 4 | #pragma GCC diagnostic push 5 | #pragma GCC diagnostic ignored "-Wsign-conversion" 6 | #endif 7 | #include 8 | #if defined(__GNUC__) 9 | #pragma GCC diagnostic pop 10 | #endif 11 | -------------------------------------------------------------------------------- /src/internal.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define IMGUI_DEFINE_MATH_OPERATORS 4 | #include 5 | 6 | namespace ImGG { namespace internal { 7 | 8 | inline auto line_height() -> float 9 | { 10 | return ImGui::GetFrameHeight(); 11 | } 12 | 13 | inline auto button_size() -> ImVec2 14 | { 15 | return ImVec2{line_height(), line_height()}; 16 | } 17 | 18 | inline auto gradient_position(float x_offset) -> ImVec2 19 | { 20 | return ImGui::GetCursorScreenPos() + ImVec2(x_offset, 0.f); 21 | } 22 | 23 | inline auto border_color() -> ImU32 24 | { 25 | return ImGui::GetColorU32(ImGuiCol_Border); 26 | } 27 | 28 | }} // namespace ImGG::internal -------------------------------------------------------------------------------- /src/maybe_disabled.cpp: -------------------------------------------------------------------------------- 1 | #include "maybe_disabled.hpp" 2 | #include 3 | 4 | namespace ImGG { 5 | 6 | void maybe_disabled( 7 | bool condition, 8 | const char* reason_to_disable, 9 | std::function const& widgets 10 | ) 11 | { 12 | if (condition) 13 | { 14 | ImGui::BeginGroup(); 15 | ImGui::BeginDisabled(true); 16 | 17 | widgets(); 18 | 19 | ImGui::EndDisabled(); 20 | ImGui::EndGroup(); 21 | ImGui::SetItemTooltip("%s", reason_to_disable); 22 | } 23 | else 24 | { 25 | ImGui::BeginGroup(); 26 | 27 | widgets(); 28 | 29 | ImGui::EndGroup(); 30 | } 31 | } 32 | 33 | } // namespace ImGG -------------------------------------------------------------------------------- /src/maybe_disabled.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace ImGG { 5 | 6 | void maybe_disabled( 7 | bool condition, 8 | const char* reason_to_disable, 9 | std::function const& widgets 10 | ); 11 | 12 | } // namespace ImGG -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.11) 2 | project(imgui_gradient-tests) 3 | 4 | add_executable(${PROJECT_NAME} tests.cpp) 5 | target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_11) 6 | 7 | # Set warning level 8 | if(MSVC) 9 | target_compile_options(${PROJECT_NAME} PRIVATE /W4) 10 | else() 11 | target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic -pedantic-errors -Wconversion -Wsign-conversion) 12 | endif() 13 | 14 | # Maybe enable warnings as errors 15 | set(WARNINGS_AS_ERRORS_FOR_IMGUI_GRADIENT OFF CACHE BOOL "ON iff you want to treat warnings as errors") 16 | 17 | if(WARNINGS_AS_ERRORS_FOR_IMGUI_GRADIENT) 18 | if(MSVC) 19 | target_compile_options(${PROJECT_NAME} PRIVATE /WX) 20 | else() 21 | target_compile_options(${PROJECT_NAME} PRIVATE -Werror) 22 | endif() 23 | endif() 24 | 25 | add_subdirectory(.. ${CMAKE_CURRENT_SOURCE_DIR}/build/imgui_gradient) 26 | target_link_libraries(${PROJECT_NAME} PRIVATE imgui_gradient::imgui_gradient) 27 | 28 | # ---Add doctest--- 29 | include(FetchContent) 30 | FetchContent_Declare( 31 | doctest 32 | GIT_REPOSITORY https://github.com/doctest/doctest 33 | GIT_TAG b7c21ec5ceeadb4951b00396fc1e4642dd347e5f 34 | ) 35 | FetchContent_MakeAvailable(doctest) 36 | target_link_libraries(${PROJECT_NAME} PRIVATE doctest::doctest) 37 | 38 | # ---Ignore .vscode/settings.json in Git--- 39 | find_package(Git QUIET) 40 | 41 | if(GIT_FOUND) 42 | get_filename_component(PARENT_DIR ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY) 43 | 44 | if(EXISTS "${PARENT_DIR}/.git") 45 | execute_process(COMMAND ${GIT_EXECUTABLE} update-index --assume-unchanged .vscode/settings.json 46 | WORKING_DIRECTORY ${PARENT_DIR} 47 | RESULT_VARIABLE ERRORS) 48 | 49 | if(NOT ERRORS EQUAL "0") 50 | message("Git assume-unchanged failed: ${ERRORS}") 51 | endif() 52 | else() 53 | message("No Git repository found.") 54 | endif() 55 | else() 56 | message("Git executable not found.") 57 | endif() 58 | 59 | # ---Add quick_imgui--- 60 | include(FetchContent) 61 | FetchContent_Declare( 62 | quick_imgui 63 | GIT_REPOSITORY https://github.com/Coollab-Art/quick_imgui 64 | GIT_TAG 904d6b9141bd7226a6e36ded6394c6706e004b7f 65 | ) 66 | FetchContent_MakeAvailable(quick_imgui) 67 | cmake_policy(SET CMP0079 NEW) 68 | target_link_libraries(imgui_gradient PUBLIC quick_imgui::quick_imgui) -------------------------------------------------------------------------------- /tests/tests.cpp: -------------------------------------------------------------------------------- 1 | #define DOCTEST_CONFIG_IMPLEMENT 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "../generated/checkboxes_for_all_flags.inl" 7 | #include "../src/Utils.hpp" // to test wrap mode functions 8 | #include "../src/maybe_disabled.hpp" 9 | 10 | auto main(int argc, char* argv[]) -> int 11 | { 12 | const int exit_code = doctest::Context{}.run(); // Run all unit tests 13 | const bool should_run_imgui_tests = argc < 2 || strcmp(argv[1], "-nogpu") != 0; 14 | if ( 15 | should_run_imgui_tests 16 | && exit_code == 0 // Only open the window if the tests passed; this makes it easier to notice when some tests fail 17 | ) 18 | { 19 | auto gradient = ImGG::GradientWidget{}; 20 | auto gradient2 = ImGG::GradientWidget{}; 21 | quick_imgui::loop("imgui_gradient tests", [&]() { 22 | ImGui::ShowDemoWindow(); 23 | ImGui::Begin("Framerate"); 24 | ImGui::Text("%.3f FPS", ImGui::GetIO().Framerate); 25 | ImGui::End(); 26 | ImGui::Begin("Flags"); 27 | const auto flags = checkboxes_for_all_flags(); 28 | ImGui::End(); 29 | ImGui::Begin("Options"); 30 | static auto custom_generator = false; 31 | ImGui::Checkbox("Use our custom generator", &custom_generator); 32 | ImGui::End(); 33 | ImGui::Begin("Settings"); 34 | static ImGG::Settings settings{}; 35 | ImGui::PushItemWidth(100.f); 36 | ImGui::DragFloat("Gradient width", &settings.gradient_width); 37 | ImGui::DragFloat("Gradient height", &settings.gradient_height); 38 | ImGui::DragFloat("Horizontal margin", &settings.horizontal_margin); 39 | ImGui::DragFloat("Distance to delete mark by dragging down", &settings.distance_to_delete_mark_by_dragging_down); 40 | ImGui::End(); 41 | ImGui::Begin("Programmatic Actions"); 42 | ImGG::maybe_disabled(gradient.gradient().is_empty(), "Gradient is empty", [&]() { 43 | if (ImGui::Button("Remove a mark")) 44 | { 45 | gradient.gradient().remove_mark(ImGG::MarkId{gradient.gradient().get_marks().front()}); 46 | } 47 | }); 48 | if (ImGui::Button("Add a mark")) 49 | { 50 | gradient.gradient().add_mark(ImGG::Mark{ImGG::RelativePosition{0.5f}, ImVec4{0.f, 0.f, 0.f, 1.f}}); 51 | }; 52 | static auto position{0.f}; 53 | if (ImGui::Button("Set mark position") && !gradient.gradient().is_empty()) 54 | { 55 | gradient.gradient().set_mark_position( 56 | ImGG::MarkId{gradient.gradient().get_marks().front()}, 57 | ImGG::RelativePosition{position} 58 | ); 59 | }; 60 | ImGui::SameLine(); 61 | ImGui::DragFloat("##260", &position, .0001f, /* speed */ 62 | 0.f, 1.f, /* min and max */ 63 | "%.4f" /* precision */); 64 | static auto color = ImVec4{0.f, 0.f, 0.f, 1.f}; 65 | if (ImGui::Button("Set mark color") && !gradient.gradient().is_empty()) 66 | { 67 | gradient.gradient().set_mark_color(ImGG::MarkId{gradient.gradient().get_marks().front()}, color); 68 | }; 69 | ImGui::SameLine(); 70 | ImGui::ColorEdit4( 71 | "##colorpicker1", 72 | reinterpret_cast(&color), 73 | ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoInputs 74 | ); 75 | static ImGG::WrapMode wrap_mode{}; 76 | ImGG::wrap_mode_widget("Position Mode", &wrap_mode); 77 | static ImGG::Interpolation interpolation_mode{}; 78 | if (ImGG::interpolation_mode_widget("Interpolation Mode", &interpolation_mode)) 79 | { 80 | gradient.gradient().interpolation_mode() = interpolation_mode; 81 | gradient2.gradient().interpolation_mode() = interpolation_mode; 82 | } 83 | ImGG::random_mode_widget("Randomize new marks' color", &settings.should_use_a_random_color_for_the_new_marks); 84 | 85 | if (ImGui::Button("Equalize")) 86 | gradient.gradient().distribute_marks_evenly(); 87 | 88 | ImGui::End(); 89 | ImGui::Begin("imgui_gradient tests"); 90 | settings.flags = flags; 91 | settings.color_edit_flags = ImGuiColorEditFlags_Float | ImGuiColorEditFlags_HDR; 92 | if (custom_generator) 93 | { 94 | const auto custom_rng = []() { 95 | static auto rng = std::default_random_engine{std::random_device{}()}; 96 | static auto distribution = std::uniform_real_distribution{0.f, 1.f}; 97 | return distribution(rng); 98 | }; 99 | gradient.widget( 100 | "Gradient", 101 | custom_rng, 102 | settings 103 | ); 104 | gradient2.widget( 105 | "Gradient2", 106 | custom_rng, 107 | settings 108 | ); 109 | } 110 | else 111 | { 112 | gradient.widget( 113 | "Gradient", 114 | settings 115 | ); 116 | gradient2.widget( 117 | "Gradient2", 118 | settings 119 | ); 120 | } 121 | ImGui::End(); 122 | }); 123 | } 124 | return exit_code; 125 | } 126 | 127 | // Check out doctest's documentation: https://github.com/doctest/doctest/blob/master/doc/markdown/tutorial.md 128 | 129 | TEST_CASE("Distribute marks evenly") 130 | { 131 | ImGG::Gradient gradient; 132 | auto const& marks = gradient.get_marks(); 133 | 134 | for (int i = 0; i < 2; ++i) 135 | { 136 | // Test with both interpolation modes 137 | gradient.interpolation_mode() = i == 0 ? ImGG::Interpolation::Linear : ImGG::Interpolation::Constant; 138 | gradient.clear(); 139 | // --- 140 | 141 | // With 0 marks 142 | gradient.distribute_marks_evenly(); // Test that this doesn't crash. 143 | 144 | // With 1 mark 145 | gradient.add_mark({ImGG::RelativePosition{0.3f}, ImGG::ColorRGBA{}}); 146 | gradient.distribute_marks_evenly(); 147 | CHECK(marks.begin()->position.get() == doctest::Approx{0.5f}); 148 | 149 | // With 2 marks 150 | gradient.add_mark({ImGG::RelativePosition{0.8f}, ImGG::ColorRGBA{}}); 151 | gradient.distribute_marks_evenly(); 152 | if (gradient.interpolation_mode() == ImGG::Interpolation::Linear) 153 | { 154 | CHECK(marks.begin()->position.get() == doctest::Approx{0.f}); 155 | CHECK(std::next(marks.begin())->position.get() == doctest::Approx{1.f}); 156 | } 157 | else 158 | { 159 | CHECK(marks.begin()->position.get() == doctest::Approx{0.5f}); 160 | CHECK(std::next(marks.begin())->position.get() == doctest::Approx{1.f}); 161 | } 162 | } 163 | } 164 | 165 | TEST_CASE("Evaluating at 0 and 1") 166 | { 167 | // Gradient from black to white 168 | ImGG::Gradient gradient{{ 169 | ImGG::Mark{ImGG::RelativePosition{0.f}, ImGG::ColorRGBA{0.f, 0.f, 0.f, 1.f}}, 170 | ImGG::Mark{ImGG::RelativePosition{1.f}, ImGG::ColorRGBA{1.f, 1.f, 1.f, 1.f}}, 171 | }}; 172 | 173 | CHECK(doctest::Approx(gradient.at({0.f, ImGG::WrapMode::Clamp}).x) == 0.f); 174 | CHECK(doctest::Approx(gradient.at({1.f, ImGG::WrapMode::Clamp}).x) == 1.f); 175 | CHECK(doctest::Approx(gradient.at({0.f, ImGG::WrapMode::Repeat}).x) == 0.f); 176 | CHECK(doctest::Approx(gradient.at({1.f, ImGG::WrapMode::Repeat}).x) == 0.f); 177 | CHECK(doctest::Approx(gradient.at({0.f, ImGG::WrapMode::MirrorRepeat}).x) == 0.f); 178 | CHECK(doctest::Approx(gradient.at({1.f, ImGG::WrapMode::MirrorRepeat}).x) == 1.f); 179 | 180 | CHECK(doctest::Approx(gradient.at({-0.000001f, ImGG::WrapMode::Clamp}).x) == 0.f); 181 | CHECK(doctest::Approx(gradient.at({0.000001f, ImGG::WrapMode::Clamp}).x) == 0.f); 182 | CHECK(doctest::Approx(gradient.at({1.0000001f, ImGG::WrapMode::Clamp}).x) == 1.f); 183 | CHECK(doctest::Approx(gradient.at({0.9999999f, ImGG::WrapMode::Clamp}).x) == 1.f); 184 | 185 | CHECK(doctest::Approx(gradient.at({-0.000001f, ImGG::WrapMode::Repeat}).x) == 1.f); 186 | CHECK(doctest::Approx(gradient.at({0.000001f, ImGG::WrapMode::Repeat}).x) == 0.f); 187 | CHECK(doctest::Approx(gradient.at({1.0000001f, ImGG::WrapMode::Repeat}).x) == 0.f); 188 | CHECK(doctest::Approx(gradient.at({0.9999999f, ImGG::WrapMode::Repeat}).x) == 1.f); 189 | 190 | CHECK(doctest::Approx(gradient.at({-0.000001f, ImGG::WrapMode::MirrorRepeat}).x) == 0.f); 191 | CHECK(doctest::Approx(gradient.at({0.000001f, ImGG::WrapMode::MirrorRepeat}).x) == 0.f); 192 | CHECK(doctest::Approx(gradient.at({1.0000001f, ImGG::WrapMode::MirrorRepeat}).x) == 1.f); 193 | CHECK(doctest::Approx(gradient.at({0.9999999f, ImGG::WrapMode::MirrorRepeat}).x) == 1.f); 194 | } 195 | 196 | TEST_CASE("Interpolation modes") 197 | { 198 | // Gradient from black to white 199 | ImGG::Gradient gradient{{ 200 | ImGG::Mark{ImGG::RelativePosition{0.f}, ImGG::ColorRGBA{0.f, 0.f, 0.f, 1.f}}, 201 | ImGG::Mark{ImGG::RelativePosition{1.f}, ImGG::ColorRGBA{1.f, 1.f, 1.f, 1.f}}, 202 | }}; 203 | 204 | gradient.interpolation_mode() = ImGG::Interpolation::Linear; 205 | CHECK(doctest::Approx(gradient.at(ImGG::RelativePosition{0.25f}).x) == 0.131499f); 206 | CHECK(doctest::Approx(gradient.at(ImGG::RelativePosition{0.75f}).x) == 0.681341f); 207 | gradient.interpolation_mode() = ImGG::Interpolation::Constant; 208 | CHECK(doctest::Approx(gradient.at(ImGG::RelativePosition{0.25f}).x) == 1.f); 209 | CHECK(doctest::Approx(gradient.at(ImGG::RelativePosition{0.75f}).x) == 1.f); 210 | } 211 | 212 | TEST_CASE("Wrap modes") 213 | { 214 | SUBCASE("clamp_position() when position in the range [0,1]") 215 | { 216 | const float position = 0.3f; 217 | const auto repeat_pos = ImGG::internal::clamp_position(position); 218 | 219 | CHECK(doctest::Approx(repeat_pos.get()) == 0.3f); 220 | } 221 | SUBCASE("clamp_position() when position in the range [-1,0]") 222 | { 223 | const float position = -0.4f; 224 | const auto clamp_pos = ImGG::internal::clamp_position(position); 225 | 226 | CHECK(doctest::Approx(clamp_pos.get()) == 0.f); 227 | } 228 | SUBCASE("clamp_position() when position is < -1") 229 | { 230 | const float position = -1.4f; 231 | const auto clamp_pos = ImGG::internal::clamp_position(position); 232 | 233 | CHECK(doctest::Approx(clamp_pos.get()) == 0.f); 234 | } 235 | SUBCASE("clamp_position() when position > 1") 236 | { 237 | const float position = 1.9f; 238 | const auto clamp_pos = ImGG::internal::clamp_position(position); 239 | 240 | CHECK(doctest::Approx(clamp_pos.get()) == 1.f); 241 | } 242 | SUBCASE("clamp_position() when position = 1") 243 | { 244 | const float position = 1.f; 245 | const auto clamp_pos = ImGG::internal::clamp_position(position); 246 | 247 | CHECK(doctest::Approx(clamp_pos.get()) == 1.f); 248 | } 249 | SUBCASE("clamp_position() when position = 0") 250 | { 251 | const float position = 0.f; 252 | const auto clamp_pos = ImGG::internal::clamp_position(position); 253 | 254 | CHECK(doctest::Approx(clamp_pos.get()) == 0.f); 255 | } 256 | 257 | SUBCASE("repeat_position() when position in the range [0,1]") 258 | { 259 | const float position = 0.2f; 260 | const auto repeat_pos = ImGG::internal::repeat_position(position); 261 | 262 | CHECK(doctest::Approx(repeat_pos.get()) == 0.2f); 263 | } 264 | SUBCASE("repeat_position() when position in the range [-1,0]") 265 | { 266 | const float position = -0.3f; 267 | const auto repeat_pos = ImGG::internal::repeat_position(position); 268 | 269 | CHECK(doctest::Approx(repeat_pos.get()) == 0.7f); 270 | } 271 | SUBCASE("repeat_position() when position is < -1") 272 | { 273 | const float position = -1.4f; 274 | const auto repeat_pos = ImGG::internal::repeat_position(position); 275 | 276 | CHECK(doctest::Approx(repeat_pos.get()) == 0.6f); 277 | } 278 | SUBCASE("repeat_position() when position > 1") 279 | { 280 | const float position = 1.8f; 281 | const auto repeat_pos = ImGG::internal::repeat_position(position); 282 | 283 | CHECK(doctest::Approx(repeat_pos.get()) == 0.8f); 284 | } 285 | SUBCASE("repeat_position() when position just before 1") 286 | { 287 | const float position = .99f; 288 | const auto repeat_pos = ImGG::internal::repeat_position(position); 289 | 290 | CHECK(doctest::Approx(repeat_pos.get()) == .99f); 291 | } 292 | SUBCASE("repeat_position() when position just after 1") 293 | { 294 | const float position = 1.01f; 295 | const auto repeat_pos = ImGG::internal::repeat_position(position); 296 | 297 | CHECK(doctest::Approx(repeat_pos.get()) == .01f); 298 | } 299 | SUBCASE("repeat_position() when position just before 0") 300 | { 301 | const float position = -0.01f; 302 | const auto repeat_pos = ImGG::internal::repeat_position(position); 303 | 304 | CHECK(doctest::Approx(repeat_pos.get()) == 0.99f); 305 | } 306 | SUBCASE("repeat_position() when position just after 0") 307 | { 308 | const float position = 0.01f; 309 | const auto repeat_pos = ImGG::internal::repeat_position(position); 310 | 311 | CHECK(doctest::Approx(repeat_pos.get()) == 0.01f); 312 | } 313 | 314 | SUBCASE("mirror_repeat_position() when position in the range [0,1]") 315 | { 316 | const float position = 0.4f; 317 | const auto mirror_repeat_pos = ImGG::internal::mirror_repeat_position(position); 318 | 319 | CHECK(doctest::Approx(mirror_repeat_pos.get()) == 0.4f); 320 | } 321 | SUBCASE("mirror_repeat_position() when position in the range [-1,0]") 322 | { 323 | const float position = -0.2f; 324 | const auto mirror_repeat_pos = ImGG::internal::mirror_repeat_position(position); 325 | 326 | CHECK(doctest::Approx(mirror_repeat_pos.get()) == 0.2f); 327 | } 328 | SUBCASE("mirror_repeat_position() when position is negative and < -1") 329 | { 330 | const float position = -1.6f; 331 | const auto mirror_repeat_pos = ImGG::internal::mirror_repeat_position(position); 332 | 333 | CHECK(doctest::Approx(mirror_repeat_pos.get()) == 0.4f); 334 | } 335 | SUBCASE("mirror_repeat_position() when position > 1") 336 | { 337 | const float position = 1.8f; 338 | const auto mirror_repeat_pos = ImGG::internal::mirror_repeat_position(position); 339 | 340 | CHECK(doctest::Approx(mirror_repeat_pos.get()) == 0.2f); 341 | } 342 | SUBCASE("mirror_repeat_position() when position = 1") 343 | { 344 | const float position = 1.f; 345 | const auto mirror_repeat_pos = ImGG::internal::mirror_repeat_position(position); 346 | 347 | CHECK(doctest::Approx(mirror_repeat_pos.get()) == 1.f); 348 | } 349 | SUBCASE("mirror_repeat_position() when position = 0") 350 | { 351 | const float position = 0.f; 352 | const auto mirror_repeat_pos = ImGG::internal::mirror_repeat_position(position); 353 | 354 | CHECK(doctest::Approx(mirror_repeat_pos.get()) == 0.f); 355 | } 356 | 357 | SUBCASE("test intermediate functions : modulo(x,mod) when x = 0") 358 | { 359 | const float x = 0.f; 360 | const float mod = 2.f; 361 | const float modulo_res = ImGG::internal::modulo(x, mod); 362 | 363 | CHECK(doctest::Approx(modulo_res) == 0.f); 364 | } 365 | SUBCASE("test intermediate functions : modulo(x, mod) when x = 1") 366 | { 367 | const float x = 1.f; 368 | const float mod = 2.f; 369 | const float modulo_res = ImGG::internal::modulo(x, mod); 370 | 371 | CHECK(doctest::Approx(modulo_res) == 1.f); 372 | } 373 | } 374 | --------------------------------------------------------------------------------